Skip to content

Commit

Permalink
Feature/keyboard navigation (#25)
Browse files Browse the repository at this point in the history
* bump JS ES target to ES2020

* extract file data-provider out of the file-list component

* open location dialog on "Control + O"

* auto-select snapshots on arrow key navigation

* file list keyboard handling:
- auto-select files on arrow key navigation
- Open directories, files with "Enter", "Space" or "o"
- Dump/Restore folders or files with "r" or "d"
- ensure a cell always is focused when loading new dirs

* add tooltips for file list buttons
  • Loading branch information
emuell committed Aug 25, 2022
1 parent b2f701b commit 402df8a
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 106 deletions.
22 changes: 22 additions & 0 deletions frontend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export class ResticBrowserApp extends MobxLitElement {
@state()
private _showLocationDialog: boolean = false;

constructor() {
super();
this._keyDownHandler = this._keyDownHandler.bind(this);
}

private _keyDownHandler(event: KeyboardEvent) {
if (event.ctrlKey && event.key == "o") {
this._showLocationDialog = true;
event.preventDefault();
}
}

static styles = css`
#layout {
align-items: stretch;
Expand All @@ -50,6 +62,16 @@ export class ResticBrowserApp extends MobxLitElement {
}
`;

connectedCallback(): void {
super.connectedCallback();
document.body.addEventListener("keydown", this._keyDownHandler);
}

disconnectedCallback(): void {
super.disconnectedCallback();
document.body.removeEventListener("keydown", this._keyDownHandler);
}

render() {
// repository location dialog
if (this._showLocationDialog) {
Expand Down
143 changes: 143 additions & 0 deletions frontend/src/components/file-list-data-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
GridDataProviderCallback, GridDataProviderParams, GridSorterDefinition
} from "@vaadin/grid";

import { restic } from "../../wailsjs/go/models";

// -------------------------------------------------------------------------------------------------

// sorting helper functions, shamelessly copied from @vaadin-grid/array-data-provider.js

function normalizeEmptyValue(value: any): any {
if ([undefined, null].includes(value)) {
return '';
} else if (isNaN(value)) {
return value.toString();
}
return value;
}

function compare(a: any, b: any): number {
a = normalizeEmptyValue(a);
b = normalizeEmptyValue(b);

if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}

function get(path: string, object: any): any {
return path.split('.').reduce((obj, property) => obj[property], object);
}

// -------------------------------------------------------------------------------------------------

/*
Vaadin grid compatible data-provider to sort and cache restic.Files
*/

export class FileListDataProvider {

private _files: restic.File[] = [];
private _sortedFiles: restic.File[] = [];
private _sortedFilesOrder?: GridSorterDefinition = undefined;

constructor() {
// ensure our callback won't loose this context
this.provider = this.provider.bind(this);
}

// get currently sorted files
get sortedFiles(): restic.File[] {
return this._files;
}

// get currently unsorted files
get files(): restic.File[] {
return this._files;
}

// set new files
set files(files: restic.File[]) {
this._files = files;
// prune cache
this._sortedFiles = [];
this._sortedFilesOrder = undefined;
}

// the actual data-provider callback
provider(
params: GridDataProviderParams<restic.File>,
callback: GridDataProviderCallback<restic.File>
) {
const items = this._sortFiles(params);
const count = Math.min(items.length, params.pageSize);
const start = params.page * count;
const end = start + count;
if (start !== 0 || end !== items.length) {
callback(items.slice(start, end), items.length);
} else {
callback(items, items.length);
}
}

private _sortFiles(params: GridDataProviderParams<restic.File>): restic.File[] {

// get sort order (multi sorting not supported ATM)
let sortOrder: GridSorterDefinition = {
path: "name",
direction: "asc"
};
if (params.sortOrders && params.sortOrders.length) {
if (params.sortOrders[0].direction) {
sortOrder = params.sortOrders[0];
}
}

// check if we need to update our files cache
if (this._sortedFilesOrder &&
this._sortedFilesOrder.direction === sortOrder.direction &&
this._sortedFilesOrder.path === sortOrder.path) {
return this._sortedFiles;
}

// get items from files and apply our customized sorting
this._sortedFiles = Array.from(this._files);
this._sortedFiles.sort((a: restic.File, b: restic.File) => {
// always keep .. item at top
if (a.type === "dir" && a.name == "..") {
return -1;
} else if (b.type === "dir" && b.name == "..") {
return 1;
}
// keep directories at top or bottom when sorting by name
if (sortOrder.path === "name") {
if (a.type === "dir" && b.type !== "dir") {
return (sortOrder.direction === "asc") ? -1 : 1;
} else if (a.type !== "dir" && b.type === "dir") {
return (sortOrder.direction === "asc") ? 1 : -1;
}
// and do a "natural" sort on names
const options = { numeric: true, sensitivity: "base" };
if (sortOrder.direction === 'asc') {
return a.name.localeCompare(b.name, undefined, options);
} else {
return b.name.localeCompare(a.name, undefined, options);
}
} else {
// apply custom sorting
if (sortOrder.direction === 'asc') {
return compare(get(sortOrder.path, a), get(sortOrder.path, b));
} else {
return compare(get(sortOrder.path, b), get(sortOrder.path, a));
}
}
});

return this._sortedFiles;
}
}
Loading

0 comments on commit 402df8a

Please sign in to comment.