Stop using script setup feature but use olden style
This commit is contained in:
+16
-14
@@ -1,20 +1,22 @@
|
||||
<script setup>
|
||||
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
let accessToken = localStorage.accessToken || '';
|
||||
|
||||
if (!accessToken) router.push('/login');
|
||||
else router.push('/home');
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
accessToken: localStorage.accessToken || ''
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.accessToken) this.$router.push('/login');
|
||||
else this.$router.push('/home');
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="directory-view">
|
||||
<div class="directory-view-header">
|
||||
<div class="directory-view-header-icon"></div>
|
||||
<div class="directory-view-header-name">Name</div>
|
||||
<div class="directory-view-header-size">Size</div>
|
||||
<div class="directory-view-header-modified">Modified</div>
|
||||
</div>
|
||||
<div class="directory-view-body-container">
|
||||
<div class="directory-view-body">
|
||||
<DirectoryViewListItem v-for="item in items" :item="item"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
|
||||
import DirectoryViewListItem from './DirectoryViewListItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'DirectoryView',
|
||||
data() {
|
||||
return {
|
||||
items: []
|
||||
};
|
||||
},
|
||||
components: {
|
||||
DirectoryViewListItem
|
||||
},
|
||||
mounted() {
|
||||
// fill with fake items
|
||||
for (let i = 0; i < 100; ++i) {
|
||||
this.items.push({
|
||||
type: i < 20 ? 'directory' : 'file',
|
||||
name: 'Entry ' + i,
|
||||
size: parseInt(Math.random() * 1000000),
|
||||
modified: new Date(+(new Date()) - Math.floor(Math.random() * 10000000000)),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.directory-view {
|
||||
background-color: white;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.directory-view-header {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.directory-view-body-container {
|
||||
overflow: hidden;
|
||||
height: calc(100% - 38px); /* 38px is the header size */
|
||||
}
|
||||
|
||||
.directory-view-body {
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.directory-view-header-icon {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.directory-view-header-name {
|
||||
padding-left: 10px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.directory-view-header-size {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.directory-view-header-modified {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col icon"><img :src="item.type === 'file' ? '/mime-types/text-x-plain.svg' : '/mime-types/inode-directory.svg'"/></div>
|
||||
<div class="col label">{{ item.name }}</div>
|
||||
<div class="col size">{{ prettyFileSize(item.size) }}</div>
|
||||
<div class="col modified">{{ prettyDate(item.modified) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { prettyDate, prettyFileSize } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'DirectoryViewListItem',
|
||||
props: {
|
||||
item: Object
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
prettyFileSize,
|
||||
prettyDate
|
||||
},
|
||||
mounted() {
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.row:hover {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
.col {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.icon > img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: cover;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-left: 5px;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.size {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.modified {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
if (!localStorage.accessToken) router.push('/login');
|
||||
|
||||
function onLogout() {
|
||||
delete localStorage.accessToken;
|
||||
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>You are logged in</p>
|
||||
<Button label="Logout" @click="onLogout"/>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
+2
-2
@@ -10,8 +10,8 @@ import PrimeVue from 'primevue/config';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
import App from './App.vue';
|
||||
import Login from './components/Login.vue';
|
||||
import Home from './components/Home.vue';
|
||||
import Login from './views/Login.vue';
|
||||
import Home from './views/Home.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/home', component: Home },
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
|
||||
import { filesize } from 'filesize';
|
||||
|
||||
function prettyDate(value) {
|
||||
var date = new Date(value),
|
||||
diff = (((new Date()).getTime() - date.getTime()) / 1000),
|
||||
day_diff = Math.floor(diff / 86400);
|
||||
|
||||
if (isNaN(day_diff) || day_diff < 0)
|
||||
return;
|
||||
|
||||
return day_diff === 0 && (
|
||||
diff < 60 && 'just now' ||
|
||||
diff < 120 && '1 min ago' ||
|
||||
diff < 3600 && Math.floor( diff / 60 ) + ' min ago' ||
|
||||
diff < 7200 && '1 hour ago' ||
|
||||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
|
||||
day_diff === 1 && 'Yesterday' ||
|
||||
day_diff < 7 && day_diff + ' days ago' ||
|
||||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
|
||||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
|
||||
Math.round( day_diff / 365 ) + ' years ago';
|
||||
}
|
||||
|
||||
function prettyLongDate(value) {
|
||||
if (!value) return 'unkown';
|
||||
|
||||
var date = new Date(value);
|
||||
return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
|
||||
}
|
||||
|
||||
function prettyFileSize(value) {
|
||||
if (typeof value !== 'number') return 'unkown';
|
||||
|
||||
return filesize(value);
|
||||
}
|
||||
|
||||
function sanitize(path) {
|
||||
path = '/' + path;
|
||||
return path.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
function encode(path) {
|
||||
return path.split('/').map(encodeURIComponent).join('/');
|
||||
}
|
||||
|
||||
function decode(path) {
|
||||
return path.split('/').map(decodeURIComponent).join('/');
|
||||
}
|
||||
|
||||
// TODO create share links instead of using access token
|
||||
function getDirectLink(entry) {
|
||||
if (entry.share) {
|
||||
let link = window.location.origin + '/api/v1/shares/' + entry.share.id + '?type=raw&path=' + encodeURIComponent(entry.filePath);
|
||||
return link;
|
||||
} else {
|
||||
return window.location.origin + '/api/v1/files?type=raw&path=' + encodeURIComponent(entry.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO the url might actually return a 412 in which case we have to keep reloading
|
||||
function getPreviewUrl(entry) {
|
||||
if (!entry.previewUrl) return '';
|
||||
return entry.previewUrl;
|
||||
}
|
||||
|
||||
function getShareLink(shareId) {
|
||||
return window.location.origin + '/api/v1/shares/' + shareId + '?type=raw';
|
||||
}
|
||||
|
||||
function download(entries, name) {
|
||||
if (!entries.length) return;
|
||||
|
||||
if (entries.length === 1) {
|
||||
if (entries[0].share) window.location.href = '/api/v1/shares/' + entries[0].share.id + '?type=download&path=' + encodeURIComponent(entries[0].filePath);
|
||||
else window.location.href = '/api/v1/files?type=download&path=' + encodeURIComponent(entries[0].filePath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// be a bit smart about the archive name and folder tree
|
||||
const folderPath = entries[0].filePath.slice(0, -entries[0].fileName.length);
|
||||
const archiveName = name || folderPath.slice(folderPath.slice(0, -1).lastIndexOf('/')+1).slice(0, -1);
|
||||
params.append('name', archiveName);
|
||||
params.append('skipPath', folderPath);
|
||||
|
||||
params.append('entries', JSON.stringify(entries.map(function (entry) {
|
||||
return {
|
||||
filePath: entry.filePath,
|
||||
shareId: entry.share ? entry.share.id : undefined
|
||||
};
|
||||
})));
|
||||
|
||||
window.location.href = '/api/v1/download?' + params.toString();
|
||||
}
|
||||
|
||||
function getFileTypeGroup(entry) {
|
||||
return entry.mimeType.split('/')[0];
|
||||
}
|
||||
|
||||
// simple extension detection, does not work with double extension like .tar.gz
|
||||
function getExtension(entry) {
|
||||
if (entry.isFile) return entry.fileName.slice(entry.fileName.lastIndexOf('.') + 1);
|
||||
return '';
|
||||
}
|
||||
|
||||
function copyToClipboard(value) {
|
||||
var elem = document.createElement('input');
|
||||
elem.value = value;
|
||||
document.body.append(elem);
|
||||
elem.select();
|
||||
document.execCommand('copy');
|
||||
elem.remove();
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
if(document.selection && document.selection.empty) {
|
||||
document.selection.empty();
|
||||
} else if(window.getSelection) {
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
function urlSearchQuery() {
|
||||
return decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
}
|
||||
|
||||
// those paths contain the internal type and path reference eg. shares/:shareId/folder/filename or files/folder/filename
|
||||
function parseResourcePath(resourcePath) {
|
||||
var result = {
|
||||
type: '',
|
||||
path: '',
|
||||
shareId: '',
|
||||
apiPath: '',
|
||||
resourcePath: ''
|
||||
};
|
||||
|
||||
if (resourcePath.indexOf('files/') === 0) {
|
||||
result.type = 'files';
|
||||
result.path = resourcePath.slice('files'.length) || '/';
|
||||
result.apiPath = '/api/v1/files';
|
||||
result.resourcePath = result.type + result.path;
|
||||
} else if (resourcePath.indexOf('shares/') === 0) {
|
||||
result.type = 'shares';
|
||||
result.shareId = resourcePath.split('/')[1];
|
||||
result.path = resourcePath.slice((result.type + '/' + result.shareId).length) || '/';
|
||||
result.apiPath = '/api/v1/shares/' + result.shareId;
|
||||
// without shareId we show the root (share listing)
|
||||
result.resourcePath = result.type + '/' + (result.shareId ? (result.shareId + result.path) : '');
|
||||
} else {
|
||||
console.error('Unknown resource path', resourcePath);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getEntryIdentifier(entry) {
|
||||
return (entry.share ? (entry.share.id + '/') : '') + entry.filePath;
|
||||
}
|
||||
|
||||
function entryListSort(list, prop, desc) {
|
||||
var tmp = list.sort(function (a, b) {
|
||||
var av = a[prop];
|
||||
var bv = b[prop];
|
||||
|
||||
if (typeof av === 'string') return (av.toUpperCase() < bv.toUpperCase()) ? -1 : 1;
|
||||
else return (av < bv) ? -1 : 1;
|
||||
});
|
||||
|
||||
if (desc) return tmp;
|
||||
return tmp.reverse();
|
||||
}
|
||||
|
||||
export {
|
||||
getDirectLink,
|
||||
getPreviewUrl,
|
||||
getShareLink,
|
||||
getFileTypeGroup,
|
||||
prettyDate,
|
||||
prettyLongDate,
|
||||
prettyFileSize,
|
||||
sanitize,
|
||||
encode,
|
||||
decode,
|
||||
download,
|
||||
getExtension,
|
||||
copyToClipboard,
|
||||
clearSelection,
|
||||
urlSearchQuery,
|
||||
parseResourcePath,
|
||||
getEntryIdentifier,
|
||||
entryListSort
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<span>You are logged in</span>
|
||||
<Button label="Logout" @click="onLogout"/>
|
||||
</div>
|
||||
<div class="main-view">
|
||||
<DirectoryView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Button from 'primevue/button';
|
||||
|
||||
import DirectoryView from '../components/DirectoryView.vue';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
DirectoryView,
|
||||
Button
|
||||
},
|
||||
methods: {
|
||||
onLogout() {
|
||||
delete localStorage.accessToken;
|
||||
this.$router.push('/login');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.main-view {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,65 +1,3 @@
|
||||
<script setup>
|
||||
|
||||
// this is run on every component instantiation, note <script setup> tag
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref } from 'vue';
|
||||
import superagent from 'superagent';
|
||||
import safe from 'safetydance';
|
||||
|
||||
// if imported in script setup tag they are also registered with the vue app as components to be used in html
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Password from 'primevue/password';
|
||||
import InputMask from 'primevue/inputmask';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// events the component can emit
|
||||
const emit = defineEmits([ 'success', 'error' ]);
|
||||
|
||||
// can be exposed as env var, see develop.sh
|
||||
// all local variables in the script setup tags are exposed to DOM elements
|
||||
const BASE_URL = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
||||
|
||||
// these are reactive variables and exposed to the dom elements
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const totpToken = ref('');
|
||||
const error = ref('');
|
||||
const busy = ref(false);
|
||||
|
||||
// exposed to dom elements normally as in angulare with $scope.onLogin
|
||||
async function onLogin() {
|
||||
error.value = false;
|
||||
|
||||
busy.value = true;
|
||||
|
||||
const [err, res] = await safe(superagent.post(`${BASE_URL}/api/v1/cloudron/login`).send({ username: username.value, password: password.value, totpToken: totpToken.value }));
|
||||
busy.value = false;
|
||||
|
||||
if (err && err.status === 401) {
|
||||
error.value = 'Invalid username or password';
|
||||
password.value = '';
|
||||
return;
|
||||
}
|
||||
if (err) return console.error(err);
|
||||
|
||||
localStorage.accessToken = res.body.accessToken;
|
||||
|
||||
router.push('/home');
|
||||
|
||||
emit('success', res.body.accessToken);
|
||||
|
||||
username.value = '';
|
||||
password.value = '';
|
||||
totpToken.value = '';
|
||||
}
|
||||
|
||||
setTimeout(function () { document.getElementById('usernameInput').focus(); }, 0);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<img style="margin-top: -84px" src="/logo.png" width="128" height="128" />
|
||||
@@ -91,6 +29,69 @@ setTimeout(function () { document.getElementById('usernameInput').focus(); }, 0)
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import superagent from 'superagent';
|
||||
import safe from 'safetydance';
|
||||
|
||||
// if imported in script setup tag they are also registered with the vue app as components to be used in html
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Password from 'primevue/password';
|
||||
import InputMask from 'primevue/inputmask';
|
||||
|
||||
// can be exposed as env var, see develop.sh
|
||||
const BASE_URL = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Button, InputText, Password, InputMask
|
||||
},
|
||||
emits: [ 'success', 'error' ],
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
totpToken: '',
|
||||
error: '',
|
||||
busy: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onLogin() {
|
||||
this.error = false;
|
||||
this.busy = true;
|
||||
|
||||
const [err, res] = await safe(superagent.post(`${BASE_URL}/api/v1/cloudron/login`).send({ username: this.username, password: this.password, totpToken: this.totpToken }));
|
||||
|
||||
this.busy = false;
|
||||
|
||||
if (err && err.status === 401) {
|
||||
this.error = 'Invalid username or password';
|
||||
this.password = '';
|
||||
return;
|
||||
}
|
||||
if (err) return console.error(err);
|
||||
|
||||
localStorage.accessToken = res.body.accessToken;
|
||||
|
||||
this.$router.push('/home');
|
||||
|
||||
this.$emit('success', res.body.accessToken);
|
||||
|
||||
this.username = '';
|
||||
this.password = '';
|
||||
this.totpToken = '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(function () { document.getElementById('usernameInput').focus(); }, 0);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
h1 {
|
||||
Reference in New Issue
Block a user