Compare commits

...
7 Commits
Author SHA1 Message Date
owendswang 48d669b848 Circle button to go back to parent folder
Block browser closing while processing tasks and displaying overlay

Auto focus to parent folder after going back
2026-08-11 22:01:25 +08:00
owendswang dafbd8dcbf Optimize address bar for path display overflow 2026-08-09 14:50:38 +08:00
owendswang 939257eda0 Fixed PKG install from internal storage.
Adjust back button display effects
2026-08-09 12:38:23 +08:00
owendswang 3efaf24385 Highlight specific directories' icon.
Moved the back button to the header of the name column.
2026-08-08 17:47:54 +08:00
owendswang 5302e971f9 Enhance credits with PKG install function reference
Updated the credits section to include PKG install function in the websrv reference.
2026-08-06 21:18:15 +08:00
owendswang 582d154cdf Changed the way to end the daemon process 2026-08-06 14:51:43 +08:00
owendswang a61ac54e1f Added PKG install function 2026-08-06 14:33:02 +08:00
17 changed files with 578 additions and 45 deletions

No files matched your search

+2 -2
View File
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
endif
endif
VERSION_TAG := v1.1
VERSION_TAG := v1.4
TITLE_ID := FMGR88888
PYTHON ?= python3
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
@@ -24,7 +24,7 @@ HOST_PKG_CONFIG ?= pkg-config
BIN := web-file-mgr.elf
LINUX_BIN := web-file-mgr-linux
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
LINUX_SRCS := $(COMMON_SRCS)
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
+2 -1
View File
@@ -38,6 +38,7 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
- Home screen launcher icon and browser favicon use the same embedded `icon0.png` data in the PS5 build to avoid storing the icon twice in the ELF.
- 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`, `.shn`.
- Preview image files ending with `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
- Install PKG files ends with `.pkg`.
## Build
@@ -120,7 +121,7 @@ On first startup the payload installs a `PS5 Web File Manager` web shortcut in t
This project was built with reference to these projects:
- **[ps5-payload-dev/websrv](https://github.com/ps5-payload-dev/websrv):** HTTP server structure, static asset embedding ideas, and PS5 browser/websrv behavior. License: GPLv3+.
- **[ps5-payload-dev/websrv](https://github.com/ps5-payload-dev/websrv):** HTTP server structure, static asset embedding ideas, PS5 browser/websrv behavior and PKG install function. License: GPLv3+.
- **[ps5-payload-dev/ftpsrv](https://github.com/ps5-payload-dev/ftpsrv):** PS5 payload conventions, home screen launcher/install flow reference, process handling style and startup installation reference. License: GPLv3+.
- **[itsPLK/ps5-payload-manager](https://github.com/itsPLK/ps5-payload-manager):** Payload building behavior. License: GPLv3.
- **[libmicrohttpd](https://ftp.gnu.org/gnu/libmicrohttpd/):** Used as the embedded HTTP server library. It is licensed by GNU under the LGPL; this payload links it as the SDK-provided static library.
Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+9 -1
View File
@@ -43,6 +43,7 @@
<button id="renameBtn" data-i18n="rename"></button>
<button id="downloadBtn" class="remote-only" data-i18n="download"></button>
<button id="deleteBtn" class="danger" data-i18n="delete"></button>
<button id="installPkgBtn" class="install-action" data-i18n="install" hidden></button>
<button id="pasteBtn" class="paste-action" hidden>
<span id="pasteVerb" data-i18n="paste"></span>
<span id="pasteName" class="paste-name"></span><span id="pasteCount" class="paste-count"></span>
@@ -71,7 +72,14 @@
<thead>
<tr>
<th class="select-col"><label class="select-hit"><input id="selectAll" type="checkbox"></label></th>
<th class="name-col sortable" data-sort="name" data-i18n="name"></th>
<th class="name-col sortable" data-sort="name">
<div class="name-head">
<button id="parentBtn" class="parent-nav-button" type="button" aria-label="Parent directory" disabled>
<img src="/icon-back.png" alt="">
</button>
<span class="name-heading" data-i18n="name"></span>
</div>
</th>
<th class="type-col sortable" data-sort="type" data-i18n="type"></th>
<th class="size-col sortable" data-sort="size" data-i18n="size"></th>
<th class="time-col sortable" data-sort="mtime" data-i18n="mtime"></th>
+9
View File
@@ -19,7 +19,13 @@ window.WFM_LANG = {
refresh: "Refresh",
mkdir: "New Folder",
newText: "New Text",
install: "Install",
installPackage: "Install package",
pkgInstalling: "Starting package installation: {name}...",
pkgInstallStarted: "Package installation started: {name}",
file: "File",
textFile: "Text",
image: "Image",
dir: "Folder",
parent: "Parent",
name: "Name",
@@ -118,6 +124,9 @@ window.WFM_LANG = {
err_source_destination_same: "Source and destination are the same",
err_destination_inside_source: "Cannot copy or move a folder inside itself",
err_invalid_path: "Invalid path",
err_pkg_type_invalid: "Only .pkg files can be installed",
err_pkg_install_failed: "Package installation failed ({arg})",
err_pkg_install_unsupported: "Package installation is only available on PS5",
err_file_not_found: "File not found",
err_file_already_exists: "A file with the same name already exists",
err_invalid_method: "Invalid request method",
+9
View File
@@ -19,7 +19,13 @@ window.WFM_LANG = {
refresh: "刷新",
mkdir: "新建目录",
newText: "新建文本",
install: "安装",
installPackage: "安装 PKG",
pkgInstalling: "正在提交安装:{name}...",
pkgInstallStarted: "已开始安装:{name}",
file: "文件",
textFile: "文本",
image: "图片",
dir: "目录",
parent: "上级目录",
name: "名称",
@@ -118,6 +124,9 @@ window.WFM_LANG = {
err_source_destination_same: "源和目标相同",
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
err_invalid_path: "路径无效",
err_pkg_type_invalid: "只能安装 .pkg 文件",
err_pkg_install_failed: "PKG 安装失败({arg})",
err_pkg_install_unsupported: "PKG 安装仅支持 PS5 环境",
err_file_not_found: "文件不存在",
err_file_already_exists: "同名文件已存在",
err_invalid_method: "请求方式无效",
+114 -9
View File
@@ -65,13 +65,13 @@ body {
}
.top-left {
flex: 1 1 420px;
flex: 1 1 0;
min-width: 0;
margin-top: 15px;
}
.top-right {
flex: 1 1 360px;
flex: 0 1 auto;
min-width: 0;
margin-left: auto;
justify-content: flex-end;
@@ -88,6 +88,7 @@ body {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
flex: 1 1 auto;
min-width: 0;
}
@@ -110,6 +111,7 @@ body {
}
.space-item {
flex: 0 0 auto;
margin-left: 14px;
max-width: 100%;
overflow: hidden;
@@ -297,8 +299,8 @@ button:focus,
outline-offset: 2px;
}
button:not(.row-action):hover,
.button:hover {
button:not(.row-action):hover:not(:disabled),
.button:hover:not(:disabled) {
background: #303945;
}
@@ -319,7 +321,7 @@ button:disabled,
background: #285943;
}
button.primary:hover {
button.primary:hover:not(:disabled) {
background: #347358;
}
@@ -329,7 +331,7 @@ button.primary:hover {
}
.paste-action {
max-width: 480px;
max-width: 360px;
}
.paste-action > * {
@@ -341,12 +343,23 @@ button.primary:hover {
display: inline-block;
flex: 1 1 auto;
min-width: 0;
max-width: 180px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.install-action {
min-width: 88px;
border-color: #8c6425;
background: #352b1c;
color: #f8dfae;
}
button.install-action:hover:not(:disabled) {
background: #433521;
}
.paste-count {
white-space: nowrap;
}
@@ -429,6 +442,80 @@ th.sorted-desc::after {
content: "v";
}
.name-head {
height: 78px;
min-height: 78px;
display: flex;
align-items: center;
min-width: 0;
}
th.name-col {
padding: 0;
}
th.name-col.sortable::after {
content: none;
}
.name-heading {
min-width: 100px;
padding-left: 18px;
overflow: hidden;
text-overflow: ellipsis;
}
.name-heading::after {
content: "";
display: inline-block;
width: 12px;
margin-left: 8px;
color: #aeb8c2;
text-align: center;
}
th.name-col.sorted-asc .name-heading::after {
content: "^";
}
th.name-col.sorted-desc .name-heading::after {
content: "v";
}
.parent-nav-button {
width: 78px;
min-width: 78px;
height: 78px;
min-height: 78px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
position: relative;
}
.parent-nav-button img {
width: 32px;
height: 32px;
display: block;
}
.parent-nav-button:hover:not(:disabled) {
background: #222932;
}
.parent-nav-button:not(:disabled):focus {
outline-offset: -5px;
}
.parent-nav-button:disabled img {
opacity: .42;
}
.parent-nav-button:disabled {
pointer-events: auto;
}
.select-col {
width: 74px;
padding: 0;
@@ -516,6 +603,13 @@ input[type="checkbox"] {
display: block;
}
.special-folder-icon {
-webkit-filter: hue-rotate(65deg);
filter: hue-rotate(65deg);
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
.row-action {
width: 100%;
height: 78px;
@@ -647,6 +741,7 @@ input[type="checkbox"] {
.image-preview-stage {
width: 100%;
height: calc(100vh - 250px);
min-width: 0;
min-height: 0;
flex: 1 1 auto;
@@ -661,8 +756,7 @@ input[type="checkbox"] {
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
object-fit: contain;
max-height: calc(100vh - 250px);
pointer-events: none;
-webkit-user-drag: none;
user-select: none;
@@ -975,12 +1069,23 @@ input[type="checkbox"] {
tr,
thead tr,
.name-head,
.parent-nav-button,
.select-hit,
.row-action {
height: 56px;
min-height: 56px;
}
.parent-nav-button {
width: 56px;
min-width: 56px;
}
.name-heading {
padding-left: 12px;
}
.row-action {
padding: 10px 12px;
}
+240 -24
View File
@@ -18,6 +18,9 @@ let directoryLoadingStartedAt = 0;
let taskOverlayTimer = 0;
let lastCompletionId = null;
let taskPollFailedAlertShown = false;
let localActionBusy = false;
let specialStoragePaths = {};
let hasSpecialMntStorage = false;
let textEditorPath = null;
let textEditorVersion = null;
let textEditorOriginal = "";
@@ -26,7 +29,7 @@ let downloadFrame = null;
let uploadXhr = null;
let L = {};
const APP_VERSION = "v1.1";
const APP_VERSION = "v1.4";
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
const SORT_KEY = "ps5-web-file-mgr:list-sort";
const LOADING_DISPLAY_DELAY = 250;
@@ -49,11 +52,13 @@ const versionEl = document.getElementById("versionText");
const tasksEl = document.getElementById("tasks");
const overlayEl = document.getElementById("taskOverlay");
const selectAllEl = document.getElementById("selectAll");
const parentBtn = document.getElementById("parentBtn");
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 installPkgBtn = document.getElementById("installPkgBtn");
const clearClipboardBtn = document.getElementById("clearClipboardBtn");
const downloadBtn = document.getElementById("downloadBtn");
const uploadMenuEl = document.getElementById("uploadMenu");
@@ -131,6 +136,8 @@ function applyStaticText() {
}
exitBtn.title = t("exit");
exitBtn.setAttribute("aria-label", t("exit"));
parentBtn.title = t("parent");
parentBtn.setAttribute("aria-label", t("parent"));
versionEl.textContent = APP_VERSION;
if (initLoadingEl) initLoadingEl.hidden = true;
}
@@ -309,6 +316,23 @@ function displayPath(path) {
return decodeFsText(path);
}
function renderAddressPath() {
const path = displayPath(cwd);
pathEl.title = path;
pathEl.textContent = path;
if (pathEl.scrollWidth <= pathEl.clientWidth) return;
let low = 0;
let high = path.length;
while (low < high) {
const count = Math.ceil((low + high) / 2);
pathEl.textContent = "..." + path.slice(-count);
if (pathEl.scrollWidth <= pathEl.clientWidth) low = count;
else high = count - 1;
}
pathEl.textContent = "..." + path.slice(-low);
}
async function fetchText(path, params) {
const response = await request(path, params, { method: "GET" });
return {
@@ -491,6 +515,54 @@ function readSavedPath() {
}
}
function historyPath() {
const hash = window.location.hash || "";
if (hash.length <= 1) return "";
try {
const path = decodeURIComponent(hash.slice(1));
return path.charAt(0) === "/" ? path : "";
} catch (err) {
return "";
}
}
function writeHistoryPath(path, replace) {
if (!window.history || !history.pushState) return;
const hash = "#" + encodeURIComponent(path || "/");
if (window.location.hash === hash) return;
const state = { path: path || "/" };
if (replace && history.replaceState) history.replaceState(state, "", hash);
else history.pushState(state, "", hash);
}
function pathHistoryChain(path) {
const clean = String(path || "/").replace(/\/+$/, "") || "/";
const parts = clean.split("/");
const chain = ["/"];
let current = "";
for (let i = 1; i < parts.length; i++) {
if (!parts[i]) continue;
current += "/" + parts[i];
chain.push(current);
}
return chain;
}
function seedHistoryPath(path) {
if (!window.history || !history.pushState || !history.replaceState) return;
const chain = pathHistoryChain(path);
for (let i = 0; i < chain.length; i++) {
writeHistoryPath(chain[i], i === 0);
}
}
function historyBlocked() {
return Boolean(busy || loadingPath || pendingAbortController ||
pendingOverlayText || taskOverlayTimer || !overlayEl.hidden ||
!contentLoadingEl.hidden || contentEl.classList.contains("loading") ||
!textEditorOverlayEl.hidden || !imagePreviewOverlayEl.hidden);
}
function savePath(path) {
try {
localStorage.setItem(LAST_PATH_KEY, path || "/");
@@ -523,6 +595,7 @@ function saveSort() {
async function refreshSpaces() {
try {
const data = await api("/api/space", { path: cwd });
updateSpecialStoragePaths(data.spaces || []);
spaceInfoEl.innerHTML = "";
if ((data.spaces || []).length) {
const prefix = document.createElement("span");
@@ -539,11 +612,31 @@ async function refreshSpaces() {
span.title = item.path + " " + span.textContent;
spaceInfoEl.appendChild(span);
}
renderAddressPath();
} catch (err) {
spaceInfoEl.textContent = "";
renderAddressPath();
}
}
function updateSpecialStoragePaths(spaces) {
const next = {};
let hasMnt = false;
for (let i = 0; i < spaces.length; i++) {
const path = spaces[i].path || "";
if (/^\/mnt\/(?:usb|ext)[0-9]+$/.test(path)) {
next[path] = true;
hasMnt = true;
}
}
const changed = hasMnt !== hasSpecialMntStorage ||
Object.keys(next).join("\n") !== Object.keys(specialStoragePaths).join("\n");
specialStoragePaths = next;
hasSpecialMntStorage = hasMnt;
if (changed && entries.length) render();
}
function selectedEntries() {
return entries.filter(item => selected.has(item.path));
}
@@ -559,6 +652,7 @@ function pathIsSameOrChild(parent, child) {
}
function typeLabel(type) {
if (type === "parent") return t("parent");
return type === "d" ? t("dir") : t("file");
}
@@ -571,6 +665,61 @@ function isPreviewableImage(item) {
return item.type === "-" && /\.(png|jpe?g|gif|bmp|webp)$/i.test(item.name);
}
function isPkgPackage(item) {
return item.type === "-" && /\.pkg$/i.test(item.name);
}
function isSpecialDirectory(item) {
if (item.type !== "d") return false;
if (item.path === "/data") return true;
if (item.path === "/mnt") return hasSpecialMntStorage;
return Boolean(specialStoragePaths[item.path]);
}
function itemTypeLabel(item) {
if (item.type === "parent" || item.type === "d") return typeLabel(item.type);
if (isPkgPackage(item)) return "PKG";
if (isPreviewableImage(item)) return t("image");
if (isEditableText(item)) return t("textFile");
return t("file");
}
async function installPkg(item) {
if (busy || loadingPath) return;
try {
localActionBusy = true;
setBusy(true);
setStatus(t("pkgInstalling", { name: displayName(item) }));
await api("/api/install-pkg", { path: item.path });
setStatus(t("pkgInstallStarted", { name: displayName(item) }));
} catch (err) {
showActionFailed(t("installPackage"), err.message);
} finally {
localActionBusy = false;
setBusy(false);
}
}
async function actionInstallSelectedPkgs() {
if (busy || loadingPath) return;
const items = selectedEntries().filter(isPkgPackage);
if (!items.length) return;
try {
localActionBusy = true;
setBusy(true);
for (let i = 0; i < items.length; i++) {
setStatus(t("pkgInstalling", { name: displayName(items[i]) }));
await api("/api/install-pkg", { path: items[i].path });
}
setStatus(t("pkgInstallStarted", { name: itemTitle(items) }));
} catch (err) {
showActionFailed(t("installPackage"), err.message);
} finally {
localActionBusy = false;
setBusy(false);
}
}
function openImagePreview(item) {
if (busy) return;
imagePreviewNameEl.textContent = displayName(item);
@@ -708,6 +857,17 @@ function resetScrollTop() {
if (window.requestAnimationFrame) requestAnimationFrame(apply);
}
function revealPathInList(path) {
const row = filesEl.querySelector("tr[data-path=\"" + cssEscape(path) + "\"]");
if (!row) return;
const top = row.offsetTop;
const maxScroll = contentEl.scrollHeight - contentEl.clientHeight;
if (maxScroll <= 0) return;
const target = top - Math.floor((contentEl.clientHeight - row.offsetHeight) / 2);
contentEl.scrollTop = Math.max(0, Math.min(maxScroll, target));
}
function clipboardTitle() {
if (!clipboard || clipboard.items.length === 0) return "";
return itemTitle(clipboard.items);
@@ -740,7 +900,7 @@ function compareEntries(a, b) {
let result = 0;
if (sortKey === "name") result = compareText(displayName(a), displayName(b));
else if (sortKey === "type") result = compareText(typeLabel(a.type), typeLabel(b.type));
else if (sortKey === "type") result = compareText(itemTypeLabel(a), itemTypeLabel(b));
else if (sortKey === "size") result = (a.type === "d" ? 0 : Number(a.size || 0)) -
(b.type === "d" ? 0 : Number(b.size || 0));
else if (sortKey === "mtime") result = Number(a.mtime || 0) - Number(b.mtime || 0);
@@ -804,6 +964,19 @@ function renderClipboard() {
clearClipboardBtn.disabled = locked;
}
function renderInstallPkgButton(items, locked) {
const pkgs = items.filter(isPkgPackage);
installPkgBtn.hidden = pkgs.length === 0;
if (!pkgs.length) {
installPkgBtn.title = "";
installPkgBtn.disabled = true;
return;
}
const title = itemTitle(pkgs);
installPkgBtn.title = t("installPackage") + ": " + title;
installPkgBtn.disabled = locked;
}
function singleSelected() {
const items = selectedEntries();
return items.length === 1 ? items[0] : null;
@@ -824,8 +997,10 @@ function updateButtons() {
document.getElementById("mkdirBtn").disabled = locked;
newTextBtn.disabled = locked;
selectAllEl.disabled = locked;
parentBtn.disabled = cwd === "/";
selectAllEl.checked = entries.length > 0 && items.length === entries.length;
selectAllEl.indeterminate = items.length > 0 && items.length < entries.length;
renderInstallPkgButton(items, locked);
renderClipboard();
}
@@ -964,14 +1139,7 @@ function render() {
emptyEl.hidden = entries.length !== 0;
const fragment = document.createDocumentFragment();
const rows = cwd === "/" ? entries : [{
name: "..",
path: parentPath(cwd),
type: "parent",
mode: 0,
size: 0,
mtime: 0
}].concat(entries);
const rows = entries;
for (const item of rows) {
const isParent = item.type === "parent";
@@ -1004,8 +1172,9 @@ function render() {
nameText.textContent = displayName(item);
nameBtn.title = displayPath(item.path);
const iconImg = document.createElement("img");
iconImg.className = "icon";
iconImg.className = "icon" + (isSpecialDirectory(item) ? " special-folder-icon" : "");
iconImg.src = isParent ? "/icon-up.png" : item.type === "d" ? "/icon-folder.png" :
isPkgPackage(item) ? "/icon-pkg.png" :
isPreviewableImage(item) ? "/icon-image.png" :
isEditableText(item) ? "/icon-file.png" : "/icon-generic.png";
iconImg.alt = "";
@@ -1017,7 +1186,7 @@ function render() {
tr,
selectTd,
nameTd,
classCell("type-col", isParent ? t("parent") : item.type === "d" ? t("dir") : t("file")),
classCell("type-col", itemTypeLabel(item)),
classCell("size-col", formatSize(item.size, item.type)),
classCell("time-col", formatTime(item.mtime)),
classCell("mode-col", isParent ? "" : "0" + Number(item.mode).toString(8))
@@ -1035,6 +1204,19 @@ function parentPath(path) {
return parts.join("/") || "/";
}
function actionParentDirectory(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (busy || loadingPath || cwd === "/") return;
if (window.history && history.back && historyPath() === cwd) {
history.back();
} else {
load(parentPath(cwd), undefined, false, true, "push");
}
}
function cell(text) {
const td = document.createElement("td");
td.textContent = text;
@@ -1048,7 +1230,7 @@ function classCell(className, text) {
return td;
}
async function load(path, scrollTop, force, alertOnError) {
async function load(path, scrollTop, force, alertOnError, historyMode) {
const shouldScrollTop = scrollTop !== false;
if ((busy && !force) || loadingPath) return;
loadingPath = path;
@@ -1061,8 +1243,9 @@ async function load(path, scrollTop, force, alertOnError) {
}
if (!contentLoadingEl.hidden) await nextPaint();
cwd = data.path;
pathEl.textContent = displayPath(cwd);
renderAddressPath();
savePath(cwd);
if (historyMode) writeHistoryPath(cwd, historyMode === "replace");
refreshSpaces();
entries = data.entries.sort(compareEntries);
selected.clear();
@@ -1076,11 +1259,17 @@ async function load(path, scrollTop, force, alertOnError) {
if (alertOnError) alert(err.message);
return false;
} finally {
hideContentLoading();
loadingPath = null;
hideContentLoading();
}
}
async function loadAndReveal(path, scrollTop, force, alertOnError, historyMode, revealPath) {
const ok = await load(path, scrollTop, force, alertOnError, historyMode);
if (ok && revealPath) revealPathInList(revealPath);
return ok;
}
async function runAction(label, fn, options) {
try {
taskRefreshPath = cwd;
@@ -1164,10 +1353,10 @@ function validatePasteTarget() {
return {
ok: false,
message: t("removeConflictFirst", {
existingType: typeLabel(existing.type),
existingType: itemTypeLabel(existing),
name: item.name,
label: clipboard.op === "move" ? t("move") : t("copy"),
sourceType: typeLabel(item.type)
sourceType: itemTypeLabel(item)
})
};
}
@@ -1334,7 +1523,7 @@ function renderTasks(tasks) {
DOWNLOAD_OVERLAY_DISPLAY_DELAY : LOADING_DISPLAY_DELAY);
else {
hideTaskOverlay();
setBusy(false);
if (!localActionBusy) setBusy(false);
}
if (!hasActive) return;
@@ -1436,8 +1625,11 @@ async function pollTasks() {
}));
}
const wasBusy = busy;
const wasTaskBusy = Boolean(trackedTask || pendingOverlayText ||
!overlayEl.hidden || taskOverlayTimer);
renderTasks(tasks);
const becameIdle = wasBusy && !busy;
const taskBecameIdle = wasTaskBusy && !busy;
let shouldRefresh = false;
let sawTrackedTask = false;
for (const task of tasks) {
@@ -1458,9 +1650,9 @@ async function pollTasks() {
shouldRefresh = true;
clearTrackedTask();
}
if (becameIdle && !tasks.length) shouldRefresh = true;
if (taskBecameIdle && !tasks.length) shouldRefresh = true;
if (shouldRefresh) {
if (becameIdle) startContentLoadingTimer();
if (taskBecameIdle) startContentLoadingTimer();
clearSelection(false);
await load(taskRefreshPath || cwd, false);
await refreshSpaces();
@@ -1690,6 +1882,7 @@ document.getElementById("mkdirBtn").addEventListener("click", () => {
document.getElementById("copyBtn").addEventListener("click", actionCopy);
document.getElementById("moveBtn").addEventListener("click", actionMove);
pasteBtn.addEventListener("click", actionPaste);
installPkgBtn.addEventListener("click", actionInstallSelectedPkgs);
clearClipboardBtn.addEventListener("click", clearClipboard);
document.getElementById("renameBtn").addEventListener("click", actionRename);
downloadBtn.addEventListener("click", actionDownload);
@@ -1700,16 +1893,31 @@ uploadFolderBtn.addEventListener("click", actionUploadFolder);
uploadFilesEl.addEventListener("change", () => uploadFiles(uploadFilesEl.files));
uploadFolderEl.addEventListener("change", () => uploadFiles(uploadFolderEl.files));
exitBtn.addEventListener("click", actionExit);
parentBtn.addEventListener("click", actionParentDirectory);
textEditorCloseBtn.addEventListener("click", requestCloseTextEditor);
textEditorSaveBtn.addEventListener("click", saveTextEditor);
newTextBtn.addEventListener("click", actionNewText);
imagePreviewCloseBtn.addEventListener("click", closeImagePreview);
window.addEventListener("resize", renderAddressPath);
window.addEventListener("popstate", event => {
if (historyBlocked()) {
writeHistoryPath(cwd, false);
setStatus(t("activeTask"));
return;
}
const path = event.state && event.state.path ? event.state.path : historyPath();
if (path && path !== cwd) {
const revealPath = parentPath(cwd) === path ? cwd : "";
loadAndReveal(path, false, false, true, null, revealPath);
}
});
document.addEventListener("click", event => {
if (!uploadMenuEl || uploadMenuEl.contains(event.target)) return;
uploadMenuEl.classList.remove("open");
});
document.querySelector("thead").addEventListener("click", event => {
if (busy || loadingPath) return;
if (parentBtn.contains(event.target)) return;
let target = event.target;
while (target && target.tagName !== "TH") target = target.parentNode;
if (target && target.getAttribute("data-sort")) setSort(target.getAttribute("data-sort"));
@@ -1755,10 +1963,14 @@ filesEl.addEventListener("click", event => {
};
focusPath(item.path);
if (item.type === "parent" || item.type === "d") {
const revealPath = item.type === "parent" ? cwd : "";
setTimeout(async () => {
if (!(await load(item.path, undefined, false, true))) await load(cwd, false, true);
if (await load(item.path, item.type === "parent" ? false : undefined, false, true, "push")) {
if (revealPath) revealPathInList(revealPath);
} else await load(cwd, false, true);
}, 0);
}
else if (isPkgPackage(item)) installPkg(item);
else if (isPreviewableImage(item)) openImagePreview(item);
else if (isEditableText(item)) openTextEditor(item);
else togglePath(item.path, !selected.has(item.path));
@@ -1797,9 +2009,9 @@ async function init() {
applyStaticText();
readSavedSort();
updateSortHeaders();
const savedPath = readSavedPath();
const savedPath = historyPath() || readSavedPath();
cwd = savedPath;
pathEl.textContent = displayPath(cwd);
renderAddressPath();
taskRefreshPath = savedPath;
refreshSpaces();
await pollTasks();
@@ -1807,7 +2019,11 @@ async function init() {
setStatus(t("activeTask"));
return;
}
if (!(await load(savedPath || "/")) && savedPath !== "/") await load("/");
if (await load(savedPath || "/", undefined, false, false)) {
seedHistoryPath(cwd);
} else if (savedPath !== "/" && await load("/", undefined, false, false)) {
seedHistoryPath(cwd);
}
}
init();
+2 -2
View File
@@ -9,6 +9,7 @@
#include <ps5/kernel.h>
#include "asset.h"
#include "pkg_installer.h"
#define INCASSET(name, file) \
__asm__(".section .rodata\n" \
@@ -28,7 +29,6 @@
INCASSET(param_json, "assets/param.json");
INCASSET(icon0_png, "assets/icon0.png");
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void *);
static int
@@ -111,7 +111,7 @@ app_install_if_needed(void) {
printf("Installing launcher app %s\n", title_id);
if((err = sceAppInstUtilInitialize())) {
if((err = pkg_installer_initialize())) {
printf("sceAppInstUtilInitialize: error 0x%08X\n", err);
return -1;
}
+6 -1
View File
@@ -2,11 +2,13 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "filemgr_internal.h"
#include "mime.h"
#include "path_util.h"
#include "websrv.h"
static ssize_t
@@ -31,7 +33,7 @@ file_close(void *cls) {
enum MHD_Result
filemgr_fs_request(struct MHD_Connection *conn) {
const char *path = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "path");
char *path = fs_path_value(query_value(conn, "path"));
struct MHD_Response *resp;
enum MHD_Result ret = MHD_NO;
struct stat st;
@@ -43,6 +45,7 @@ filemgr_fs_request(struct MHD_Connection *conn) {
if(!path || stat(path, &st) || !S_ISREG(st.st_mode) ||
!(file = fopen(path, "rb"))) {
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
}
@@ -55,9 +58,11 @@ filemgr_fs_request(struct MHD_Connection *conn) {
}
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
free(path);
return ret;
}
fclose(file);
free(path);
return MHD_NO;
}
+45 -3
View File
@@ -18,6 +18,7 @@
#include "filemgr_internal.h"
#include "json_util.h"
#include "path_util.h"
#include "pkg_installer.h"
#include "websrv.h"
#define COPY_BUFFER_SIZE (8 * 1024 * 1024)
@@ -1867,10 +1868,10 @@ api_cancel(struct MHD_Connection *conn) {
}
static void *
exit_later(void *arg) {
stop_websrv_later(void *arg) {
(void)arg;
usleep(250000);
exit(0);
websrv_stop();
return NULL;
}
@@ -1878,7 +1879,7 @@ static enum MHD_Result
api_exit(struct MHD_Connection *conn) {
pthread_t thread;
if(!pthread_create(&thread, NULL, exit_later, NULL)) {
if(!pthread_create(&thread, NULL, stop_websrv_later, NULL)) {
pthread_detach(thread);
}
return send_json_ok(conn);
@@ -2008,6 +2009,42 @@ api_mkdir(struct MHD_Connection *conn) {
: send_json_ok(conn);
}
static enum MHD_Result
api_install_pkg(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
const char *extension;
struct stat st;
char error_code[16];
int result;
if(!path || !(extension = strrchr(path, '.')) ||
strcasecmp(extension, ".pkg")) {
free(path);
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
"file is not a PKG package",
"pkg_type_invalid", NULL);
}
if(stat(path, &st) || !S_ISREG(st.st_mode)) {
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
}
result = pkg_installer_install(path);
free(path);
if(result == PKG_INSTALL_UNSUPPORTED) {
return send_json_error_detail(conn, MHD_HTTP_NOT_IMPLEMENTED,
"package installation is only available on PS5",
"pkg_install_unsupported", NULL);
}
if(result) {
snprintf(error_code, sizeof(error_code), "0x%08X", (unsigned int)result);
return send_json_error_detail(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"package installation failed",
"pkg_install_failed", error_code);
}
return send_json_ok(conn);
}
enum MHD_Result
filemgr_api_request(struct MHD_Connection *conn, const char *url,
const char *method, const char *body, size_t body_size) {
@@ -2029,6 +2066,11 @@ filemgr_api_request(struct MHD_Connection *conn, const char *url,
if(!strcmp(url, "/api/upload/finish")) return api_upload_finish(conn);
if(!strcmp(url, "/api/rename")) return api_rename(conn);
if(!strcmp(url, "/api/mkdir")) return api_mkdir(conn);
if(!strcmp(url, "/api/install-pkg")) {
return strcmp(method, MHD_HTTP_METHOD_POST) ?
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
api_install_pkg(conn);
}
if(!strcmp(url, "/api/text")) {
return strcmp(method, MHD_HTTP_METHOD_GET) ?
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
+3
View File
@@ -150,6 +150,9 @@ main(int argc, char **argv) {
#endif
websrv_listen(port);
if(websrv_stop_requested()) {
break;
}
sleep(3);
}
+108
View File
@@ -0,0 +1,108 @@
#include "pkg_installer.h"
#ifndef __linux__
#include <limits.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
typedef struct pkg_metadata {
const char *uri;
const char *ex_uri;
const char *playgo_scenario_id;
const char *content_id;
const char *content_name;
const char *icon_url;
} pkg_metadata_t;
typedef struct pkg_info {
char content_id[48];
int type;
int platform;
} pkg_info_t;
typedef struct playgo_info {
char lang[8][30];
char scenario_ids[3][64];
char content_ids[64];
long unknown[810];
} playgo_info_t;
int sceAppInstUtilInitialize(void);
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *,
playgo_info_t *);
static pthread_mutex_t installer_lock = PTHREAD_MUTEX_INITIALIZER;
static int installer_initialized;
static int
initialize_locked(void) {
int result;
if(!installer_initialized) {
result = sceAppInstUtilInitialize();
if(result) return result;
installer_initialized = 1;
}
return 0;
}
int
pkg_installer_initialize(void) {
int result;
pthread_mutex_lock(&installer_lock);
result = initialize_locked();
pthread_mutex_unlock(&installer_lock);
return result;
}
int
pkg_installer_install(const char *path) {
char install_path[PATH_MAX + sizeof("/user")];
const char *uri = path;
pkg_metadata_t metadata = {
.uri = NULL,
.ex_uri = "",
.playgo_scenario_id = "",
.content_id = "",
.content_name = "",
.icon_url = ""
};
pkg_info_t pkg_info = {0};
playgo_info_t playgo_info = {0};
int result;
if(!path) return -1;
if(!strncmp(path, "/data/", 6)) {
snprintf(install_path, sizeof(install_path), "/user%s", path);
uri = install_path;
}
metadata.uri = uri;
pthread_mutex_lock(&installer_lock);
result = initialize_locked();
if(result) {
pthread_mutex_unlock(&installer_lock);
return result;
}
result = sceAppInstUtilInstallByPackage(&metadata, &pkg_info, &playgo_info);
pthread_mutex_unlock(&installer_lock);
return result;
}
#else
int
pkg_installer_initialize(void) {
return PKG_INSTALL_UNSUPPORTED;
}
int
pkg_installer_install(const char *path) {
(void)path;
return PKG_INSTALL_UNSUPPORTED;
}
#endif
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#define PKG_INSTALL_UNSUPPORTED 0x7fffffff
int pkg_installer_initialize(void);
int pkg_installer_install(const char *path);
+21 -2
View File
@@ -15,6 +15,9 @@
#define REQUEST_BODY_MAX (4 * 1024 * 1024)
static volatile sig_atomic_t g_stop_requested;
static int g_listen_fd = -1;
typedef struct request_context {
char *body;
size_t size;
@@ -51,6 +54,19 @@ websrv_queue_response(struct MHD_Connection *conn, unsigned int status,
return MHD_queue_response(conn, status, resp);
}
void
websrv_stop(void) {
g_stop_requested = 1;
if(g_listen_fd >= 0) {
shutdown(g_listen_fd, SHUT_RDWR);
}
}
int
websrv_stop_requested(void) {
return g_stop_requested;
}
static enum MHD_Result
websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
const char *method, const char *version,
@@ -190,6 +206,8 @@ websrv_listen(unsigned short port) {
close(srvfd);
return -1;
}
g_stop_requested = 0;
g_listen_fd = srvfd;
if(!(httpd = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_ITC |
MHD_USE_NO_LISTEN_SOCKET | MHD_USE_DEBUG |
@@ -202,10 +220,10 @@ websrv_listen(unsigned short port) {
return -1;
}
while(1) {
while(!g_stop_requested) {
addr_len = sizeof(client_addr);
if((connfd = accept(srvfd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
perror("accept");
if(!g_stop_requested) perror("accept");
break;
}
if(MHD_add_connection(httpd, connfd, (struct sockaddr *)&client_addr,
@@ -217,5 +235,6 @@ websrv_listen(unsigned short port) {
}
MHD_stop_daemon(httpd);
g_listen_fd = -1;
return close(srvfd);
}
+2
View File
@@ -23,3 +23,5 @@ enum MHD_Result websrv_queue_response(struct MHD_Connection *conn,
struct MHD_Response *resp);
int websrv_listen(unsigned short port);
void websrv_stop(void);
int websrv_stop_requested(void);