next4: next2 7cd8eb8 (M2 · 줄바꿈 되돌림 · 글 뷰어 이모지) 합침
# Conflicts: # assets/l10n/en.json # assets/l10n/ja.json # assets/l10n/zh-Hans.jsonnext4 v2026.10.18_977
commit
65f5e49aad
|
|
@ -356,6 +356,7 @@
|
||||||
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} failed: {1} {2}{3}",
|
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} failed: {1} {2}{3}",
|
||||||
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "Keeps WebDAV and OpenSubtitles passwords in this device's secure storage. When off, nothing is saved (existing ones are removed) and you enter them each time the app starts",
|
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "Keeps WebDAV and OpenSubtitles passwords in this device's secure storage. When off, nothing is saved (existing ones are removed) and you enter them each time the app starts",
|
||||||
"WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다": "WebDAV, SFTP, SMB, FTP and OpenSubtitles passwords, SFTP keys and API keys. Server lists and user names are kept",
|
"WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다": "WebDAV, SFTP, SMB, FTP and OpenSubtitles passwords, SFTP keys and API keys. Server lists and user names are kept",
|
||||||
|
"WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV · SFTP · SMB · FTP: the app syncs these itself instead of rsync (compares size and modified time; follows -u, --delete and --remove-source-files from the options).",
|
||||||
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV / SFTP reads everything inside from the server, which can take a long time. Analyze?",
|
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV / SFTP reads everything inside from the server, which can take a long time. Analyze?",
|
||||||
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "Videos on WebDAV / SFTP can't be added to the MKV list. Copy them to this device first.",
|
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "Videos on WebDAV / SFTP can't be added to the MKV list. Copy them to this device first.",
|
||||||
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV / SFTP files show only name, size and date",
|
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV / SFTP files show only name, size and date",
|
||||||
|
|
@ -1894,6 +1895,7 @@
|
||||||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
|
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
|
||||||
"우르두어": "Urdu",
|
"우르두어": "Urdu",
|
||||||
"우크라이나어": "Ukrainian",
|
"우크라이나어": "Ukrainian",
|
||||||
|
"원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.": "Remote locations (WebDAV · SFTP · SMB · FTP) do not follow the filter options (--exclude, --include, --filter, …) - excluded items are copied too, and deleted if the run deletes. The comparison list above is what will actually happen.",
|
||||||
"원래 이름으로 되돌리지 못한 것: {0}": "Couldn't restore the original names of: {0}",
|
"원래 이름으로 되돌리지 못한 것: {0}": "Couldn't restore the original names of: {0}",
|
||||||
"원래 자리에 같은 이름이 이미 있습니다": "An item with the same name already exists in the original place",
|
"원래 자리에 같은 이름이 이미 있습니다": "An item with the same name already exists in the original place",
|
||||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "The original folder no longer exists, so it was not restored (restore it from the Recycle Bin yourself)",
|
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "The original folder no longer exists, so it was not restored (restore it from the Recycle Bin yourself)",
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,7 @@
|
||||||
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失敗: {1} {2}{3}",
|
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失敗: {1} {2}{3}",
|
||||||
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "WebDAV・OpenSubtitles のパスワードをこの端末の安全な保存領域に保存します。オフにすると保存せず (既存のものも削除)、アプリを起動するたびに入力します",
|
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "WebDAV・OpenSubtitles のパスワードをこの端末の安全な保存領域に保存します。オフにすると保存せず (既存のものも削除)、アプリを起動するたびに入力します",
|
||||||
"WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다": "WebDAV・SFTP・SMB・FTP・OpenSubtitles のパスワード、SFTP キー、API キー。サーバー一覧と ID は残します",
|
"WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다": "WebDAV・SFTP・SMB・FTP・OpenSubtitles のパスワード、SFTP キー、API キー。サーバー一覧と ID は残します",
|
||||||
|
"WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV・SFTP・SMB・FTP: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションのうち -u・--delete・--remove-source-files に従う)。",
|
||||||
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV・SFTP はサーバーから中身をすべて読むため、時間がかかることがあります。分析しますか?",
|
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV・SFTP はサーバーから中身をすべて読むため、時間がかかることがあります。分析しますか?",
|
||||||
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "WebDAV・SFTP の動画は MKV リストに追加できません。先にこの端末へコピーしてください。",
|
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "WebDAV・SFTP の動画は MKV リストに追加できません。先にこの端末へコピーしてください。",
|
||||||
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV・SFTP のファイルは名前・サイズ・日付のみ表示します",
|
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV・SFTP のファイルは名前・サイズ・日付のみ表示します",
|
||||||
|
|
@ -1894,6 +1895,7 @@
|
||||||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
|
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
|
||||||
"우르두어": "ウルドゥー語",
|
"우르두어": "ウルドゥー語",
|
||||||
"우크라이나어": "ウクライナ語",
|
"우크라이나어": "ウクライナ語",
|
||||||
|
"원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.": "リモート (WebDAV・SFTP・SMB・FTP) はオプションのフィルター (--exclude・--include・--filter など) に従いません - 除外したものもコピーし、削除を含む場合は削除します。上の比較一覧が実際に行う内容です。",
|
||||||
"원래 이름으로 되돌리지 못한 것: {0}": "元の名前に戻せなかったもの: {0}",
|
"원래 이름으로 되돌리지 못한 것: {0}": "元の名前に戻せなかったもの: {0}",
|
||||||
"원래 자리에 같은 이름이 이미 있습니다": "元の場所に同じ名前が既にあります",
|
"원래 자리에 같은 이름이 이미 있습니다": "元の場所に同じ名前が既にあります",
|
||||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "元のフォルダーがないため元に戻せませんでした (ごみ箱から直接復元してください)",
|
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "元のフォルダーがないため元に戻せませんでした (ごみ箱から直接復元してください)",
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,7 @@
|
||||||
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失败: {1} {2}{3}",
|
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失败: {1} {2}{3}",
|
||||||
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "将 WebDAV · OpenSubtitles 密码保存在本设备的安全存储中。关闭后不保存 (已有的也会删除),每次启动应用时重新输入",
|
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "将 WebDAV · OpenSubtitles 密码保存在本设备的安全存储中。关闭后不保存 (已有的也会删除),每次启动应用时重新输入",
|
||||||
"WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다": "WebDAV · SFTP · SMB · FTP · OpenSubtitles 密码、SFTP 密钥和 API 密钥。保留服务器列表和用户名",
|
"WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다": "WebDAV · SFTP · SMB · FTP · OpenSubtitles 密码、SFTP 密钥和 API 密钥。保留服务器列表和用户名",
|
||||||
|
"WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV · SFTP · SMB · FTP:由应用代替 rsync 直接同步 (比较大小和修改时间,遵循选项中的 -u · --delete · --remove-source-files)。",
|
||||||
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV · SFTP 需要从服务器读取全部内容,可能需要较长时间。要分析吗?",
|
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV · SFTP 需要从服务器读取全部内容,可能需要较长时间。要分析吗?",
|
||||||
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "无法将 WebDAV · SFTP 上的视频加入 MKV 列表。请先复制到本设备。",
|
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "无法将 WebDAV · SFTP 上的视频加入 MKV 列表。请先复制到本设备。",
|
||||||
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV · SFTP 文件仅显示名称、大小和日期",
|
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV · SFTP 文件仅显示名称、大小和日期",
|
||||||
|
|
@ -1894,6 +1895,7 @@
|
||||||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
|
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
|
||||||
"우르두어": "乌尔都语",
|
"우르두어": "乌尔都语",
|
||||||
"우크라이나어": "乌克兰语",
|
"우크라이나어": "乌克兰语",
|
||||||
|
"원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.": "远程 (WebDAV · SFTP · SMB · FTP) 不遵循选项中的过滤规则 (--exclude · --include · --filter 等) - 被排除的项目也会复制,如包含删除则会删除。上面的比较列表就是实际会执行的操作。",
|
||||||
"원래 이름으로 되돌리지 못한 것: {0}": "未能恢复原名称: {0}",
|
"원래 이름으로 되돌리지 못한 것: {0}": "未能恢复原名称: {0}",
|
||||||
"원래 자리에 같은 이름이 이미 있습니다": "原位置已存在同名项目",
|
"원래 자리에 같은 이름이 이미 있습니다": "原位置已存在同名项目",
|
||||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "原文件夹已不存在,未能恢复 (请在回收站中手动还原)",
|
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "原文件夹已不存在,未能恢复 (请在回收站中手动还原)",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ void main() {
|
||||||
('ascii', List.generate(3000, (i) => String.fromCharCode(0x61 + i % 26)).join()),
|
('ascii', List.generate(3000, (i) => String.fromCharCode(0x61 + i % 26)).join()),
|
||||||
('mixed', List.generate(3000, (i) => i.isEven ? '한' : 'x').join()),
|
('mixed', List.generate(3000, (i) => i.isEven ? '한' : 'x').join()),
|
||||||
('korean', '가나다라마바사아자차카타파하' * 200),
|
('korean', '가나다라마바사아자차카타파하' * 200),
|
||||||
|
// 28 지적 (10/19): 이모지 (두 단위 글자) 가 든 긴 줄 - 재는 조각이 글자 가운데서 끊기면 UTF-16 오류
|
||||||
|
('emoji', List.generate(3000, (i) => 'abc😀').join()),
|
||||||
]) {
|
]) {
|
||||||
testWidgets('글 뷰어 긴 줄 $name ${w}x$h', (t) async {
|
testWidgets('글 뷰어 긴 줄 $name ${w}x$h', (t) async {
|
||||||
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
||||||
|
|
@ -44,6 +46,7 @@ void main() {
|
||||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||||
Directory(out).createSync(recursive: true);
|
Directory(out).createSync(recursive: true);
|
||||||
File(p.join(out, 'textview_${name}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
File(p.join(out, 'textview_${name}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||||
|
expect(t.takeException(), isNull, reason: '그리는 중 오류 없음 ($name $w)');
|
||||||
await t.pumpWidget(const SizedBox());
|
await t.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,16 @@ bool isRsyncDeleteOption(String o) => o == '--del' || o.startsWith('--delete') |
|
||||||
/// rsync 옵션에 지우기 (--delete 계열) 가 있는지
|
/// rsync 옵션에 지우기 (--delete 계열) 가 있는지
|
||||||
bool optionsDelete(String options) => splitOptions(options).any(isRsyncDeleteOption);
|
bool optionsDelete(String options) => splitOptions(options).any(isRsyncDeleteOption);
|
||||||
|
|
||||||
|
/// M2 (관리자 지시 10/19): rsync 옵션에 거르기 (--exclude · --include · --filter · -f · --exclude-from · --include-from · -C · -F) 가 있는지.
|
||||||
|
/// 원격 (WebDAV · SFTP · SMB · FTP) 은 앱이 직접 맞추며 이 거르기를 따르지 않으므로 확인 창에 알린다
|
||||||
|
bool optionsFilter(String options) => splitOptions(options).any((o) =>
|
||||||
|
o == '-f' ||
|
||||||
|
o == '-F' ||
|
||||||
|
o == '--cvs-exclude' ||
|
||||||
|
o == '-C' ||
|
||||||
|
RegExp(r'^--(exclude|include|filter|exclude-from|include-from)(=|$)').hasMatch(o) ||
|
||||||
|
(RegExp(r'^-[a-zA-Z]+$').hasMatch(o) && (o.contains('C') || o.contains('F'))));
|
||||||
|
|
||||||
/// 옵션 글을 낱말로 나누되 따옴표는 그대로 둔 원래 글 조각 (106: --exclude="My Folder" 가 갈라지지 않게)
|
/// 옵션 글을 낱말로 나누되 따옴표는 그대로 둔 원래 글 조각 (106: --exclude="My Folder" 가 갈라지지 않게)
|
||||||
List<String> rawOptionTokens(String s) {
|
List<String> rawOptionTokens(String s) {
|
||||||
final out = <String>[];
|
final out = <String>[];
|
||||||
|
|
|
||||||
|
|
@ -3541,8 +3541,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (_viaDav([l, r]))
|
if (_viaDav([l, r]))
|
||||||
Text(tr('WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'),
|
Text(tr('WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'),
|
||||||
style: const TextStyle(fontSize: 12, color: JjColors.accent)),
|
style: const TextStyle(fontSize: 12, color: JjColors.accent)),
|
||||||
|
// M2 (관리자 지시 10/19): 원격이고 옵션에 거르기가 있으면 - 앱 맞추기는 거르기를 따르지 않는다 (뺀 것도 복사 · 지움)
|
||||||
|
if (_viaDav([l, r]) && tasks.any((t) => optionsFilter(opts(t))))
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(tr('원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.'),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,13 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
||||||
],
|
],
|
||||||
// 230: 칸에 적힌 옵션이 아니라 실제로 실행할 것 (원본 지우기 옵션은 이동일 때만 붙는다)
|
// 230: 칸에 적힌 옵션이 아니라 실제로 실행할 것 (원본 지우기 옵션은 이동일 때만 붙는다)
|
||||||
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||||
|
// M2 (관리자 지시 10/19): 원격 (앱이 직접 맞춤) 인데 옵션에 거르기가 있으면 - 거르기를 따르지 않는다
|
||||||
|
if (direct && t.method == 'rsync' && optionsFilter(t.options))
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(tr('원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.'),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
// 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히
|
// 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히
|
||||||
if (t.move)
|
if (t.move)
|
||||||
Padding(
|
Padding(
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ class _TextViewerPageState extends State<TextViewerPage> {
|
||||||
|
|
||||||
/// 넓은 글자 (한글 · 한자 · 가나 · 전각) 하나가 고정폭 글자 몇 개 폭인지 (관리자 지시 10/19: 넓은 글자가 섞인 줄도 줄 끝이 비지 않게)
|
/// 넓은 글자 (한글 · 한자 · 가나 · 전각) 하나가 고정폭 글자 몇 개 폭인지 (관리자 지시 10/19: 넓은 글자가 섞인 줄도 줄 끝이 비지 않게)
|
||||||
double _wideRatio(BuildContext context) {
|
double _wideRatio(BuildContext context) {
|
||||||
|
// l10n-skip: 폭을 재는 넓은 글자 (화면 글이 아님)
|
||||||
final m = _measure(context, 'M'), w = _measure(context, '가');
|
final m = _measure(context, 'M'), w = _measure(context, '가');
|
||||||
return m == null || w == null ? 2 : w / m;
|
return m == null || w == null ? 2 : w / m;
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +159,10 @@ class _TextViewerPageState extends State<TextViewerPage> {
|
||||||
_rows = viewerRows(_text, perRow, wide: _wideRatio(context), cut: (line, from) {
|
_rows = viewerRows(_text, perRow, wide: _wideRatio(context), cut: (line, from) {
|
||||||
var size = perRow * 8;
|
var size = perRow * 8;
|
||||||
while (true) {
|
while (true) {
|
||||||
final end = (from + size).clamp(0, line.length);
|
var end = (from + size).clamp(0, line.length);
|
||||||
|
// 28 지적 (10/19): 재는 조각이 이모지 같은 두 단위 글자의 가운데에서 끝나면 반쪽 글자라 TextSpan 이
|
||||||
|
// "not well-formed UTF-16" 으로 실패했다 (화면은 정상이나 오류가 쏟아지고 더 큰 조각을 다시 잼) - 글자 앞에서 자른다
|
||||||
|
if (end < line.length && end > from + 1 && (line.codeUnitAt(end) & 0xFC00) == 0xDC00) end--;
|
||||||
tp.text = TextSpan(text: line.substring(from, end), style: style);
|
tp.text = TextSpan(text: line.substring(from, end), style: style);
|
||||||
tp.layout(maxWidth: room);
|
tp.layout(maxWidth: room);
|
||||||
final m = tp.computeLineMetrics();
|
final m = tp.computeLineMetrics();
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,9 @@ void main() {
|
||||||
// 72: 비교 - 보낼 것 · 받는 쪽 (WebDAV) 에만 있는 것 (지우기 없으니 그대로 둠)
|
// 72: 비교 - 보낼 것 · 받는 쪽 (WebDAV) 에만 있는 것 (지우기 없으니 그대로 둠)
|
||||||
expect(find.textContaining('→ + inner.txt'), findsOneWidget);
|
expect(find.textContaining('→ + inner.txt'), findsOneWidget);
|
||||||
expect(find.textContaining('= note.txt'), findsOneWidget);
|
expect(find.textContaining('= note.txt'), findsOneWidget);
|
||||||
expect(find.textContaining('WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다'), findsOneWidget);
|
expect(find.textContaining('WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다'), findsOneWidget);
|
||||||
|
// M2: 옵션에 거르기가 없으면 거르기 경고는 없다
|
||||||
|
expect(find.textContaining('거르기 (--exclude'), findsNothing);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행')));
|
||||||
final got = File(p.join(remote.path, '원격', 'inner.txt'));
|
final got = File(p.join(remote.path, '원격', 'inner.txt'));
|
||||||
await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200);
|
await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200);
|
||||||
|
|
|
||||||
|
|
@ -104,4 +104,23 @@ void main() {
|
||||||
expect(r['only_b.txt'], (PreviewAction.delete, true));
|
expect(r['only_b.txt'], (PreviewAction.delete, true));
|
||||||
expect(r['new.txt'], (PreviewAction.add, true));
|
expect(r['new.txt'], (PreviewAction.add, true));
|
||||||
});
|
});
|
||||||
|
test('M2 (10/19): optionsFilter - 거르기 옵션이 있는지 (원격 확인 창의 경고)', () {
|
||||||
|
for (final o in [
|
||||||
|
'-av --exclude=*.tmp',
|
||||||
|
'-av --exclude "My Folder"',
|
||||||
|
'-a --include=*.mkv --exclude=*',
|
||||||
|
'--filter="- *.bak"',
|
||||||
|
'-a -f "- x"',
|
||||||
|
'--exclude-from=list.txt',
|
||||||
|
'--include-from list.txt',
|
||||||
|
'-avC',
|
||||||
|
'--cvs-exclude',
|
||||||
|
'-aF',
|
||||||
|
]) {
|
||||||
|
expect(optionsFilter(o), isTrue, reason: o);
|
||||||
|
}
|
||||||
|
for (final o in ['-avPog', '-a --delete', '-avu --remove-source-files', '--delete-excluded', '-e "ssh -C"', '']) {
|
||||||
|
expect(optionsFilter(o), isFalse, reason: o);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,27 @@ void main() {
|
||||||
expect(lines, 4, reason: '네 줄 분량으로 나눔 (다섯째 반쪽 줄 없음)');
|
expect(lines, 4, reason: '네 줄 분량으로 나눔 (다섯째 반쪽 줄 없음)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('h 글 뷰어 (28 지적 10/19): 이모지가 든 긴 줄 - 재는 조각이 두 단위 글자 가운데서 끊기지 않음 (UTF-16 오류 없음)', (t) async {
|
||||||
|
final line = List.generate(3000, (i) => 'abc😀').join();
|
||||||
|
final f = File(p.join(tmp.path, 'emoji.txt'))..writeAsStringSync('$line\nend');
|
||||||
|
for (final w in [412.0, 960.0]) {
|
||||||
|
t.view.physicalSize = Size(w, 900);
|
||||||
|
t.view.devicePixelRatio = 1;
|
||||||
|
await t.pumpWidget(MaterialApp(home: TextViewerPage(key: ValueKey(w), path: f.path)));
|
||||||
|
for (var i = 0; i < 40 && find.textContaining('abc😀abc').evaluate().isEmpty; i++) {
|
||||||
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||||
|
await t.pump();
|
||||||
|
}
|
||||||
|
expect(t.takeException(), isNull, reason: '폭 $w');
|
||||||
|
final texts = t.widgetList<Text>(find.textContaining('abc😀')).map((x) => x.data!).toList();
|
||||||
|
expect(texts, isNotEmpty);
|
||||||
|
for (final s in texts) {
|
||||||
|
expect((s.codeUnitAt(s.length - 1) & 0xFC00) == 0xD800, isFalse, reason: '조각이 반쪽 이모지로 끝나지 않음');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.view.reset();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('g 격자 보기: 그림 · 동영상만, 누르면 열기', (t) async {
|
testWidgets('g 격자 보기: 그림 · 동영상만, 누르면 열기', (t) async {
|
||||||
final png = File(p.join(tmp.path, 'a.png'))..writeAsBytesSync([0x89, 0x50]);
|
final png = File(p.join(tmp.path, 'a.png'))..writeAsBytesSync([0x89, 0x50]);
|
||||||
final opened = <String>[];
|
final opened = <String>[];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue