This commit is contained in:
Graham Steffaniak 2023-08-31 17:15:44 -05:00
parent 8b6a6a1afe
commit bc768ea12f
7 changed files with 871 additions and 52 deletions

View File

@ -2,7 +2,7 @@ package version
var (
// Version is the current File Browser version.
Version = "(0.1.4)"
Version = "(0.2.0)"
// CommitSHA is the commmit sha.
CommitSHA = "(unknown)"
)

192
frontend/index.html Normal file
View File

@ -0,0 +1,192 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>File Browser</title>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta name="theme-color" content="#2979ff" />
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="/img/icons/mstile-144x144.png"
/>
<meta name="msapplication-TileColor" content="#2979ff" />
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = {
AuthMethod: "json",
BaseURL: "",
CSS: false,
Color: "",
DisableExternal: false,
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LoginPage: true,
Name: "",
NoAuth: false,
ReCaptcha: false,
ResizePreview: true,
Signup: false,
StaticURL: "",
Theme: "",
TusSettings: { chunkSize: 10485760, retryCount: 5 },
Version: "(untracked)",
};
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-192x192.png"
),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-512x512.png"
),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -213,3 +213,7 @@ nav {
background: var(--background);
color: white
}
#result-desktop #result-list {
background: #2a3137;
}

View File

@ -211,7 +211,7 @@
border-top: none;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
background-color: white;
background: white;
max-height: 80vh;
left: 50%;
-webkit-transform: translateX(-50%);

View File

@ -1,24 +1,12 @@
<template>
<header>
<action
class="menu-button"
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="toggleSidebar()"
/>
<slot />
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }">
<slot name="actions" />
</div>
</header>
</template>
<script>
import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action";
import Action from "@/components/header/Action.vue";
export default {
name: "header-bar",

View File

@ -3,12 +3,10 @@
<div v-if="progress" class="progress">
<div v-bind:style="{ width: this.progress + '%' }"></div>
</div>
<header-bar showMenu showLogo>
<search />
<template #actions>
<action icon="grid_view" :label="$t('buttons.switchView')" @action="switchView" />
</template>
</header-bar>
<editorBar v-if="getCurrentView === 'editor'"></editorBar>
<listingBar v-else-if="getCurrentView === 'listing'"></listingBar>
<previewBar v-else-if="getCurrentView === 'preview'"></previewBar>
<defaultBar v-else></defaultBar>
<sidebar></sidebar>
<main>
<router-view></router-view>
@ -20,60 +18,66 @@
</template>
<script>
import editorBar from "./files/Editor.vue"
import defaultBar from "./files/Default.vue"
import listingBar from"./files/Listing.vue"
import previewBar from "./files/Preview.vue"
import Action from "@/components/header/Action.vue";
import { mapState, mapGetters } from "vuex";
import Sidebar from "@/components/Sidebar";
import Prompts from "@/components/prompts/Prompts";
import Shell from "@/components/Shell";
import UploadFiles from "../components/prompts/UploadFiles";
import Sidebar from "@/components/Sidebar.vue";
import Prompts from "@/components/header/Action.vue";
import Shell from "@/components/Shell.vue";
import UploadFiles from "../components/prompts/UploadFiles.vue";
import { enableExec } from "@/utils/constants";
import HeaderBar from "@/components/header/HeaderBar";
import Search from "@/components/Search";
import Action from "@/components/header/Action";
export default {
name: "layout",
components: {
defaultBar,
editorBar,
listingBar,
previewBar,
Action,
HeaderBar,
Search,
Sidebar,
Prompts,
Shell,
UploadFiles,
},
data: function () {
return {
showContexts: true,
dragCounter: 0,
width: window.innerWidth,
itemWeight: 0,
};
},
computed: {
...mapGetters(["isLogged", "progress"]),
...mapState(["user"]),
...mapState(["req", "user", "currentView"]),
isExecEnabled: () => enableExec,
getCurrentView() {
return this.currentView;
},
},
watch: {
getCurrentView: function () {
console.log(this.currentView)
},
$route: function () {
this.$store.commit("resetSelected");
this.$store.commit("multiple", false);
if (this.$store.state.show !== "success")
this.$store.commit("closeHovers");
if (this.$store.state.show !== "success") this.$store.commit("closeHovers");
},
},
methods: {
switchView: async function () {
this.$store.commit("closeHovers");
const modes = {
list: "mosaic",
mosaic: "mosaic gallery",
"mosaic gallery": "list",
};
const data = {
id: this.user.id,
viewMode: modes[this.user.viewMode] || "list",
};
//users.update(data, ["viewMode"]).catch(this.$showError);
this.$store.commit("updateUser", data);
//this.setItemWeight();
//this.fillWindow();
},
getTitle() {
let title = "Title"
if (this.$route.path.startsWith('/settings/')){
title = "Settings"
}
return title
},
},
};
</script>

View File

@ -0,0 +1,631 @@
<template>
<header-bar>
<action
class="menu-button"
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="toggleSidebar()"
/>
<search />
<action
class="menu-button"
icon="grid_view"
:label="$t('buttons.switchView')"
@action="switchView"
/>
</header-bar>
</template>
<style>
.flexbar {
display:flex;
flex-direction:block;
justify-content: space-between;
}
</style>
<script>
import Vue from "vue";
import { mapState, mapGetters, mapMutations } from "vuex";
import { users, files as api } from "@/api";
import { enableExec } from "@/utils/constants";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import * as upload from "@/utils/upload";
import css from "@/utils/css";
import throttle from "lodash.throttle";
import Search from "@/components/Search.vue";
import Item from "@/components/files/ListingItem.vue";
export default {
name: "listing",
components: {
HeaderBar,
Action,
Search,
Item,
},
data: function () {
return {
showLimit: 50,
columnWidth: 280,
dragCounter: 0,
width: window.innerWidth,
itemWeight: 0,
};
},
computed: {
...mapState(["req", "selected", "user", "show", "multiple", "selected", "loading"]),
...mapGetters(["selectedCount"]),
nameSorted() {
return this.req.sorting.by === "name";
},
sizeSorted() {
return this.req.sorting.by === "size";
},
modifiedSorted() {
return this.req.sorting.by === "modified";
},
ascOrdered() {
return this.req.sorting.asc;
},
items() {
const dirs = [];
const files = [];
this.req.items.forEach((item) => {
if (item.isDir) {
dirs.push(item);
} else {
files.push(item);
}
});
return { dirs, files };
},
dirs() {
return this.items.dirs.slice(0, this.showLimit);
},
files() {
let showLimit = this.showLimit - this.items.dirs.length;
if (showLimit < 0) showLimit = 0;
return this.items.files.slice(0, showLimit);
},
nameIcon() {
if (this.nameSorted && !this.ascOrdered) {
return "arrow_upward";
}
return "arrow_downward";
},
sizeIcon() {
if (this.sizeSorted && this.ascOrdered) {
return "arrow_downward";
}
return "arrow_upward";
},
modifiedIcon() {
if (this.modifiedSorted && this.ascOrdered) {
return "arrow_downward";
}
return "arrow_upward";
},
viewIcon() {
const icons = {
list: "view_module",
mosaic: "grid_view",
"mosaic gallery": "view_list",
};
return icons[this.user.viewMode];
},
headerButtons() {
return {
select: this.selectedCount > 0,
upload: this.user.perm.create && this.selectedCount > 0,
download: this.user.perm.download && this.selectedCount > 0,
shell: this.user.perm.execute && enableExec,
delete: this.selectedCount > 0 && this.user.perm.delete,
rename: this.selectedCount === 1 && this.user.perm.rename,
share: this.selectedCount === 1 && this.user.perm.share,
move: this.selectedCount > 0 && this.user.perm.rename,
copy: this.selectedCount > 0 && this.user.perm.create,
};
},
},
watch: {
req: function () {
// Reset the show value
this.showLimit = 50;
// Ensures that the listing is displayed
Vue.nextTick(() => {
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listing items
this.fillWindow(true);
});
},
},
mounted: function () {
// Check the columns size for the first time.
this.colunmsResize();
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listing items
this.fillWindow(true);
// Add the needed event listeners to the window and document.
window.addEventListener("keydown", this.keyEvent);
window.addEventListener("scroll", this.scrollEvent);
window.addEventListener("resize", this.windowsResize);
if (!this.user.perm.create) return;
document.addEventListener("dragover", this.preventDefault);
document.addEventListener("dragenter", this.dragEnter);
document.addEventListener("dragleave", this.dragLeave);
document.addEventListener("drop", this.drop);
},
beforeDestroy() {
// Remove event listeners before destroying this page.
window.removeEventListener("keydown", this.keyEvent);
window.removeEventListener("scroll", this.scrollEvent);
window.removeEventListener("resize", this.windowsResize);
if (this.user && !this.user.perm.create) return;
document.removeEventListener("dragover", this.preventDefault);
document.removeEventListener("dragenter", this.dragEnter);
document.removeEventListener("dragleave", this.dragLeave);
document.removeEventListener("drop", this.drop);
},
methods: {
action: function () {
if (this.show) {
this.$store.commit("showHover", this.show);
}
this.$emit("action");
},
toggleSidebar() {
if (this.$store.state.show == "sidebar") {
this.$store.commit("closeHovers");
} else {
this.$store.commit("showHover", "sidebar");
}
},
...mapMutations(["updateUser", "addSelected"]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)));
},
keyEvent(event) {
// No prompts are shown
if (this.show !== null) {
return;
}
// Esc!
if (event.keyCode === 27) {
// Reset files selection.
this.$store.commit("resetSelected");
}
// Del!
if (event.keyCode === 46) {
if (!this.user.perm.delete || this.selectedCount == 0) return;
// Show delete prompt.
this.$store.commit("showHover", "delete");
}
// F2!
if (event.keyCode === 113) {
if (!this.user.perm.rename || this.selectedCount !== 1) return;
// Show rename prompt.
this.$store.commit("showHover", "rename");
}
// Ctrl is pressed
if (!event.ctrlKey && !event.metaKey) {
return;
}
let key = String.fromCharCode(event.which).toLowerCase();
switch (key) {
case "f":
event.preventDefault();
this.$store.commit("showHover", "search");
break;
case "c":
case "x":
this.copyCut(event, key);
break;
case "v":
this.paste(event);
break;
case "a":
event.preventDefault();
for (let file of this.items.files) {
if (this.$store.state.selected.indexOf(file.index) === -1) {
this.addSelected(file.index);
}
}
for (let dir of this.items.dirs) {
if (this.$store.state.selected.indexOf(dir.index) === -1) {
this.addSelected(dir.index);
}
}
break;
case "s":
event.preventDefault();
document.getElementById("download-button").click();
break;
}
},
switchView: async function () {
this.$store.commit("closeHovers");
const modes = {
list: "mosaic",
mosaic: "mosaic gallery",
"mosaic gallery": "list",
};
const data = {
id: this.user.id,
viewMode: modes[this.user.viewMode] || "list",
};
//users.update(data, ["viewMode"]).catch(this.$showError);
this.$store.commit("updateUser", data);
//this.setItemWeight();
//this.fillWindow();
},
preventDefault(event) {
// Wrapper around prevent default.
event.preventDefault();
},
copyCut(event, key) {
if (event.target.tagName.toLowerCase() === "input") {
return;
}
let items = [];
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
name: this.req.items[i].name,
});
}
if (items.length == 0) {
return;
}
this.$store.commit("updateClipboard", {
key: key,
items: items,
path: this.$route.path,
});
},
paste(event) {
if (event.target.tagName.toLowerCase() === "input") {
return;
}
let items = [];
for (let item of this.$store.state.clipboard.items) {
const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from;
const to = this.$route.path + encodeURIComponent(item.name);
items.push({ from, to, name: item.name });
}
if (items.length === 0) {
return;
}
let action = (overwrite, rename) => {
api
.copy(items, overwrite, rename)
.then(() => {
this.$store.commit("setReload", true);
})
.catch(this.$showError);
};
if (this.$store.state.clipboard.key === "x") {
action = (overwrite, rename) => {
api
.move(items, overwrite, rename)
.then(() => {
this.$store.commit("resetClipboard");
this.$store.commit("setReload", true);
})
.catch(this.$showError);
};
}
if (this.$store.state.clipboard.path == this.$route.path) {
action(false, true);
return;
}
let conflict = upload.checkConflict(items, this.req.items);
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
},
colunmsResize() {
// Update the columns size based on the window width.
let columns = Math.floor(
document.querySelector("main").offsetWidth / this.columnWidth
);
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
if (columns === 0) columns = 1;
items.style.width = `calc(${100 / columns}% - 1em)`;
},
scrollEvent: throttle(function () {
const totalItems = this.req.numDirs + this.req.numFiles;
// All items are displayed
if (this.showLimit >= totalItems) return;
const currentPos = window.innerHeight + window.scrollY;
// Trigger at the 75% of the window height
const triggerPos = document.body.offsetHeight - window.innerHeight * 0.25;
if (currentPos > triggerPos) {
// Quantity of items needed to fill 2x of the window height
const showQuantity = Math.ceil((window.innerHeight * 2) / this.itemWeight);
// Increase the number of displayed items
this.showLimit += showQuantity;
}
}, 100),
dragEnter() {
this.dragCounter++;
// When the user starts dragging an item, put every
// file on the listing with 50% opacity.
let items = document.getElementsByClassName("item");
Array.from(items).forEach((file) => {
file.style.opacity = 0.5;
});
},
dragLeave() {
this.dragCounter--;
if (this.dragCounter == 0) {
this.resetOpacity();
}
},
drop: async function (event) {
event.preventDefault();
this.dragCounter = 0;
this.resetOpacity();
let dt = event.dataTransfer;
let el = event.target;
if (dt.files.length <= 0) return;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
}
let files = await upload.scanFiles(dt);
let items = this.req.items;
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") {
// Get url from ListingItem instance
path = el.__vue__.url;
try {
items = (await api.fetch(path)).items;
} catch (error) {
this.$showError(error);
}
}
let conflict = upload.checkConflict(files, items);
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace",
confirm: (event) => {
event.preventDefault();
this.$store.commit("closeHovers");
upload.handleFiles(files, path, true);
},
});
return;
}
upload.handleFiles(files, path);
},
uploadInput(event) {
this.$store.commit("closeHovers");
let files = event.currentTarget.files;
let folder_upload =
files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== "";
if (folder_upload) {
for (let i = 0; i < files.length; i++) {
let file = files[i];
files[i].fullPath = file.webkitRelativePath;
}
}
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let conflict = upload.checkConflict(files, this.req.items);
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace",
confirm: (event) => {
event.preventDefault();
this.$store.commit("closeHovers");
upload.handleFiles(files, path, true);
},
});
return;
}
upload.handleFiles(files, path);
},
resetOpacity() {
let items = document.getElementsByClassName("item");
Array.from(items).forEach((file) => {
file.style.opacity = 1;
});
},
async sort(by) {
let asc = false;
if (by === "name") {
if (this.nameIcon === "arrow_upward") {
asc = true;
}
} else if (by === "size") {
if (this.sizeIcon === "arrow_upward") {
asc = true;
}
} else if (by === "modified") {
if (this.modifiedIcon === "arrow_upward") {
asc = true;
}
}
try {
await users.update({ id: this.user.id, sorting: { by, asc } }, ["sorting"]);
} catch (e) {
this.$showError(e);
}
this.$store.commit("setReload", true);
},
openSearch() {
this.$store.commit("showHover", "search");
},
toggleMultipleSelection() {
this.$store.commit("multiple", !this.multiple);
this.$store.commit("closeHovers");
},
windowsResize: throttle(function () {
this.colunmsResize();
this.width = window.innerWidth;
// Listing element is not displayed
if (this.$refs.listing == null) return;
// How much every listing item affects the window height
this.setItemWeight();
// Fill but not fit the window
this.fillWindow();
}, 100),
download() {
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.req.items[this.selected[0]].url);
return;
}
this.$store.commit("showHover", {
prompt: "download",
confirm: (format) => {
this.$store.commit("closeHovers");
let files = [];
if (this.selectedCount > 0) {
for (let i of this.selected) {
files.push(this.req.items[i].url);
}
} else {
files.push(this.$route.path);
}
api.download(format, ...files);
},
});
},
upload: function () {
if (
typeof window.DataTransferItem !== "undefined" &&
typeof DataTransferItem.prototype.webkitGetAsEntry !== "undefined"
) {
this.$store.commit("showHover", "upload");
} else {
document.getElementById("upload-input").click();
}
},
setItemWeight() {
// Listing element is not displayed
if (this.$refs.listing == null) return;
let itemQuantity = this.req.numDirs + this.req.numFiles;
if (itemQuantity > this.showLimit) itemQuantity = this.showLimit;
// How much every listing item affects the window height
this.itemWeight = this.$refs.listing.offsetHeight / itemQuantity;
},
fillWindow(fit = false) {
const totalItems = this.req.numDirs + this.req.numFiles;
// More items are displayed than the total
if (this.showLimit >= totalItems && !fit) return;
const windowHeight = window.innerHeight;
// Quantity of items needed to fill 2x of the window height
const showQuantity = Math.ceil((windowHeight + windowHeight * 2) / this.itemWeight);
// Less items to display than current
if (this.showLimit > showQuantity && !fit) return;
// Set the number of displayed items
this.showLimit = showQuantity > totalItems ? totalItems : showQuantity;
},
},
};
</script>