mirror of
https://github.com/ItsBlurf/BFpilot.git
synced 2026-10-18 23:06:53 -05:00
feat: improve file manager clipboard and layout
This commit is contained in:
1 parent
e00627b42a
commit
f7dee7f7e8
11 files changed
+338
-86
No files matched your search
+17
-1
@@ -1,6 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased performance pass
|
||||
## v0.4.3 Alpha 1 (2026-07-26)
|
||||
|
||||
* Copy and Move now work like a file-manager clipboard: select items, stage the
|
||||
action, navigate to a destination, then choose **Paste Here**
|
||||
* Restored double-click folder opening while keeping single-click selection and
|
||||
the explicit **Open** button
|
||||
* Fixed the upload drop area consuming the file-list height in short or empty
|
||||
folders
|
||||
* Dragging files anywhere over the browser now reveals the upload state and
|
||||
accepts the drop
|
||||
* Fixed virtual scrolling to listen to the actual file-list scroller
|
||||
* Reorganized file actions and disabled controls that do not apply to the
|
||||
current selection
|
||||
* The clipboard panel now explains the workflow, shows staged items and the
|
||||
current destination, and includes quick destination shortcuts
|
||||
* The three-column layout collapses earlier so the clipboard no longer gets
|
||||
clipped on common laptop-sized browser windows
|
||||
|
||||
* Large regular-file copies overlap sequential reads with one writer through
|
||||
two fixed 8 MiB, PS5-page-aligned slots; small files and initialization
|
||||
|
||||
@@ -24,8 +24,8 @@ PYTHON ?= python3
|
||||
# Ship / daily use must stay 5905.
|
||||
WEB_PORT ?= 5905
|
||||
|
||||
VERSION_TAG := bfpilot-v0.4.2-test
|
||||
BUILD_VERSION ?= v0.4.2-test
|
||||
VERSION_TAG := bfpilot-v0.4.3-alpha.1
|
||||
BUILD_VERSION ?= v0.4.3-alpha.1
|
||||
|
||||
LLVM_CONFIG ?= $(if $(wildcard $(PS5_PAYLOAD_SDK)/bin/prospero-llvm-config),$(PS5_PAYLOAD_SDK)/bin/prospero-llvm-config,$(CURDIR)/build-tools/llvm-config)
|
||||
LLVM_BINDIR ?= $(shell "$(LLVM_CONFIG)" --bindir 2>/dev/null || dirname "$$(command -v clang 2>/dev/null || command -v clang.exe 2>/dev/null || command -v llvm-strip 2>/dev/null || command -v llvm-strip.exe 2>/dev/null || echo clang)" 2>/dev/null || echo .)
|
||||
|
||||
@@ -8,7 +8,7 @@ http://<PS5_IP>:5905/
|
||||
|
||||
Works from the PS5 browser or any PC/phone on the same network.
|
||||
|
||||
Version **v0.4.2-test**. This is a test build; the v0.4.1 baseline was tested
|
||||
Version **v0.4.3-alpha.1**. This is an alpha build; the v0.4.1 baseline was tested
|
||||
on FW **11.60**.
|
||||
|
||||
## Payloads
|
||||
@@ -26,7 +26,7 @@ on FW **11.60**.
|
||||
## Features
|
||||
|
||||
**Browse & transfer**
|
||||
- Dual panels: left = browse, right = Target for copy/move/extract
|
||||
- File clipboard: select items, choose Copy or Move, open the destination, then choose Paste Here
|
||||
- Places: `/`, `/data`, `/data/homebrew`, `/user`, `/mnt`, USB/ext when plugged in, custom shortcuts
|
||||
- Upload files or a whole folder using the picker or drag-and-drop (recursive
|
||||
dropped folders need the browser's WebKit entry API)
|
||||
@@ -36,7 +36,8 @@ on FW **11.60**.
|
||||
- Free space check before copy/move when size is known
|
||||
- Same-device, no-symlink recursive traversal with a 64-level depth cap and immediate abort on fatal storage/vnode errors
|
||||
- ShadowMountPlus warning before writes to its common scan roots
|
||||
- Folder size on click
|
||||
- Single-click selects; double-click opens folders; the Open button works with controllers
|
||||
- Folder size is calculated when a folder is selected
|
||||
|
||||
**Selection**
|
||||
- Checkboxes + select all (works fine with a controller)
|
||||
|
||||
+235
-70
@@ -268,7 +268,7 @@
|
||||
.browser {
|
||||
min-height: 650px;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr);
|
||||
grid-template-rows: auto auto auto auto minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -331,9 +331,25 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-right: 8px;
|
||||
margin-right: 2px;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.action-group:last-of-type {
|
||||
border-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.upload-drop {
|
||||
margin: 0 12px 12px;
|
||||
padding: 12px;
|
||||
min-height: 44px;
|
||||
border: 1px dashed #465b70;
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
@@ -343,9 +359,16 @@
|
||||
}
|
||||
|
||||
.upload-drop.active {
|
||||
border-color: var(--accent);
|
||||
background: rgba(76, 201, 240, 0.08);
|
||||
color: var(--text);
|
||||
border-color: var(--deep-2);
|
||||
background: rgba(52, 211, 153, 0.12);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.browser.drag-active .upload-drop {
|
||||
border-color: var(--deep-2);
|
||||
background: rgba(52, 211, 153, 0.12);
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 0 0 1px rgba(94, 224, 170, 0.2);
|
||||
}
|
||||
|
||||
.count {
|
||||
@@ -473,6 +496,30 @@
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.target-hint {
|
||||
margin: -2px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.clipboard-status {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #0c1219;
|
||||
padding: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.clipboard-status strong {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.target-path {
|
||||
height: 36px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -653,7 +700,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
@media (max-width: 1300px) {
|
||||
.shell {
|
||||
grid-template-columns: 132px minmax(0, 1fr);
|
||||
}
|
||||
@@ -736,6 +783,14 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
width: 100%;
|
||||
padding: 0 0 6px;
|
||||
margin: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.file-head,
|
||||
.file-row {
|
||||
grid-template-columns: 28px 40px minmax(0, 1fr) 78px;
|
||||
@@ -824,18 +879,22 @@
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="new-folder">New Folder</button>
|
||||
<button id="new-text">New Text File</button>
|
||||
<button id="rename">Rename</button>
|
||||
<button id="download">Download</button>
|
||||
<button id="load-payload" class="primary" title="Send selected .elf/.bin to elfldr on port 9021">Load payload</button>
|
||||
<button id="open-folder" title="Open selected folder">Open</button>
|
||||
<button id="chmod" title="Set permissions on selected paths">chmod</button>
|
||||
<button id="edit-file" class="primary">Edit</button>
|
||||
<button id="extract" class="primary">Extract</button>
|
||||
<button id="upload-files" class="warn">Upload Files</button>
|
||||
<button id="upload-folder" class="warn">Upload Folder</button>
|
||||
<button id="delete" class="danger">Delete</button>
|
||||
<span class="action-group">
|
||||
<button id="new-folder">New Folder</button>
|
||||
<button id="new-text">New Text File</button>
|
||||
<button id="upload-files" class="warn">Upload Files</button>
|
||||
<button id="upload-folder" class="warn">Upload Folder</button>
|
||||
</span>
|
||||
<span class="action-group">
|
||||
<button id="open-folder" title="Open the selected folder">Open</button>
|
||||
<button id="rename">Rename</button>
|
||||
<button id="download">Download</button>
|
||||
<button id="edit-file" class="primary">Edit</button>
|
||||
<button id="load-payload" class="primary" title="Send selected .elf/.bin/.js to elfldr on port 9021">Load payload</button>
|
||||
<button id="extract" class="primary">Extract</button>
|
||||
<button id="chmod" title="Set permissions on selected paths">chmod</button>
|
||||
<button id="delete" class="danger">Delete</button>
|
||||
</span>
|
||||
<span id="selection" class="count">0 selected</span>
|
||||
</div>
|
||||
|
||||
@@ -854,18 +913,24 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="target" aria-label="Transfer target">
|
||||
<h2>Target</h2>
|
||||
<input id="target-path" class="target-path" spellcheck="false" value="/data">
|
||||
<aside class="target" aria-label="File clipboard">
|
||||
<h2>Clipboard</h2>
|
||||
<p class="target-hint">Select files or folders, choose Copy or Move, open the destination, then choose Paste Here.</p>
|
||||
<input id="target-path" class="target-path" spellcheck="false" value="/data" readonly aria-label="Current paste destination">
|
||||
<div class="target-buttons">
|
||||
<button id="copy" class="primary">Copy Here</button>
|
||||
<button id="move">Move Here</button>
|
||||
<button id="copy" class="primary">Copy</button>
|
||||
<button id="move">Move</button>
|
||||
</div>
|
||||
<div id="clipboard-status" class="clipboard-status">
|
||||
<strong>Clipboard empty</strong>
|
||||
Select items in the file list.
|
||||
</div>
|
||||
<div class="target-buttons">
|
||||
<button id="paste" class="primary" disabled>Paste Here</button>
|
||||
<button id="clear-clipboard" disabled>Clear</button>
|
||||
</div>
|
||||
<div class="rail-title">Jump to destination</div>
|
||||
<div class="target-grid" id="target-grid"></div>
|
||||
<div class="target-actions">
|
||||
<button id="target-current">Current</button>
|
||||
<button id="open-target">Open</button>
|
||||
</div>
|
||||
<div class="detail" id="detail">
|
||||
<strong>No selection</strong>
|
||||
<span>Ready</span>
|
||||
@@ -954,7 +1019,8 @@
|
||||
active: false,
|
||||
root: 'all',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
clipboard: null
|
||||
};
|
||||
|
||||
const el = id => document.getElementById(id);
|
||||
@@ -1163,8 +1229,7 @@
|
||||
}
|
||||
button.title = (place.label ? place.label + ' | ' : '') + path;
|
||||
button.addEventListener('click', () => {
|
||||
el('target-path').value = path;
|
||||
updatePlaces();
|
||||
openPath(path);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
@@ -1180,7 +1245,8 @@
|
||||
seen.add(path);
|
||||
}
|
||||
for (const place of state.places) {
|
||||
if (!place.custom) continue;
|
||||
const isStorage = /^\/mnt\/(usb[0-7]|ext[01])(?:\/|$)/.test(cleanPath(place.path));
|
||||
if (!place.custom && !isStorage) continue;
|
||||
const path = cleanPath(place.path);
|
||||
if (seen.has(path)) continue;
|
||||
seen.add(path);
|
||||
@@ -1225,12 +1291,8 @@
|
||||
|
||||
function activeShortcut() {
|
||||
const current = cleanPath(state.path);
|
||||
let match = state.places.find((place) => place.custom && cleanPath(place.path) === current);
|
||||
if (!match) {
|
||||
const target = cleanPath(el('target-path').value || '');
|
||||
match = state.places.find((place) => place.custom && cleanPath(place.path) === target);
|
||||
}
|
||||
return match || null;
|
||||
return state.places.find((place) =>
|
||||
place.custom && cleanPath(place.path) === current) || null;
|
||||
}
|
||||
|
||||
async function addShortcut() {
|
||||
@@ -1251,7 +1313,7 @@
|
||||
async function removeShortcut() {
|
||||
const match = activeShortcut();
|
||||
if (!match) {
|
||||
toast('Open a custom shortcut first, or put one in Target', 'err');
|
||||
toast('Open a custom shortcut first', 'err');
|
||||
return;
|
||||
}
|
||||
if (!confirm('Remove shortcut "' + (match.label || match.path) + '"?')) return;
|
||||
@@ -1267,7 +1329,7 @@
|
||||
async function customizeShortcut() {
|
||||
const match = activeShortcut();
|
||||
if (!match) {
|
||||
toast('Open a custom shortcut first, or put one in Target', 'err');
|
||||
toast('Open a custom shortcut first', 'err');
|
||||
return;
|
||||
}
|
||||
const label = prompt('Shortcut name', match.label || match.path);
|
||||
@@ -1597,16 +1659,78 @@
|
||||
const currentCustom = state.places.some((place) => {
|
||||
return place.custom && cleanPath(place.path) === state.path;
|
||||
});
|
||||
const targetCustom = state.places.some((place) => place.custom &&
|
||||
cleanPath(place.path) === cleanPath(el('target-path').value || ''));
|
||||
el('remove-shortcut').disabled = !currentCustom && !targetCustom;
|
||||
el('customize-shortcut').disabled = !currentCustom && !targetCustom;
|
||||
el('remove-shortcut').disabled = !currentCustom;
|
||||
el('customize-shortcut').disabled = !currentCustom;
|
||||
}
|
||||
|
||||
function updateClipboard() {
|
||||
const status = el('clipboard-status');
|
||||
const clipboard = state.clipboard;
|
||||
el('target-path').value = state.path;
|
||||
status.textContent = '';
|
||||
|
||||
const title = document.createElement('strong');
|
||||
if (!clipboard || !clipboard.entries.length) {
|
||||
title.textContent = 'Clipboard empty';
|
||||
status.appendChild(title);
|
||||
status.appendChild(document.createTextNode('Select items in the file list.'));
|
||||
el('paste').disabled = true;
|
||||
el('clear-clipboard').disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const count = clipboard.entries.length;
|
||||
title.textContent = (clipboard.verb === 'move' ? 'Move ' : 'Copy ') +
|
||||
count + (count === 1 ? ' item' : ' items');
|
||||
status.appendChild(title);
|
||||
const names = clipboard.entries.slice(0, 3).map((entry) => entry.name).join(', ');
|
||||
status.appendChild(document.createTextNode(
|
||||
names + (count > 3 ? ' +' + (count - 3) + ' more' : '')
|
||||
));
|
||||
el('paste').disabled = false;
|
||||
el('clear-clipboard').disabled = false;
|
||||
}
|
||||
|
||||
function stageClipboard(verb) {
|
||||
const entries = selectedEntries();
|
||||
if (!entries.length) {
|
||||
toast('Select files or folders first', 'err');
|
||||
return;
|
||||
}
|
||||
state.clipboard = {
|
||||
verb,
|
||||
entries: entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
dir: !!entry.dir,
|
||||
path: entryPath(entry)
|
||||
}))
|
||||
};
|
||||
state.selected.clear();
|
||||
drawList();
|
||||
updateClipboard();
|
||||
toast((verb === 'move' ? 'Move' : 'Copy') +
|
||||
' ready. Open a destination and choose Paste Here.', 'ok');
|
||||
}
|
||||
|
||||
function clearClipboard() {
|
||||
state.clipboard = null;
|
||||
updateClipboard();
|
||||
}
|
||||
|
||||
function updateSelection() {
|
||||
const token = ++state.detailToken;
|
||||
const entries = selectedEntries();
|
||||
const one = entries.length === 1 ? entries[0] : null;
|
||||
el('selection').textContent = entries.length + ' selected';
|
||||
el('open-folder').disabled = !(one && one.dir);
|
||||
el('rename').disabled = !one;
|
||||
el('download').disabled = !(one && !one.dir);
|
||||
el('load-payload').disabled = !(one && canLaunchPayload(one));
|
||||
el('chmod').disabled = entries.length === 0;
|
||||
el('extract').disabled = !(one && !one.dir && isArchiveName(one.name));
|
||||
el('delete').disabled = entries.length === 0;
|
||||
el('copy').disabled = entries.length === 0;
|
||||
el('move').disabled = entries.length === 0;
|
||||
if (el('edit-file')) {
|
||||
const hasSelection = entries.length === 1;
|
||||
const isEditable = hasSelection && canEdit(entries[0]);
|
||||
@@ -1614,6 +1738,7 @@
|
||||
el('edit-file').disabled = !(isEditable || isViewable);
|
||||
el('edit-file').textContent = isViewable ? 'View' : 'Edit';
|
||||
}
|
||||
updateClipboard();
|
||||
|
||||
const detail = el('detail');
|
||||
if (!entries.length) {
|
||||
@@ -1715,11 +1840,6 @@
|
||||
drawList();
|
||||
return;
|
||||
}
|
||||
/* Click folder name (or whole row without ctrl) opens directory (#7) */
|
||||
if (entry.dir && !event.ctrlKey && !event.metaKey) {
|
||||
openPath(entryPath(entry));
|
||||
return;
|
||||
}
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (state.selected.has(key)) state.selected.delete(key);
|
||||
else state.selected.add(key);
|
||||
@@ -1791,7 +1911,7 @@
|
||||
scroller.style.position = 'relative';
|
||||
list.appendChild(scroller);
|
||||
|
||||
const parent = list.parentElement; // .main-body
|
||||
const parent = list;
|
||||
|
||||
function renderViewport() {
|
||||
const viewportHeight = parent.clientHeight || 600;
|
||||
@@ -1992,15 +2112,8 @@
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function transfer(verb) {
|
||||
const entries = selectedEntries();
|
||||
const target = cleanPath(el('target-path').value);
|
||||
if (!entries.length) {
|
||||
toast('No selection', 'err');
|
||||
return;
|
||||
}
|
||||
if (!confirmShadowMountWrite(target, verb)) return;
|
||||
|
||||
async function executeTransfer(verb, entries, target) {
|
||||
let completed = true;
|
||||
for (const entry of entries) {
|
||||
const source = entryPath(entry);
|
||||
try {
|
||||
@@ -2031,21 +2144,52 @@
|
||||
} catch (retryErr) {
|
||||
if ((retryErr.message || '').toLowerCase().includes('job')) pollJob();
|
||||
toast(verb + ': ' + retryErr.message, 'err');
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// skip (continue to next entry)
|
||||
completed = false;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if ((err.message || '').toLowerCase().includes('job')) pollJob();
|
||||
toast(verb + ': ' + err.message, 'err');
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await refreshCurrentView();
|
||||
return completed;
|
||||
}
|
||||
|
||||
async function pasteClipboard() {
|
||||
const clipboard = state.clipboard;
|
||||
if (!clipboard || !clipboard.entries.length) {
|
||||
toast('Clipboard is empty', 'err');
|
||||
return;
|
||||
}
|
||||
|
||||
const target = state.path;
|
||||
for (const entry of clipboard.entries) {
|
||||
const source = cleanPath(entry.path);
|
||||
if (entry.dir && (target === source || target.startsWith(source + '/'))) {
|
||||
toast('Cannot paste a folder into itself', 'err');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!confirmShadowMountWrite(target, clipboard.verb)) return;
|
||||
|
||||
const completed = await executeTransfer(
|
||||
clipboard.verb, clipboard.entries, target);
|
||||
if (completed) {
|
||||
toast((clipboard.verb === 'move' ? 'Moved' : 'Copied') +
|
||||
' to ' + target, 'ok');
|
||||
if (clipboard.verb === 'move') clearClipboard();
|
||||
} else {
|
||||
toast('Some clipboard items were not pasted', 'err');
|
||||
}
|
||||
}
|
||||
|
||||
async function waitJobIdle() {
|
||||
@@ -2320,6 +2464,9 @@
|
||||
};
|
||||
readNext();
|
||||
});
|
||||
if (!parent && entry.dir) {
|
||||
row.title = 'Double-click to open folder';
|
||||
}
|
||||
}
|
||||
|
||||
async function droppedUploads(dataTransfer) {
|
||||
@@ -2824,15 +2971,10 @@
|
||||
el('chmod').addEventListener('click', chmodSelected);
|
||||
el('extract').addEventListener('click', extractSelected);
|
||||
el('delete').addEventListener('click', deleteSelected);
|
||||
el('copy').addEventListener('click', () => transfer('copy'));
|
||||
el('move').addEventListener('click', () => transfer('move'));
|
||||
el('target-current').addEventListener('click', () => {
|
||||
el('target-path').value = state.path;
|
||||
});
|
||||
el('open-target').addEventListener('click', () => {
|
||||
openPath(el('target-path').value);
|
||||
});
|
||||
el('target-path').addEventListener('input', updatePlaces);
|
||||
el('copy').addEventListener('click', () => stageClipboard('copy'));
|
||||
el('move').addEventListener('click', () => stageClipboard('move'));
|
||||
el('paste').addEventListener('click', pasteClipboard);
|
||||
el('clear-clipboard').addEventListener('click', clearClipboard);
|
||||
el('job-cancel').addEventListener('click', () => {
|
||||
if (state.archiveTimer) {
|
||||
toast('Archive extraction cannot be cancelled yet', 'err');
|
||||
@@ -2876,13 +3018,36 @@
|
||||
} catch (err) {}
|
||||
|
||||
const drop = el('upload-drop');
|
||||
drop.addEventListener('dragover', (event) => {
|
||||
const browser = document.querySelector('.browser');
|
||||
let dragDepth = 0;
|
||||
const hasDroppedFiles = (event) => {
|
||||
const types = event.dataTransfer && event.dataTransfer.types;
|
||||
return types && Array.prototype.indexOf.call(types, 'Files') >= 0;
|
||||
};
|
||||
browser.addEventListener('dragenter', (event) => {
|
||||
if (!hasDroppedFiles(event)) return;
|
||||
event.preventDefault();
|
||||
dragDepth++;
|
||||
browser.classList.add('drag-active');
|
||||
drop.classList.add('active');
|
||||
});
|
||||
drop.addEventListener('dragleave', () => drop.classList.remove('active'));
|
||||
drop.addEventListener('drop', async (event) => {
|
||||
browser.addEventListener('dragover', (event) => {
|
||||
if (!hasDroppedFiles(event)) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
browser.addEventListener('dragleave', () => {
|
||||
dragDepth = Math.max(0, dragDepth - 1);
|
||||
if (dragDepth === 0) {
|
||||
browser.classList.remove('drag-active');
|
||||
drop.classList.remove('active');
|
||||
}
|
||||
});
|
||||
browser.addEventListener('drop', async (event) => {
|
||||
if (!hasDroppedFiles(event)) return;
|
||||
event.preventDefault();
|
||||
dragDepth = 0;
|
||||
browser.classList.remove('drag-active');
|
||||
drop.classList.remove('active');
|
||||
const files = await droppedUploads(event.dataTransfer);
|
||||
if (!files.length) {
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
# BFpilot v0.4.3 Alpha 1
|
||||
|
||||
This alpha contains the work added after v0.4.0:
|
||||
|
||||
- Separate Media tile installer, one `/data/BFpilot` data directory, and clean
|
||||
reinjection/shutdown behavior
|
||||
- Payload loading through elfldr, chmod, case-insensitive paths, multi-select,
|
||||
and RAR/7z/ZIP extraction
|
||||
- Safe text editing, recursive folder upload, conflict handling, staged
|
||||
overwrite/merge, physical no-symlink traversal, and ShadowMountPlus warnings
|
||||
- Faster bounded large-file copies, writeback checkpoints, download tuning,
|
||||
and earlier-return search queries with expanded telemetry
|
||||
- Clipboard-style Copy/Move/Paste, restored double-click folder opening,
|
||||
selection-aware actions, fixed drag feedback, correct empty-folder sizing,
|
||||
and responsive clipboard layout
|
||||
|
||||
This still needs hardware testing for copy/move conflicts, large transfers,
|
||||
Rest Mode, and USB disconnect behavior before it should be considered stable.
|
||||
@@ -13,8 +13,9 @@ const pathOps = fs.readFileSync(path.join(root, 'src', 'path_ops.c'), 'utf8')
|
||||
const versionH = fs.readFileSync(path.join(root, 'src', 'version.h'), 'utf8')
|
||||
const makefile = fs.readFileSync(path.join(root, 'Makefile'), 'utf8')
|
||||
|
||||
// #7 folder open via click on dir name
|
||||
assert(html.includes('entry.dir') && html.includes('openPath(entryPath(entry))'), '#7 openPath on dir click')
|
||||
// #7 folder open via double-click or the explicit Open button
|
||||
assert(html.includes("row.addEventListener('dblclick'") &&
|
||||
html.includes('openPath(next)'), '#7 double-click folder open')
|
||||
assert(html.includes('open-folder') || html.includes('openSelectedFolder'), '#7 open folder control')
|
||||
|
||||
// #8 case-insensitive resolve
|
||||
@@ -38,7 +39,7 @@ assert(html.includes('select-all-checkbox'), '#11 select all')
|
||||
assert(html.includes('selectedEntries'), '#11 selection helper')
|
||||
|
||||
// #12 version
|
||||
assert(versionH.includes('v0.4.2-test'), 'version.h v0.4.2-test')
|
||||
assert(makefile.includes('v0.4.2-test'), 'Makefile v0.4.2-test')
|
||||
assert(versionH.includes('v0.4.3-alpha.1'), 'version.h v0.4.3-alpha.1')
|
||||
assert(makefile.includes('v0.4.3-alpha.1'), 'Makefile v0.4.3-alpha.1')
|
||||
|
||||
console.log('issue surface checks passed (7-12,10)')
|
||||
@@ -72,13 +72,14 @@ assert(tileBoot.includes('BFPILOT_INSTALLER_ELF_PATH') ||
|
||||
tileBoot.includes('/data/BFpilot/bfpilot-launcher-installer.elf'))
|
||||
|
||||
// --- Prior issue surface (#7-12, #10) ---
|
||||
assert(html.includes('openPath(entryPath(entry))'), '#7')
|
||||
assert(html.includes("row.addEventListener('dblclick'") &&
|
||||
html.includes('openPath(next)'), '#7')
|
||||
assert(pathOps.includes('bfpilot_path_resolve_ci'), '#8')
|
||||
assert(html.includes("endsWith('.7z')"), '#9')
|
||||
assert(transfer.includes('chmod_request') || transfer.includes('/api/fs/chmod'), '#10')
|
||||
assert(html.includes('chmodSelected') || html.includes('id="chmod"'), '#10 ui')
|
||||
assert(html.includes('row-checkbox') && html.includes('select-all-checkbox'), '#11')
|
||||
assert(versionH.includes('v0.4.2-test') && makefile.includes('v0.4.2-test'),
|
||||
'current test-build version')
|
||||
assert(versionH.includes('v0.4.3-alpha.1') && makefile.includes('v0.4.3-alpha.1'),
|
||||
'current alpha version')
|
||||
|
||||
console.log('v0.4.2-test separate-launcher + UX host checks passed')
|
||||
console.log('v0.4.3-alpha.1 separate-launcher + UX host checks passed')
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from 'assert'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const html = fs.readFileSync(path.join(root, 'assets', 'files.html'), 'utf8')
|
||||
const version = fs.readFileSync(path.join(root, 'src', 'version.h'), 'utf8')
|
||||
const makefile = fs.readFileSync(path.join(root, 'Makefile'), 'utf8')
|
||||
|
||||
assert(
|
||||
/\.browser\s*\{[^}]*grid-template-rows:\s*auto auto auto auto minmax\(0,\s*1fr\)/s.test(html),
|
||||
'upload row cannot consume the flexible file-list height'
|
||||
)
|
||||
assert(!html.includes('var(--accent)') && !html.includes('var(--text)'),
|
||||
'drag state uses defined theme colors')
|
||||
assert(html.includes("const parent = list;"), 'virtual list listens to its own scroller')
|
||||
assert(html.includes("row.addEventListener('dblclick'"), 'double-click handler exists')
|
||||
|
||||
const clickBlock = html.slice(
|
||||
html.indexOf("row.addEventListener('click'"),
|
||||
html.indexOf("row.addEventListener('dblclick'")
|
||||
)
|
||||
assert(!clickBlock.includes('if (entry.dir'), 'single click selects instead of opening a folder')
|
||||
|
||||
for (const marker of [
|
||||
'id="clipboard-status"',
|
||||
'id="paste"',
|
||||
'stageClipboard',
|
||||
'pasteClipboard',
|
||||
'clearClipboard',
|
||||
'Paste Here'
|
||||
]) {
|
||||
assert(html.includes(marker), `clipboard flow includes ${marker}`)
|
||||
}
|
||||
|
||||
assert(html.includes("el('open-folder').disabled"), 'Open disabled-state is selection-aware')
|
||||
assert(html.includes("el('extract').disabled"), 'Extract disabled-state is selection-aware')
|
||||
assert(html.includes("browser.addEventListener('dragenter'"), 'whole browser reveals drag state')
|
||||
assert(html.includes("browser.addEventListener('drop'"), 'whole browser accepts dropped files')
|
||||
assert(!html.includes('id="target-current"') && !html.includes('id="open-target"'),
|
||||
'old direct-target controls removed')
|
||||
assert(version.includes('v0.4.3-alpha.1') && makefile.includes('v0.4.3-alpha.1'),
|
||||
'alpha version is consistent')
|
||||
|
||||
const scriptMatch = html.match(/<script>([\s\S]*)<\/script>/)
|
||||
assert(scriptMatch, 'inline application script found')
|
||||
new Function(scriptMatch[1])
|
||||
|
||||
console.log('v0.4.3 alpha UI checks passed')
|
||||
+2
-2
@@ -5,11 +5,11 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef VERSION_TAG
|
||||
#define VERSION_TAG "bfpilot-v0.4.2-test"
|
||||
#define VERSION_TAG "bfpilot-v0.4.3-alpha.1"
|
||||
#endif
|
||||
|
||||
#ifndef BUILD_VERSION
|
||||
#define BUILD_VERSION "v0.4.2-test"
|
||||
#define BUILD_VERSION "v0.4.3-alpha.1"
|
||||
#endif
|
||||
|
||||
#ifndef BFPILOT_SDK_PATH
|
||||
|
||||
Reference in new issue
Block a user