From 013364afac285e9fd3d71d27d52c8fb27e4551a6 Mon Sep 17 00:00:00 2001 From: joshuaboud Date: Wed, 25 May 2022 16:12:31 -0300 Subject: [PATCH] work on selection --- .../src/components/DirectoryEntryList.vue | 144 +++++++++--------- 1 file changed, 71 insertions(+), 73 deletions(-) diff --git a/navigator-vue/src/components/DirectoryEntryList.vue b/navigator-vue/src/components/DirectoryEntryList.vue index cd8b50f..673054b 100644 --- a/navigator-vue/src/components/DirectoryEntryList.vue +++ b/navigator-vue/src/components/DirectoryEntryList.vue @@ -7,14 +7,15 @@ :searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)" @edit="(...args) => $emit('edit', ...args)" - @toggleSelected="entry.selected = !entry.selected" + @toggleSelected="modifiers => selection.toggle(entry, index, modifiers)" + @deselectAll="selection.deselectAllBackward()" @sortEntries="sortEntries" @updateStats="emitStats" @startProcessing="(...args) => $emit('startProcessing', ...args)" @stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level" - :class="['border-2 box-border', entry.selected ? 'border-dashed border-x-red-600/50' : 'border-x-transparent', (entry.selected && !entries[index - 1]?.selected) ? 'border-t-red-600/50' : 'border-t-transparent', (entry.selected && !entries[index + 1]?.selected) ? 'border-b-red-600/50' : 'border-b-transparent']" + :neighboursSelected="{ above: entries[index - 1]?.selected ?? false, below: entries[index + 1]?.selected ?? false }" /> { + const entrySelectedValue = entry.selected; + if (!modifiers.ctrlKey) { + const tmpLastSelectedInd = selection.lastSelectedInd; + selection.deselectAllBackward(); + selection.lastSelectedInd = tmpLastSelectedInd; + } + if (modifiers.shiftKey && selection.lastSelectedInd !== null) { + let [startInd, endInd] = [selection.lastSelectedInd, index]; + if (endInd < startInd) + [startInd, endInd] = [endInd, startInd]; + entries.value + .slice(startInd, endInd + 1) + .filter(entryFilterCallback) + .map(entry => entry.selected = true); + } else { + entry.selected = modifiers.ctrlKey ? !entrySelectedValue : true; + if (entry.selected) + selection.lastSelectedInd = index; + else + selection.lastSelectedInd = null; + } + }, + getSelected: () => [ + ...entries.value.filter(entry => entry.selected), + ...entryRefs.value + .filter(entryRef => entryRef.showEntries) + .map(entryRef => entryRef.getSelected()) + .flat(1), + ], + clear: () => { + entries.value.map(entry => entry.selected = false); + }, + selectAll: () => { + entries.value + .filter(entryFilterCallback) + .map(entry => entry.selected = true); + entryRefs.value + .map(entryRef => entryRef.selectAll()); + }, + deselectAllBackward: () => { + if (props.level > 0) + emit('deselectAll'); + else + selection.deselectAllForward(); + }, + deselectAllForward: () => { + selection.clear(); + selection.lastSelectedInd = null; + entryRefs.value + .map(entryRef => entryRef.deselectAllForward()); + }, + }); const processingHandler = { count: 0, start: () => { @@ -95,6 +153,7 @@ export default { if (!props.path) { return; } + selection.lastSelectedInd = null; processingHandler.start(); const US = '\x1F'; const RS = '\x1E'; @@ -125,72 +184,15 @@ export default { try { const cwd = props.path; const procs = []; - let tmpEntries; procs.push(...entryRefs.value.filter(entryRef => entryRef.showEntries).map(entryRef => entryRef.getEntries())); - const entryNames = - (await useSpawn(['dir', '--almost-all', '--dereference-command-line-symlink-to-dir', '--quoting-style=c', '-1', cwd], { superuser: 'try' }).promise()).stdout - .split('\n') - .filter(name => name) - .map(escaped => { - try { - return JSON.parse(escaped); - } catch (error) { - notifications.value.constructNotification("Failed to parse file name", `${errorStringHTML(error)}\ncaused by ${escaped}`, 'error'); - return null; - } - }) - .filter(entry => entry !== null); - const fields = [ - '%n', // path - '%f', // mode (raw hex) - '%A', // modeStr - '%s', // size - '%U', // owner - '%G', // group - '%W', // ctime - '%Y', // mtime - '%X', // atime - '%F', // type - '%N', // quoted name with symlink - ] - tmpEntries = - entryNames.length - ? (await useSpawn(['stat', `--printf=${fields.join(US)}${RS}`, ...entryNames], { superuser: 'try', directory: cwd }).promise().catch(state => state)).stdout - .split(RS) - .filter(record => record) // remove empty lines - .map(record => { - try { - let [name, mode, modeStr, size, owner, group, ctime, mtime, atime, type, symlinkStr] = record.split(US); - [size, ctime, mtime, atime] = [size, ctime, mtime, atime].map(num => parseInt(num)); - [ctime, mtime, atime] = [ctime, mtime, atime].map(ts => ts ? new Date(ts * 1000) : null); - mode = parseInt(mode, 16); - const entry = reactive({ - name, - path: `${cwd}/${name}`.replace(/\/+/g, '/'), - mode, - modeStr, - size, - sizeHuman: cockpit.format_bytes(size, 1000).replace(/(?\s*/)[1].trim().replace(/^['"]|['"]$/g, ''); - entry.target.path = entry.target.rawPath.replace(/^(?!\/)/, `${cwd}/`); - } - return entry; - } catch (error) { - console.error(errorString(error)); - return null; - } - }).filter(entry => entry !== null) - : []; + const entryNames = await getDirListing(cwd, (message) => notifications.value.constructNotification("Failed to parse file name", message, 'error')); + const tmpEntries = ( + await getDirEntryObjects( + entryNames, + cwd, + (message) => notifications.value.constructNotification("Failed to parse file name", message, 'error') + ) + ).map(entry => reactive(entry)); procs.push(processLinks(tmpEntries.filter(entry => entry.type === 'symbolic link').map(entry => entry.target))); processingHandler.start(); return Promise.all(procs) @@ -235,11 +237,6 @@ export default { (!/^\./.test(entry.name) || settings?.directoryView?.showHidden) && (props.searchFilterRegExp?.test(entry.name) ?? true); - const getSelected = () => [ - ...entries.value.filter(entry => entry.selected), - ...entryRefs.value.filter(entryRef => entryRef.showEntries).map(entryRef => entryRef.getSelected()).flat(1), - ]; - onBeforeUnmount(() => { processingHandler.resolveDangling(); }); @@ -257,11 +254,11 @@ export default { settings, entries, entryRefs, + selection, getEntries, emitStats, sortEntries, entryFilterCallback, - getSelected, } }, components: { @@ -274,6 +271,7 @@ export default { 'startProcessing', 'stopProcessing', 'cancelShowEntries', + 'deselectAll', ] }