mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:27:07 +00:00
Add file browsing components.
Essentially, these are a file selection dialog.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
max-width="600"
|
||||
v-model="open"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-icon
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
:title="title"
|
||||
>mdi-folder-network-outline</v-icon>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>File picker</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<dougal-file-browser
|
||||
v-model="selected"
|
||||
:mimetypes="mimetypes"
|
||||
:root="root"
|
||||
ref="browser"
|
||||
>
|
||||
</dougal-file-browser>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text @click="save" :disabled="!selected">
|
||||
<v-icon small flat color="primary" class="mr-2">mdi-content-save</v-icon>
|
||||
Ok
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="refresh">
|
||||
<v-icon small flat class="mr-2">mdi-reload</v-icon>
|
||||
Refresh
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="close">
|
||||
<v-icon small flat color="red" class="mr-2">mdi-close</v-icon>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DougalFileBrowser from './file-browser';
|
||||
|
||||
export default {
|
||||
name: "DougalFileBrowserDialog",
|
||||
|
||||
components: { DougalFileBrowser },
|
||||
|
||||
props: [ "path", "mimetypes", "root", "title" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
open: false,
|
||||
selected: ""
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
refresh () {
|
||||
this.$refs.browser.refresh();
|
||||
},
|
||||
|
||||
close () {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
save () {
|
||||
this.$emit('input', this.selected);
|
||||
this.close();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.selected = this.path;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<v-layout fill-height style="overflow-y:auto;max-height:400px;">
|
||||
<v-progress-circular v-if="loading && !items.length"></v-progress-circular>
|
||||
<v-treeview v-else
|
||||
activatable
|
||||
:active.sync="active"
|
||||
:items="items"
|
||||
item-key="path"
|
||||
item-name="basename"
|
||||
:load-children="readdir"
|
||||
@update:active="activeChanged"
|
||||
style="min-width:100%"
|
||||
>
|
||||
<template v-slot:label="{item}">
|
||||
<div style="cursor:pointer;">
|
||||
{{ item.basename }}
|
||||
</div>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</v-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
function find(haystack, needle) {
|
||||
for (const item of haystack) {
|
||||
if (item.path == needle) {
|
||||
return item;
|
||||
} else if (item.children) {
|
||||
const found = find(item.children, needle);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "DougalFileBrowser",
|
||||
|
||||
props: [ "value", "mimetypes", "root" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
items: [],
|
||||
active: [],
|
||||
selected: null,
|
||||
path: "",
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
dirsAreSelectable () {
|
||||
return !this.mimetypes ||
|
||||
this.mimetypes == "inode/directory" ||
|
||||
(Array.isArray(this.mimetypes) && this.mimetypes.includes("inode/directory"));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
activeChanged (active) {
|
||||
const candidate = find(this.items, active[0]);
|
||||
if (!this.dirsAreSelectable && this.isDirectory(candidate)) {
|
||||
this.selected = null;
|
||||
} else {
|
||||
this.selected = candidate;
|
||||
}
|
||||
this.$emit("input", this.selected?.path);
|
||||
},
|
||||
|
||||
isDirectory (item) {
|
||||
return item && item["Content-Type"] == "inode/directory";
|
||||
},
|
||||
|
||||
filterMimetypes (item) {
|
||||
if (!this.mimetypes) {
|
||||
return true;
|
||||
} else if (Array.isArray(this.mimetypes)) {
|
||||
return item["Content-Type"] == "inode/directory" ||
|
||||
this.mimetypes.includes(item["Content-Type"].split(";")[0]) ||
|
||||
this.filterGlob(item);
|
||||
} else {
|
||||
return item["Content-Type"] == "inode/directory" ||
|
||||
this.mimetypes == item["Content-Type"].split(";")[0];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
filterGlob (item) {
|
||||
const globs = (Array.isArray(this.mimetypes)
|
||||
? this.mimetypes
|
||||
: [ this.mimetypes ])
|
||||
.filter(i => /^\*\..+$/.test(i));
|
||||
|
||||
for (const glob of globs) {
|
||||
const ext = (glob.match(/^\*\.(.+)$/)||[])[1];
|
||||
if (item.path.toLowerCase().endsWith(ext.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
async readdir (item) {
|
||||
this.loading = true;
|
||||
const url = `/files/${item? item.path : (this.root || this.path || "")}`;
|
||||
const list = await this.api([url]);
|
||||
this.loading = false;
|
||||
const items = list.map(item => {
|
||||
if (item["Content-Type"] == "inode/directory") {
|
||||
item.children = [];
|
||||
}
|
||||
item.id = item.path;
|
||||
item.name = item.basename;
|
||||
return item;
|
||||
}).filter(this.filterMimetypes);
|
||||
if (item) {
|
||||
item.children = items;
|
||||
} else {
|
||||
this.items = items;
|
||||
}
|
||||
},
|
||||
|
||||
async refresh () {
|
||||
this.items = []
|
||||
this.$nextTick(this.readdir);
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.value) {
|
||||
this.path = this.value;
|
||||
}
|
||||
this.readdir();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user