파일 탐색기: 한 번 = 선택 · 두 번 = 실행, 스타일 (X-plore · Windows 탐색기 · Total Commander), 폴더 + 파일 목록 배치
- 환경 설정 > 파일 탐색기: 누르기 (한 번 선택 · 두 번 실행 (기본) / 한 번 바로 실행), 스타일, 창 배치 - 선택 모드: 폴더는 고르기만 (› 화살표로 펼치기), 두 번 누르면 펼치기 · 실행, Ctrl + 클릭 = 여러 개 고르기, 길게 누르기 · 오른쪽 클릭 = 기능 메뉴. 표시 동그라미는 두 번 누르기를 기다리지 않고 바로 - 스타일: X-plore (썸네일 · 두 줄), Windows 탐색기 (컬러 아이콘 · 이름 / 수정한 날짜 / 유형 / 크기 열), Total Commander (촘촘한 줄 · [폴더] · 확장자 / <DIR> / 날짜 열) - 창 배치 "폴더 + 파일 목록": 왼쪽 폴더 트리, 오른쪽 고른 폴더의 내용 (맨 위 ".." = 상위 폴더, 두 번 누르면 들어가기) - 모양 · 배치가 바뀌면 보던 폴더로 다시 스크롤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음3
parent
aa9c431a47
commit
a485d0d997
|
|
@ -125,6 +125,7 @@
|
||||||
"Whisper small (정확)": "Whisper small (accurate)",
|
"Whisper small (정확)": "Whisper small (accurate)",
|
||||||
"Windows 기본 브라우저": "Windows default browser",
|
"Windows 기본 브라우저": "Windows default browser",
|
||||||
"Windows 기본 프로그램": "Windows default program",
|
"Windows 기본 프로그램": "Windows default program",
|
||||||
|
"Windows 탐색기": "Windows Explorer",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp, magnet · .torrent → aria2 (no seeding after completion)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp, magnet · .torrent → aria2 (no seeding after completion)",
|
||||||
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube is asking for a robot check. In Settings > Downloads > YouTube cookies, choose a browser (Firefox recommended) or a cookies.txt, then download again.",
|
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube is asking for a robot check. In Settings > Downloads > YouTube cookies, choose a browser (Firefox recommended) or a cookies.txt, then download again.",
|
||||||
"YouTube 광고 배너 숨기기": "Hide YouTube ad banners",
|
"YouTube 광고 배너 숨기기": "Hide YouTube ad banners",
|
||||||
|
|
@ -165,6 +166,7 @@
|
||||||
"{0} 에서 받을 파일을 찾지 못했습니다": "Could not find a file to download from {0}",
|
"{0} 에서 받을 파일을 찾지 못했습니다": "Could not find a file to download from {0}",
|
||||||
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.",
|
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.",
|
||||||
"{0} 추가": "Add {0}",
|
"{0} 추가": "Add {0}",
|
||||||
|
"{0} 파일": "{0} file",
|
||||||
"{0}. {1} (대기)": "{0}. {1} (waiting)",
|
"{0}. {1} (대기)": "{0}. {1} (waiting)",
|
||||||
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: moves this video to {1}",
|
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: moves this video to {1}",
|
||||||
"{0}: 이미 이동 폴더에 있습니다": "{0}: already in the move folder",
|
"{0}: 이미 이동 폴더에 있습니다": "{0}: already in the move folder",
|
||||||
|
|
@ -240,6 +242,7 @@
|
||||||
"기본: MP4. 음성만 받으면 MP3 · M4A 로 저장": "Default: MP4. Audio only is saved as MP3 · M4A",
|
"기본: MP4. 음성만 받으면 MP3 · M4A 로 저장": "Default: MP4. Audio only is saved as MP3 · M4A",
|
||||||
"기본값": "Default",
|
"기본값": "Default",
|
||||||
"기타 즐겨찾기": "Other bookmarks",
|
"기타 즐겨찾기": "Other bookmarks",
|
||||||
|
"길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). Ctrl + 클릭: 여러 개 고르기": "Long press · right click always opens the action menu (copy · move · delete · rename …). Ctrl + click: select several",
|
||||||
"길이 {0}": "Length {0}",
|
"길이 {0}": "Length {0}",
|
||||||
"끄기": "Off",
|
"끄기": "Off",
|
||||||
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "When off, starts right away with the last settings: source {0}, languages {1}, {2}",
|
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "When off, starts right away with the last settings: source {0}, languages {1}, {2}",
|
||||||
|
|
@ -284,6 +287,7 @@
|
||||||
"노랑": "Yellow",
|
"노랑": "Yellow",
|
||||||
"노르웨이어": "Norwegian",
|
"노르웨이어": "Norwegian",
|
||||||
"누르고 있으면 조정 전": "Hold to see before",
|
"누르고 있으면 조정 전": "Hold to see before",
|
||||||
|
"누르기": "Tap",
|
||||||
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "As soon as they finish, put them in the Make MKV video list and remove them from the download list (same as the checkbox on the download screen)",
|
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "As soon as they finish, put them in the Make MKV video list and remove them from the download list (same as the checkbox on the download screen)",
|
||||||
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "When finished, automatically add to the Make MKV video list and remove from this download list (the downloaded file stays)",
|
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "When finished, automatically add to the Make MKV video list and remove from this download list (the downloaded file stays)",
|
||||||
"다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser",
|
"다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser",
|
||||||
|
|
@ -450,6 +454,7 @@
|
||||||
"비밀번호 (선택)": "Password (optional)",
|
"비밀번호 (선택)": "Password (optional)",
|
||||||
"비어 있는 폴더": "Empty folder",
|
"비어 있는 폴더": "Empty folder",
|
||||||
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "This folder is empty.\nPress [Add bookmark] · [New folder] above, or drag bookmarks onto a folder on the left.",
|
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "This folder is empty.\nPress [Add bookmark] · [New folder] above, or drag bookmarks onto a folder on the left.",
|
||||||
|
"비어 있음": "Empty",
|
||||||
"사용 안 함": "Off",
|
"사용 안 함": "Off",
|
||||||
"사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "Off. Set this if the \"Sign in to confirm you're not a bot\" error appears",
|
"사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "Off. Set this if the \"Sign in to confirm you're not a bot\" error appears",
|
||||||
"사용자가 취소했습니다.": "Cancelled by the user.",
|
"사용자가 취소했습니다.": "Cancelled by the user.",
|
||||||
|
|
@ -499,6 +504,7 @@
|
||||||
"세로 고정": "Portrait lock",
|
"세로 고정": "Portrait lock",
|
||||||
"수정": "Edit",
|
"수정": "Edit",
|
||||||
"수정 · 이동": "Edit · move",
|
"수정 · 이동": "Edit · move",
|
||||||
|
"수정한 날짜": "Date modified",
|
||||||
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "Even when hidden, you can change the pane layout · buttons from the ⋮ menu at the top.",
|
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "Even when hidden, you can change the pane layout · buttons from the ⋮ menu at the top.",
|
||||||
"숨김": "Hidden",
|
"숨김": "Hidden",
|
||||||
"숨은 항목 표시": "Show hidden items",
|
"숨은 항목 표시": "Show hidden items",
|
||||||
|
|
@ -506,6 +512,7 @@
|
||||||
"숨은 항목을 보입니다.": "Hidden items are shown.",
|
"숨은 항목을 보입니다.": "Hidden items are shown.",
|
||||||
"쉬는 중": "Idle",
|
"쉬는 중": "Idle",
|
||||||
"스웨덴어": "Swedish",
|
"스웨덴어": "Swedish",
|
||||||
|
"스타일": "Style",
|
||||||
"스페인어": "Spanish",
|
"스페인어": "Spanish",
|
||||||
"시간 형식 오류 {0}곳": "{0} time format errors",
|
"시간 형식 오류 {0}곳": "{0} time format errors",
|
||||||
"시간순 정렬": "Sort by time",
|
"시간순 정렬": "Sort by time",
|
||||||
|
|
@ -531,6 +538,7 @@
|
||||||
"아래에 줄 추가": "Add line below",
|
"아래에 줄 추가": "Add line below",
|
||||||
"아이디 (선택)": "Username (optional)",
|
"아이디 (선택)": "Username (optional)",
|
||||||
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "You can enter the username · password in Settings (optional, raises the daily download limit).",
|
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "You can enter the username · password in Settings (optional, raises the daily download limit).",
|
||||||
|
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "Icons · list look: X-plore (thumbnails · two lines) · Windows Explorer (color icons · columns) · Total Commander (dense list · [folders])",
|
||||||
"아직 만든 MKV · 자막이 없습니다: {0}": "No MKV · subtitles made yet: {0}",
|
"아직 만든 MKV · 자막이 없습니다: {0}": "No MKV · subtitles made yet: {0}",
|
||||||
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "None yet. Videos come in automatically once downloaded (change in Settings).",
|
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "None yet. Videos come in automatically once downloaded (change in Settings).",
|
||||||
"아직 지원하지 않는 플랫폼입니다: {0}": "This platform is not supported yet: {0}",
|
"아직 지원하지 않는 플랫폼입니다: {0}": "This platform is not supported yet: {0}",
|
||||||
|
|
@ -585,6 +593,7 @@
|
||||||
"왼쪽": "Left",
|
"왼쪽": "Left",
|
||||||
"왼쪽 90°": "Left 90°",
|
"왼쪽 90°": "Left 90°",
|
||||||
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "Reorder with the handle on the left · drag the icon onto a folder on the left or in the list to move it there · right-click menu",
|
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "Reorder with the handle on the left · drag the icon onto a folder on the left or in the list to move it there · right-click menu",
|
||||||
|
"왼쪽에 폴더, 오른쪽에 왼쪽에서 고른 폴더의 파일들을 보여 줍니다 (Windows 탐색기처럼).": "Folders on the left, the files of the folder chosen on the left on the right (like Windows Explorer).",
|
||||||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
|
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
|
||||||
"우르두어": "Urdu",
|
"우르두어": "Urdu",
|
||||||
"우크라이나어": "Ukrainian",
|
"우크라이나어": "Ukrainian",
|
||||||
|
|
@ -607,6 +616,7 @@
|
||||||
"위 폴더": "Parent folder",
|
"위 폴더": "Parent folder",
|
||||||
"위로": "Up",
|
"위로": "Up",
|
||||||
"위아래": "Top and bottom",
|
"위아래": "Top and bottom",
|
||||||
|
"유형": "Type",
|
||||||
"음성": "Audio",
|
"음성": "Audio",
|
||||||
"음성 {0}개": "{0} audio",
|
"음성 {0}개": "{0} audio",
|
||||||
"음성 선택": "Choose audio",
|
"음성 선택": "Choose audio",
|
||||||
|
|
@ -871,12 +881,15 @@
|
||||||
"트랙 {0}": "Track {0}",
|
"트랙 {0}": "Track {0}",
|
||||||
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "Reopen with the tray icon or the hotkey below",
|
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "Reopen with the tray icon or the hotkey below",
|
||||||
"트레이 정리": "Clean up tray",
|
"트레이 정리": "Clean up tray",
|
||||||
|
"파일": "File",
|
||||||
"파일 관리자에서 보기": "Show in file manager",
|
"파일 관리자에서 보기": "Show in file manager",
|
||||||
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "When JJ_MKVMaker is chosen from \"Open with\" · Share in a file app",
|
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "When JJ_MKVMaker is chosen from \"Open with\" · Share in a file app",
|
||||||
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "The file location is unknown, so it will only be played. To make an MKV, choose it with \"Add videos\".",
|
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "The file location is unknown, so it will only be played. To make an MKV, choose it with \"Add videos\".",
|
||||||
"파일 이름": "File name",
|
"파일 이름": "File name",
|
||||||
"파일 탐색기": "File explorer",
|
"파일 탐색기": "File explorer",
|
||||||
"파일 탐색기 (지금 여기)": "File explorer (you are here)",
|
"파일 탐색기 (지금 여기)": "File explorer (you are here)",
|
||||||
|
"파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "You can also change it from the ⋮ menu at the top of the file explorer (side by side · top and bottom · button bar position · buttons)",
|
||||||
|
"파일 폴더": "File folder",
|
||||||
"파일 확인 중…": "Checking files…",
|
"파일 확인 중…": "Checking files…",
|
||||||
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
|
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
|
||||||
"팝업 끄기": "Close pop-up",
|
"팝업 끄기": "Close pop-up",
|
||||||
|
|
@ -891,6 +904,7 @@
|
||||||
"포르투갈어": "Portuguese",
|
"포르투갈어": "Portuguese",
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)",
|
||||||
"폴더": "Folder",
|
"폴더": "Folder",
|
||||||
|
"폴더 + 파일 목록": "Folders + file list",
|
||||||
"폴더 만들기": "New folder",
|
"폴더 만들기": "New folder",
|
||||||
"폴더 삭제: {0} (안의 {1}개 포함)": "Folder deleted: {0} (including {1} inside)",
|
"폴더 삭제: {0} (안의 {1}개 포함)": "Folder deleted: {0} (including {1} inside)",
|
||||||
"폴더 선택": "Choose folder",
|
"폴더 선택": "Choose folder",
|
||||||
|
|
@ -926,6 +940,8 @@
|
||||||
"필수 프로그램 점검": "Check required programs",
|
"필수 프로그램 점검": "Check required programs",
|
||||||
"필수 프로그램이 모두 설치되어 있습니다.": "All required programs are installed.",
|
"필수 프로그램이 모두 설치되어 있습니다.": "All required programs are installed.",
|
||||||
"하위 폴더가 없습니다": "No subfolders",
|
"하위 폴더가 없습니다": "No subfolders",
|
||||||
|
"한 번: 바로 실행": "Single: open right away",
|
||||||
|
"한 번: 선택 · 두 번: 실행 (기본)": "Single: select · Double: open (default)",
|
||||||
"한 창": "One pane",
|
"한 창": "One pane",
|
||||||
"한국어": "Korean",
|
"한국어": "Korean",
|
||||||
"한글": "Hangul",
|
"한글": "Hangul",
|
||||||
|
|
@ -954,6 +970,7 @@
|
||||||
"화질": "Quality",
|
"화질": "Quality",
|
||||||
"확인": "OK",
|
"확인": "OK",
|
||||||
"확인 · 압축 푸는 중…": "Verifying · extracting…",
|
"확인 · 압축 푸는 중…": "Verifying · extracting…",
|
||||||
|
"확장자": "Ext",
|
||||||
"확장자별 재생 프로그램": "Player per file extension",
|
"확장자별 재생 프로그램": "Player per file extension",
|
||||||
"환경 설정": "Settings",
|
"환경 설정": "Settings",
|
||||||
"회전": "Rotation",
|
"회전": "Rotation",
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@
|
||||||
"Whisper small (정확)": "Whisper small(高精度)",
|
"Whisper small (정확)": "Whisper small(高精度)",
|
||||||
"Windows 기본 브라우저": "Windows の既定のブラウザー",
|
"Windows 기본 브라우저": "Windows の既定のブラウザー",
|
||||||
"Windows 기본 프로그램": "Windows の既定のプログラム",
|
"Windows 기본 프로그램": "Windows の既定のプログラム",
|
||||||
|
"Windows 탐색기": "Windows エクスプローラー",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp、マグネット・.torrent → aria2(完了後のアップロードなし)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp、マグネット・.torrent → aria2(完了後のアップロードなし)",
|
||||||
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube がロボット確認を求めています。環境設定 > ダウンロード > YouTube クッキー でブラウザー(Firefox 推奨)または cookies.txt を指定してから、もう一度ダウンロードしてください。",
|
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube がロボット確認を求めています。環境設定 > ダウンロード > YouTube クッキー でブラウザー(Firefox 推奨)または cookies.txt を指定してから、もう一度ダウンロードしてください。",
|
||||||
"YouTube 광고 배너 숨기기": "YouTube の広告バナーを隠す",
|
"YouTube 광고 배너 숨기기": "YouTube の広告バナーを隠す",
|
||||||
|
|
@ -165,6 +166,7 @@
|
||||||
"{0} 에서 받을 파일을 찾지 못했습니다": "{0} からダウンロードするファイルが見つかりませんでした",
|
"{0} 에서 받을 파일을 찾지 못했습니다": "{0} からダウンロードするファイルが見つかりませんでした",
|
||||||
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。",
|
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。",
|
||||||
"{0} 추가": "{0} を追加",
|
"{0} 추가": "{0} を追加",
|
||||||
|
"{0} 파일": "{0} ファイル",
|
||||||
"{0}. {1} (대기)": "{0}. {1}(待機)",
|
"{0}. {1} (대기)": "{0}. {1}(待機)",
|
||||||
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: この動画を {1} に移動します",
|
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: この動画を {1} に移動します",
|
||||||
"{0}: 이미 이동 폴더에 있습니다": "{0}: 既に移動フォルダーにあります",
|
"{0}: 이미 이동 폴더에 있습니다": "{0}: 既に移動フォルダーにあります",
|
||||||
|
|
@ -240,6 +242,7 @@
|
||||||
"기본: MP4. 음성만 받으면 MP3 · M4A 로 저장": "既定: MP4。音声のみなら MP3・M4A で保存",
|
"기본: MP4. 음성만 받으면 MP3 · M4A 로 저장": "既定: MP4。音声のみなら MP3・M4A で保存",
|
||||||
"기본값": "既定値",
|
"기본값": "既定値",
|
||||||
"기타 즐겨찾기": "その他のブックマーク",
|
"기타 즐겨찾기": "その他のブックマーク",
|
||||||
|
"길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). Ctrl + 클릭: 여러 개 고르기": "長押し · 右クリックは常に機能メニュー (コピー · 移動 · 削除 · 名前の変更 …)。Ctrl + クリック: 複数選択",
|
||||||
"길이 {0}": "長さ {0}",
|
"길이 {0}": "長さ {0}",
|
||||||
"끄기": "オフ",
|
"끄기": "オフ",
|
||||||
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "オフにすると前回の設定ですぐ開始: 原語 {0}、言語 {1}、{2}",
|
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "オフにすると前回の設定ですぐ開始: 原語 {0}、言語 {1}、{2}",
|
||||||
|
|
@ -284,6 +287,7 @@
|
||||||
"노랑": "黄色",
|
"노랑": "黄色",
|
||||||
"노르웨이어": "ノルウェー語",
|
"노르웨이어": "ノルウェー語",
|
||||||
"누르고 있으면 조정 전": "押している間は調整前",
|
"누르고 있으면 조정 전": "押している間は調整前",
|
||||||
|
"누르기": "タップ",
|
||||||
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "ダウンロードが終わり次第 MKV 作成の動画リストに入れ、ダウンロードリストからは外します(ダウンロード画面のチェックボックスと同じ)",
|
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "ダウンロードが終わり次第 MKV 作成の動画リストに入れ、ダウンロードリストからは外します(ダウンロード画面のチェックボックスと同じ)",
|
||||||
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "ダウンロードが終わると MKV 作成の動画リストに自動で入れ、このダウンロードリストからは外します(ダウンロードしたファイルはそのまま)",
|
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "ダウンロードが終わると MKV 作成の動画リストに自動で入れ、このダウンロードリストからは外します(ダウンロードしたファイルはそのまま)",
|
||||||
"다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む",
|
"다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む",
|
||||||
|
|
@ -450,6 +454,7 @@
|
||||||
"비밀번호 (선택)": "パスワード(任意)",
|
"비밀번호 (선택)": "パスワード(任意)",
|
||||||
"비어 있는 폴더": "空のフォルダー",
|
"비어 있는 폴더": "空のフォルダー",
|
||||||
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "空のフォルダーです。\n上の[ブックマーク追加]・[新しいフォルダー]を押すか、ブックマークを左のフォルダーにドラッグしてください。",
|
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "空のフォルダーです。\n上の[ブックマーク追加]・[新しいフォルダー]を押すか、ブックマークを左のフォルダーにドラッグしてください。",
|
||||||
|
"비어 있음": "空",
|
||||||
"사용 안 함": "使用しない",
|
"사용 안 함": "使用しない",
|
||||||
"사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "使用しない。「Sign in to confirm you're not a bot」エラーが出たら設定してください",
|
"사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "使用しない。「Sign in to confirm you're not a bot」エラーが出たら設定してください",
|
||||||
"사용자가 취소했습니다.": "ユーザーがキャンセルしました。",
|
"사용자가 취소했습니다.": "ユーザーがキャンセルしました。",
|
||||||
|
|
@ -499,6 +504,7 @@
|
||||||
"세로 고정": "縦向き固定",
|
"세로 고정": "縦向き固定",
|
||||||
"수정": "編集",
|
"수정": "編集",
|
||||||
"수정 · 이동": "編集・移動",
|
"수정 · 이동": "編集・移動",
|
||||||
|
"수정한 날짜": "更新日時",
|
||||||
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "非表示にしても、上の ⋮ メニューからペインの配置 · ボタン構成を変えられます。",
|
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "非表示にしても、上の ⋮ メニューからペインの配置 · ボタン構成を変えられます。",
|
||||||
"숨김": "非表示",
|
"숨김": "非表示",
|
||||||
"숨은 항목 표시": "隠し項目を表示",
|
"숨은 항목 표시": "隠し項目を表示",
|
||||||
|
|
@ -506,6 +512,7 @@
|
||||||
"숨은 항목을 보입니다.": "隠し項目を表示します。",
|
"숨은 항목을 보입니다.": "隠し項目を表示します。",
|
||||||
"쉬는 중": "休止中",
|
"쉬는 중": "休止中",
|
||||||
"스웨덴어": "スウェーデン語",
|
"스웨덴어": "スウェーデン語",
|
||||||
|
"스타일": "スタイル",
|
||||||
"스페인어": "スペイン語",
|
"스페인어": "スペイン語",
|
||||||
"시간 형식 오류 {0}곳": "時間形式エラー {0}か所",
|
"시간 형식 오류 {0}곳": "時間形式エラー {0}か所",
|
||||||
"시간순 정렬": "時間順に並べ替え",
|
"시간순 정렬": "時間順に並べ替え",
|
||||||
|
|
@ -531,6 +538,7 @@
|
||||||
"아래에 줄 추가": "下に行を追加",
|
"아래에 줄 추가": "下に行を追加",
|
||||||
"아이디 (선택)": "ユーザー名(任意)",
|
"아이디 (선택)": "ユーザー名(任意)",
|
||||||
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "ユーザー名・パスワードは環境設定で入力できます(任意、1日のダウンロード回数が増えます)。",
|
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "ユーザー名・パスワードは環境設定で入力できます(任意、1日のダウンロード回数が増えます)。",
|
||||||
|
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "アイコン · 一覧の見た目: X-plore (サムネイル · 2 行) · Windows エクスプローラー (カラーアイコン · 列) · Total Commander (詰めた一覧 · [フォルダー])",
|
||||||
"아직 만든 MKV · 자막이 없습니다: {0}": "まだ作成した MKV・字幕がありません: {0}",
|
"아직 만든 MKV · 자막이 없습니다: {0}": "まだ作成した MKV・字幕がありません: {0}",
|
||||||
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "まだありません。動画のダウンロードが終わると自動で入ります(設定で変更)。",
|
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "まだありません。動画のダウンロードが終わると自動で入ります(設定で変更)。",
|
||||||
"아직 지원하지 않는 플랫폼입니다: {0}": "まだ対応していないプラットフォームです: {0}",
|
"아직 지원하지 않는 플랫폼입니다: {0}": "まだ対応していないプラットフォームです: {0}",
|
||||||
|
|
@ -585,6 +593,7 @@
|
||||||
"왼쪽": "左",
|
"왼쪽": "左",
|
||||||
"왼쪽 90°": "左 90°",
|
"왼쪽 90°": "左 90°",
|
||||||
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "左のハンドルで並べ替え・アイコンを左のフォルダーかリスト内のフォルダーにドラッグするとそのフォルダーへ移動・右クリックメニュー",
|
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "左のハンドルで並べ替え・アイコンを左のフォルダーかリスト内のフォルダーにドラッグするとそのフォルダーへ移動・右クリックメニュー",
|
||||||
|
"왼쪽에 폴더, 오른쪽에 왼쪽에서 고른 폴더의 파일들을 보여 줍니다 (Windows 탐색기처럼).": "左にフォルダー、右に左で選んだフォルダーのファイルを表示します (Windows エクスプローラーのように)。",
|
||||||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
|
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
|
||||||
"우르두어": "ウルドゥー語",
|
"우르두어": "ウルドゥー語",
|
||||||
"우크라이나어": "ウクライナ語",
|
"우크라이나어": "ウクライナ語",
|
||||||
|
|
@ -607,6 +616,7 @@
|
||||||
"위 폴더": "上のフォルダー",
|
"위 폴더": "上のフォルダー",
|
||||||
"위로": "上へ",
|
"위로": "上へ",
|
||||||
"위아래": "上下",
|
"위아래": "上下",
|
||||||
|
"유형": "種類",
|
||||||
"음성": "音声",
|
"음성": "音声",
|
||||||
"음성 {0}개": "音声 {0}個",
|
"음성 {0}개": "音声 {0}個",
|
||||||
"음성 선택": "音声を選択",
|
"음성 선택": "音声を選択",
|
||||||
|
|
@ -871,12 +881,15 @@
|
||||||
"트랙 {0}": "トラック {0}",
|
"트랙 {0}": "トラック {0}",
|
||||||
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "トレイアイコンを押すか、下のショートカットで再表示します",
|
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "トレイアイコンを押すか、下のショートカットで再表示します",
|
||||||
"트레이 정리": "トレイの後片付け",
|
"트레이 정리": "トレイの後片付け",
|
||||||
|
"파일": "ファイル",
|
||||||
"파일 관리자에서 보기": "ファイルマネージャーで表示",
|
"파일 관리자에서 보기": "ファイルマネージャーで表示",
|
||||||
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "ファイルアプリの「アプリで開く」・共有で JJ_MKVMaker を選んだとき",
|
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "ファイルアプリの「アプリで開く」・共有で JJ_MKVMaker を選んだとき",
|
||||||
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "ファイルの場所がわからないため再生のみ行います。MKV を作成するには「動画を追加」で選んでください。",
|
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "ファイルの場所がわからないため再生のみ行います。MKV を作成するには「動画を追加」で選んでください。",
|
||||||
"파일 이름": "ファイル名",
|
"파일 이름": "ファイル名",
|
||||||
"파일 탐색기": "ファイルエクスプローラー",
|
"파일 탐색기": "ファイルエクスプローラー",
|
||||||
"파일 탐색기 (지금 여기)": "ファイルエクスプローラー (現在地)",
|
"파일 탐색기 (지금 여기)": "ファイルエクスプローラー (現在地)",
|
||||||
|
"파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "ファイルエクスプローラー上部の ⋮ メニューからも変えられます (左右 · 上下 · ボタン列の位置 · ボタン構成)",
|
||||||
|
"파일 폴더": "ファイル フォルダー",
|
||||||
"파일 확인 중…": "ファイルを確認中…",
|
"파일 확인 중…": "ファイルを確認中…",
|
||||||
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
|
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
|
||||||
"팝업 끄기": "ポップアップを閉じる",
|
"팝업 끄기": "ポップアップを閉じる",
|
||||||
|
|
@ -891,6 +904,7 @@
|
||||||
"포르투갈어": "ポルトガル語",
|
"포르투갈어": "ポルトガル語",
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)",
|
||||||
"폴더": "フォルダー",
|
"폴더": "フォルダー",
|
||||||
|
"폴더 + 파일 목록": "フォルダー + ファイル一覧",
|
||||||
"폴더 만들기": "フォルダーを作成",
|
"폴더 만들기": "フォルダーを作成",
|
||||||
"폴더 삭제: {0} (안의 {1}개 포함)": "フォルダーを削除: {0}(中の {1}個を含む)",
|
"폴더 삭제: {0} (안의 {1}개 포함)": "フォルダーを削除: {0}(中の {1}個を含む)",
|
||||||
"폴더 선택": "フォルダーを選択",
|
"폴더 선택": "フォルダーを選択",
|
||||||
|
|
@ -926,6 +940,8 @@
|
||||||
"필수 프로그램 점검": "必須プログラムの点検",
|
"필수 프로그램 점검": "必須プログラムの点検",
|
||||||
"필수 프로그램이 모두 설치되어 있습니다.": "必須プログラムはすべてインストールされています。",
|
"필수 프로그램이 모두 설치되어 있습니다.": "必須プログラムはすべてインストールされています。",
|
||||||
"하위 폴더가 없습니다": "サブフォルダーがありません",
|
"하위 폴더가 없습니다": "サブフォルダーがありません",
|
||||||
|
"한 번: 바로 실행": "1 回: すぐに開く",
|
||||||
|
"한 번: 선택 · 두 번: 실행 (기본)": "1 回: 選択 · 2 回: 開く (既定)",
|
||||||
"한 창": "1 ペイン",
|
"한 창": "1 ペイン",
|
||||||
"한국어": "韓国語",
|
"한국어": "韓国語",
|
||||||
"한글": "ハングル",
|
"한글": "ハングル",
|
||||||
|
|
@ -954,6 +970,7 @@
|
||||||
"화질": "画質",
|
"화질": "画質",
|
||||||
"확인": "OK",
|
"확인": "OK",
|
||||||
"확인 · 압축 푸는 중…": "確認・展開中…",
|
"확인 · 압축 푸는 중…": "確認・展開中…",
|
||||||
|
"확장자": "拡張子",
|
||||||
"확장자별 재생 프로그램": "拡張子ごとの再生プログラム",
|
"확장자별 재생 프로그램": "拡張子ごとの再生プログラム",
|
||||||
"환경 설정": "環境設定",
|
"환경 설정": "環境設定",
|
||||||
"회전": "回転",
|
"회전": "回転",
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@
|
||||||
"Whisper small (정확)": "Whisper small(准确)",
|
"Whisper small (정확)": "Whisper small(准确)",
|
||||||
"Windows 기본 브라우저": "Windows 默认浏览器",
|
"Windows 기본 브라우저": "Windows 默认浏览器",
|
||||||
"Windows 기본 프로그램": "Windows 默认程序",
|
"Windows 기본 프로그램": "Windows 默认程序",
|
||||||
|
"Windows 탐색기": "Windows 资源管理器",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp,磁力链接 · .torrent → aria2(完成后不做种)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp,磁力链接 · .torrent → aria2(完成后不做种)",
|
||||||
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube 要求进行机器人验证。请在 设置 > 下载 > YouTube Cookie 中指定浏览器(推荐 Firefox)或 cookies.txt 后重新下载。",
|
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube 要求进行机器人验证。请在 设置 > 下载 > YouTube Cookie 中指定浏览器(推荐 Firefox)或 cookies.txt 后重新下载。",
|
||||||
"YouTube 광고 배너 숨기기": "隐藏 YouTube 广告横幅",
|
"YouTube 광고 배너 숨기기": "隐藏 YouTube 广告横幅",
|
||||||
|
|
@ -165,6 +166,7 @@
|
||||||
"{0} 에서 받을 파일을 찾지 못했습니다": "未在 {0} 找到要下载的文件",
|
"{0} 에서 받을 파일을 찾지 못했습니다": "未在 {0} 找到要下载的文件",
|
||||||
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。",
|
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。",
|
||||||
"{0} 추가": "添加 {0}",
|
"{0} 추가": "添加 {0}",
|
||||||
|
"{0} 파일": "{0} 文件",
|
||||||
"{0}. {1} (대기)": "{0}. {1}(等待)",
|
"{0}. {1} (대기)": "{0}. {1}(等待)",
|
||||||
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: 将此视频移到 {1}",
|
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: 将此视频移到 {1}",
|
||||||
"{0}: 이미 이동 폴더에 있습니다": "{0}: 已在移动文件夹中",
|
"{0}: 이미 이동 폴더에 있습니다": "{0}: 已在移动文件夹中",
|
||||||
|
|
@ -240,6 +242,7 @@
|
||||||
"기본: MP4. 음성만 받으면 MP3 · M4A 로 저장": "默认: MP4。仅音频时保存为 MP3 · M4A",
|
"기본: MP4. 음성만 받으면 MP3 · M4A 로 저장": "默认: MP4。仅音频时保存为 MP3 · M4A",
|
||||||
"기본값": "默认值",
|
"기본값": "默认值",
|
||||||
"기타 즐겨찾기": "其他书签",
|
"기타 즐겨찾기": "其他书签",
|
||||||
|
"길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). Ctrl + 클릭: 여러 개 고르기": "长按 · 右键总是打开功能菜单 (复制 · 移动 · 删除 · 重命名 …)。Ctrl + 点击: 多选",
|
||||||
"길이 {0}": "时长 {0}",
|
"길이 {0}": "时长 {0}",
|
||||||
"끄기": "关闭",
|
"끄기": "关闭",
|
||||||
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "关闭后将直接使用上次的设置开始: 原语言 {0},语言 {1},{2}",
|
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "关闭后将直接使用上次的设置开始: 原语言 {0},语言 {1},{2}",
|
||||||
|
|
@ -284,6 +287,7 @@
|
||||||
"노랑": "黄色",
|
"노랑": "黄色",
|
||||||
"노르웨이어": "挪威语",
|
"노르웨이어": "挪威语",
|
||||||
"누르고 있으면 조정 전": "按住查看调整前",
|
"누르고 있으면 조정 전": "按住查看调整前",
|
||||||
|
"누르기": "点按",
|
||||||
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "下载完成后立即放入制作 MKV 的视频列表,并从下载列表移除(与下载界面的复选框相同)",
|
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "下载完成后立即放入制作 MKV 的视频列表,并从下载列表移除(与下载界面的复选框相同)",
|
||||||
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "下载完成后自动加入制作 MKV 的视频列表,并从此下载列表移除(已下载的文件保留)",
|
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "下载完成后自动加入制作 MKV 的视频列表,并从此下载列表移除(已下载的文件保留)",
|
||||||
"다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签",
|
"다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签",
|
||||||
|
|
@ -450,6 +454,7 @@
|
||||||
"비밀번호 (선택)": "密码(可选)",
|
"비밀번호 (선택)": "密码(可选)",
|
||||||
"비어 있는 폴더": "空文件夹",
|
"비어 있는 폴더": "空文件夹",
|
||||||
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "这是空文件夹。\n请点击上方的[添加书签] · [新建文件夹],或将书签拖到左侧文件夹。",
|
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "这是空文件夹。\n请点击上方的[添加书签] · [新建文件夹],或将书签拖到左侧文件夹。",
|
||||||
|
"비어 있음": "空",
|
||||||
"사용 안 함": "不使用",
|
"사용 안 함": "不使用",
|
||||||
"사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "不使用。如出现“Sign in to confirm you're not a bot”错误请设置",
|
"사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "不使用。如出现“Sign in to confirm you're not a bot”错误请设置",
|
||||||
"사용자가 취소했습니다.": "用户已取消。",
|
"사용자가 취소했습니다.": "用户已取消。",
|
||||||
|
|
@ -499,6 +504,7 @@
|
||||||
"세로 고정": "锁定竖屏",
|
"세로 고정": "锁定竖屏",
|
||||||
"수정": "编辑",
|
"수정": "编辑",
|
||||||
"수정 · 이동": "编辑 · 移动",
|
"수정 · 이동": "编辑 · 移动",
|
||||||
|
"수정한 날짜": "修改日期",
|
||||||
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "即使隐藏,也可以在顶部 ⋮ 菜单中更改窗格布局 · 按钮配置。",
|
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "即使隐藏,也可以在顶部 ⋮ 菜单中更改窗格布局 · 按钮配置。",
|
||||||
"숨김": "隐藏",
|
"숨김": "隐藏",
|
||||||
"숨은 항목 표시": "显示隐藏项目",
|
"숨은 항목 표시": "显示隐藏项目",
|
||||||
|
|
@ -506,6 +512,7 @@
|
||||||
"숨은 항목을 보입니다.": "已显示隐藏项目。",
|
"숨은 항목을 보입니다.": "已显示隐藏项目。",
|
||||||
"쉬는 중": "空闲",
|
"쉬는 중": "空闲",
|
||||||
"스웨덴어": "瑞典语",
|
"스웨덴어": "瑞典语",
|
||||||
|
"스타일": "样式",
|
||||||
"스페인어": "西班牙语",
|
"스페인어": "西班牙语",
|
||||||
"시간 형식 오류 {0}곳": "{0} 处时间格式错误",
|
"시간 형식 오류 {0}곳": "{0} 处时间格式错误",
|
||||||
"시간순 정렬": "按时间排序",
|
"시간순 정렬": "按时间排序",
|
||||||
|
|
@ -531,6 +538,7 @@
|
||||||
"아래에 줄 추가": "在下方添加一行",
|
"아래에 줄 추가": "在下方添加一行",
|
||||||
"아이디 (선택)": "用户名(可选)",
|
"아이디 (선택)": "用户名(可选)",
|
||||||
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "可在设置中填写用户名 · 密码(可选,可增加每日下载次数)。",
|
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "可在设置中填写用户名 · 密码(可选,可增加每日下载次数)。",
|
||||||
|
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "图标 · 列表外观: X-plore (缩略图 · 两行) · Windows 资源管理器 (彩色图标 · 列) · Total Commander (紧凑列表 · [文件夹])",
|
||||||
"아직 만든 MKV · 자막이 없습니다: {0}": "尚未制作 MKV · 字幕: {0}",
|
"아직 만든 MKV · 자막이 없습니다: {0}": "尚未制作 MKV · 字幕: {0}",
|
||||||
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "暂无。视频下载完成后会自动加入(可在设置中更改)。",
|
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "暂无。视频下载完成后会自动加入(可在设置中更改)。",
|
||||||
"아직 지원하지 않는 플랫폼입니다: {0}": "尚不支持此平台: {0}",
|
"아직 지원하지 않는 플랫폼입니다: {0}": "尚不支持此平台: {0}",
|
||||||
|
|
@ -585,6 +593,7 @@
|
||||||
"왼쪽": "左侧",
|
"왼쪽": "左侧",
|
||||||
"왼쪽 90°": "向左 90°",
|
"왼쪽 90°": "向左 90°",
|
||||||
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "用左侧手柄调整顺序 · 将图标拖到左侧文件夹或列表中的文件夹即可移入 · 右键菜单",
|
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "用左侧手柄调整顺序 · 将图标拖到左侧文件夹或列表中的文件夹即可移入 · 右键菜单",
|
||||||
|
"왼쪽에 폴더, 오른쪽에 왼쪽에서 고른 폴더의 파일들을 보여 줍니다 (Windows 탐색기처럼).": "左侧显示文件夹,右侧显示左侧所选文件夹中的文件 (类似 Windows 资源管理器)。",
|
||||||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
|
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
|
||||||
"우르두어": "乌尔都语",
|
"우르두어": "乌尔都语",
|
||||||
"우크라이나어": "乌克兰语",
|
"우크라이나어": "乌克兰语",
|
||||||
|
|
@ -607,6 +616,7 @@
|
||||||
"위 폴더": "上级文件夹",
|
"위 폴더": "上级文件夹",
|
||||||
"위로": "向上",
|
"위로": "向上",
|
||||||
"위아래": "上下",
|
"위아래": "上下",
|
||||||
|
"유형": "类型",
|
||||||
"음성": "音频",
|
"음성": "音频",
|
||||||
"음성 {0}개": "音频 {0} 个",
|
"음성 {0}개": "音频 {0} 个",
|
||||||
"음성 선택": "选择音轨",
|
"음성 선택": "选择音轨",
|
||||||
|
|
@ -871,12 +881,15 @@
|
||||||
"트랙 {0}": "轨道 {0}",
|
"트랙 {0}": "轨道 {0}",
|
||||||
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "点击托盘图标或用下方快捷键重新打开",
|
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "点击托盘图标或用下方快捷键重新打开",
|
||||||
"트레이 정리": "清理托盘",
|
"트레이 정리": "清理托盘",
|
||||||
|
"파일": "文件",
|
||||||
"파일 관리자에서 보기": "在文件管理器中显示",
|
"파일 관리자에서 보기": "在文件管理器中显示",
|
||||||
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "在文件应用的“打开方式” · 分享中选择 JJ_MKVMaker 时",
|
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "在文件应用的“打开方式” · 分享中选择 JJ_MKVMaker 时",
|
||||||
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "无法确定文件位置,因此仅播放。要制作 MKV,请用“添加视频”选择。",
|
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "无法确定文件位置,因此仅播放。要制作 MKV,请用“添加视频”选择。",
|
||||||
"파일 이름": "文件名",
|
"파일 이름": "文件名",
|
||||||
"파일 탐색기": "文件浏览器",
|
"파일 탐색기": "文件浏览器",
|
||||||
"파일 탐색기 (지금 여기)": "文件浏览器 (当前位置)",
|
"파일 탐색기 (지금 여기)": "文件浏览器 (当前位置)",
|
||||||
|
"파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "也可以在文件浏览器顶部的 ⋮ 菜单中更改 (左右 · 上下 · 按钮栏位置 · 按钮配置)",
|
||||||
|
"파일 폴더": "文件夹",
|
||||||
"파일 확인 중…": "正在检查文件…",
|
"파일 확인 중…": "正在检查文件…",
|
||||||
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
|
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
|
||||||
"팝업 끄기": "关闭弹出窗口",
|
"팝업 끄기": "关闭弹出窗口",
|
||||||
|
|
@ -891,6 +904,7 @@
|
||||||
"포르투갈어": "葡萄牙语",
|
"포르투갈어": "葡萄牙语",
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)",
|
||||||
"폴더": "文件夹",
|
"폴더": "文件夹",
|
||||||
|
"폴더 + 파일 목록": "文件夹 + 文件列表",
|
||||||
"폴더 만들기": "新建文件夹",
|
"폴더 만들기": "新建文件夹",
|
||||||
"폴더 삭제: {0} (안의 {1}개 포함)": "已删除文件夹: {0}(含其中 {1} 个)",
|
"폴더 삭제: {0} (안의 {1}개 포함)": "已删除文件夹: {0}(含其中 {1} 个)",
|
||||||
"폴더 선택": "选择文件夹",
|
"폴더 선택": "选择文件夹",
|
||||||
|
|
@ -926,6 +940,8 @@
|
||||||
"필수 프로그램 점검": "检查必需程序",
|
"필수 프로그램 점검": "检查必需程序",
|
||||||
"필수 프로그램이 모두 설치되어 있습니다.": "所有必需程序均已安装。",
|
"필수 프로그램이 모두 설치되어 있습니다.": "所有必需程序均已安装。",
|
||||||
"하위 폴더가 없습니다": "没有子文件夹",
|
"하위 폴더가 없습니다": "没有子文件夹",
|
||||||
|
"한 번: 바로 실행": "单击: 立即打开",
|
||||||
|
"한 번: 선택 · 두 번: 실행 (기본)": "单击: 选择 · 双击: 打开 (默认)",
|
||||||
"한 창": "单窗格",
|
"한 창": "单窗格",
|
||||||
"한국어": "韩语",
|
"한국어": "韩语",
|
||||||
"한글": "韩文",
|
"한글": "韩文",
|
||||||
|
|
@ -954,6 +970,7 @@
|
||||||
"화질": "画质",
|
"화질": "画质",
|
||||||
"확인": "确定",
|
"확인": "确定",
|
||||||
"확인 · 압축 푸는 중…": "正在校验 · 解压…",
|
"확인 · 압축 푸는 중…": "正在校验 · 解压…",
|
||||||
|
"확장자": "扩展名",
|
||||||
"확장자별 재생 프로그램": "按扩展名设置播放程序",
|
"확장자별 재생 프로그램": "按扩展名设置播放程序",
|
||||||
"환경 설정": "设置",
|
"환경 설정": "设置",
|
||||||
"회전": "旋转",
|
"회전": "旋转",
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,16 @@ void main() {
|
||||||
final c = AppController(PlatformServices.create());
|
final c = AppController(PlatformServices.create());
|
||||||
c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path];
|
c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path];
|
||||||
final key = GlobalKey();
|
final key = GlobalKey();
|
||||||
for (final (name, size, orient) in [
|
for (final (name, size, layout, style) in [
|
||||||
('wide', const Size(1600, 900), 'auto'),
|
('wide', const Size(1600, 900), 'dual', 'xplore'),
|
||||||
('tall', const Size(900, 1400), 'auto'),
|
('tall', const Size(900, 1400), 'dual', 'xplore'),
|
||||||
|
('windows', const Size(1600, 900), 'dual', 'windows'),
|
||||||
|
('totalcmd', const Size(1600, 900), 'dual', 'totalcmd'),
|
||||||
|
('split', const Size(1600, 900), 'split', 'windows'),
|
||||||
]) {
|
]) {
|
||||||
c.settings.explorerOrientation = orient;
|
c.settings
|
||||||
|
..explorerLayout = layout
|
||||||
|
..explorerStyle = style;
|
||||||
tester.view.physicalSize = size;
|
tester.view.physicalSize = size;
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(
|
||||||
|
|
|
||||||
|
|
@ -134,9 +134,16 @@ class AppSettings {
|
||||||
bool webJavaScript = true;
|
bool webJavaScript = true;
|
||||||
|
|
||||||
// ── 파일 탐색기 (X-plore 참고) ──
|
// ── 파일 탐색기 (X-plore 참고) ──
|
||||||
/// 창: 'dual' 두 창 (기본) · 'single' 한 창
|
/// 창: 'dual' 두 창 (기본) · 'split' 왼쪽 폴더 트리 + 오른쪽 그 폴더의 파일 목록 · 'single' 한 창
|
||||||
String explorerLayout = 'dual';
|
String explorerLayout = 'dual';
|
||||||
|
|
||||||
|
/// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본) / 'open' 한 번 = 바로 열기.
|
||||||
|
/// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴.
|
||||||
|
String explorerClick = 'select';
|
||||||
|
|
||||||
|
/// 모양: 'xplore' (기본) · 'windows' Windows 탐색기 · 'totalcmd' Total Commander
|
||||||
|
String explorerStyle = 'xplore';
|
||||||
|
|
||||||
/// 두 창 배치: 'auto' 화면 모양 따라 (가로로 넓으면 좌우, 세로로 길면 위아래) · 'side' 좌우 · 'stacked' 위아래
|
/// 두 창 배치: 'auto' 화면 모양 따라 (가로로 넓으면 좌우, 세로로 길면 위아래) · 'side' 좌우 · 'stacked' 위아래
|
||||||
String explorerOrientation = 'auto';
|
String explorerOrientation = 'auto';
|
||||||
|
|
||||||
|
|
@ -224,6 +231,8 @@ class AppSettings {
|
||||||
'webTranslate': webTranslate,
|
'webTranslate': webTranslate,
|
||||||
'webJavaScript': webJavaScript,
|
'webJavaScript': webJavaScript,
|
||||||
'explorerLayout': explorerLayout,
|
'explorerLayout': explorerLayout,
|
||||||
|
'explorerClick': explorerClick,
|
||||||
|
'explorerStyle': explorerStyle,
|
||||||
'explorerOrientation': explorerOrientation,
|
'explorerOrientation': explorerOrientation,
|
||||||
'explorerToolbar': explorerToolbar,
|
'explorerToolbar': explorerToolbar,
|
||||||
'explorerButtons': explorerButtons,
|
'explorerButtons': explorerButtons,
|
||||||
|
|
@ -306,7 +315,9 @@ class AppSettings {
|
||||||
..externalBrowser = j['externalBrowser'] as String? ?? 'system'
|
..externalBrowser = j['externalBrowser'] as String? ?? 'system'
|
||||||
..webTranslate = j['webTranslate'] as bool? ?? false
|
..webTranslate = j['webTranslate'] as bool? ?? false
|
||||||
..webJavaScript = j['webJavaScript'] as bool? ?? true
|
..webJavaScript = j['webJavaScript'] as bool? ?? true
|
||||||
..explorerLayout = j['explorerLayout'] == 'single' ? 'single' : 'dual'
|
..explorerLayout = const ['single', 'split'].contains(j['explorerLayout']) ? j['explorerLayout'] as String : 'dual'
|
||||||
|
..explorerClick = j['explorerClick'] == 'open' ? 'open' : 'select'
|
||||||
|
..explorerStyle = const ['windows', 'totalcmd'].contains(j['explorerStyle']) ? j['explorerStyle'] as String : 'xplore'
|
||||||
..explorerOrientation = const ['side', 'stacked'].contains(j['explorerOrientation'])
|
..explorerOrientation = const ['side', 'stacked'].contains(j['explorerOrientation'])
|
||||||
? j['explorerOrientation'] as String
|
? j['explorerOrientation'] as String
|
||||||
: 'auto'
|
: 'auto'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../core/file_ops.dart';
|
||||||
|
import '../core/playlist.dart' show isVideoFile;
|
||||||
|
import '../l10n/tr.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// 파일 탐색기 모양 (환경 설정 > 파일 탐색기 > 스타일)
|
||||||
|
///
|
||||||
|
/// - X-plore: 큰 아이콘 · 동영상 썸네일 · 두 줄 (이름 / 크기 · 날짜 · 해상도 · 길이)
|
||||||
|
/// - Windows 탐색기: 작은 컬러 아이콘 · 한 줄 · 열 (이름 · 수정한 날짜 · 유형 · 크기)
|
||||||
|
/// - Total Commander: 아주 작은 아이콘 · 촘촘한 한 줄 · 폴더는 [이름] · 열 (이름 · 확장자 · 크기 · 날짜), 크기 칸의 `<DIR>`
|
||||||
|
enum ExplorerStyle {
|
||||||
|
xplore('X-plore'),
|
||||||
|
windows('Windows 탐색기'),
|
||||||
|
totalcmd('Total Commander');
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
const ExplorerStyle(this.label);
|
||||||
|
|
||||||
|
static ExplorerStyle of(String name) => values.firstWhere((s) => s.name == name, orElse: () => xplore);
|
||||||
|
|
||||||
|
double get rowHeight => switch (this) { xplore => 52, windows => 34, totalcmd => 26 };
|
||||||
|
double get iconSize => switch (this) { xplore => 30, windows => 20, totalcmd => 16 };
|
||||||
|
double get indent => switch (this) { xplore => 16, windows => 14, totalcmd => 12 };
|
||||||
|
double get fontSize => switch (this) { xplore => 14, windows => 13, totalcmd => 12.5 };
|
||||||
|
|
||||||
|
/// 썸네일 · 두 줄 정보 (X-plore 만)
|
||||||
|
bool get rich => this == xplore;
|
||||||
|
|
||||||
|
/// 열 머리 (이름 · 날짜 · 유형 · 크기)
|
||||||
|
bool get columns => this != xplore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _images = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic'};
|
||||||
|
const _audio = {'mp3', 'm4a', 'flac', 'wav', 'ogg', 'aac', 'opus', 'wma'};
|
||||||
|
const _subs = {'srt', 'ass', 'ssa', 'smi', 'sami', 'vtt'};
|
||||||
|
const _archives = {'zip', '7z', 'rar', 'tar', 'gz', 'xz', 'bz2'};
|
||||||
|
const _texts = {'txt', 'log', 'md', 'json', 'xml', 'ini', 'csv', 'yaml', 'yml'};
|
||||||
|
const _docs = {'doc', 'docx', 'hwp', 'hwpx', 'odt', 'rtf'};
|
||||||
|
const _sheets = {'xls', 'xlsx', 'ods'};
|
||||||
|
const _slides = {'ppt', 'pptx', 'odp'};
|
||||||
|
const _programs = {'exe', 'msi', 'bat', 'cmd', 'ps1', 'apk'};
|
||||||
|
|
||||||
|
/// 파일 종류별 아이콘 · 색 (스타일마다 다르게)
|
||||||
|
(IconData, Color) fileIcon(ExplorerStyle style, FileEntry e, {bool open = false}) {
|
||||||
|
final x = e.ext;
|
||||||
|
switch (style) {
|
||||||
|
case ExplorerStyle.xplore:
|
||||||
|
if (e.isDir) return (open ? Icons.folder_open : Icons.folder, Colors.amber);
|
||||||
|
return (
|
||||||
|
switch (x) {
|
||||||
|
_ when _subs.contains(x) => Icons.subtitles_outlined,
|
||||||
|
_ when _audio.contains(x) => Icons.audiotrack_outlined,
|
||||||
|
'pdf' => Icons.picture_as_pdf_outlined,
|
||||||
|
_ when _archives.contains(x) => Icons.folder_zip_outlined,
|
||||||
|
'apk' => Icons.android,
|
||||||
|
_ when _texts.contains(x) => Icons.description_outlined,
|
||||||
|
_ when _images.contains(x) => Icons.image_outlined,
|
||||||
|
_ when isVideoFile(e.path) => Icons.movie_outlined,
|
||||||
|
_ => Icons.insert_drive_file_outlined,
|
||||||
|
},
|
||||||
|
JjColors.textDim
|
||||||
|
);
|
||||||
|
case ExplorerStyle.windows:
|
||||||
|
// Windows 11 탐색기 느낌: 노란 폴더, 종류마다 색
|
||||||
|
if (e.isDir) return (open ? Icons.folder_open : Icons.folder, const Color(0xFFFFC83D));
|
||||||
|
if (isVideoFile(e.path)) return (Icons.video_file, const Color(0xFF5B9BD5));
|
||||||
|
if (_images.contains(x)) return (Icons.image, const Color(0xFF3FB3A8));
|
||||||
|
if (_audio.contains(x)) return (Icons.audio_file, const Color(0xFFB57EDC));
|
||||||
|
if (_subs.contains(x)) return (Icons.closed_caption, const Color(0xFF9FA8DA));
|
||||||
|
if (x == 'pdf') return (Icons.picture_as_pdf, const Color(0xFFE5534B));
|
||||||
|
if (_archives.contains(x)) return (Icons.folder_zip, const Color(0xFFD8A657));
|
||||||
|
if (_docs.contains(x)) return (Icons.article, const Color(0xFF4F8EDC));
|
||||||
|
if (_sheets.contains(x)) return (Icons.table_chart, const Color(0xFF4CAF7A));
|
||||||
|
if (_slides.contains(x)) return (Icons.slideshow, const Color(0xFFE8804F));
|
||||||
|
if (_programs.contains(x)) return (x == 'apk' ? Icons.android : Icons.terminal, const Color(0xFF8BC34A));
|
||||||
|
if (_texts.contains(x)) return (Icons.description, const Color(0xFFB0BEC5));
|
||||||
|
return (Icons.insert_drive_file, const Color(0xFFB0BEC5));
|
||||||
|
case ExplorerStyle.totalcmd:
|
||||||
|
// Total Commander 느낌: 단순한 작은 아이콘, 폴더는 노랑, 실행 파일은 초록
|
||||||
|
if (e.isDir) return (Icons.folder, const Color(0xFFE8C547));
|
||||||
|
if (_programs.contains(x)) return (Icons.settings_applications, const Color(0xFF7CB342));
|
||||||
|
if (isVideoFile(e.path)) return (Icons.movie, const Color(0xFF64B5F6));
|
||||||
|
if (_images.contains(x)) return (Icons.image, const Color(0xFF64B5F6));
|
||||||
|
if (_archives.contains(x)) return (Icons.inventory_2, const Color(0xFFBCAAA4));
|
||||||
|
return (Icons.description, const Color(0xFFCFD8DC));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows 탐색기의 "유형" 칸: "파일 폴더" · "MP4 파일"
|
||||||
|
String typeLabel(FileEntry e) => e.isDir
|
||||||
|
? tr('파일 폴더')
|
||||||
|
: e.ext.isEmpty
|
||||||
|
? tr('파일')
|
||||||
|
: trf('{0} 파일', [e.ext.toUpperCase()]);
|
||||||
|
|
||||||
|
/// Total Commander 의 이름 칸: 폴더는 [이름], 파일은 확장자를 뺀 이름 (확장자는 따로)
|
||||||
|
String totalCmdName(FileEntry e) {
|
||||||
|
if (e.isDir) return '[${e.name}]';
|
||||||
|
final n = e.name;
|
||||||
|
final dot = n.lastIndexOf('.');
|
||||||
|
return dot > 0 ? n.substring(0, dot) : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 열 머리 (Windows 탐색기 · Total Commander)
|
||||||
|
class ExplorerColumnsHeader extends StatelessWidget {
|
||||||
|
final ExplorerStyle style;
|
||||||
|
const ExplorerColumnsHeader({super.key, required this.style});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const st = TextStyle(fontSize: 11.5, color: JjColors.textDim);
|
||||||
|
Widget cell(String t, int flex, {TextAlign align = TextAlign.start}) =>
|
||||||
|
Expanded(flex: flex, child: Text(t, style: st, textAlign: align, maxLines: 1, overflow: TextOverflow.clip));
|
||||||
|
return Container(
|
||||||
|
height: 24,
|
||||||
|
padding: const EdgeInsets.only(left: 10, right: 40),
|
||||||
|
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: JjColors.border))),
|
||||||
|
child: Row(
|
||||||
|
children: style == ExplorerStyle.windows
|
||||||
|
? [
|
||||||
|
cell(tr('이름'), 6),
|
||||||
|
cell(tr('수정한 날짜'), 3),
|
||||||
|
cell(tr('유형'), 2),
|
||||||
|
cell(tr('크기'), 2, align: TextAlign.end),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
cell(tr('이름'), 6),
|
||||||
|
cell(tr('확장자'), 1),
|
||||||
|
cell(tr('크기'), 2, align: TextAlign.end),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
cell(tr('날짜'), 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import '../core/file_ops.dart';
|
||||||
import '../core/playlist.dart' show isVideoFile;
|
import '../core/playlist.dart' show isVideoFile;
|
||||||
import '../platform/android/android_storage.dart';
|
import '../platform/android/android_storage.dart';
|
||||||
import 'app_actions.dart';
|
import 'app_actions.dart';
|
||||||
|
import 'explorer_look.dart';
|
||||||
import 'player_page.dart';
|
import 'player_page.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
|
|
@ -97,6 +98,9 @@ class _Pane extends ChangeNotifier {
|
||||||
/// 목록 스크롤 (폴더로 이동하면 그 폴더가 보이게)
|
/// 목록 스크롤 (폴더로 이동하면 그 폴더가 보이게)
|
||||||
final scroll = ScrollController();
|
final scroll = ScrollController();
|
||||||
|
|
||||||
|
/// "폴더 + 파일 목록" 배치의 오른쪽 파일 목록 스크롤
|
||||||
|
final listScroll = ScrollController();
|
||||||
|
|
||||||
_Pane(this.root) : current = root;
|
_Pane(this.root) : current = root;
|
||||||
|
|
||||||
void changed() => notifyListeners();
|
void changed() => notifyListeners();
|
||||||
|
|
@ -104,16 +108,20 @@ class _Pane extends ChangeNotifier {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
scroll.dispose();
|
scroll.dispose();
|
||||||
|
listScroll.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 트리 목록의 한 줄
|
/// 트리 · 목록의 한 줄
|
||||||
class _Row {
|
class _Row {
|
||||||
final FileEntry entry;
|
final FileEntry entry;
|
||||||
final int depth;
|
final int depth;
|
||||||
final bool isRoot;
|
final bool isRoot;
|
||||||
const _Row(this.entry, this.depth, {this.isRoot = false});
|
|
||||||
|
/// 파일 목록 맨 위의 ".." (상위 폴더로)
|
||||||
|
final bool isUp;
|
||||||
|
const _Row(this.entry, this.depth, {this.isRoot = false, this.isUp = false});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ExplorerPageState extends State<ExplorerPage> {
|
class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
@ -228,21 +236,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
..current = dir
|
..current = dir
|
||||||
..focused = dir;
|
..focused = dir;
|
||||||
pane.changed();
|
pane.changed();
|
||||||
|
if (pane.listScroll.hasClients) pane.listScroll.jumpTo(0);
|
||||||
_reveal(pane, dir);
|
_reveal(pane, dir);
|
||||||
if (remember) _remember(pane, dir);
|
if (remember) _remember(pane, dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 목록 줄 높이
|
|
||||||
static const _rowHeight = 52.0;
|
|
||||||
|
|
||||||
/// 그 항목이 목록 위쪽 1/4 쯤에 보이게 스크롤 (깊은 폴더로 가면 위쪽 폴더들의 다른 항목에 밀려 안 보이므로)
|
/// 그 항목이 목록 위쪽 1/4 쯤에 보이게 스크롤 (깊은 폴더로 가면 위쪽 폴더들의 다른 항목에 밀려 안 보이므로)
|
||||||
void _reveal(_Pane pane, String path) {
|
void _reveal(_Pane pane, String path) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted || !pane.scroll.hasClients) return;
|
if (!mounted || !pane.scroll.hasClients) return;
|
||||||
final i = _rows(pane).indexWhere((r) => samePath(r.entry.path, path));
|
final i = _rows(pane, foldersOnly: _split).indexWhere((r) => samePath(r.entry.path, path));
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
final pos = pane.scroll.position;
|
final pos = pane.scroll.position;
|
||||||
final want = (i * _rowHeight - pos.viewportDimension / 4).clamp(0.0, pos.maxScrollExtent);
|
final want = (i * _look.rowHeight - pos.viewportDimension / 4).clamp(0.0, pos.maxScrollExtent);
|
||||||
pane.scroll.jumpTo(want);
|
pane.scroll.jumpTo(want);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -258,13 +264,24 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
if (i >= 0) _active = i;
|
if (i >= 0) _active = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<_Row> _rows(_Pane pane) {
|
/// 지금 모양 (환경 설정 > 파일 탐색기 > 스타일)
|
||||||
|
ExplorerStyle get _look => ExplorerStyle.of(c.settings.explorerStyle);
|
||||||
|
|
||||||
|
/// 왼쪽 폴더 트리 + 오른쪽 파일 목록 배치
|
||||||
|
bool get _split => c.settings.explorerLayout == 'split';
|
||||||
|
|
||||||
|
/// 한 번 누르면 선택 · 두 번 누르면 열기 (아니면 한 번에 바로 열기)
|
||||||
|
bool get _selectMode => c.settings.explorerClick != 'open';
|
||||||
|
|
||||||
|
/// 트리 줄. [foldersOnly]: 폴더만 ("폴더 + 파일 목록" 배치의 왼쪽)
|
||||||
|
List<_Row> _rows(_Pane pane, {bool foldersOnly = false}) {
|
||||||
final out = <_Row>[];
|
final out = <_Row>[];
|
||||||
final rootEntry = FileEntry(pane.root, isDir: true, modified: DateTime(0));
|
final rootEntry = FileEntry(pane.root, isDir: true, modified: DateTime(0));
|
||||||
out.add(_Row(rootEntry, 0, isRoot: true));
|
out.add(_Row(rootEntry, 0, isRoot: true));
|
||||||
void walk(String dir, int depth) {
|
void walk(String dir, int depth) {
|
||||||
if (!pane.expanded.contains(dir)) return;
|
if (!pane.expanded.contains(dir)) return;
|
||||||
for (final e in pane.cache[dir] ?? const <FileEntry>[]) {
|
for (final e in pane.cache[dir] ?? const <FileEntry>[]) {
|
||||||
|
if (foldersOnly && !e.isDir) continue;
|
||||||
out.add(_Row(e, depth));
|
out.add(_Row(e, depth));
|
||||||
if (e.isDir) walk(e.path, depth + 1);
|
if (e.isDir) walk(e.path, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
@ -276,28 +293,73 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
// ───────── 누르기 ─────────
|
// ───────── 누르기 ─────────
|
||||||
|
|
||||||
Future<void> _tap(_Pane pane, FileEntry e) async {
|
/// 한 번 누르기.
|
||||||
|
/// - 선택 (기본): 고르기만. 폴더는 그 폴더가 "지금 폴더" (오른쪽 파일 목록 · 새 폴더 등의 기준).
|
||||||
|
/// - 바로 열기: 열기 ([_onOpen]).
|
||||||
|
/// Ctrl 을 누른 채면 표시 (여러 개 고르기). 목록의 ".." 은 늘 상위 폴더로.
|
||||||
|
Future<void> _onTap(_Pane pane, _Row row, {bool list = false}) async {
|
||||||
|
final e = row.entry;
|
||||||
setState(() => _active = _panes.indexOf(pane));
|
setState(() => _active = _panes.indexOf(pane));
|
||||||
pane.focused = e.path;
|
if (row.isUp) {
|
||||||
if (e.isDir) {
|
await _goTo(pane, e.path);
|
||||||
if (pane.expanded.contains(e.path) && samePath(pane.current, e.path)) {
|
|
||||||
pane.expanded.remove(e.path); // 지금 폴더를 다시 누르면 접기
|
|
||||||
pane.current = p.dirname(e.path).length < pane.root.length ? pane.root : p.dirname(e.path);
|
|
||||||
pane.changed();
|
|
||||||
} else {
|
|
||||||
pane
|
|
||||||
..expanded.add(e.path)
|
|
||||||
..current = e.path;
|
|
||||||
await _load(pane, e.path);
|
|
||||||
_remember(pane, e.path);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pane.current = p.dirname(e.path);
|
if (HardwareKeyboard.instance.isControlPressed && !row.isRoot) {
|
||||||
|
setState(() => pane.marked.contains(e.path) ? pane.marked.remove(e.path) : pane.marked.add(e.path));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_selectMode) return _onOpen(pane, row, list: list);
|
||||||
|
pane.focused = e.path;
|
||||||
|
if (e.isDir && !list) {
|
||||||
|
pane.current = e.path;
|
||||||
|
pane.changed();
|
||||||
|
await _load(pane, e.path);
|
||||||
|
if (pane.listScroll.hasClients) pane.listScroll.jumpTo(0);
|
||||||
|
_remember(pane, e.path);
|
||||||
|
} else {
|
||||||
|
if (!list) pane.current = p.dirname(e.path);
|
||||||
|
pane.changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 열기 (선택 모드는 두 번 누르기, 바로 열기 모드는 한 번 누르기):
|
||||||
|
/// 트리의 폴더는 펼치기 · 접기, 목록의 폴더는 들어가기 (왼쪽 트리도 따라감), 파일은 실행.
|
||||||
|
Future<void> _onOpen(_Pane pane, _Row row, {bool list = false}) async {
|
||||||
|
final e = row.entry;
|
||||||
|
setState(() => _active = _panes.indexOf(pane));
|
||||||
|
pane.focused = e.path;
|
||||||
|
if (row.isUp || (e.isDir && list)) {
|
||||||
|
await _goTo(pane, e.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.isDir) {
|
||||||
|
await _toggle(pane, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!list) pane.current = p.dirname(e.path);
|
||||||
pane.changed();
|
pane.changed();
|
||||||
await _open(e.path);
|
await _open(e.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 트리 폴더 펼치기 · 접기 (› 화살표 · 열기)
|
||||||
|
Future<void> _toggle(_Pane pane, FileEntry e) async {
|
||||||
|
setState(() => _active = _panes.indexOf(pane));
|
||||||
|
if (pane.expanded.contains(e.path)) {
|
||||||
|
pane.expanded.remove(e.path);
|
||||||
|
// 접은 폴더 안이 지금 폴더였으면 접은 폴더로
|
||||||
|
if (isSameOrInside(pane.current, e.path)) pane.current = e.path;
|
||||||
|
pane.changed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pane
|
||||||
|
..expanded.add(e.path)
|
||||||
|
..current = e.path
|
||||||
|
..focused = e.path;
|
||||||
|
await _load(pane, e.path);
|
||||||
|
if (pane.listScroll.hasClients) pane.listScroll.jumpTo(0);
|
||||||
|
_remember(pane, e.path);
|
||||||
|
}
|
||||||
|
|
||||||
/// 파일 열기 (기본 동작): 동영상은 내장 플레이어 (또는 확장자별 프로그램), 그 밖은 기본 연결 프로그램
|
/// 파일 열기 (기본 동작): 동영상은 내장 플레이어 (또는 확장자별 프로그램), 그 밖은 기본 연결 프로그램
|
||||||
Future<void> _open(String path) async {
|
Future<void> _open(String path) async {
|
||||||
if (isVideoFile(path)) {
|
if (isVideoFile(path)) {
|
||||||
|
|
@ -762,15 +824,25 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 420,
|
width: 420,
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
group(tr('창'), layout, [('dual', tr('두 창')), ('single', tr('한 창'))], (v) => layout = v),
|
group(tr('창'), layout, [
|
||||||
if (layout == 'dual')
|
('dual', tr('두 창')),
|
||||||
|
('split', tr('폴더 + 파일 목록')),
|
||||||
|
('single', tr('한 창')),
|
||||||
|
], (v) => layout = v),
|
||||||
|
if (layout == 'split')
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 6, left: 4),
|
||||||
|
child: Text(tr('왼쪽에 폴더, 오른쪽에 왼쪽에서 고른 폴더의 파일들을 보여 줍니다 (Windows 탐색기처럼).'),
|
||||||
|
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
|
),
|
||||||
|
if (layout != 'single')
|
||||||
group(tr('두 창 배치'), orient, [
|
group(tr('두 창 배치'), orient, [
|
||||||
('auto', tr('화면 모양 따라')),
|
('auto', tr('화면 모양 따라')),
|
||||||
('side', tr('좌우')),
|
('side', tr('좌우')),
|
||||||
('stacked', tr('위아래')),
|
('stacked', tr('위아래')),
|
||||||
], (v) => orient = v),
|
], (v) => orient = v),
|
||||||
group(tr('기능 버튼 줄'), bar, [
|
group(tr('기능 버튼 줄'), bar, [
|
||||||
('middle', layout == 'dual' ? tr('두 창 사이') : tr('왼쪽')),
|
('middle', layout != 'single' ? tr('두 창 사이') : tr('왼쪽')),
|
||||||
('edge', tr('끝 (오른쪽 · 아래)')),
|
('edge', tr('끝 (오른쪽 · 아래)')),
|
||||||
('hidden', tr('숨김')),
|
('hidden', tr('숨김')),
|
||||||
], (v) => bar = v),
|
], (v) => bar = v),
|
||||||
|
|
@ -918,46 +990,67 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// 모양 · 배치가 바뀌면 (줄 높이 · 창 너비가 달라져) 보던 폴더가 밀려나므로 다시 그 폴더로
|
||||||
|
String _shape = '';
|
||||||
|
|
||||||
Widget _body() {
|
Widget _body() {
|
||||||
final s = c.settings;
|
final s = c.settings;
|
||||||
|
final shape = '${s.explorerStyle}|${s.explorerLayout}|${s.explorerOrientation}|${s.explorerToolbar}';
|
||||||
|
if (_ready && shape != _shape) {
|
||||||
|
if (_shape.isNotEmpty) {
|
||||||
|
for (final pane in _panes) {
|
||||||
|
_reveal(pane, pane.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_shape = shape;
|
||||||
|
}
|
||||||
final dual = s.explorerLayout == 'dual';
|
final dual = s.explorerLayout == 'dual';
|
||||||
|
final split = _split;
|
||||||
return LayoutBuilder(builder: (context, box) {
|
return LayoutBuilder(builder: (context, box) {
|
||||||
final side = !dual || switch (s.explorerOrientation) {
|
final side = !(dual || split) ||
|
||||||
'side' => true,
|
switch (s.explorerOrientation) {
|
||||||
'stacked' => false,
|
'side' => true,
|
||||||
_ => box.maxWidth >= box.maxHeight,
|
'stacked' => false,
|
||||||
};
|
_ => box.maxWidth >= box.maxHeight,
|
||||||
final bar = s.explorerToolbar == 'hidden' ? null : _Toolbar(
|
};
|
||||||
buttons: ExplorerButton.fromSettings(s.explorerButtons),
|
final bar = s.explorerToolbar == 'hidden'
|
||||||
vertical: side,
|
? null
|
||||||
onPressed: _button,
|
: _Toolbar(
|
||||||
enabled: (b) => switch (b) {
|
buttons: ExplorerButton.fromSettings(s.explorerButtons),
|
||||||
ExplorerButton.copy || ExplorerButton.move => dual,
|
vertical: side,
|
||||||
_ => true,
|
onPressed: _button,
|
||||||
},
|
enabled: (b) => switch (b) {
|
||||||
selected: (b) => b == ExplorerButton.hidden && s.explorerShowHidden,
|
ExplorerButton.copy || ExplorerButton.move => dual,
|
||||||
);
|
_ => true,
|
||||||
Widget pane(int i) => Expanded(child: _paneView(i));
|
},
|
||||||
|
selected: (b) => b == ExplorerButton.hidden && s.explorerShowHidden,
|
||||||
|
);
|
||||||
|
final middle = bar != null && s.explorerToolbar == 'middle';
|
||||||
|
final edge = bar != null && s.explorerToolbar == 'edge';
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
if (dual) ...[
|
if (dual) ...[
|
||||||
pane(0),
|
Expanded(child: _paneView(0)),
|
||||||
if (bar != null && s.explorerToolbar == 'middle') bar,
|
if (middle) bar,
|
||||||
pane(1),
|
Expanded(child: _paneView(1)),
|
||||||
if (bar != null && s.explorerToolbar == 'edge') bar,
|
if (edge) bar,
|
||||||
|
] else if (split) ...[
|
||||||
|
// 왼쪽 폴더 트리 (좁게) · 오른쪽 그 폴더의 파일 목록
|
||||||
|
Expanded(flex: 2, child: _paneView(0, foldersOnly: true)),
|
||||||
|
if (middle) bar,
|
||||||
|
Expanded(flex: 3, child: _listView(_panes[0])),
|
||||||
|
if (edge) bar,
|
||||||
] else ...[
|
] else ...[
|
||||||
if (bar != null && s.explorerToolbar == 'middle') bar,
|
if (middle) bar,
|
||||||
pane(0),
|
Expanded(child: _paneView(0)),
|
||||||
if (bar != null && s.explorerToolbar == 'edge') bar,
|
if (edge) bar,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
return side ? Row(children: children) : Column(children: children);
|
return side ? Row(children: children) : Column(children: children);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _paneView(int i) {
|
Widget _frame(int i, List<Widget> children) {
|
||||||
final pane = _panes[i];
|
|
||||||
final active = i == _active;
|
final active = i == _active;
|
||||||
final rows = _rows(pane);
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTapDown: (_) {
|
onTapDown: (_) {
|
||||||
if (_active != i) setState(() => _active = i);
|
if (_active != i) setState(() => _active = i);
|
||||||
|
|
@ -969,32 +1062,77 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
border: Border.all(color: active ? JjColors.accent : JjColors.border, width: active ? 1.5 : 1),
|
border: Border.all(color: active ? JjColors.accent : JjColors.border, width: active ? 1.5 : 1),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Column(children: [
|
child: Column(children: children),
|
||||||
_paneHeader(pane),
|
|
||||||
const Divider(height: 1),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: pane.scroll,
|
|
||||||
itemCount: rows.length,
|
|
||||||
itemExtent: _rowHeight,
|
|
||||||
itemBuilder: (_, k) => _rowView(pane, rows[k]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (pane.marked.isNotEmpty)
|
|
||||||
Container(
|
|
||||||
color: JjColors.panel,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
||||||
child: Row(children: [
|
|
||||||
Text(trf('{0}개 표시함', [pane.marked.length]), style: const TextStyle(fontSize: 12)),
|
|
||||||
const Spacer(),
|
|
||||||
TextButton(onPressed: () => setState(pane.marked.clear), child: Text(tr('표시 지우기'))),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _marksBar(_Pane pane) => Container(
|
||||||
|
color: JjColors.panel,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
child: Row(children: [
|
||||||
|
Text(trf('{0}개 표시함', [pane.marked.length]), style: const TextStyle(fontSize: 12)),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton(onPressed: () => setState(pane.marked.clear), child: Text(tr('표시 지우기'))),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 트리 창 (저장 장치 고르기 + 트리)
|
||||||
|
Widget _paneView(int i, {bool foldersOnly = false}) {
|
||||||
|
final pane = _panes[i];
|
||||||
|
final rows = _rows(pane, foldersOnly: foldersOnly);
|
||||||
|
final look = _look;
|
||||||
|
return _frame(i, [
|
||||||
|
_paneHeader(pane),
|
||||||
|
const Divider(height: 1),
|
||||||
|
if (look.columns && !foldersOnly) ExplorerColumnsHeader(style: look),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: pane.scroll,
|
||||||
|
itemCount: rows.length,
|
||||||
|
itemExtent: look.rowHeight,
|
||||||
|
itemBuilder: (_, k) => _rowView(pane, rows[k], compact: foldersOnly),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (pane.marked.isNotEmpty && !foldersOnly) _marksBar(pane),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "폴더 + 파일 목록" 배치의 오른쪽: 왼쪽 트리에서 고른 폴더의 내용 (맨 위 ".." = 상위 폴더)
|
||||||
|
Widget _listView(_Pane pane) {
|
||||||
|
final dir = pane.current;
|
||||||
|
final entries = pane.cache[dir] ?? const <FileEntry>[];
|
||||||
|
final up = !samePath(dir, pane.root);
|
||||||
|
final rows = [
|
||||||
|
if (up) _Row(FileEntry(p.dirname(dir), isDir: true, modified: DateTime(0)), 0, isUp: true),
|
||||||
|
for (final e in entries) _Row(e, 0),
|
||||||
|
];
|
||||||
|
final look = _look;
|
||||||
|
return _frame(0, [
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Text(dir, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
if (look.columns) ExplorerColumnsHeader(style: look),
|
||||||
|
Expanded(
|
||||||
|
child: pane.loading.contains(dir)
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: entries.isEmpty && !up
|
||||||
|
? Center(child: Text(tr('비어 있음'), style: const TextStyle(color: JjColors.textDim)))
|
||||||
|
: ListView.builder(
|
||||||
|
controller: pane.listScroll,
|
||||||
|
itemCount: rows.length,
|
||||||
|
itemExtent: look.rowHeight,
|
||||||
|
itemBuilder: (_, k) => _rowView(pane, rows[k], list: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (pane.marked.isNotEmpty) _marksBar(pane),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/// 창 위쪽: 저장 장치 고르기
|
/// 창 위쪽: 저장 장치 고르기
|
||||||
Widget _paneHeader(_Pane pane) => SizedBox(
|
Widget _paneHeader(_Pane pane) => SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|
@ -1024,106 +1162,166 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _rowView(_Pane pane, _Row row) {
|
/// 한 줄. [list]: "폴더 + 파일 목록" 의 오른쪽 목록 (트리 아님). [compact]: 폴더 트리 (열 · 표시 버튼 없이)
|
||||||
|
Widget _rowView(_Pane pane, _Row row, {bool list = false, bool compact = false}) {
|
||||||
final e = row.entry;
|
final e = row.entry;
|
||||||
final isCurrent = e.isDir && samePath(pane.current, e.path);
|
final look = _look;
|
||||||
final isFocused = pane.focused != null && samePath(pane.focused!, e.path);
|
final isCurrent = e.isDir && !list && !row.isUp && samePath(pane.current, e.path);
|
||||||
|
final isFocused = !row.isUp && pane.focused != null && samePath(pane.focused!, e.path);
|
||||||
final marked = pane.marked.contains(e.path);
|
final marked = pane.marked.contains(e.path);
|
||||||
final open = e.isDir && pane.expanded.contains(e.path);
|
final open = e.isDir && !list && pane.expanded.contains(e.path);
|
||||||
final loading = pane.loading.contains(e.path);
|
final loading = pane.loading.contains(e.path);
|
||||||
final video = !e.isDir && isVideoFile(e.path);
|
final name = row.isUp
|
||||||
final name = row.isRoot ? (_volumes.firstWhere((v) => samePath(v.$1, e.path), orElse: () => (e.path, e.path)).$2) : e.name;
|
? '..'
|
||||||
|
: row.isRoot
|
||||||
|
? (_volumes.firstWhere((v) => samePath(v.$1, e.path), orElse: () => (e.path, e.path)).$2)
|
||||||
|
: look == ExplorerStyle.totalcmd
|
||||||
|
? totalCmdName(e)
|
||||||
|
: e.name;
|
||||||
|
final dim = TextStyle(fontSize: look.fontSize - 2, color: JjColors.textDim);
|
||||||
|
final nameStyle = TextStyle(
|
||||||
|
fontSize: look.fontSize,
|
||||||
|
fontWeight: isCurrent || (look == ExplorerStyle.totalcmd && e.isDir) ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
color: isCurrent ? JjColors.accent : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 트리의 펼치기 화살표 (따로 누름)
|
||||||
|
Widget chevron() => SizedBox(
|
||||||
|
width: 20,
|
||||||
|
child: e.isDir && !list && !row.isUp
|
||||||
|
? (loading
|
||||||
|
? const Center(
|
||||||
|
child: SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 1.5)))
|
||||||
|
: InkResponse(
|
||||||
|
radius: 16,
|
||||||
|
onTap: () => _toggle(pane, e),
|
||||||
|
child: Icon(open ? Icons.expand_more : Icons.chevron_right, size: 18, color: JjColors.textDim),
|
||||||
|
))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget nameCell() => Row(children: [
|
||||||
|
SizedBox(width: list ? 0 : row.depth * look.indent),
|
||||||
|
if (!list) chevron(),
|
||||||
|
SizedBox(
|
||||||
|
width: look.rich ? 56 : look.iconSize + 6,
|
||||||
|
height: look.rowHeight - 8,
|
||||||
|
child: Center(child: _icon(e, row.isRoot, open, up: row.isUp)),
|
||||||
|
),
|
||||||
|
SizedBox(width: look.rich ? 8 : 6),
|
||||||
|
Expanded(
|
||||||
|
child: look.rich && !e.isDir && !row.isUp
|
||||||
|
? Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(name, maxLines: 1, overflow: TextOverflow.ellipsis, style: nameStyle),
|
||||||
|
Row(children: [
|
||||||
|
Text('${formatSize(e.size)} · ${_date(e.modified)}', style: dim),
|
||||||
|
if (isVideoFile(e.path)) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(child: _MetaText(c: c, path: e.path)),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(name, maxLines: 1, overflow: TextOverflow.ellipsis, style: nameStyle),
|
||||||
|
if (look.rich && e.isDir && pane.errors[e.path] != null)
|
||||||
|
Text(tr('읽을 수 없음'), style: dim.copyWith(color: Colors.redAccent)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final real = !row.isRoot && !row.isUp;
|
||||||
|
Widget cell(String t, int flex, {TextAlign align = TextAlign.start}) => Expanded(
|
||||||
|
flex: flex,
|
||||||
|
child: Text(t, style: dim, textAlign: align, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
);
|
||||||
|
final cells = !look.columns || compact || !real
|
||||||
|
? <Widget>[Expanded(child: nameCell())]
|
||||||
|
: look == ExplorerStyle.windows
|
||||||
|
? [
|
||||||
|
Expanded(flex: 6, child: nameCell()),
|
||||||
|
cell(_date(e.modified), 3),
|
||||||
|
cell(typeLabel(e), 2),
|
||||||
|
cell(e.isDir ? '' : formatSize(e.size), 2, align: TextAlign.end),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
Expanded(flex: 6, child: nameCell()),
|
||||||
|
cell(e.isDir ? '' : e.ext, 1),
|
||||||
|
cell(e.isDir ? '<DIR>' : formatSize(e.size), 2, align: TextAlign.end),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
cell(_date(e.modified), 3),
|
||||||
|
];
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onSecondaryTapUp: (d) => _menu(pane, e, d.globalPosition),
|
onSecondaryTapUp: real ? (d) => _menu(pane, e, d.globalPosition) : null,
|
||||||
onLongPressStart: (d) => _menu(pane, e, d.globalPosition),
|
onLongPressStart: real ? (d) => _menu(pane, e, d.globalPosition) : null,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: marked
|
color: marked
|
||||||
? JjColors.accent.withValues(alpha: 0.18)
|
? JjColors.accent.withValues(alpha: 0.18)
|
||||||
: isFocused
|
: isFocused
|
||||||
? JjColors.panel
|
? JjColors.panel
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
child: InkWell(
|
child: Row(children: [
|
||||||
onTap: () => _tap(pane, e),
|
Expanded(
|
||||||
child: Padding(
|
child: InkWell(
|
||||||
padding: EdgeInsets.only(left: 6.0 + row.depth * 16, right: 4),
|
onTap: () => _onTap(pane, row, list: list),
|
||||||
child: Row(children: [
|
// 선택 모드: 두 번 누르면 열기 (바로 열기 모드는 두 번 누르기를 받지 않음 - 한 번 누르기가 늦어지지 않게)
|
||||||
SizedBox(
|
onDoubleTap: _selectMode ? () => _onOpen(pane, row, list: list) : null,
|
||||||
width: 18,
|
child: Padding(
|
||||||
child: e.isDir
|
padding: const EdgeInsets.only(left: 6, right: 4),
|
||||||
? (loading
|
child: Row(children: cells),
|
||||||
? const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 1.5))
|
|
||||||
: Icon(open ? Icons.expand_more : Icons.chevron_right, size: 18, color: JjColors.textDim))
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
SizedBox(width: 56, height: 44, child: Center(child: _icon(e, row.isRoot, open))),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(name,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: isCurrent ? FontWeight.w600 : FontWeight.normal,
|
|
||||||
color: isCurrent ? JjColors.accent : null)),
|
|
||||||
if (!e.isDir)
|
|
||||||
Row(children: [
|
|
||||||
Text('${formatSize(e.size)} · ${_date(e.modified)}',
|
|
||||||
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
|
||||||
if (video) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Flexible(child: _MetaText(c: c, path: e.path)),
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
if (e.isDir && pane.errors[e.path] != null)
|
|
||||||
Text(tr('읽을 수 없음'), style: const TextStyle(fontSize: 11, color: Colors.redAccent)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!row.isRoot)
|
|
||||||
IconButton(
|
|
||||||
tooltip: marked ? tr('표시 지우기') : tr('표시 (여러 개 고르기)'),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
icon: Icon(marked ? Icons.check_circle : Icons.radio_button_unchecked,
|
|
||||||
size: 20, color: marked ? JjColors.accent : JjColors.textDim),
|
|
||||||
onPressed: () => setState(() {
|
|
||||||
_active = _panes.indexOf(pane);
|
|
||||||
marked ? pane.marked.remove(e.path) : pane.marked.add(e.path);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
),
|
// 표시 동그라미는 두 번 누르기 영역 밖 (기다리지 않고 바로)
|
||||||
|
if (real && !compact)
|
||||||
|
SizedBox(
|
||||||
|
width: 36,
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: marked ? tr('표시 지우기') : tr('표시 (여러 개 고르기)'),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: Icon(marked ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||||
|
size: look.rich ? 20 : 16, color: marked ? JjColors.accent : JjColors.textDim),
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_active = _panes.indexOf(pane);
|
||||||
|
marked ? pane.marked.remove(e.path) : pane.marked.add(e.path);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (!compact)
|
||||||
|
const SizedBox(width: 36),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _icon(FileEntry e, bool isRoot, bool open) {
|
Widget _icon(FileEntry e, bool isRoot, bool open, {bool up = false}) {
|
||||||
if (isRoot) return const Icon(Icons.sd_storage, color: JjColors.accent, size: 28);
|
final look = _look;
|
||||||
if (e.isDir) return Icon(open ? Icons.folder_open : Icons.folder, color: Colors.amber, size: 30);
|
if (up) return Icon(Icons.arrow_upward, size: look.iconSize, color: JjColors.textDim);
|
||||||
if (isVideoFile(e.path)) return _Thumb(c: c, entry: e);
|
if (isRoot) return Icon(Icons.sd_storage, color: JjColors.accent, size: look.iconSize - 2);
|
||||||
const images = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'};
|
// X-plore: 동영상 썸네일 · 그림 미리 보기
|
||||||
if (images.contains(e.ext)) {
|
if (look.rich && !e.isDir) {
|
||||||
return ClipRRect(
|
if (isVideoFile(e.path)) return _Thumb(c: c, entry: e);
|
||||||
borderRadius: BorderRadius.circular(3),
|
const images = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'};
|
||||||
child: Image.file(File(e.path),
|
if (images.contains(e.ext)) {
|
||||||
width: 56, height: 44, fit: BoxFit.cover, cacheWidth: 112,
|
return ClipRRect(
|
||||||
errorBuilder: (_, _, _) => const Icon(Icons.image_outlined, size: 28)),
|
borderRadius: BorderRadius.circular(3),
|
||||||
);
|
child: Image.file(File(e.path),
|
||||||
|
width: 56, height: 44, fit: BoxFit.cover, cacheWidth: 112,
|
||||||
|
errorBuilder: (_, _, _) => const Icon(Icons.image_outlined, size: 28)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final icon = switch (e.ext) {
|
final (icon, color) = fileIcon(look, e, open: open);
|
||||||
'srt' || 'ass' || 'ssa' || 'smi' || 'sami' || 'vtt' => Icons.subtitles_outlined,
|
return Icon(icon, size: look.rich && !e.isDir ? 28 : look.iconSize, color: color);
|
||||||
'mp3' || 'm4a' || 'flac' || 'wav' || 'ogg' || 'aac' || 'opus' => Icons.audiotrack_outlined,
|
|
||||||
'pdf' => Icons.picture_as_pdf_outlined,
|
|
||||||
'zip' || '7z' || 'rar' || 'tar' || 'gz' => Icons.folder_zip_outlined,
|
|
||||||
'apk' => Icons.android,
|
|
||||||
'txt' || 'log' || 'md' || 'json' || 'xml' => Icons.description_outlined,
|
|
||||||
_ => Icons.insert_drive_file_outlined,
|
|
||||||
};
|
|
||||||
return Icon(icon, size: 28, color: JjColors.textDim);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import 'language_settings.dart';
|
||||||
import 'app_actions.dart';
|
import 'app_actions.dart';
|
||||||
import 'cef_setup.dart';
|
import 'cef_setup.dart';
|
||||||
import 'cleanup_dialog.dart';
|
import 'cleanup_dialog.dart';
|
||||||
|
import 'explorer_look.dart' show ExplorerStyle;
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
|
|
||||||
|
|
@ -554,6 +555,47 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
|
_group('files', Icons.folder_copy_outlined, tr('파일 탐색기'), [
|
||||||
|
ListTile(
|
||||||
|
title: Text(tr('누르기')),
|
||||||
|
subtitle: Text(tr('길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). '
|
||||||
|
'Ctrl + 클릭: 여러 개 고르기')),
|
||||||
|
trailing: DropdownButton<String>(
|
||||||
|
value: s.explorerClick,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'select', child: Text(tr('한 번: 선택 · 두 번: 실행 (기본)'))),
|
||||||
|
DropdownMenuItem(value: 'open', child: Text(tr('한 번: 바로 실행'))),
|
||||||
|
],
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(tr('스타일')),
|
||||||
|
subtitle: Text(tr('아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · '
|
||||||
|
'Total Commander (촘촘한 목록 · [폴더])')),
|
||||||
|
trailing: DropdownButton<String>(
|
||||||
|
value: s.explorerStyle,
|
||||||
|
items: [
|
||||||
|
for (final st in ExplorerStyle.values)
|
||||||
|
DropdownMenuItem(value: st.name, child: Text(tr(st.label))),
|
||||||
|
],
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.explorerStyle = v!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(tr('창 배치')),
|
||||||
|
subtitle: Text(tr('파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)')),
|
||||||
|
trailing: DropdownButton<String>(
|
||||||
|
value: s.explorerLayout,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'dual', child: Text(tr('두 창'))),
|
||||||
|
DropdownMenuItem(value: 'split', child: Text(tr('폴더 + 파일 목록'))),
|
||||||
|
DropdownMenuItem(value: 'single', child: Text(tr('한 창'))),
|
||||||
|
],
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.explorerLayout = v!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
_group('download', Icons.download_outlined, tr('다운로드'), [
|
_group('download', Icons.download_outlined, tr('다운로드'), [
|
||||||
if (desk)
|
if (desk)
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
|
|
@ -791,7 +833,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
final _groupKeys = <String, GlobalKey>{};
|
final _groupKeys = <String, GlobalKey>{};
|
||||||
|
|
||||||
static const _groupIds = [
|
static const _groupIds = [
|
||||||
'general', 'display', 'mkv', 'subtitle', 'play', 'browser', 'download', 'run', 'cleanup', 'about', //
|
'general', 'display', 'mkv', 'subtitle', 'play', 'browser', 'files', 'download', 'run', 'cleanup', 'about', //
|
||||||
];
|
];
|
||||||
|
|
||||||
String _groupTitle(String id) => switch (id) {
|
String _groupTitle(String id) => switch (id) {
|
||||||
|
|
@ -801,6 +843,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
'subtitle' => tr('자막 (AI · 인터넷)'),
|
'subtitle' => tr('자막 (AI · 인터넷)'),
|
||||||
'play' => tr('재생'),
|
'play' => tr('재생'),
|
||||||
'browser' => tr('웹 브라우저'),
|
'browser' => tr('웹 브라우저'),
|
||||||
|
'files' => tr('파일 탐색기'),
|
||||||
'download' => tr('다운로드'),
|
'download' => tr('다운로드'),
|
||||||
'run' => tr('실행 · 종료'),
|
'run' => tr('실행 · 종료'),
|
||||||
'cleanup' => tr('저장 공간 정리'),
|
'cleanup' => tr('저장 공간 정리'),
|
||||||
|
|
|
||||||
|
|
@ -53,19 +53,27 @@ void main() {
|
||||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('두 창: 폴더 펼치기 · 파일 누르면 기본 앱 · 길게 눌러 다른 앱으로 · 다른 창으로 복사', (tester) async {
|
testWidgets('두 창 (선택 모드): 한 번 = 선택 · 화살표 = 펼치기 · 두 번 = 실행 · 길게 = 메뉴 · 다른 창으로 복사', (tester) async {
|
||||||
await open(tester);
|
await open(tester);
|
||||||
// 두 창 + 가운데 버튼 줄
|
// 두 창 + 가운데 버튼 줄
|
||||||
expect(find.text('상위 폴더'), findsOneWidget);
|
expect(find.text('상위 폴더'), findsOneWidget);
|
||||||
expect(find.text('doc.txt'), findsOneWidget);
|
expect(find.text('doc.txt'), findsOneWidget);
|
||||||
|
|
||||||
// 폴더를 누르면 그 자리에서 펼친다
|
// 폴더를 한 번 누르면 고르기만 (펼치지 않음)
|
||||||
await act(tester, () => tester.tap(find.text('sub')));
|
await act(tester, () => tester.tap(find.text('sub')));
|
||||||
|
await settle(tester, () => false, rounds: 15);
|
||||||
|
expect(find.text('inner.txt'), findsNothing);
|
||||||
|
// › 화살표로 펼친다
|
||||||
|
await act(tester, () => tester.tap(chevronOf('sub')));
|
||||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||||
expect(find.text('inner.txt'), findsOneWidget);
|
expect(find.text('inner.txt'), findsOneWidget);
|
||||||
|
|
||||||
// 파일 (동영상 아님) 을 누르면 기본 앱으로
|
// 파일을 한 번 누르면 고르기만 (실행 안 함)
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
|
await settle(tester, () => false, rounds: 15);
|
||||||
|
expect(shell.opened, isEmpty);
|
||||||
|
// 두 번 누르면 기본 앱으로 실행
|
||||||
|
await doubleTap(tester, find.text('doc.txt'));
|
||||||
await settle(tester, () => shell.opened.isNotEmpty);
|
await settle(tester, () => shell.opened.isNotEmpty);
|
||||||
expect(shell.opened.last, (p.join(left, 'doc.txt'), false));
|
expect(shell.opened.last, (p.join(left, 'doc.txt'), false));
|
||||||
|
|
||||||
|
|
@ -73,13 +81,14 @@ void main() {
|
||||||
await tester.longPress(find.text('doc.txt'));
|
await tester.longPress(find.text('doc.txt'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('다른 창으로 복사'), findsOneWidget);
|
expect(find.text('다른 창으로 복사'), findsOneWidget);
|
||||||
|
expect(find.text('삭제'), findsWidgets);
|
||||||
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
|
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
|
||||||
await settle(tester, () => shell.opened.length == 2);
|
await settle(tester, () => shell.opened.length == 2);
|
||||||
expect(shell.opened.last, (p.join(left, 'doc.txt'), true));
|
expect(shell.opened.last, (p.join(left, 'doc.txt'), true));
|
||||||
|
|
||||||
// doc.txt 를 고른 채 [복사] → 확인 → 오른쪽 창 폴더에 생긴다
|
// doc.txt 를 고른 채 [복사] → 확인 → 오른쪽 창 폴더에 생긴다
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
await tester.pump();
|
await settle(tester, () => false, rounds: 15);
|
||||||
await act(tester, () => tester.tap(find.text('복사').first));
|
await act(tester, () => tester.tap(find.text('복사').first));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||||||
|
|
@ -88,11 +97,61 @@ void main() {
|
||||||
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async {
|
||||||
|
c.settings.explorerClick = 'open';
|
||||||
|
await open(tester);
|
||||||
|
await act(tester, () => tester.tap(find.text('sub')));
|
||||||
|
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('inner.txt'), findsOneWidget);
|
||||||
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
|
await settle(tester, () => shell.opened.isNotEmpty);
|
||||||
|
expect(shell.opened.single, (p.join(left, 'doc.txt'), false));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('폴더 + 파일 목록 배치 · Windows 탐색기 / Total Commander 스타일', (tester) async {
|
||||||
|
c.settings
|
||||||
|
..explorerLayout = 'split'
|
||||||
|
..explorerStyle = 'windows';
|
||||||
|
await open(tester);
|
||||||
|
// 왼쪽 트리는 폴더만, 오른쪽 목록은 고른 폴더 (left) 의 내용
|
||||||
|
expect(find.text('sub'), findsNWidgets(2)); // 트리 + 목록
|
||||||
|
expect(find.text('doc.txt'), findsOneWidget); // 목록에만
|
||||||
|
expect(find.text('수정한 날짜'), findsOneWidget); // Windows 탐색기 열 머리
|
||||||
|
expect(find.text('TXT 파일'), findsOneWidget);
|
||||||
|
expect(find.text('상위 폴더'), findsOneWidget); // 버튼 줄은 두 창 사이
|
||||||
|
|
||||||
|
// 왼쪽 트리에서 sub 를 고르면 오른쪽 목록이 그 폴더로
|
||||||
|
await act(tester, () => tester.tap(find.text('sub').first));
|
||||||
|
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('inner.txt'), findsOneWidget);
|
||||||
|
expect(find.text('doc.txt'), findsNothing);
|
||||||
|
// 목록의 ".." 을 누르면 상위 폴더로
|
||||||
|
await act(tester, () => tester.tap(find.text('..')));
|
||||||
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('doc.txt'), findsOneWidget);
|
||||||
|
// 목록의 폴더를 두 번 누르면 들어간다
|
||||||
|
await doubleTap(tester, find.text('sub').last);
|
||||||
|
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('inner.txt'), findsOneWidget);
|
||||||
|
|
||||||
|
// Total Commander: [폴더] · <DIR> · 확장자 칸
|
||||||
|
await act(tester, () => tester.tap(find.text('..')));
|
||||||
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
|
await c.updateSettings((s) => s.explorerStyle = 'totalcmd');
|
||||||
|
await settle(tester, () => find.text('[sub]').evaluate().length == 2);
|
||||||
|
expect(find.text('[sub]'), findsNWidgets(2));
|
||||||
|
expect(find.text('<DIR>'), findsOneWidget);
|
||||||
|
expect(find.text('doc'), findsOneWidget); // 확장자를 뺀 이름
|
||||||
|
expect(find.text('txt'), findsOneWidget); // 확장자 칸
|
||||||
|
expect(find.text('확장자'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('여러 개 표시 → 삭제 · 새 폴더 · 이름 변경', (tester) async {
|
testWidgets('여러 개 표시 → 삭제 · 새 폴더 · 이름 변경', (tester) async {
|
||||||
await open(tester);
|
await open(tester);
|
||||||
// 동그라미로 doc.txt · sub 표시 (반드시 이름으로 찾은 줄의 버튼만: 목록에는 시험 폴더 위쪽의 실제 폴더도 보인다)
|
// 동그라미로 doc.txt · sub 표시 (반드시 이름으로 찾은 줄의 버튼만: 목록에는 시험 폴더 위쪽의 실제 폴더도 보인다)
|
||||||
await act(tester, () => tester.tap(markOf('sub')));
|
await act(tester, () => tester.tap(markOf('sub')));
|
||||||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||||
|
// 표시 버튼은 두 번 누르기를 기다리지 않고 바로
|
||||||
expect(find.text('2개 표시함'), findsOneWidget);
|
expect(find.text('2개 표시함'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.text('삭제').first));
|
await act(tester, () => tester.tap(find.text('삭제').first));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -157,7 +216,7 @@ void main() {
|
||||||
|
|
||||||
/// [name] 줄의 "표시" 동그라미 버튼
|
/// [name] 줄의 "표시" 동그라미 버튼
|
||||||
Finder markOf(String name) => find.descendant(
|
Finder markOf(String name) => find.descendant(
|
||||||
of: find.ancestor(of: find.text(name), matching: find.byType(InkWell)).first,
|
of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first,
|
||||||
matching: find.byTooltip('표시 (여러 개 고르기)'),
|
matching: find.byTooltip('표시 (여러 개 고르기)'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -167,9 +226,25 @@ Future<void> act(WidgetTester tester, Future<void> Function() f) async {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [name] 트리 줄의 › (펼치기) 화살표
|
||||||
|
Finder chevronOf(String name) => find.descendant(
|
||||||
|
of: find.ancestor(of: find.text(name), matching: find.byType(InkWell)).first,
|
||||||
|
matching: find.byIcon(Icons.chevron_right),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 두 번 누르기 (실제 비동기 구역에서: 그때 시작한 파일 작업이 테스트의 가짜 시간에 묶이지 않게)
|
||||||
|
Future<void> doubleTap(WidgetTester tester, Finder f) async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
await tester.tap(f);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 80));
|
||||||
|
await tester.tap(f);
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
/// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신
|
/// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신
|
||||||
Future<void> settle(WidgetTester tester, bool Function() done) async {
|
Future<void> settle(WidgetTester tester, bool Function() done, {int rounds = 100}) async {
|
||||||
for (var i = 0; i < 100; i++) {
|
for (var i = 0; i < rounds; i++) {
|
||||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
if (done()) break;
|
if (done()) break;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue