Compare commits

..
7 Commits
Author SHA1 Message Date
owendswang 1a24160b8b Release v0.9 2026-07-19 00:20:15 +08:00
owendswang 3696468611 Fixed display for available spacee of interal M.2 2026-07-18 09:58:05 +08:00
owendswang 777bb4abd4 Release v0.8 2026-07-14 18:49:24 +08:00
owendswang 76f8fe21d2 Release v0.7 2026-07-09 20:09:02 +08:00
owendswang 5a91067c2e Release v0.6 2026-06-24 01:19:18 +08:00
owendswang ba0704d48c Release v0.5 2026-06-22 02:33:23 +08:00
owendswang 4e10dee5fb Update README.md 2026-06-20 23:45:21 +08:00
12 changed files with 2053 additions and 286 deletions

No files matched your search

+2
View File
@@ -3,3 +3,5 @@ gen/
web-file-mgr-linux
*.o
*.d
tools/
test/
+1 -1
View File
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
endif
endif
VERSION_TAG := v0.4
VERSION_TAG := v0.9
TITLE_ID := FMGR88888
PYTHON ?= python3
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
+7
View File
@@ -15,6 +15,7 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
<a href="docs/screenshots/20260617_232348.855.jpg" target="_blank"><img src="docs/screenshots/20260617_232348.855.jpg" width="31%" alt="PS5 Web File Manager screenshot 3"></a>
<a href="docs/screenshots/20260619_131811.644.jpg" target="_blank"><img src="docs/screenshots/20260619_131811.644.jpg" width="31%" alt="PS5 Web File Manager screenshot 4"></a>
<a href="docs/screenshots/20260619_131535.239.jpg" target="_blank"><img src="docs/screenshots/20260619_131535.239.jpg" width="31%" alt="PS5 Web File Manager screenshot 5"></a>
<a href="docs/screenshots/20260620_232728.533.jpg" target="_blank"><img src="docs/screenshots/20260620_232728.533.jpg" width="31%" alt="PS5 Web File Manager screenshot 6"></a>
</p>
## Features
@@ -30,6 +31,7 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
- Chinese and English UI. The browser language is read from `navigator.languages` / `navigator.language`; Chinese uses `zh`, everything else uses English.
- Startup notification showing the app name, version, and listen port.
- Create/edit text files ending with `.txt`, `.json`, `.xml`, `.ini`, `.cfg`, `.conf`, `.md`, `.log`, `.lua`, `.js`, `.css`, `.html`, `.htm`, `.c`, `.h`, `.cpp`, `.hpp`, `.sh`, `.csv`, `.yaml`, `.yml`.
- Preview image files ending with `.png,`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
## Build
@@ -91,6 +93,11 @@ On first startup the payload installs a `PS5 Web File Manager` web shortcut in t
- The UI can recover the active task display if the browser is closed and reopened while the payload process is still running.
- Text editing is limited to common text-file extensions. Non-UTF-8 and oversized files are rejected.
## FAQ
- This is a homebrew app which does not affect system process or memory. It should crash itself instead of kernel panic theoretically. If kernel panic happens, make sure you'r using the latest jailbreak methods which includes latest elfldr. Or switch back to the stable method you have been using.
- If you're using P2JB, I suggest not to use this if kernel panic happened. It is suffering to wait another 50 min because of kernel panic. Stability first for P2JB.
- The preparing stage is calculating the folder size. It would be pretty slow if there are too many files in this folder. It make sure if there is enough available space for this folder you were trying to copy/move.
## Credits
This project was built with reference to these projects:
+26 -4
View File
@@ -4,13 +4,34 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PS5 Web File Manager</title>
<style>
html, body { margin: 0; min-height: 100%; background: #111316; color: #edf0f2; }
.init-loading {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
background: #111316;
color: #aab4be;
font: 18px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
</style>
<link rel="stylesheet" href="/main.css">
</head>
<body>
<main class="shell">
<header class="topbar">
<div id="path" class="path">/</div>
<div id="spaceInfo" class="space-info"></div>
<div class="top-left">
<div id="path" class="path">/</div>
</div>
<div class="top-right">
<div id="spaceInfo" class="space-info"></div>
</div>
<button id="exitBtn" class="icon-button power-button" type="button" aria-label="Exit"></button>
</header>
@@ -22,7 +43,7 @@
<button id="deleteBtn" class="danger" data-i18n="delete"></button>
<button id="pasteBtn" class="paste-action" hidden>
<span id="pasteVerb" data-i18n="paste"></span>
<span id="pasteName" class="paste-name"></span>
<span id="pasteName" class="paste-name"></span><span id="pasteCount" class="paste-count"></span>
<span id="pasteTargetText" data-i18n="pasteToCurrent"></span>
</button>
<button id="clearClipboardBtn" data-i18n="cancel" hidden></button>
@@ -75,6 +96,7 @@
<div id="imagePreviewOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel image-preview-panel">
<div id="imagePreviewName" class="text-editor-path"></div>
<div class="image-preview-stage">
<img id="imagePreview" class="image-preview" alt="">
</div>
@@ -88,7 +110,7 @@
</div>
</div>
<div id="initLoading" class="init-loading" aria-hidden="true"></div>
<div id="initLoading" class="init-loading" aria-hidden="true">Loading...</div>
<script src="/main.js"></script>
</body>
+15 -3
View File
@@ -3,6 +3,9 @@ window.WFM_LANG = {
copy: "Copy",
move: "Move",
delete: "Delete",
copying: "Copying",
moving: "Moving",
deleting: "Deleting",
rename: "Rename",
paste: "Paste",
cancel: "Cancel",
@@ -36,12 +39,17 @@ window.WFM_LANG = {
speedLabel: "Speed",
progressLabel: "Progress",
etaLabel: "ETA",
elapsedLabel: "Elapsed",
durationHours: "{hours}h {minutes}m",
durationMinutes: "{minutes}m {seconds}s",
durationSeconds: "{seconds}s",
preparingTask: "Preparing task",
canceling: "Canceling...",
selectedItems: "{name} and {count} items",
countItems: "{count} items",
totalItems: "{count} items",
readDir: "Reading folder...",
processing: "Processing...",
actionBusy: "{label}...",
taskCreated: "{label} task created",
actionDone: "{label} done",
@@ -55,14 +63,14 @@ window.WFM_LANG = {
overwriteFiles: "Files with the same name will be overwritten: {names}",
mergeDirs: "Folders with the same name will be merged. Internal files with the same name will be overwritten: {names}",
continueConfirm: "Continue?",
preparingPaste: "{label} preparing: calculating size and checking target space...",
preparingPaste: "Preparing: calculating size and checking target space...",
preparingStatus: "{label} preparing...",
cancelPrepare: "Preparation canceled",
cancelTaskConfirm: "Cancel current {label} task?",
cancelFailed: "Cancel failed: {error}",
tasksPollFailed: "Failed to read task status: {error}",
transferComplete: "{label} completed\n\nTotal time: {duration}\nTransferred: {size}",
transferCompleteFiles: "{label} completed\n\nTotal time: {duration}\nTransferred: {size}\nFiles: {count}",
transferComplete: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}",
transferCompleteFiles: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}\nFiles: {count}",
renamePrompt: "New name",
mkdirPrompt: "Folder name",
newTextPrompt: "Text file name",
@@ -84,11 +92,14 @@ window.WFM_LANG = {
storageUsb: "USB",
storageM2: "M.2",
storageExtended: "Extended",
storageRoot: "Root",
storageCurrent: "Current",
err_target_dir_not_writable: "Target folder is not writable: {path}",
err_target_file_not_writable: "Target file is not writable: {path}",
err_target_parent_not_writable: "Current folder is not writable: {path}",
err_target_check_failed: "Cannot check target path: {path}",
err_target_path_too_long: "Target path is too long",
err_path_too_long: "Path is too long",
err_space_check_failed: "Cannot read target free space: {path}",
err_no_space: "Not enough target space. Required {required}, available {available}",
err_active_task: "A task is running",
@@ -104,6 +115,7 @@ window.WFM_LANG = {
err_text_invalid_utf8: "The file is not valid UTF-8 text",
err_text_file_changed: "The file changed after it was opened. Close and reopen it",
err_text_file_not_writable: "The text file is not writable",
err_request_body_too_large: "Too many selected items. Select fewer items and try again",
err_unknown_api: "Unknown API",
err_out_of_memory: "Out of memory",
err_no_source_paths: "No source paths",
+15 -3
View File
@@ -3,6 +3,9 @@ window.WFM_LANG = {
copy: "复制",
move: "移动",
delete: "删除",
copying: "复制",
moving: "移动",
deleting: "删除",
rename: "重命名",
paste: "粘贴",
cancel: "取消",
@@ -36,12 +39,17 @@ window.WFM_LANG = {
speedLabel: "速度",
progressLabel: "进度",
etaLabel: "剩余",
elapsedLabel: "耗时",
durationHours: "{hours}小时 {minutes}分",
durationMinutes: "{minutes}分 {seconds}秒",
durationSeconds: "{seconds}秒",
preparingTask: "正在准备任务",
canceling: "正在取消...",
selectedItems: "{name} 等 {count} 项",
countItems: "{count} 项",
totalItems: "共 {count} 项",
readDir: "读取目录中...",
processing: "处理中...",
actionBusy: "{label}...",
taskCreated: "{label}任务已创建",
actionDone: "{label}完成",
@@ -55,14 +63,14 @@ window.WFM_LANG = {
overwriteFiles: "同名文件将被覆盖: {names}",
mergeDirs: "同名目录将被合并,内部同名文件会被覆盖: {names}",
continueConfirm: "继续?",
preparingPaste: "{label}准备中:正在计算大小并检查目标空间...",
preparingPaste: "准备中:正在计算大小并检查目标空间...",
preparingStatus: "{label}准备中...",
cancelPrepare: "已取消准备操作",
cancelTaskConfirm: "取消当前{label}任务?",
cancelFailed: "取消失败: {error}",
tasksPollFailed: "任务状态读取失败: {error}",
transferComplete: "{label}完成\n\n总耗时:{duration}\n传输大小:{size}",
transferCompleteFiles: "{label}完成\n\n总耗时:{duration}\n传输大小:{size}\n文件数量:{count}",
transferComplete: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}",
transferCompleteFiles: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}\n文件数量:{count}",
renamePrompt: "新名称",
mkdirPrompt: "目录名",
newTextPrompt: "文本文件名",
@@ -84,11 +92,14 @@ window.WFM_LANG = {
storageUsb: "USB存储",
storageM2: "M2扩充存储",
storageExtended: "扩展存储",
storageRoot: "根分区",
storageCurrent: "当前目录",
err_target_dir_not_writable: "目标目录不可写: {path}",
err_target_file_not_writable: "目标文件不可写: {path}",
err_target_parent_not_writable: "当前目录不可写: {path}",
err_target_check_failed: "无法检查目标路径: {path}",
err_target_path_too_long: "目标路径过长",
err_path_too_long: "路径过长",
err_space_check_failed: "无法读取目标剩余空间: {path}",
err_no_space: "目标空间不足,需要 {required},可用 {available}",
err_active_task: "有任务正在执行",
@@ -104,6 +115,7 @@ window.WFM_LANG = {
err_text_invalid_utf8: "文件不是有效的 UTF-8 文本",
err_text_file_changed: "文件在打开后已发生变化,请关闭后重新打开",
err_text_file_not_writable: "文本文件不可写",
err_request_body_too_large: "选择的项目过多,请减少选择后重试",
err_unknown_api: "未知接口",
err_out_of_memory: "内存不足",
err_no_source_paths: "没有源路径",
+313 -30
View File
@@ -43,18 +43,48 @@ body {
}
.topbar {
position: relative;
flex: 0 0 auto;
height: auto;
min-height: 54px;
display: flex;
align-items: center;
padding: 12px 30px;
align-items: start;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 18px;
background: #1b2026;
border-bottom: 1px solid #2d343d;
overflow: visible;
}
.top-left,
.top-right {
height: auto;
display: flex;
align-items: center;
}
.top-left {
flex: 1 1 420px;
min-width: 0;
margin-top: 15px;
}
.top-right {
flex: 1 1 360px;
min-width: 0;
margin-left: auto;
justify-content: flex-end;
flex-wrap: wrap;
padding: 12px 40px 12px 0;
text-align: right;
}
.path {
color: #aab4be;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 22px;
line-height: 1.35;
line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -63,19 +93,25 @@ body {
}
.space-info {
height: auto;
min-height: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
flex: 0 1 auto;
max-width: 58vw;
flex: 1 1 auto;
max-width: none;
color: #c9d3dc;
font-size: 18px;
line-height: 1.35;
line-height: 30px;
white-space: nowrap;
overflow: hidden;
overflow: visible;
text-align: right;
}
.space-item {
margin-left: 14px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -96,8 +132,6 @@ body {
width: 24px;
height: 24px;
padding: 0;
flex: 0 0 24px;
margin-left: 16px;
}
.power-button {
@@ -127,6 +161,13 @@ body {
transform: translateX(-50%);
}
#exitBtn {
position: absolute;
top: 15px;
right: 18px;
margin-left: 0;
}
.toolbar,
.tool-left,
.tool-right {
@@ -136,10 +177,14 @@ body {
}
.toolbar {
flex: 0 0 auto;
height: auto;
min-height: 84px;
justify-content: space-between;
padding: 14px 30px;
padding: 15px 18px 0;
background: #15191e;
border-bottom: 1px solid #2d343d;
overflow: visible;
}
.tool-left {
@@ -147,9 +192,15 @@ body {
flex: 1 1 auto;
}
.toolbar .tool-left,
.toolbar .tool-right {
min-height: 54px;
flex-wrap: wrap;
}
.tool-left > *,
.tool-right > * {
margin: 0 12px 0 0;
margin: 0 12px 15px 0;
}
.tool-right > * {
@@ -159,11 +210,12 @@ body {
.tool-right {
justify-content: flex-end;
flex: 0 0 auto;
flex: 1 1 auto;
}
button,
.button {
height: 54px;
min-height: 54px;
min-width: 96px;
padding: 0 20px;
@@ -223,17 +275,24 @@ button.primary:hover {
}
.paste-action > * {
flex: 0 0 auto;
margin-right: 8px;
}
.paste-name {
display: inline-block;
flex: 1 1 auto;
min-width: 0;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.paste-count {
white-space: nowrap;
}
.content {
position: relative;
flex: 1 1 auto;
@@ -264,6 +323,7 @@ button.primary:hover {
table {
width: 100%;
min-width: 1034px;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
@@ -278,6 +338,7 @@ td {
overflow: hidden;
text-overflow: ellipsis;
font-size: 20px;
line-height: 24px;
}
th {
@@ -295,7 +356,7 @@ th {
}
.name-col {
min-width: 360px;
min-width: 600px;
}
td.name-col {
@@ -332,6 +393,7 @@ tr {
}
thead tr {
height: 78px;
cursor: default;
}
@@ -349,6 +411,7 @@ tr[data-type="parent"] {
}
.select-hit {
height: 78px;
min-height: 78px;
width: 100%;
display: flex;
@@ -372,6 +435,7 @@ input[type="checkbox"] {
.row-action {
width: 100%;
height: 78px;
min-height: 78px;
padding: 16px 18px;
border: 0;
@@ -402,10 +466,11 @@ input[type="checkbox"] {
.status {
color: #aab4be;
padding: 10px 25px;
}
.status {
flex: 0 0 auto;
height: 46px;
min-height: 46px;
padding: 0 25px;
line-height: 46px;
border-top: 1px solid #2d343d;
background: #15191e;
font-size: 18px;
@@ -436,6 +501,7 @@ input[type="checkbox"] {
.text-editor-panel {
width: 1200px;
height: calc(100vh - 96px);
min-width: 0;
max-width: calc(100vw - 96px);
display: flex;
flex-direction: column;
@@ -447,6 +513,7 @@ input[type="checkbox"] {
}
.text-editor-path {
flex: 0 0 auto;
margin-bottom: 14px;
overflow: hidden;
color: #c9d3dc;
@@ -496,6 +563,8 @@ input[type="checkbox"] {
}
.image-preview-stage {
width: 100%;
min-width: 0;
min-height: 0;
flex: 1 1 auto;
display: flex;
@@ -506,8 +575,10 @@ input[type="checkbox"] {
.image-preview {
display: block;
width: 100%;
height: 100%;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
object-fit: contain;
pointer-events: none;
-webkit-user-drag: none;
@@ -567,6 +638,8 @@ input[type="checkbox"] {
display: flex;
align-items: flex-start;
font-size: 24px;
justify-content: space-between;
position: relative;
}
.task-head.has-amount {
@@ -583,13 +656,18 @@ input[type="checkbox"] {
.task-current {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-current-plain {
text-overflow: ellipsis;
}
.task-name {
display: flex;
min-width: 0;
text-overflow: ellipsis;
flex: 1 1 auto;
}
.task-name > * {
@@ -609,6 +687,14 @@ input[type="checkbox"] {
white-space: nowrap;
}
.task-elapsed {
flex: 0 0 auto;
white-space: nowrap;
text-align: right;
margin-left: 16px;
color: #aab4be;
}
.task-meta {
display: flex;
color: #c9d3dc;
@@ -677,18 +763,215 @@ input[type="checkbox"] {
background: #8a929b;
}
@media (max-width: 900px) {
.topbar {
align-items: stretch;
flex-direction: column;
@media (max-width: 815px) {
.top-left {
padding: 0 40px 0 0;
}
th:nth-child(4),
td:nth-child(4),
th:nth-child(5),
td:nth-child(5),
th:nth-child(6),
td:nth-child(6) {
.top-right {
padding: 10px 0;
}
}
@media (max-width: 640px) {
body {
font-size: 16px;
}
.topbar {
min-height: 42px;
padding: 0 10px;
align-items: center;
flex-wrap: nowrap;
}
.top-left {
flex: 1 1 auto;
margin-top: 0;
padding-right: 34px;
}
.top-right {
display: none;
}
.path {
font-size: 16px;
line-height: 42px;
}
#exitBtn {
top: 9px;
right: 10px;
}
.toolbar {
min-height: 0;
padding: 8px 8px 0;
}
.toolbar .tool-left,
.toolbar .tool-right {
min-height: 0;
}
.tool-left,
.tool-right {
flex: 1 1 100%;
}
.tool-right {
justify-content: flex-start;
}
.tool-left > *,
.tool-right > * {
margin: 0 8px 8px 0;
}
.tool-right > * {
margin-left: 0;
}
button,
.button {
height: 40px;
min-height: 40px;
min-width: 76px;
padding: 0 12px;
border-radius: 6px;
font-size: 16px;
}
.paste-action {
max-width: 100%;
}
.paste-name {
max-width: 120px;
}
table {
min-width: 960px;
}
.name-col {
width: 600px;
}
th,
td {
padding: 10px 12px;
font-size: 16px;
line-height: 20px;
}
tr,
thead tr,
.select-hit,
.row-action {
height: 56px;
min-height: 56px;
}
.row-action {
padding: 10px 12px;
}
input[type="checkbox"] {
width: 28px;
height: 28px;
}
.icon {
width: 26px;
height: 26px;
flex-basis: 26px;
}
.status {
height: 34px;
min-height: 34px;
padding: 0 12px;
line-height: 34px;
font-size: 14px;
}
.task-overlay {
padding: 18px;
align-items: center;
}
.task-panel {
width: 100%;
max-width: none;
padding: 18px;
border-radius: 8px;
}
.task > * + * {
margin-top: 12px;
}
.task-head {
display: block;
font-size: 20px;
}
.task-name {
display: flex;
max-width: none;
white-space: nowrap;
}
.task-name > * {
margin: 0 8px 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-name-subject {
flex: 1 1 auto;
max-width: none;
min-width: 0;
}
.task-elapsed {
margin: 8px 0 0;
text-align: left;
font-size: 18px;
}
.task-current {
font-size: 18px;
white-space: normal;
word-break: break-all;
}
.task-meta {
display: block;
font-size: 18px;
}
.task-meta div,
.task-meta div:nth-child(2),
.task-meta div:nth-child(3) {
display: block;
margin: 0 0 8px;
text-align: left;
white-space: normal;
}
.task-meta div:last-child {
margin-bottom: 0;
}
.progress {
height: 32px;
}
.task .danger {
width: 100%;
}
}
+365 -88
View File
@@ -6,6 +6,7 @@ let trackedTask = null;
let busy = false;
let clipboard = null;
let pendingOverlayText = "";
let pendingOverlayLabel = "";
let pendingAbortController = null;
let taskRefreshPath = "/";
let hoverPaused = false;
@@ -13,20 +14,25 @@ let hoverResumeTimer = 0;
let hoveredRow = null;
let loadingPath = null;
let directoryLoadingTimer = 0;
let directoryLoadingStartedAt = 0;
let taskOverlayTimer = 0;
let lastCompletionId = null;
let taskPollFailedAlertShown = false;
let textEditorPath = null;
let textEditorVersion = null;
let textEditorOriginal = "";
let textEditorBusy = false;
let L = {};
const APP_VERSION = "v0.4";
const APP_VERSION = "v0.9";
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
const DIRECTORY_LOADING_DELAY = 250;
const LOADING_DISPLAY_DELAY = 250;
const SELECT_ALL_LOADING_THRESHOLD = 1000;
const filesEl = document.getElementById("files");
const contentEl = document.getElementById("content");
const contentLoadingEl = document.getElementById("contentLoading");
const contentLoadingTextEl = contentLoadingEl.querySelector(".content-loading-text");
const emptyEl = document.getElementById("empty");
const pathEl = document.getElementById("path");
const spaceInfoEl = document.getElementById("spaceInfo");
@@ -38,6 +44,7 @@ const selectAllEl = document.getElementById("selectAll");
const pasteBtn = document.getElementById("pasteBtn");
const pasteVerbEl = document.getElementById("pasteVerb");
const pasteNameEl = document.getElementById("pasteName");
const pasteCountEl = document.getElementById("pasteCount");
const pasteTargetTextEl = document.getElementById("pasteTargetText");
const clearClipboardBtn = document.getElementById("clearClipboardBtn");
const initLoadingEl = document.getElementById("initLoading");
@@ -50,6 +57,7 @@ const textEditorCloseBtn = document.getElementById("textEditorCloseBtn");
const textEditorSaveBtn = document.getElementById("textEditorSaveBtn");
const newTextBtn = document.getElementById("newTextBtn");
const imagePreviewOverlayEl = document.getElementById("imagePreviewOverlay");
const imagePreviewNameEl = document.getElementById("imagePreviewName");
const imagePreviewEl = document.getElementById("imagePreview");
const imagePreviewCloseBtn = document.getElementById("imagePreviewCloseBtn");
@@ -109,6 +117,55 @@ function applyStaticText() {
if (initLoadingEl) initLoadingEl.hidden = true;
}
function nextPaint() {
return new Promise(resolve => {
if (window.requestAnimationFrame) {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
}
else setTimeout(resolve, 0);
});
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function showContentLoading(text) {
contentLoadingTextEl.textContent = text || t("readDir");
contentLoadingEl.style.top = contentEl.scrollTop + "px";
contentLoadingEl.hidden = false;
contentEl.classList.add("loading");
updateButtons();
}
function startContentLoadingTimer() {
if (!contentLoadingEl.hidden || directoryLoadingTimer) return;
directoryLoadingStartedAt = Date.now();
directoryLoadingTimer = setTimeout(() => {
directoryLoadingTimer = 0;
showContentLoading();
}, LOADING_DISPLAY_DELAY);
}
async function showContentLoadingAfterDelay() {
if (!contentLoadingEl.hidden) return;
const elapsed = directoryLoadingStartedAt ? Date.now() - directoryLoadingStartedAt : 0;
const remaining = LOADING_DISPLAY_DELAY - elapsed;
if (remaining > 0) await delay(remaining);
clearTimeout(directoryLoadingTimer);
directoryLoadingTimer = 0;
showContentLoading();
}
function hideContentLoading() {
clearTimeout(directoryLoadingTimer);
directoryLoadingTimer = 0;
directoryLoadingStartedAt = 0;
contentEl.classList.remove("loading");
contentLoadingEl.hidden = true;
updateButtons();
}
async function request(path, params, options) {
const qs = new URLSearchParams(params || {});
const fetchOptions = Object.assign({ method: "POST" }, options || {});
@@ -126,6 +183,92 @@ async function api(path, params, options) {
return data;
}
async function apiForm(path, params, options) {
const fetchOptions = Object.assign({
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(params || {}).toString()
}, options || {});
const data = await (await request(path, null, fetchOptions)).json();
if (!data.ok) throw new Error(backendErrorText(data.error_code, data.error_arg, data.error));
return data;
}
function decodeFsText(text) {
text = String(text || "");
let needsDecode = false;
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if (code > 0xff) return text;
if (code >= 0x80) needsDecode = true;
}
if (!needsDecode || typeof TextDecoder === "undefined" || typeof Uint8Array === "undefined") return text;
const bytes = new Uint8Array(text.length);
for (let i = 0; i < text.length; i++) bytes[i] = text.charCodeAt(i) & 0xff;
if (isUtf8Bytes(bytes)) {
try {
return new TextDecoder("utf-8").decode(bytes);
} catch (err) {
return text;
}
}
try {
return new TextDecoder("gbk").decode(bytes);
} catch (err) {
try {
return new TextDecoder("gb18030").decode(bytes);
} catch (err2) {
return text;
}
}
}
function isUtf8Bytes(bytes) {
for (let i = 0; i < bytes.length;) {
const c = bytes[i];
let n = 0;
let cp = 0;
if (c < 0x80) {
i++;
continue;
}
if ((c & 0xe0) === 0xc0) {
n = 2;
cp = c & 0x1f;
} else if ((c & 0xf0) === 0xe0) {
n = 3;
cp = c & 0x0f;
} else if ((c & 0xf8) === 0xf0) {
n = 4;
cp = c & 0x07;
} else {
return false;
}
if (i + n > bytes.length) return false;
for (let j = 1; j < n; j++) {
const t = bytes[i + j];
if ((t & 0xc0) !== 0x80) return false;
cp = (cp << 6) | (t & 0x3f);
}
if ((n === 2 && cp < 0x80) ||
(n === 3 && cp < 0x800) ||
(n === 4 && (cp < 0x10000 || cp > 0x10ffff)) ||
(cp >= 0xd800 && cp <= 0xdfff)) return false;
i += n;
}
return true;
}
function displayName(item) {
return item.displayName || decodeFsText(item.name);
}
function displayPath(path) {
return decodeFsText(path);
}
async function fetchText(path, params) {
const response = await request(path, params, { method: "GET" });
return {
@@ -174,22 +317,30 @@ function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours) return hours + "h " + minutes + "m";
if (minutes) return minutes + "m " + secs + "s";
return secs + "s";
if (hours) return t("durationHours", { hours, minutes });
if (minutes) return t("durationMinutes", { minutes, seconds: secs });
return t("durationSeconds", { seconds: secs });
}
function averageEta(task, done, total) {
if (!total || !done || done >= total) return "--";
const elapsed = Math.max(1, Number(task.elapsed || 0));
const averageSpeed = done / elapsed;
return averageSpeed > 0 ? formatDuration((total - done) / averageSpeed) : "--";
const eta = Number(task.eta || 0);
return eta > 0 ? formatDuration(eta) : "--";
}
function taskElapsed(task) {
if (!task || !Object.prototype.hasOwnProperty.call(task, "total_elapsed")) return "--";
return formatDuration(Number(task.total_elapsed || 0));
}
function opLabel(op) {
return { copy: t("copy"), move: t("move"), delete: t("delete") }[op] || op;
}
function taskOpLabel(op) {
return { copy: t("copying"), move: t("moving"), delete: t("deleting") }[op] || op;
}
function stateLabel(state) {
return { queued: t("queued"), running: t("running"), done: t("done"), failed: t("failed"), canceled: t("canceled") }[state] || state;
}
@@ -198,10 +349,12 @@ function isTerminalTask(task) {
return ["done", "failed", "canceled"].includes(task.state);
}
function trackTask(id, fromClipboard) {
function trackTask(id, op, fromClipboard, clearClipboardOnDone) {
trackedTask = {
id,
op,
fromClipboard: Boolean(fromClipboard),
clearClipboardOnDone: Boolean(clearClipboardOnDone),
cancelRequested: false,
state: "queued"
};
@@ -241,7 +394,8 @@ function handleTerminalTask(task) {
return;
}
if (task.state === "done") {
if (trackedTask && trackedTask.id === task.id && trackedTask.fromClipboard) clearClipboardAfterPaste();
if (trackedTask && trackedTask.id === task.id &&
(trackedTask.fromClipboard || trackedTask.clearClipboardOnDone)) clearClipboardAfterPaste();
clearTrackedTask();
setStatus(t("actionDone", { label: opLabel(task.op) }));
}
@@ -256,6 +410,25 @@ function setBusy(value) {
updateButtons();
}
function showTaskOverlay(immediate) {
setBusy(true);
if (!overlayEl.hidden || taskOverlayTimer) return;
if (immediate) {
overlayEl.hidden = false;
return;
}
taskOverlayTimer = setTimeout(() => {
taskOverlayTimer = 0;
overlayEl.hidden = false;
}, LOADING_DISPLAY_DELAY);
}
function hideTaskOverlay() {
clearTimeout(taskOverlayTimer);
taskOverlayTimer = 0;
overlayEl.hidden = true;
}
function showActionFailed(label, error) {
const message = t("actionFailed", { label, error });
setStatus(message);
@@ -330,6 +503,8 @@ function isPreviewableImage(item) {
function openImagePreview(item) {
if (busy) return;
imagePreviewNameEl.textContent = displayName(item);
imagePreviewNameEl.title = displayName(item);
imagePreviewEl.src = "/fs?path=" + encodeURIComponent(item.path);
imagePreviewOverlayEl.hidden = false;
imagePreviewCloseBtn.focus();
@@ -337,6 +512,8 @@ function openImagePreview(item) {
function closeImagePreview() {
imagePreviewOverlayEl.hidden = true;
imagePreviewNameEl.textContent = "";
imagePreviewNameEl.title = "";
imagePreviewEl.removeAttribute("src");
}
@@ -369,8 +546,8 @@ function requestCloseTextEditor() {
function showTextEditor(item, text, version) {
textEditorPath = item.path;
textEditorVersion = version;
textEditorPathEl.textContent = item.path;
textEditorPathEl.title = item.path;
textEditorPathEl.textContent = displayPath(item.path);
textEditorPathEl.title = displayPath(item.path);
textEditorEl.value = text;
textEditorOriginal = textEditorEl.value;
textEditorOverlayEl.hidden = false;
@@ -383,8 +560,8 @@ async function openTextEditor(item) {
textEditorPath = item.path;
textEditorVersion = null;
textEditorOriginal = "";
textEditorPathEl.textContent = item.path;
textEditorPathEl.title = item.path;
textEditorPathEl.textContent = displayPath(item.path);
textEditorPathEl.title = displayPath(item.path);
textEditorEl.value = "";
textEditorOverlayEl.hidden = false;
setTextEditorBusy(true, t("loadingText"));
@@ -438,7 +615,6 @@ async function actionNewText() {
setBusy(true);
setStatus(t("creatingText"));
const data = await api("/api/text/create", { path: dir, name });
await load(dir, false, true);
setBusy(false);
showTextEditor(item, "", data.version);
} catch (err) {
@@ -468,29 +644,36 @@ function clipboardTitle() {
function itemTitle(items) {
if (!items.length) return "";
return items.length === 1 ? items[0].name : t("selectedItems", { name: items[0].name, count: items.length });
return items.length === 1 ? displayName(items[0]) : t("selectedItems", { name: displayName(items[0]), count: items.length });
}
function itemCountSuffix(items) {
if (items.length <= 1) return "";
const text = t("selectedItems", { name: "", count: items.length });
return text.replace(/^\s+/, "");
}
function pathName(path) {
const clean = String(path || "").replace(/\/+$/, "");
const pos = clean.lastIndexOf("/");
return pos >= 0 ? clean.slice(pos + 1) || "/" : clean;
return displayPath(pos >= 0 ? clean.slice(pos + 1) || "/" : clean);
}
function taskSubject(task) {
const count = Number(task.src_count || 0);
return count > 1 ? t("countItems", { count }) : pathName(task.src);
}
function renderTaskName(el, task) {
const count = Number(task.src_count || 0);
const target = count > 1 ? t("countItems", { count }) : pathName(task.src);
const target = taskSubject(task);
const op = document.createElement("span");
op.className = "task-name-op";
op.textContent = opLabel(task.op);
op.textContent = taskOpLabel(task.op);
const subject = document.createElement("span");
subject.className = "task-name-subject";
subject.textContent = target;
subject.title = target;
const state = document.createElement("span");
state.className = "task-name-state";
state.textContent = stateLabel(task.state);
appendChildren(el, op, subject, state);
appendChildren(el, op, subject);
}
function renderClipboard() {
@@ -501,6 +684,7 @@ function renderClipboard() {
if (!hasClipboard) {
pasteVerbEl.textContent = t("paste");
pasteNameEl.textContent = "";
pasteCountEl.textContent = "";
pasteBtn.title = "";
pasteBtn.disabled = true;
clearClipboardBtn.disabled = true;
@@ -509,7 +693,8 @@ function renderClipboard() {
const title = clipboardTitle();
pasteVerbEl.textContent = clipboard.op === "move" ? t("move") : t("paste");
pasteNameEl.textContent = title;
pasteNameEl.textContent = clipboard.items.length === 1 ? title : displayName(clipboard.items[0]);
pasteCountEl.textContent = itemCountSuffix(clipboard.items);
pasteBtn.title = t("pasteTitle", { label: clipboard.op === "move" ? t("move") : t("paste"), name: title, path: cwd });
pasteBtn.disabled = locked;
clearClipboardBtn.disabled = locked;
@@ -598,7 +783,30 @@ function findRowFromTarget(target) {
function togglePath(path, checked) {
if (checked) selected.add(path);
else selected.delete(path);
render();
const row = filesEl.querySelector("tr[data-path=\"" + cssEscape(path) + "\"]");
if (row) {
if (checked) row.classList.add("selected");
else row.classList.remove("selected");
const checkbox = row.querySelector("input[type=\"checkbox\"]");
if (checkbox) checkbox.checked = checked;
}
updateButtons();
}
function clearSelection() {
if (selected.size === 0) {
updateButtons();
return;
}
selected.forEach(path => {
const row = filesEl.querySelector("tr[data-path=\"" + cssEscape(path) + "\"]");
if (!row) return;
row.classList.remove("selected");
const checkbox = row.querySelector("input[type=\"checkbox\"]");
if (checkbox) checkbox.checked = false;
});
selected.clear();
updateButtons();
}
function render() {
@@ -644,8 +852,8 @@ function render() {
nameBtn.type = "button";
const nameText = document.createElement("span");
nameText.className = "name-text";
nameText.textContent = item.name;
nameBtn.title = item.path;
nameText.textContent = displayName(item);
nameBtn.title = displayPath(item.path);
const iconImg = document.createElement("img");
iconImg.className = "icon";
iconImg.src = isParent ? "/icon-up.png" : item.type === "d" ? "/icon-folder.png" :
@@ -695,23 +903,22 @@ async function load(path, scrollTop, force) {
const shouldScrollTop = scrollTop !== false;
if ((busy && !force) || loadingPath) return;
loadingPath = path;
directoryLoadingTimer = setTimeout(() => {
contentLoadingEl.style.top = contentEl.scrollTop + "px";
contentLoadingEl.hidden = false;
contentEl.classList.add("loading");
updateButtons();
}, DIRECTORY_LOADING_DELAY);
startContentLoadingTimer();
try {
setStatus(t("readDir"));
const data = await api("/api/list", { path });
if (contentLoadingEl.hidden && data.entries && data.entries.length > 80) {
await showContentLoadingAfterDelay();
}
if (!contentLoadingEl.hidden) await nextPaint();
cwd = data.path;
pathEl.textContent = cwd;
pathEl.textContent = displayPath(cwd);
savePath(cwd);
refreshSpaces();
entries = data.entries.sort((a, b) => {
if (a.type === "d" && b.type !== "d") return -1;
if (a.type !== "d" && b.type === "d") return 1;
return a.name.localeCompare(b.name);
return displayName(a).localeCompare(displayName(b));
});
selected.clear();
focusedPath = null;
@@ -721,24 +928,20 @@ async function load(path, scrollTop, force) {
} catch (err) {
setStatus(err.message);
} finally {
clearTimeout(directoryLoadingTimer);
contentEl.classList.remove("loading");
contentLoadingEl.hidden = true;
hideContentLoading();
loadingPath = null;
updateButtons();
}
}
async function runAction(label, fn) {
async function runAction(label, fn, options) {
try {
taskRefreshPath = cwd;
setBusy(true);
setStatus(t("actionBusy", { label }));
const data = await fn();
setStatus(t("taskCreated", { label }));
trackTask(data.task_id, false);
selected.clear();
render();
trackTask(data.task_id, "delete", false, options && options.clearClipboardOnDone);
clearSelection();
await pollTasks();
} catch (err) {
setBusy(false);
@@ -752,8 +955,7 @@ async function runImmediateAction(label, fn) {
setBusy(true);
setStatus(t("actionBusy", { label }));
await fn();
selected.clear();
render();
clearSelection();
await load(refreshPath, false, true);
setStatus(t("actionDone", { label }));
} catch (err) {
@@ -768,8 +970,7 @@ function setClipboard(op) {
const items = selectedEntries().map(item => ({ path: item.path, name: item.name, type: item.type }));
if (items.length === 0) return;
clipboard = { op, items };
selected.clear();
render();
renderClipboard();
setStatus(t("selectedForPaste", { name: itemTitle(items), verb: op === "move" ? t("move") : t("paste") }));
}
@@ -780,8 +981,19 @@ function clearClipboard() {
setStatus(t("clipboardCleared"));
}
function pathsTouchClipboard(paths) {
if (!clipboard || !clipboard.items.length) return false;
for (const path of paths) {
for (const item of clipboard.items) {
if (pathIsSameOrChild(path, item.path)) return true;
}
}
return false;
}
function validatePasteTarget() {
const byName = new Map(entries.map(item => [item.name, item]));
const sameTargets = [];
const sameFiles = [];
const sameDirs = [];
@@ -789,10 +1001,8 @@ function validatePasteTarget() {
const targetPath = pathJoin(cwd, item.name);
const existing = byName.get(item.name);
if (item.path === targetPath) {
return {
ok: false,
message: t("sameSourceTarget", { label: clipboard.op === "move" ? t("move") : t("copy"), name: item.name })
};
sameTargets.push(item.name);
continue;
}
if (item.type === "d" && pathIsSameOrChild(item.path, targetPath)) {
return {
@@ -816,17 +1026,26 @@ function validatePasteTarget() {
if (item.type === "d") sameDirs.push(item.name);
else sameFiles.push(item.name);
}
if (sameTargets.length) {
return {
ok: false,
message: t("sameSourceTarget", {
label: clipboard.op === "move" ? t("move") : t("copy"),
name: conflictText(sameTargets)
})
};
}
return { ok: true, sameFiles, sameDirs };
}
function conflictText(names) {
if (!names.length) return "";
const shown = names.slice(0, 4).join(", ");
const shown = names.slice(0, 4).map(decodeFsText).join(", ");
return names.length > 4 ? t("selectedItems", { name: shown, count: names.length }) : shown;
}
function renderPendingOverlay(text) {
function renderPendingOverlay(text, label) {
tasksEl.innerHTML = "";
const div = document.createElement("div");
div.className = "task running";
@@ -835,7 +1054,7 @@ function renderPendingOverlay(text) {
head.className = "task-head has-amount";
const name = document.createElement("div");
name.className = "task-name";
name.textContent = t("preparingTask");
name.textContent = label || t("preparingTask");
const amount = document.createElement("div");
amount.className = "task-amount";
amount.textContent = t("pleaseWait");
@@ -859,7 +1078,8 @@ function renderPendingOverlay(text) {
if (pendingAbortController) pendingAbortController.abort();
pendingAbortController = null;
pendingOverlayText = "";
overlayEl.hidden = true;
pendingOverlayLabel = "";
hideTaskOverlay();
setStatus(t("cancelPrepare"));
setBusy(false);
});
@@ -868,11 +1088,35 @@ function renderPendingOverlay(text) {
tasksEl.appendChild(div);
}
function showPendingOverlay(text) {
function showPendingOverlay(text, label) {
pendingOverlayText = text;
overlayEl.hidden = false;
renderPendingOverlay(text);
setBusy(true);
pendingOverlayLabel = label || "";
renderPendingOverlay(text, pendingOverlayLabel);
showTaskOverlay(true);
}
function renderTaskPath(element, path) {
path = displayPath(path);
element.title = path;
element.textContent = path;
if (element.scrollWidth <= element.clientWidth) return;
let low = 0;
let high = path.length;
while (low < high) {
const count = Math.ceil((low + high) / 2);
const left = Math.ceil(count / 2);
const right = Math.floor(count / 2);
element.textContent = path.slice(0, left) + "..." +
(right ? path.slice(-right) : "");
if (element.scrollWidth <= element.clientWidth) low = count;
else high = count - 1;
}
const left = Math.ceil(low / 2);
const right = Math.floor(low / 2);
element.textContent = path.slice(0, left) + "..." +
(right ? path.slice(-right) : "");
}
async function actionPaste() {
@@ -895,29 +1139,32 @@ async function actionPaste() {
if (!confirm(lines.join("\n") + "\n\n" + t("continueConfirm"))) return;
}
const payload = {
paths: clipboard.items.map(item => item.path).join("\n"),
dst: cwd,
overwrite: hasFileConflicts || hasDirConflicts ? "1" : "0"
};
try {
pendingAbortController = new AbortController();
taskRefreshPath = cwd;
showPendingOverlay(t("preparingPaste", { label }));
showPendingOverlay(t("preparingPaste", { label }), label);
setBusy(true);
setStatus(t("preparingStatus", { label }));
const data = await api("/api/" + op, payload, { signal: pendingAbortController.signal });
await nextPaint();
const payload = {
paths: clipboard.items.map(item => item.path).join("\n"),
dst: cwd,
overwrite: hasFileConflicts || hasDirConflicts ? "1" : "0"
};
const data = await apiForm("/api/" + op, payload, { signal: pendingAbortController.signal });
pendingAbortController = null;
pendingOverlayText = "";
pendingOverlayLabel = "";
setStatus(t("taskCreated", { label }));
trackTask(data.task_id, true);
selected.clear();
render();
trackTask(data.task_id, op, true);
clearSelection();
await pollTasks();
} catch (err) {
const aborted = err && err.name === "AbortError";
pendingAbortController = null;
pendingOverlayText = "";
overlayEl.hidden = true;
pendingOverlayLabel = "";
hideTaskOverlay();
if (aborted) setStatus(t("cancelPrepare"));
else showActionFailed(label, err.message);
renderClipboard();
@@ -928,16 +1175,18 @@ async function actionPaste() {
function renderTasks(tasks) {
const active = tasks.find(task => task.state === "queued" || task.state === "running");
if (!active && pendingOverlayText) {
overlayEl.hidden = false;
renderPendingOverlay(pendingOverlayText);
setBusy(true);
renderPendingOverlay(pendingOverlayText, pendingOverlayLabel);
showTaskOverlay();
return;
}
tasksEl.innerHTML = "";
const hasActive = Boolean(active);
overlayEl.hidden = !hasActive;
setBusy(hasActive);
if (hasActive) showTaskOverlay();
else {
hideTaskOverlay();
setBusy(false);
}
if (!hasActive) return;
@@ -949,7 +1198,7 @@ function renderTasks(tasks) {
const isPreparing = (task.op === "copy" || task.op === "move") &&
task.state === "running" && done === 0;
const isFinishing = !isDelete && task.state === "running" && total > 0 && done >= total;
const pct = total > 0 ? Math.min(100, Math.round(done * 100 / total)) : 0;
const pct = total > 0 ? Math.min(100, Math.floor(done * 100 / total)) : 0;
const div = document.createElement("div");
div.className = "task " + task.state;
@@ -959,10 +1208,18 @@ function renderTasks(tasks) {
name.className = "task-name";
renderTaskName(name, task);
head.appendChild(name);
const elapsed = document.createElement("div");
elapsed.className = "task-elapsed";
elapsed.textContent = t("elapsedLabel") + ": " + taskElapsed(task);
head.appendChild(elapsed);
const current = document.createElement("div");
current.className = "task-current";
current.textContent = task.error || task.current || task.src;
if (task.error) {
current.className += " task-current-plain";
current.textContent = task.error;
}
else current.textContent = displayPath(task.current || task.src);
const meta = document.createElement("div");
meta.className = "task-meta";
@@ -1002,11 +1259,13 @@ function renderTasks(tasks) {
else if (total && !isFinishing) appendChildren(div, head, current, meta, progress, cancel);
else appendChildren(div, head, current, progress, cancel);
tasksEl.appendChild(div);
if (!task.error) renderTaskPath(current, task.current || task.src);
}
async function pollTasks() {
try {
const data = await api("/api/tasks");
taskPollFailedAlertShown = false;
const tasks = data.tasks || [];
const completion = data.completion;
if (lastCompletionId === null) {
@@ -1015,6 +1274,7 @@ async function pollTasks() {
lastCompletionId = completion.id;
alert(t(completion.file_count ? "transferCompleteFiles" : "transferComplete", {
label: opLabel(completion.op),
name: taskSubject(completion),
duration: formatDuration(Number(completion.elapsed || 0)),
size: formatBytes(completion.total, true),
count: completion.file_count
@@ -1022,6 +1282,7 @@ async function pollTasks() {
}
const wasBusy = busy;
renderTasks(tasks);
if (wasBusy && !busy) startContentLoadingTimer();
let shouldRefresh = false;
let sawTrackedTask = false;
for (const task of tasks) {
@@ -1037,7 +1298,8 @@ async function pollTasks() {
}
}
if (trackedTask && wasBusy && !busy && !sawTrackedTask) {
if (trackedTask.fromClipboard && !trackedTask.cancelRequested) clearClipboardAfterPaste();
if ((trackedTask.fromClipboard || trackedTask.clearClipboardOnDone) &&
!trackedTask.cancelRequested) clearClipboardAfterPaste();
clearTrackedTask();
shouldRefresh = true;
}
@@ -1046,7 +1308,8 @@ async function pollTasks() {
await refreshSpaces();
}
} catch (err) {
setStatus(t("tasksPollFailed", { error: err.message }));
const message = t("tasksPollFailed", { error: err.message });
setStatus(message);
}
}
@@ -1062,8 +1325,8 @@ function actionRename() {
if (busy || loadingPath) return;
const item = singleSelected();
if (!item) return;
const name = (prompt(t("renamePrompt"), item.name) || "").trim();
if (!name || name === item.name) return;
const name = (prompt(t("renamePrompt"), displayName(item)) || "").trim();
if (!name || name === displayName(item)) return;
runImmediateAction(t("rename"), () => api("/api/rename", { path: item.path, name }));
}
@@ -1075,7 +1338,9 @@ function actionDelete() {
const confirmKey = items.some(item => item.type === "d") ?
"deleteConfirmRecursive" : "deleteConfirm";
if (!confirm(t(confirmKey, { name: target }))) return;
runAction(t("delete"), () => api("/api/delete", { paths: paths.join("\n") }));
runAction(t("delete"), () => apiForm("/api/delete", { paths: paths.join("\n") }), {
clearClipboardOnDone: pathsTouchClipboard(paths)
});
}
function actionExit() {
@@ -1131,17 +1396,29 @@ filesEl.addEventListener("click", event => {
let target = event.target;
while (target && target !== row && !target.classList.contains("row-action")) target = target.parentNode;
if (target === row && row.dataset.type !== "parent") return;
const item = { path: row.dataset.path, name: row.dataset.name, type: row.dataset.type };
const item = {
path: row.dataset.path,
name: row.dataset.name,
type: row.dataset.type,
displayName: decodeFsText(row.dataset.name)
};
focusPath(item.path);
if (item.type === "parent" || item.type === "d") load(item.path);
if (item.type === "parent" || item.type === "d") setTimeout(() => load(item.path), 0);
else if (isPreviewableImage(item)) openImagePreview(item);
else if (isEditableText(item)) openTextEditor(item);
else togglePath(item.path, !selected.has(item.path));
});
selectAllEl.addEventListener("change", () => {
selectAllEl.addEventListener("change", async () => {
const checked = selectAllEl.checked;
const useLoading = entries.length >= SELECT_ALL_LOADING_THRESHOLD;
if (useLoading) {
showContentLoading(t("processing"));
await nextPaint();
}
selected.clear();
if (selectAllEl.checked) entries.forEach(item => selected.add(item.path));
if (checked) entries.forEach(item => selected.add(item.path));
render();
if (useLoading) hideContentLoading();
});
contentEl.addEventListener("mousemove", event => {
hoverRow(findRowFromTarget(event.target));
@@ -1161,7 +1438,7 @@ async function init() {
applyStaticText();
const savedPath = readSavedPath();
cwd = savedPath;
pathEl.textContent = cwd;
pathEl.textContent = displayPath(cwd);
taskRefreshPath = savedPath;
refreshSpaces();
await pollTasks();
Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

+12 -27
View File
@@ -4,8 +4,6 @@
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <ps5/kernel.h>
@@ -33,9 +31,16 @@ int sceAppInstUtilAppInstallAll(void *);
static int
install_file(const char *path, const uint8_t *data, size_t size) {
struct stat st;
FILE *f;
if(!(f = fopen(path, "w"))) {
if(!stat(path, &st)) {
return 0;
}
if(errno != ENOENT) {
return -1;
}
if(!(f = fopen(path, "wb"))) {
return -1;
}
if(fwrite(data, size, 1, f) != 1) {
@@ -65,32 +70,13 @@ install_app(const char *title_id, const char *dir) {
}
static int
needs_update(const char *path, const uint8_t *expected, size_t expected_size) {
needs_install_file(const char *path) {
struct stat st;
uint8_t *buf;
FILE *f;
int mismatch;
if(stat(path, &st) || st.st_size != (off_t)expected_size) {
if(stat(path, &st)) {
return 1;
}
if(!(f = fopen(path, "r"))) {
return 1;
}
if(!(buf = malloc(expected_size))) {
fclose(f);
return 1;
}
if(fread(buf, 1, expected_size, f) != expected_size) {
free(buf);
fclose(f);
return 1;
}
fclose(f);
mismatch = memcmp(buf, expected, expected_size);
free(buf);
return mismatch != 0;
return 0;
}
int
@@ -111,8 +97,7 @@ app_install_if_needed(void) {
if(stat(base_dir, &st)) {
update_needed = 1;
} else if(needs_update(param_path, param_json, param_json_size) ||
needs_update(icon_path, icon0_png, icon0_png_size)) {
} else if(needs_install_file(param_path) || needs_install_file(icon_path)) {
update_needed = 1;
}
+1295 -128
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -13,7 +13,7 @@
#include "filemgr.h"
#include "websrv.h"
#define REQUEST_BODY_MAX (1024 * 1024)
#define REQUEST_BODY_MAX (4 * 1024 * 1024)
typedef struct request_context {
char *body;
@@ -25,7 +25,7 @@ static enum MHD_Result
websrv_body_too_large(struct MHD_Connection *conn) {
static const char json[] =
"{\"ok\":false,\"error\":\"request body is too large\","
"\"error_code\":\"text_file_too_large\",\"error_arg\":\"\"}";
"\"error_code\":\"request_body_too_large\",\"error_arg\":\"\"}";
struct MHD_Response *resp =
MHD_create_response_from_buffer(sizeof(json) - 1, (void *)json,
MHD_RESPMEM_PERSISTENT);