aboutsummaryrefslogtreecommitdiff
path: root/src/site/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/site/components')
-rw-r--r--src/site/components/album/AlbumDetails.vue211
-rw-r--r--src/site/components/album/AlbumEntry.vue187
-rw-r--r--src/site/components/footer/Footer.vue72
-rw-r--r--src/site/components/grid/Grid.vue604
-rw-r--r--src/site/components/grid/waterfall/Waterfall.vue261
-rw-r--r--src/site/components/grid/waterfall/WaterfallItem.vue52
-rw-r--r--src/site/components/image-modal/ImageInfo.vue179
-rw-r--r--src/site/components/loading/BulmaLoading.vue33
-rw-r--r--src/site/components/loading/CubeShadow.vue17
-rw-r--r--src/site/components/loading/Origami.vue18
-rw-r--r--src/site/components/loading/PingPong.vue26
-rw-r--r--src/site/components/loading/RotateSquare.vue13
-rw-r--r--src/site/components/navbar/Navbar.vue72
-rw-r--r--src/site/components/sidebar/Sidebar.vue116
-rw-r--r--src/site/components/uploader/Uploader.vue75
15 files changed, 1283 insertions, 653 deletions
diff --git a/src/site/components/album/AlbumDetails.vue b/src/site/components/album/AlbumDetails.vue
new file mode 100644
index 0000000..b411f13
--- /dev/null
+++ b/src/site/components/album/AlbumDetails.vue
@@ -0,0 +1,211 @@
+<template>
+ <div class="details">
+ <h2>Public links for this album:</h2>
+
+ <b-table
+ :data="details.links || []"
+ :mobile-cards="true">
+ <template slot-scope="props">
+ <b-table-column
+ field="identifier"
+ label="Link"
+ centered>
+ <a
+ :href="`${config.URL}/a/${props.row.identifier}`"
+ target="_blank">
+ {{ props.row.identifier }}
+ </a>
+ </b-table-column>
+
+ <b-table-column
+ field="views"
+ label="Views"
+ centered>
+ {{ props.row.views }}
+ </b-table-column>
+
+ <b-table-column
+ field="enableDownload"
+ label="Allow download"
+ centered>
+ <b-switch
+ v-model="props.row.enableDownload"
+ @input="updateLinkOptions(albumId, props.row)" />
+ </b-table-column>
+
+ <b-table-column
+ field="enabled"
+ numeric>
+ <button
+ :class="{ 'is-loading': isDeleting(props.row.identifier) }"
+ class="button is-danger"
+ :disabled="isDeleting(props.row.identifier)"
+ @click="promptDeleteAlbumLink(albumId, props.row.identifier)">
+ Delete link
+ </button>
+ </b-table-column>
+ </template>
+ <template slot="empty">
+ <div class="has-text-centered">
+ <i class="icon-misc-mood-sad" />
+ </div>
+ <div class="has-text-centered">
+ Nothing here
+ </div>
+ </template>
+
+ <template slot="footer">
+ <div class="level is-paddingless">
+ <div class="level-left">
+ <div class="level-item">
+ <button
+ :class="{ 'is-loading': isCreatingLink }"
+ class="button is-primary"
+ style="float: left"
+ @click="createLink(albumId)">
+ Create new link
+ </button>
+ </div>
+ <div class="level-item">
+ <span class="has-text-default">{{ details.links.length }} / {{ config.maxLinksPerAlbum }} links created</span>
+ </div>
+ </div>
+
+ <div class="level-right">
+ <div class="level-item">
+ <button
+ class="button is-danger"
+ style="float: right"
+ @click="promptDeleteAlbum(albumId)">
+ Delete album
+ </button>
+ </div>
+ </div>
+ </div>
+ </template>
+ </b-table>
+ </div>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex';
+
+export default {
+ props: {
+ albumId: {
+ type: Number,
+ default: 0,
+ },
+ details: {
+ type: Object,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ isCreatingLink: false,
+ isDeletingLinks: [],
+ };
+ },
+ computed: mapState(['config']),
+ methods: {
+ ...mapActions({
+ deleteAlbumAction: 'albums/deleteAlbum',
+ deleteAlbumLinkAction: 'albums/deleteLink',
+ updateLinkOptionsAction: 'albums/updateLinkOptions',
+ createLinkAction: 'albums/createLink',
+ alert: 'alert/set',
+ }),
+ promptDeleteAlbum(id) {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to delete this album?',
+ onConfirm: () => this.deleteAlbum(id),
+ });
+ },
+ promptDeleteAlbumLink(albumId, identifier) {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to delete this album link?',
+ onConfirm: () => this.deleteAlbumLink(albumId, identifier),
+ });
+ },
+ async deleteAlbum(id) {
+ try {
+ const response = await this.deleteAlbumAction(id);
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ }
+ },
+ async deleteAlbumLink(albumId, identifier) {
+ this.isDeletingLinks.push(identifier);
+ try {
+ const response = await this.deleteAlbumLinkAction({ albumId, identifier });
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ } finally {
+ this.isDeletingLinks = this.isDeletingLinks.filter((e) => e !== identifier);
+ }
+ },
+ async createLink(albumId) {
+ this.isCreatingLink = true;
+ try {
+ const response = await this.createLinkAction(albumId);
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ } finally {
+ this.isCreatingLink = false;
+ }
+ },
+ async updateLinkOptions(albumId, linkOpts) {
+ try {
+ const response = await this.updateLinkOptionsAction({ albumId, linkOpts });
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ }
+ },
+ isDeleting(identifier) {
+ return this.isDeletingLinks.indexOf(identifier) > -1;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+ @import '~/assets/styles/_colors.scss';
+
+ div.details {
+ flex: 0 1 100%;
+ padding-left: 2em;
+ padding-top: 1em;
+ min-height: 50px;
+
+ .b-table {
+ padding: 2em 0em;
+
+ .table-wrapper {
+ -webkit-box-shadow: $boxShadowLight;
+ box-shadow: $boxShadowLight;
+ }
+ }
+ }
+</style>
+
+<style lang="scss">
+ @import '~/assets/styles/_colors.scss';
+
+ .b-table {
+ .table-wrapper {
+ -webkit-box-shadow: $boxShadowLight;
+ box-shadow: $boxShadowLight;
+ }
+ }
+</style>
diff --git a/src/site/components/album/AlbumEntry.vue b/src/site/components/album/AlbumEntry.vue
new file mode 100644
index 0000000..2723b49
--- /dev/null
+++ b/src/site/components/album/AlbumEntry.vue
@@ -0,0 +1,187 @@
+<template>
+ <div class="album">
+ <div
+ class="arrow-container"
+ @click="toggleDetails(album)">
+ <i
+ :class="{ active: isExpanded }"
+ class="icon-arrow" />
+ </div>
+ <div class="thumb">
+ <figure class="image is-64x64 thumb">
+ <img src="~/assets/images/blank_darker.png">
+ </figure>
+ </div>
+ <div class="info">
+ <h4>
+ <router-link :to="`/dashboard/albums/${album.id}`">
+ {{ album.name }}
+ </router-link>
+ </h4>
+ <span>
+ Created <span class="is-inline has-text-weight-semibold"><timeago :since="album.createdAt" /></span>
+ </span>
+ <span>{{ album.fileCount || 0 }} files</span>
+ </div>
+ <div class="latest is-hidden-mobile">
+ <template v-if="album.fileCount > 0">
+ <div
+ v-for="file of album.files"
+ :key="file.id"
+ class="thumb">
+ <figure class="image is-64x64">
+ <a
+ :href="file.url"
+ target="_blank">
+ <img :src="file.thumbSquare">
+ </a>
+ </figure>
+ </div>
+ <div
+ v-if="album.fileCount > 5"
+ class="thumb more no-background">
+ <router-link :to="`/dashboard/albums/${album.id}`">
+ {{ album.fileCount - 5 }}+ more
+ </router-link>
+ </div>
+ </template>
+ <template v-else>
+ <span class="no-files">Nothing to show here</span>
+ </template>
+ </div>
+
+ <AlbumDetails
+ v-if="isExpanded"
+ :details="getDetails(album.id)"
+ :albumId="album.id" />
+ </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import AlbumDetails from '~/components/album/AlbumDetails.vue';
+
+export default {
+ components: {
+ AlbumDetails,
+ },
+ props: {
+ album: {
+ type: Object,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ ...mapGetters({
+ isExpandedGetter: 'albums/isExpanded',
+ getDetails: 'albums/getDetails',
+ }),
+ isExpanded() {
+ return this.isExpandedGetter(this.album.id);
+ },
+ },
+ methods: {
+ async toggleDetails(album) {
+ if (!this.isExpanded) {
+ await this.$store.dispatch('albums/fetchDetails', album.id);
+ }
+ this.$store.commit('albums/toggleExpandedState', album.id);
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+ @import '~/assets/styles/_colors.scss';
+
+ div.album {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+
+ div.arrow-container {
+ width: 2em;
+ height: 64px;
+ position: relative;
+ cursor: pointer;
+
+ i {
+ border: 2px solid $defaultTextColor;
+ border-right: 0;
+ border-top: 0;
+ display: block;
+ height: 1em;
+ position: absolute;
+ transform: rotate(-135deg);
+ transform-origin: center;
+ width: 1em;
+ z-index: 4;
+ top: 22px;
+
+ -webkit-transition: transform 0.1s linear;
+ -moz-transition: transform 0.1s linear;
+ -ms-transition: transform 0.1s linear;
+ -o-transition: transform 0.1s linear;
+ transition: transform 0.1s linear;
+
+ &.active {
+ transform: rotate(-45deg);
+ }
+ }
+ }
+
+ div.thumb {
+ width: 64px;
+ height: 64px;
+ -webkit-box-shadow: $boxShadowLight;
+ box-shadow: $boxShadowLight;
+ }
+
+ div.info {
+ margin-left: 15px;
+ text-align: left;
+ h4 {
+ font-size: 1.5rem;
+ a {
+ color: $defaultTextColor;
+ font-weight: 400;
+ &:hover { text-decoration: underline; }
+ }
+ }
+ span { display: block; }
+ span:nth-child(3) {
+ font-size: 0.9rem;
+ }
+ }
+
+ div.latest {
+ flex-grow: 1;
+ justify-content: flex-end;
+ display: flex;
+ margin-left: 15px;
+
+ span.no-files {
+ font-size: 1.5em;
+ color: #b1b1b1;
+ padding-top: 17px;
+ }
+
+ div.more {
+ width: 64px;
+ height: 64px;
+ background: white;
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ text-align: center;
+ a {
+ line-height: 1rem;
+ color: $defaultTextColor;
+ &:hover { text-decoration: underline; }
+ }
+ }
+ }
+ }
+
+ div.no-background { background: none !important; }
+</style>
diff --git a/src/site/components/footer/Footer.vue b/src/site/components/footer/Footer.vue
index 9f51fee..19d18f2 100644
--- a/src/site/components/footer/Footer.vue
+++ b/src/site/components/footer/Footer.vue
@@ -1,12 +1,18 @@
<template>
+ <!-- eslint-disable max-len -->
<footer>
- <svg viewBox="0 0 1920 250"
+ <svg
+ viewBox="0 0 1920 250"
class="waves">
- <path d="M1920 250H0V0s126.707 78.536 349.975 80.05c177.852 1.203 362.805-63.874 553.803-63.874 290.517 0 383.458 57.712 603.992 61.408 220.527 3.696 278.059-61.408 412.23-17.239"
+
+ <path
+ d="M1920 250H0V0s126.707 78.536 349.975 80.05c177.852 1.203 362.805-63.874 553.803-63.874 290.517 0 383.458 57.712 603.992 61.408 220.527 3.696 278.059-61.408 412.23-17.239"
class="wave-1" />
- <path d="M1920 144s-467.917 116.857-1027.243-17.294C369.986 1.322 0 45.578 0 45.578V250h1920V144z"
+ <path
+ d="M1920 144s-467.917 116.857-1027.243-17.294C369.986 1.322 0 45.578 0 45.578V250h1920V144z"
class="wave-2" />
- <path d="M0 195.553s208.547-75.581 701.325-20.768c376.707 41.908 520.834-67.962 722.545-67.962 222.926 0 311.553 83.523 496.129 86.394V250H0v-54.447z"
+ <path
+ d="M0 195.553s208.547-75.581 701.325-20.768c376.707 41.908 520.834-67.962 722.545-67.962 222.926 0 311.553 83.523 496.129 86.394V250H0v-54.447z"
class="wave-3" />
</svg>
<div>
@@ -15,7 +21,8 @@
<div class="column is-narrow">
<h4>lolisafe</h4>
<span>© 2017-2020
- <a href="https://github.com/pitu"
+ <a
+ href="https://github.com/pitu"
class="no-block">Pitu</a>
</span><br>
<span>v{{ version }}</span>
@@ -24,22 +31,33 @@
<div class="columns is-gapless">
<div class="column" />
<div class="column">
- <nuxt-link to="/">Home</nuxt-link>
- <nuxt-link to="/faq">FAQ</nuxt-link>
+ <nuxt-link to="/">
+ Home
+ </nuxt-link>
+ <nuxt-link to="/faq">
+ FAQ
+ </nuxt-link>
</div>
<div class="column">
- <nuxt-link to="/dashboard">Dashboard</nuxt-link>
- <nuxt-link to="/dashboard">Files</nuxt-link>
- <nuxt-link to="/dashboard/albums">Albums</nuxt-link>
- <nuxt-link to="/dashboard/account">Account</nuxt-link>
+ <nuxt-link to="/dashboard">
+ Dashboard
+ </nuxt-link>
+ <nuxt-link to="/dashboard">
+ Files
+ </nuxt-link>
+ <nuxt-link to="/dashboard/albums">
+ Albums
+ </nuxt-link>
+ <nuxt-link to="/dashboard/account">
+ Account
+ </nuxt-link>
</div>
<div class="column">
<a href="https://github.com/weebdev/lolisafe">GitHub</a>
- <a href="https://patreon.com/pitu">Patreon</a>
- <a href="https://discord.gg/5g6vgwn">Discord</a>
</div>
<div class="column">
- <a v-if="loggedIn"
+ <a
+ v-if="loggedIn"
@click="createShareXThing">ShareX Config</a>
<a href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">Chrome Extension</a>
</div>
@@ -50,27 +68,32 @@
</div>
</footer>
</template>
+
<script>
+/* eslint-disable no-restricted-globals */
+
+import { mapState, mapGetters } from 'vuex';
import { saveAs } from 'file-saver';
+
export default {
computed: {
- loggedIn() {
- return this.$store.state.loggedIn;
- },
- version() {
- return this.$store.state.config.version;
- }
+ ...mapGetters({ loggedIn: 'auth/isLoggedIn' }),
+ ...mapState({
+ version: (state) => state.config.version,
+ serviceName: (state) => state.config.serviceName,
+ token: (state) => state.auth.token,
+ }),
},
methods: {
createShareXThing() {
const sharexFile = `{
- "Name": "${this.$store.state.config.serviceName}",
+ "Name": "${this.serviceName}",
"DestinationType": "ImageUploader, FileUploader",
"RequestType": "POST",
"RequestURL": "${location.origin}/api/upload",
"FileFormName": "files[]",
"Headers": {
- "authorization": "Bearer ${this.$store.state.token}",
+ "authorization": "Bearer ${this.token}",
"accept": "application/vnd.lolisafe.json"
},
"ResponseType": "Text",
@@ -79,10 +102,11 @@ export default {
}`;
const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' });
saveAs(sharexBlob, `${location.hostname}.sxcu`);
- }
- }
+ },
+ },
};
</script>
+
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
footer {
diff --git a/src/site/components/grid/Grid.vue b/src/site/components/grid/Grid.vue
index d0a5ea3..1e427fc 100644
--- a/src/site/components/grid/Grid.vue
+++ b/src/site/components/grid/Grid.vue
@@ -1,156 +1,136 @@
<template>
<div>
- <div v-if="enableToolbar"
- class="toolbar">
- <div class="block">
- <b-radio v-model="showList"
- name="name"
- :native-value="true">
- List
- </b-radio>
- <b-radio v-model="showList"
- name="name"
- :native-value="false">
- Grid
- </b-radio>
+ <nav class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <slot name="pagination" />
+ </div>
</div>
- </div>
+ <div v-if="enableToolbar" class="level-right toolbar">
+ <div class="level-item">
+ <div class="block">
+ <b-radio v-model="showList" name="name" :native-value="true">
+ List
+ </b-radio>
+ <b-radio v-model="showList" name="name" :native-value="false">
+ Grid
+ </b-radio>
+ </div>
+ </div>
+ </div>
+ </nav>
<template v-if="!showList">
- <Waterfall :gutterWidth="10"
- :gutterHeight="4">
- <!--
- TODO: Implement search based on originalName, albumName and tags
- <input v-if="enableSearch"
- v-model="searchTerm"
- type="text"
- placeholder="Search..."
- @input="search()"
- @keyup.enter="search()">
- -->
-
- <!-- TODO: Implement pagination -->
-
- <WaterfallItem v-for="(item, index) in gridFiles"
- v-if="showWaterfall"
- :key="index"
- :width="width"
- move-class="item-move">
+ <Waterfall
+ v-if="showWaterfall"
+ :gutterWidth="10"
+ :gutterHeight="4"
+ :options="{fitWidth: true}"
+ :itemWidth="width"
+ :items="gridFiles">
+ <template v-slot="{item}">
<template v-if="isPublic">
- <a :href="`${item.url}`"
- target="_blank">
+ <a
+ :href="`${item.url}`"
+ class="preview-container"
+ target="_blank"
+ @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)"
+ @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)">
+
<img :src="item.thumb ? item.thumb : blank">
- <span v-if="!item.thumb && item.name"
- class="extension">{{ item.name.split('.').pop() }}</span>
+ <div v-if="item.preview && isHovered(item.id)" class="preview">
+ <video ref="video" class="preview" autoplay loop muted>
+ <source :src="item.preview" type="video/mp4">
+ </video>
+ </div>
+
+ <span v-if="!item.thumb && item.name" class="extension">{{
+ item.name.split('.').pop()
+ }}</span>
</a>
</template>
<template v-else>
<img :src="item.thumb ? item.thumb : blank">
- <span v-if="!item.thumb && item.name"
- class="extension">{{ item.name.split('.').pop() }}</span>
- <div v-if="!isPublic"
+ <div v-if="item.preview && isHovered(item.id)" class="preview">
+ <video ref="video" class="preview" autoplay loop muted>
+ <source :src="item.preview" type="video/mp4">
+ </video>
+ </div>
+
+ <span v-if="!item.thumb && item.name" class="extension">{{ item.name.split('.').pop() }}</span>
+ <div
+ v-if="!isPublic"
:class="{ fixed }"
- class="actions">
- <b-tooltip label="Link"
- position="is-top">
- <a :href="`${item.url}`"
- target="_blank"
- class="btn">
+ class="actions"
+ @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)"
+ @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)">
+ <b-tooltip label="Link" position="is-top">
+ <a :href="`${item.url}`" target="_blank" class="btn">
<i class="icon-web-code" />
</a>
</b-tooltip>
- <b-tooltip label="Albums"
- position="is-top">
- <a class="btn"
- @click="openAlbumModal(item)">
- <i class="icon-interface-window" />
+ <b-tooltip label="Tags" position="is-top">
+ <a class="btn" @click="false && manageTags(item)">
+ <i class="icon-ecommerce-tag-c" />
</a>
</b-tooltip>
- <!--
- <b-tooltip label="Tags"
- position="is-top">
- <a @click="manageTags(item)">
- <i class="icon-ecommerce-tag-c" />
+ <b-tooltip label="Albums" position="is-top">
+ <a class="btn" @click="handleFileModal(item)">
+ <i class="icon-interface-window" />
</a>
</b-tooltip>
- -->
- <b-tooltip label="Delete"
- position="is-top">
- <a class="btn"
- @click="deleteFile(item, index)">
+ <b-tooltip label="Delete" position="is-top">
+ <a class="btn" @click="deleteFile(item)">
<i class="icon-editorial-trash-a-l" />
</a>
</b-tooltip>
- <b-tooltip v-if="user && user.isAdmin"
- label="More info"
- position="is-top"
- class="more">
+ <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more">
<nuxt-link :to="`/dashboard/admin/file/${item.id}`">
<i class="icon-interface-more" />
</nuxt-link>
</b-tooltip>
</div>
</template>
- </WaterfallItem>
+ </template>
</Waterfall>
- <button
- v-if="moreFiles"
- class="button is-primary"
- @click="loadMoreFiles">Load more</button>
</template>
<div v-else>
- <b-table
- :data="gridFiles || []"
- :mobile-cards="true">
+ <b-table :data="gridFiles || []" :mobile-cards="true">
<template slot-scope="props">
<template v-if="!props.row.hideFromList">
- <b-table-column field="url"
- label="URL">
- <a :href="props.row.url"
- target="_blank">{{ props.row.url }}</a>
+ <b-table-column field="url" label="URL">
+ <a :href="props.row.url" target="_blank">{{ props.row.url }}</a>
</b-table-column>
- <b-table-column field="albums"
- label="Albums"
- centered>
+ <b-table-column field="albums" label="Albums" centered>
<template v-for="(album, index) in props.row.albums">
- <nuxt-link :key="index"
- :to="`/dashboard/albums/${album.id}`">
+ <nuxt-link :key="index" :to="`/dashboard/albums/${album.id}`">
{{ album.name }}
</nuxt-link>
- <template v-if="index < props.row.albums.length - 1">, </template>
+ <template v-if="index < props.row.albums.length - 1">
+ ,
+ </template>
</template>
{{ props.row.username }}
</b-table-column>
- <b-table-column field="uploaded"
- label="Uploaded"
- centered>
+ <b-table-column field="uploaded" label="Uploaded" centered>
<span><timeago :since="props.row.createdAt" /></span>
</b-table-column>
- <b-table-column field="purge"
- centered>
- <b-tooltip label="Albums"
- position="is-top">
- <a class="btn"
- @click="openAlbumModal(props.row)">
+ <b-table-column field="purge" centered>
+ <b-tooltip label="Albums" position="is-top">
+ <a class="btn" @click="handleFileModal(props.row)">
<i class="icon-interface-window" />
</a>
</b-tooltip>
- <b-tooltip label="Delete"
- position="is-top"
- class="is-danger">
- <a class="is-danger"
- @click="deleteFile(props.row)">
+ <b-tooltip label="Delete" position="is-top" class="is-danger">
+ <a class="is-danger" @click="deleteFile(props.row)">
<i class="icon-editorial-trash-a-l" />
</a>
</b-tooltip>
- <b-tooltip v-if="user && user.isAdmin"
- label="More info"
- position="is-top"
- class="more">
+ <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more">
<nuxt-link :to="`/dashboard/admin/file/${props.row.id}`">
<i class="icon-interface-more" />
</nuxt-link>
@@ -167,283 +147,361 @@
</div>
</template>
<template slot="footer">
- <div class="has-text-right">
- {{ files.length }} files
+ <div class="has-text-right has-text-default">
+ Showing {{ files.length }} files ({{ total }} total)
</div>
</template>
</b-table>
<button
v-if="moreFiles"
class="button is-primary mt2"
- @click="loadMoreFiles">Load more</button>
+ @click="loadMoreFiles">
+ Load more
+ </button>
</div>
- <b-modal :active.sync="isAlbumsModalActive"
- :width="640"
- scroll="keep">
+ <b-modal :active.sync="isAlbumsModalActive" scroll="keep">
+ <ImageInfo :file="modalData.file" />
+ </b-modal>
+ <!-- <b-modal :active.sync="isAlbumsModalActive" :width="640" scroll="keep">
<div class="card albumsModal">
<div class="card-content">
<div class="content">
- <h3 class="subtitle">Select the albums this file should be a part of</h3>
+ <h3 class="subtitle">
+ Select the albums this file should be a part of
+ </h3>
<hr>
+
<div class="albums-container">
- <div v-for="(album, index) in albums"
- :key="index"
- class="album">
+ <div v-for="album in albums" :key="album.id" class="album">
<div class="field">
- <b-checkbox :value="isAlbumSelected(album.id)"
- @input="albumCheckboxClicked($event, album.id)">{{ album.name }}</b-checkbox>
+ <b-checkbox
+ :value="isAlbumSelected(album.id)"
+ @input="albumCheckboxClicked($event, album.id)">
+ {{ album.name }}
+ </b-checkbox>
</div>
</div>
</div>
</div>
</div>
</div>
- </b-modal>
+ </b-modal> -->
</div>
</template>
+
<script>
+import { mapState } from 'vuex';
+
import Waterfall from './waterfall/Waterfall.vue';
-import WaterfallItem from './waterfall/WaterfallItem.vue';
+import ImageInfo from '~/components/image-modal/ImageInfo.vue';
export default {
components: {
Waterfall,
- WaterfallItem
+ ImageInfo,
},
props: {
files: {
type: Array,
- default: () => []
+ default: () => [],
+ },
+ total: {
+ type: Number,
+ default: 0,
},
fixed: {
type: Boolean,
- default: false
+ default: false,
},
isPublic: {
type: Boolean,
- default: false
+ default: false,
},
width: {
type: Number,
- default: 150
+ default: 150,
},
enableSearch: {
type: Boolean,
- default: true
+ default: true,
},
enableToolbar: {
type: Boolean,
- default: true
- }
+ default: true,
+ },
},
data() {
return {
showWaterfall: true,
searchTerm: null,
showList: false,
- albums: [],
+ hoveredItems: [],
isAlbumsModalActive: false,
showingModalForFile: null,
- filesOffset: 0,
- filesOffsetEnd: 50,
- filesPerPage: 50
+ filesOffsetWaterfall: 0,
+ filesOffsetEndWaterfall: 50,
+ filesPerPageWaterfall: 50,
+ modalData: {
+ file: null,
+ tags: null,
+ albums: null,
+ },
};
},
computed: {
- user() {
- return this.$store.state.user;
- },
+ ...mapState({
+ user: (state) => state.auth.user,
+ albums: (state) => state.albums.tinyDetails,
+ images: (state) => state.images,
+ }),
blank() {
- return require('@/assets/images/blank2.jpg');
+ // eslint-disable-next-line global-require, import/no-unresolved
+ return require('@/assets/images/blank.png');
},
gridFiles() {
- return this.files.slice(this.filesOffset, this.filesOffsetEnd);
+ return this.files;
},
- moreFiles() {
- return this.files.length > this.filesOffsetEnd;
- }
+ },
+ created() {
+ this.getAlbums();
},
methods: {
- loadMoreFiles() {
- this.filesOffsetEnd = this.filesOffsetEnd + this.filesPerPage;
- },
async search() {
- const data = await this.$search.do(this.searchTerm, [
- 'name',
- 'original',
- 'type',
- 'albums:name'
- ]);
- console.log('> Search result data', data);
+ const data = await this.$search.do(this.searchTerm, ['name', 'original', 'type', 'albums:name']);
+ console.log('> Search result data', data); // eslint-disable-line no-console
},
- deleteFile(file, index) {
+ deleteFile(file) {
+ // this.$emit('delete', file);
this.$buefy.dialog.confirm({
title: 'Deleting file',
message: 'Are you sure you want to <b>delete</b> this file?',
confirmText: 'Delete File',
type: 'is-danger',
- hasIcon: true,
onConfirm: async () => {
- const response = await this.$axios.$delete(`file/${file.id}`);
- if (this.showList) {
- file.hideFromList = true;
- this.$forceUpdate();
- } else {
- this.showWaterfall = false;
- this.files.splice(index, 1);
- this.$nextTick(() => {
- this.showWaterfall = true;
- });
+ try {
+ const response = await this.$store.dispatch('images/deleteFile', file.id);
+
+ this.$buefy.toast.open(response.message);
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
}
- return this.$buefy.toast.open(response.message);
- }
+ },
});
},
isAlbumSelected(id) {
- if (!this.showingModalForFile) return;
- const found = this.showingModalForFile.albums.find(el => el.id === id);
- return found ? found.id ? true : false : false;
+ if (!this.showingModalForFile) return false;
+ const found = this.showingModalForFile.albums.find((el) => el.id === id);
+ return !!(found && found.id);
},
async openAlbumModal(file) {
+ const { id } = file;
this.showingModalForFile = file;
this.showingModalForFile.albums = [];
- this.isAlbumsModalActive = true;
- const response = await this.$axios.$get(`file/${file.id}/albums`);
- this.showingModalForFile.albums = response.albums;
+ try {
+ await this.$store.dispatch('images/getFileAlbums', id);
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+ this.showingModalForFile.albums = this.images.fileAlbumsMap[id];
- this.getAlbums();
+ this.isAlbumsModalActive = true;
},
- async albumCheckboxClicked(value, id) {
- const response = await this.$axios.$post(`file/album/${value ? 'add' : 'del'}`, {
- albumId: id,
- fileId: this.showingModalForFile.id
- });
- this.$buefy.toast.open(response.message);
+ async albumCheckboxClicked(add, id) {
+ try {
+ let response;
+ if (add) {
+ response = await this.$store.dispatch('images/addToAlbum', {
+ albumId: id,
+ fileId: this.showingModalForFile.id,
+ });
+ } else {
+ response = await this.$store.dispatch('images/removeFromAlbum', {
+ albumId: id,
+ fileId: this.showingModalForFile.id,
+ });
+ }
- // Not the prettiest solution to refetch on each click but it'll do for now
- this.$parent.getFiles();
+ this.$buefy.toast.open(response.message);
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
},
async getAlbums() {
- const response = await this.$axios.$get(`albums/dropdown`);
- this.albums = response.albums;
- this.$forceUpdate();
- }
- }
+ try {
+ await this.$store.dispatch('albums/getTinyDetails');
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+ },
+ async handleFileModal(file) {
+ const { id } = file;
+
+ try {
+ await this.$store.dispatch('images/fetchFileMeta', id);
+ this.modalData.file = this.images.fileExtraInfoMap[id];
+ this.modalData.albums = this.images.fileAlbumsMap[id];
+ this.modalData.tags = this.images.fileTagsMap[id];
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+
+ this.isAlbumsModalActive = true;
+ },
+ mouseOver(id) {
+ const foundIndex = this.hoveredItems.indexOf(id);
+ if (foundIndex > -1) return;
+ this.hoveredItems.push(id);
+ },
+ mouseOut(id) {
+ const foundIndex = this.hoveredItems.indexOf(id);
+ if (foundIndex > -1) this.hoveredItems.splice(foundIndex, 1);
+ },
+ isHovered(id) {
+ return this.hoveredItems.includes(id);
+ },
+ },
};
</script>
+
<style lang="scss" scoped>
- @import '~/assets/styles/_colors.scss';
- .item-move {
- transition: all .25s cubic-bezier(.55,0,.1,1);
- }
+@import '~/assets/styles/_colors.scss';
+.item-move {
+ transition: all 0.25s cubic-bezier(0.55, 0, 0.1, 1);
+}
- div.toolbar {
- padding: 1rem;
+div.toolbar {
+ padding: 1rem;
- .block {
- text-align: right;
- }
+ .block {
+ text-align: right;
}
+}
- span.extension {
- position: absolute;
- width: 100%;
- height: 100%;
- z-index: 0;
- top: 0;
- left: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 2rem;
- pointer-events: none;
- opacity: .75;
- max-width: 150px;
- }
+span.extension {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
+ pointer-events: none;
+ opacity: 0.75;
+ max-width: 150px;
+}
- div.actions {
- opacity: 0;
- -webkit-transition: opacity 0.1s linear;
- -moz-transition: opacity 0.1s linear;
- -ms-transition: opacity 0.1s linear;
- -o-transition: opacity 0.1s linear;
- transition: opacity 0.1s linear;
- position: absolute;
- top: 0px;
- left: 0px;
- width: 100%;
- height: calc(100% - 6px);
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
-
- span {
- padding: 3px;
- &.more {
- position: absolute;
- top: 0;
- right: 0;
- }
+div.preview {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: calc(100% - 6px);
+ overflow: hidden;
+}
- &:nth-child(1), &:nth-child(2) {
- align-items: flex-end;
- }
+.preview-container {
+ display: inline-block;
+}
- &:nth-child(1), &:nth-child(3) {
- justify-content: flex-end;
- }
- a {
+div.actions {
+ opacity: 0;
+ -webkit-transition: opacity 0.1s linear;
+ -moz-transition: opacity 0.1s linear;
+ -ms-transition: opacity 0.1s linear;
+ -o-transition: opacity 0.1s linear;
+ transition: opacity 0.1s linear;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: calc(100% - 6px);
+ // background: rgba(0, 0, 0, 0.5);
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 60px),
+ linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 45px);
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+
+ span {
+ padding: 3px;
+
+ &.more {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+
+ &:nth-child(1),
+ &:nth-child(2) {
+ align-items: flex-end;
+ padding-bottom: 10px;
+ }
+
+ &:nth-child(3),
+ &:nth-child(4) {
+ justify-content: flex-end;
+ padding-bottom: 10px;
+ }
+
+ a {
+ width: 30px;
+ height: 30px;
+ color: white;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ &.btn:before {
+ content: '';
width: 30px;
height: 30px;
- color: white;
- justify-content: center;
- align-items: center;
- display: flex;
- &.btn:before {
- content: '';
- width: 30px;
- height: 30px;
- border: 1px solid white;
- border-radius: 50%;
- position: absolute;
- }
+ border: 1px solid white;
+ border-radius: 50%;
+ position: absolute;
}
}
+ }
- &.fixed {
- position: relative;
- opacity: 1;
- background: none;
-
- a {
- width: auto;
- height: auto;
- color: $defaultTextColor;
- &:before {
- display: none;
- }
- }
+ &.fixed {
+ position: relative;
+ opacity: 1;
+ background: none;
+ a {
+ width: auto;
+ height: auto;
+ color: $defaultTextColor;
+ &:before {
+ display: none;
+ }
}
}
+}
- .albums-container {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- .album {
- flex-basis: 33%;
- text-align: left;
- }
+.albums-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ .album {
+ flex-basis: 33%;
+ text-align: left;
}
-</style>
+}
-<style lang="scss">
- .waterfall-item:hover {
- div.actions {
- opacity: 1
- }
+.hidden {
+ display: none;
+}
+
+.waterfall {
+ margin: 0 auto;
+}
+
+.waterfall-item:hover {
+ div.actions {
+ opacity: 1;
}
+}
</style>
diff --git a/src/site/components/grid/waterfall/Waterfall.vue b/src/site/components/grid/waterfall/Waterfall.vue
index 8631ea5..79a330a 100644
--- a/src/site/components/grid/waterfall/Waterfall.vue
+++ b/src/site/components/grid/waterfall/Waterfall.vue
@@ -1,180 +1,133 @@
-<style>
- .waterfall {
- position: relative;
- }
-</style>
<template>
- <div class="waterfall">
- <slot />
+ <div ref="waterfall" class="waterfall">
+ <WaterfallItem
+ v-for="(item, index) in items"
+ :key="item.id"
+ :style="{ width: `${itemWidth}px`, marginBottom: `${gutterHeight}px` }"
+ :width="itemWidth">
+ <slot :item="item" />
+ </WaterfallItem>
</div>
</template>
<script>
-// import {quickSort, getMinIndex, _, sum} from './util'
-
-const quickSort = (arr, type) => {
- const left = [];
- const right = [];
- if (arr.length <= 1) {
- return arr;
- }
- const povis = arr[0];
- for (let i = 1; i < arr.length; i++) {
- if (arr[i][type] < povis[type]) {
- left.push(arr[i]);
- } else {
- right.push(arr[i]);
- }
- }
- return quickSort(left, type).concat(povis, quickSort(right, type))
-};
-
-const getMinIndex = arr => {
- let pos = 0;
- for (let i = 0; i < arr.length; i++) {
- if (arr[pos] > arr[i]) {
- pos = i;
- }
- }
- return pos;
-};
+import WaterfallItem from './WaterfallItem.vue';
-const _ = {
- on(el, type, func, capture = false) {
- el.addEventListener(type, func, capture);
- },
- off(el, type, func, capture = false) {
- el.removeEventListener(type, func, capture);
- }
-};
+const isBrowser = typeof window !== 'undefined';
+const Masonry = isBrowser ? window.Masonry || require('masonry-layout') : null;
+const imagesloaded = isBrowser ? require('imagesloaded') : null;
-const sum = arr => arr.reduce((sum, val) => sum + val);
export default {
name: 'Waterfall',
+ components: {
+ WaterfallItem,
+ },
props: {
- gutterWidth: {
- type: Number,
- default: 0
- },
- gutterHeight: {
- type: Number,
- default: 0
- },
- resizable: {
- type: Boolean,
- default: true
+ options: {
+ type: Object,
+ default: () => {},
},
- align: {
- type: String,
- default: 'center'
+ items: {
+ type: Array,
+ default: () => [],
},
- fixWidth: {
- type: Number
+ itemWidth: {
+ type: Number,
+ default: 150,
},
- minCol: {
+ gutterWidth: {
type: Number,
- default: 1
+ default: 10,
},
- maxCol: {
- type: Number
+ gutterHeight: {
+ type: Number,
+ default: 4,
},
- percent: {
- type: Array
- }
},
- data() {
- return {
- timer: null,
- colNum: 0,
- lastWidth: 0,
- percentWidthArr: []
- };
+ mounted() {
+ this.initializeMasonry();
+ this.imagesLoaded();
},
- created() {
- this.$on('itemRender', () => {
- if (this.timer) {
- clearTimeout(this.timer);
- }
- this.timer = setTimeout(() => {
- this.render();
- }, 0);
- });
+ updated() {
+ this.performLayout();
+ this.imagesLoaded();
},
- mounted() {
- this.resizeHandle();
- this.$watch('resizable', this.resizeHandle);
+ unmounted() {
+ this.masonry.destroy();
},
methods: {
- calulate(arr) {
- let pageWidth = this.fixWidth ? this.fixWidth : this.$el.offsetWidth;
- // 百分比布局计算
- if (this.percent) {
- this.colNum = this.percent.length;
- const total = sum(this.percent);
- this.percentWidthArr = this.percent.map(value => (value / total) * pageWidth);
- this.lastWidth = 0;
- // 正常布局计算
- } else {
- this.colNum = parseInt(pageWidth / (arr.width + this.gutterWidth));
- if (this.minCol && this.colNum < this.minCol) {
- this.colNum = this.minCol;
- this.lastWidth = 0;
- } else if (this.maxCol && this.colNum > this.maxCol) {
- this.colNum = this.maxCol;
- this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
- } else {
- this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
- }
+ imagesLoaded() {
+ const node = this.$refs.waterfall;
+ imagesloaded(
+ node,
+ () => {
+ this.masonry.layout();
+ },
+ );
+ },
+ performLayout() {
+ const diff = this.diffDomChildren();
+ if (diff.removed.length > 0) {
+ this.masonry.remove(diff.removed);
+ this.masonry.reloadItems();
+ }
+ if (diff.appended.length > 0) {
+ this.masonry.appended(diff.appended);
+ this.masonry.reloadItems();
+ }
+ if (diff.prepended.length > 0) {
+ this.masonry.prepended(diff.prepended);
}
+ if (diff.moved.length > 0) {
+ this.masonry.reloadItems();
+ }
+ this.masonry.layout();
+ },
+ diffDomChildren() {
+ const oldChildren = this.domChildren.filter((element) => !!element.parentNode);
+ const newChildren = this.getNewDomChildren();
+ const removed = oldChildren.filter((oldChild) => !newChildren.includes(oldChild));
+ const domDiff = newChildren.filter((newChild) => !oldChildren.includes(newChild));
+ const prepended = domDiff.filter((newChild, index) => newChildren[index] === newChild);
+ const appended = domDiff.filter((el) => !prepended.includes(el));
+ let moved = [];
+ if (removed.length === 0) {
+ moved = oldChildren.filter((child, index) => index !== newChildren.indexOf(child));
+ }
+ this.domChildren = newChildren;
+ return {
+ old: oldChildren,
+ new: newChildren,
+ removed,
+ appended,
+ prepended,
+ moved,
+ };
},
- resizeHandle() {
- if (this.resizable) {
- _.on(window, 'resize', this.render, false);
- } else {
- _.off(window, 'resize', this.render, false);
+ initializeMasonry() {
+ if (!this.masonry) {
+ this.masonry = new Masonry(
+ this.$refs.waterfall,
+ {
+ columnWidth: this.itemWidth,
+ gutter: this.gutterWidth,
+ ...this.options,
+ },
+ );
+ this.domChildren = this.getNewDomChildren();
}
},
- render() {
- // 重新排序
- let childArr = [];
- childArr = this.$children.map(child => child.getMeta());
- childArr = quickSort(childArr, 'order');
- // 计算列数
- this.calulate(childArr[0])
- let offsetArr = Array(this.colNum).fill(0);
- // 渲染
- childArr.forEach(child => {
- let position = getMinIndex(offsetArr);
- // 百分比布局渲染
- if (this.percent) {
- let left = 0;
- child.el.style.width = `${this.percentWidthArr[position]}px`;
- if (position === 0) {
- left = 0;
- } else {
- for (let i = 0; i < position; i++) {
- left += this.percentWidthArr[i];
- }
- }
- child.el.style.left = `${left}px`;
- // 正常布局渲染
- } else {
- if (this.align === 'left') { // eslint-disable-line no-lonely-if
- child.el.style.left = `${position * (child.width + this.gutterWidth)}px`;
- } else if (this.align === 'right') {
- child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth}px`;
- } else {
- child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth / 2}px`;
- }
- }
- if (child.height === 0) {
- return;
- }
- child.el.style.top = `${offsetArr[position]}px`;
- offsetArr[position] += (child.height + this.gutterHeight);
- this.$el.style.height = `${Math.max.apply(Math, offsetArr)}px`;
- });
- this.$emit('rendered', this);
- }
- }
+ getNewDomChildren() {
+ const node = this.$refs.waterfall;
+ const children = this.options && this.options.itemSelector
+ ? node.querySelectorAll(this.options.itemSelector) : node.children;
+ return Array.prototype.slice.call(children);
+ },
+ },
};
</script>
+
+<style lang="scss" scoped>
+.wfi {
+
+}
+</style>
diff --git a/src/site/components/grid/waterfall/WaterfallItem.vue b/src/site/components/grid/waterfall/WaterfallItem.vue
index a02ea1f..c5cade1 100644
--- a/src/site/components/grid/waterfall/WaterfallItem.vue
+++ b/src/site/components/grid/waterfall/WaterfallItem.vue
@@ -1,60 +1,10 @@
-<style>
- .waterfall-item {
- position: absolute;
- }
-</style>
<template>
<div class="waterfall-item">
<slot />
</div>
</template>
<script>
-import imagesLoaded from 'imagesloaded';
export default {
name: 'WaterfallItem',
- props: {
- order: {
- type: Number,
- default: 0
- },
- width: {
- type: Number,
- default: 150
- }
- },
- data() {
- return {
- itemWidth: 0,
- height: 0
- };
- },
- created() {
- this.$watch(() => this.height, this.emit);
- },
- mounted() {
- this.$el.style.display = 'none';
- this.$el.style.width = `${this.width}px`;
- this.emit();
- imagesLoaded(this.$el, () => {
- this.$el.style.left = '-9999px';
- this.$el.style.top = '-9999px';
- this.$el.style.display = 'block';
- this.height = this.$el.offsetHeight;
- this.itemWidth = this.$el.offsetWidth;
- });
- },
- methods: {
- emit() {
- this.$parent.$emit('itemRender');
- },
- getMeta() {
- return {
- el: this.$el,
- height: this.height,
- width: this.itemWidth,
- order: this.order
- };
- }
- }
-}
+};
</script>
diff --git a/src/site/components/image-modal/ImageInfo.vue b/src/site/components/image-modal/ImageInfo.vue
new file mode 100644
index 0000000..c9dba1a
--- /dev/null
+++ b/src/site/components/image-modal/ImageInfo.vue
@@ -0,0 +1,179 @@
+<template>
+ <div class="container has-background-lolisafe">
+ <div class="columns is-marginless">
+ <div class="column fucking-opl-shut-up">
+ <img src="https://placehold.it/1024x10024">
+ </div>
+ <div class="column is-one-third">
+ <div class="sticky">
+ <div class="divider is-lolisafe has-text-light">
+ File information
+ </div>
+ <b-field
+ label="ID"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <span class="fake-input">{{ file.id }}</span>
+ </div>
+ </b-field>
+ <b-field
+ label="Name"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <span class="fake-input">{{ file.name }}</span>
+ </div>
+ </b-field>
+
+ <b-field
+ label="Original Name"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <span class="fake-input">{{ file.original }}</span>
+ </div>
+ </b-field>
+
+ <b-field
+ label="IP"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <span class="fake-input">{{ file.ip }}</span>
+ </div>
+ </b-field>
+
+ <b-field
+ label="Link"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <a
+ class="fake-input"
+ :href="file.url"
+ target="_blank">{{ file.url }}</a>
+ </div>
+ </b-field>
+
+ <b-field
+ label="Size"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <span class="fake-input">{{ formatBytes(file.size) }}</span>
+ </div>
+ </b-field>
+
+ <b-field
+ label="Hash"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <span class="fake-input">{{ file.hash }}</span>
+ </div>
+ </b-field>
+
+ <b-field
+ label="Uploaded"
+ label-position="on-border"
+ type="is-lolisafe"
+ class="lolisafe-on-border">
+ <div class="control">
+ <span class="fake-input"><timeago :since="file.createdAt" /></span>
+ </div>
+ </b-field>
+ <div class="divider is-lolisafe has-text-light">
+ Albums
+ </div>
+
+ <div class="divider is-lolisafe has-text-light">
+ Tags
+ </div>
+ <b-field label="Add some tags">
+ <b-taginput
+ v-model="tags"
+ class="lolisafe"
+ ellipsis
+ icon="label"
+ placeholder="Add a tag" />
+ </b-field>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+
+export default {
+ props: {
+ file: {
+ type: Object,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ tags: [],
+ };
+ },
+ computed: mapState(['images']),
+ methods: {
+ formatBytes(bytes, decimals = 2) {
+ if (bytes === 0) return '0 Bytes';
+
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+@import '~/assets/styles/_colors.scss';
+.modal-content, .modal-card {
+ max-height: 100%;
+}
+
+.fake-input {
+ font-size: 1rem !important;
+ height: 2.5rem;
+ border-color: #323846; /* $lolisafe */
+ max-width: 100%;
+ width: 100%;
+ border-radius: 4px;
+ display: inline-block;
+ font-size: 1rem;
+ justify-content: flex-start;
+ line-height: 1.5;
+ padding-bottom: calc(0.375em - 1px);
+ padding-left: calc(0.625em - 1px);
+ padding-right: calc(0.625em - 1px);
+ padding-top: calc(0.375em - 1px);
+ background-color: #21252d;
+ border: 2px solid #21252d;
+ border-radius: 0.3em !important;
+
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.divider:first-child {
+ margin: 10px 0 25px;
+}
+</style>
diff --git a/src/site/components/loading/BulmaLoading.vue b/src/site/components/loading/BulmaLoading.vue
new file mode 100644
index 0000000..37cc5a5
--- /dev/null
+++ b/src/site/components/loading/BulmaLoading.vue
@@ -0,0 +1,33 @@
+<template>
+ <div class="loader-wrapper">
+ <div class="loader is-loading" />
+ </div>
+</template>
+
+<style lang="scss" scoped>
+ .loader-wrapper {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ background: #fff;
+ opacity: 0;
+ z-index: -1;
+ transition: opacity .3s;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 6px;
+
+ .loader {
+ height: 80px;
+ width: 80px;
+ }
+
+ &.is-active {
+ opacity: 1;
+ z-index: 1;
+ }
+ }
+</style>
diff --git a/src/site/components/loading/CubeShadow.vue b/src/site/components/loading/CubeShadow.vue
index af31dac..bbfdb52 100644
--- a/src/site/components/loading/CubeShadow.vue
+++ b/src/site/components/loading/CubeShadow.vue
@@ -1,5 +1,6 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner--cube-shadow" />
</template>
@@ -8,16 +9,16 @@ export default {
props: {
size: {
type: String,
- default: '60px'
+ default: '60px',
},
background: {
type: String,
- default: '#9C27B0'
+ default: '#9C27B0',
},
duration: {
type: String,
- default: '1.8s'
- }
+ default: '1.8s',
+ },
},
computed: {
styles() {
@@ -25,10 +26,10 @@ export default {
width: this.size,
height: this.size,
backgroundColor: this.background,
- animationDuration: this.duration
+ animationDuration: this.duration,
};
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/components/loading/Origami.vue b/src/site/components/loading/Origami.vue
index d1b523d..cd1c087 100644
--- a/src/site/components/loading/Origami.vue
+++ b/src/site/components/loading/Origami.vue
@@ -1,7 +1,9 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner-origami">
- <div :style="innerStyles"
+ <div
+ :style="innerStyles"
class="spinner-inner loading">
<span class="slice" />
<span class="slice" />
@@ -18,21 +20,21 @@ export default {
props: {
size: {
type: String,
- default: '40px'
- }
+ default: '40px',
+ },
},
computed: {
innerStyles() {
- let size = parseInt(this.size);
+ const size = parseInt(this.size, 10);
return { transform: `scale(${(size / 60)})` };
},
styles() {
return {
width: this.size,
- height: this.size
+ height: this.size,
};
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/components/loading/PingPong.vue b/src/site/components/loading/PingPong.vue
index ac33e28..d562e9f 100644
--- a/src/site/components/loading/PingPong.vue
+++ b/src/site/components/loading/PingPong.vue
@@ -1,12 +1,14 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner--ping-pong">
- <div :style="innerStyles"
+ <div
+ :style="innerStyles"
class="spinner-inner">
<div class="board">
- <div class="left"/>
- <div class="right"/>
- <div class="ball"/>
+ <div class="left" />
+ <div class="right" />
+ <div class="ball" />
</div>
</div>
</div>
@@ -17,22 +19,22 @@ export default {
props: {
size: {
type: String,
- default: '60px'
- }
+ default: '60px',
+ },
},
computed: {
innerStyles() {
- let size = parseInt(this.size);
+ const size = parseInt(this.size, 10);
return { transform: `scale(${size / 250})` };
},
styles() {
return {
width: this.size,
- height: this.size
+ height: this.size,
};
- }
- }
-}
+ },
+ },
+};
</script>
<style lang="scss" scoped>
diff --git a/src/site/components/loading/RotateSquare.vue b/src/site/components/loading/RotateSquare.vue
index 4da8300..089e01a 100644
--- a/src/site/components/loading/RotateSquare.vue
+++ b/src/site/components/loading/RotateSquare.vue
@@ -1,5 +1,6 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner--rotate-square-2" />
</template>
@@ -8,18 +9,18 @@ export default {
props: {
size: {
type: String,
- default: '40px'
- }
+ default: '40px',
+ },
},
computed: {
styles() {
return {
width: this.size,
height: this.size,
- display: 'inline-block'
+ display: 'inline-block',
};
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/components/navbar/Navbar.vue b/src/site/components/navbar/Navbar.vue
index 48dfeb1..0f82200 100644
--- a/src/site/components/navbar/Navbar.vue
+++ b/src/site/components/navbar/Navbar.vue
@@ -1,95 +1,97 @@
<template>
- <nav :class="{ isWhite }"
- class="navbar is-transparent">
- <div class="navbar-brand">
- <a role="button"
- class="navbar-burger burger"
- aria-label="menu"
- aria-expanded="false"
- data-target="navbarBasicExample">
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- </a>
- </div>
- <div class="navbar-menu">
- <div class="navbar-end">
+ <b-navbar
+ :class="{ isWhite }"
+ transparent>
+ <template slot="end">
+ <b-navbar-item tag="div">
<router-link
to="/"
class="navbar-item no-active"
exact>
Home
</router-link>
+ </b-navbar-item>
+ <b-navbar-item tag="div">
<router-link
- to="/"
+ to="/faq"
class="navbar-item no-active"
exact>
Docs
</router-link>
- <template v-if="loggedIn">
+ </b-navbar-item>
+ <template v-if="loggedIn">
+ <b-navbar-item tag="div">
<router-link
to="/dashboard"
class="navbar-item no-active"
exact>
Uploads
</router-link>
+ </b-navbar-item>
+ <b-navbar-item tag="div">
<router-link
to="/dashboard/albums"
class="navbar-item no-active"
exact>
Albums
</router-link>
+ </b-navbar-item>
+ <b-navbar-item tag="div">
<router-link
to="/dashboard/account"
class="navbar-item no-active"
exact>
Account
</router-link>
+ </b-navbar-item>
+ <b-navbar-item tag="div">
<router-link
to="/"
class="navbar-item no-active"
@click.native="logOut">
Logout
</router-link>
- </template>
- <template v-else>
+ </b-navbar-item>
+ </template>
+ <template v-else>
+ <b-navbar-item tag="div">
<router-link
class="navbar-item"
to="/login">
Login
</router-link>
- </template>
- </div>
- </div>
- </nav>
+ </b-navbar-item>
+ </template>
+ </template>
+ </b-navbar>
</template>
<script>
+import { mapState, mapGetters } from 'vuex';
+
export default {
props: {
isWhite: {
type: Boolean,
- default: false
- }
+ default: false,
+ },
},
data() {
return { hamburger: false };
},
computed: {
- loggedIn() {
- return this.$store.state.loggedIn;
- },
- config() {
- return this.$store.state.config;
- }
+ ...mapGetters({ loggedIn: 'auth/isLoggedIn' }),
+ ...mapState(['config']),
},
methods: {
- logOut() {
- this.$store.dispatch('logout');
- }
- }
+ async logOut() {
+ await this.$store.dispatch('auth/logout');
+ this.$router.replace('/login');
+ },
+ },
};
</script>
+
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
nav.navbar {
diff --git a/src/site/components/sidebar/Sidebar.vue b/src/site/components/sidebar/Sidebar.vue
index a2ad3f4..d586122 100644
--- a/src/site/components/sidebar/Sidebar.vue
+++ b/src/site/components/sidebar/Sidebar.vue
@@ -1,64 +1,80 @@
<template>
- <div class="dashboard-menu">
- <router-link to="/dashboard">
- <i class="icon-com-pictures" />Files
- </router-link>
- <router-link to="/dashboard/albums">
- <i class="icon-interface-window" />Albums
- </router-link>
- <!--
- <router-link to="/dashboard/tags">
- <i class="icon-ecommerce-tag-c" />Tags
- </router-link>
- -->
- <router-link to="/dashboard/account">
- <i class="icon-ecommerce-tag-c" />Account
- </router-link>
- <template v-if="user && user.isAdmin">
- <router-link to="/dashboard/admin/users">
- <i class="icon-setting-gear-a" />Users
- </router-link>
- <!--
- TODO: Dont wanna deal with this now
- <router-link to="/dashboard/admin/settings">
- <i class="icon-setting-gear-a" />Settings
- </router-link>
- -->
- </template>
- </div>
+ <b-menu class="dashboard-menu">
+ <b-menu-list label="Menu">
+ <b-menu-item
+ class="item"
+ icon="information-outline"
+ label="Dashboard"
+ tag="nuxt-link"
+ to="/dashboard"
+ exact />
+ <b-menu-item
+ class="item"
+ icon="image-multiple-outline"
+ label="Albums"
+ tag="nuxt-link"
+ to="/dashboard/albums"
+ exact />
+ <b-menu-item
+ class="item"
+ icon="tag-outline"
+ label="Tags"
+ tag="nuxt-link"
+ to="/dashboard/tags"
+ exact />
+ <b-menu-item icon="settings" expanded>
+ <template slot="label" slot-scope="props">
+ Administration
+ <b-icon class="is-pulled-right" :icon="props.expanded ? 'menu-down' : 'menu-up'" />
+ </template>
+ <b-menu-item icon="account" label="Users" tag="nuxt-link" to="/dashboard/admin/users" exact />
+ <b-menu-item icon="cog-outline" label="Settings" tag="nuxt-link" to="/dashboard/admin/settings" exact />
+ </b-menu-item>
+ <b-menu-item
+ class="item"
+ icon="account-cog-outline"
+ label="My account"
+ tag="nuxt-link"
+ to="/dashboard/account"
+ exact />
+ </b-menu-list>
+ <b-menu-list label="Actions">
+ <b-menu-item icon="exit-to-app" label="Logout" tag="nuxt-link" to="/logout" exact />
+ </b-menu-list>
+ </b-menu>
</template>
<script>
+import { mapState } from 'vuex';
+
export default {
- computed: {
- user() {
- return this.$store.state.user;
- }
- }
+ computed: mapState({
+ user: (state) => state.auth.user,
+ }),
+ methods: {
+ isRouteActive(id) {
+ if (this.$route.path.includes(id)) {
+ return true;
+ }
+ return false;
+ },
+ },
};
+
</script>
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
.dashboard-menu {
- padding: 2rem;
- border-radius: 8px;
+ ::v-deep a:hover {
+ cursor: pointer;
+ text-decoration: none;
+ }
- a {
- display: block;
- font-weight: 700;
- color: $textColor;
- position: relative;
- padding-left: 40px;
- height: 35px;
- &:hover{
- color: white;
- }
+ ::v-deep .icon {
+ margin-right: 0.5rem;
+ }
- i {
- position: absolute;
- font-size: 1.5em;
- top: -4px;
- left: 5px;
- }
+ ::v-deep .icon.is-pulled-right {
+ margin-right: 0;
}
hr { margin-top: 0.6em; }
diff --git a/src/site/components/uploader/Uploader.vue b/src/site/components/uploader/Uploader.vue
index 1b03ff8..7e2d446 100644
--- a/src/site/components/uploader/Uploader.vue
+++ b/src/site/components/uploader/Uploader.vue
@@ -1,7 +1,9 @@
<template>
- <div :class="{ 'has-files': alreadyAddedFiles }"
+ <div
+ :class="{ 'has-files': alreadyAddedFiles }"
class="uploader-wrapper">
- <b-select v-if="loggedIn"
+ <b-select
+ v-if="loggedIn"
v-model="selectedAlbum"
placeholder="Upload to album"
size="is-medium"
@@ -13,7 +15,8 @@
{{ album.name }}
</option>
</b-select>
- <dropzone v-if="showDropzone"
+ <dropzone
+ v-if="showDropzone"
id="dropzone"
ref="el"
:options="dropzoneOptions"
@@ -25,16 +28,22 @@
Add or drop more files
</label>
- <div id="template"
+ <div
+ id="template"
ref="template">
<div class="dz-preview dz-file-preview">
<div class="dz-details">
- <div class="dz-filename"><span data-dz-name /></div>
- <div class="dz-size"><span data-dz-size /></div>
+ <div class="dz-filename">
+ <span data-dz-name />
+ </div>
+ <div class="dz-size">
+ <span data-dz-size />
+ </div>
</div>
<div class="result">
<div class="openLink">
- <a class="link"
+ <a
+ class="link"
target="_blank">
Link
</a>
@@ -43,14 +52,16 @@
<div class="error">
<div>
<span>
- <span class="error-message"
+ <span
+ class="error-message"
data-dz-errormessage />
<i class="icon-web-warning" />
</span>
</div>
</div>
<div class="dz-progress">
- <span class="dz-upload"
+ <span
+ class="dz-upload"
data-dz-uploadprogress />
</div>
<!--
@@ -64,6 +75,8 @@
</template>
<script>
+import { mapState, mapGetters } from 'vuex';
+
import Dropzone from 'nuxt-dropzone';
import '~/assets/styles/dropzone.scss';
@@ -75,20 +88,15 @@ export default {
files: [],
dropzoneOptions: {},
showDropzone: false,
- albums: [],
- selectedAlbum: null
+ selectedAlbum: null,
};
},
computed: {
- config() {
- return this.$store.state.config;
- },
- token() {
- return this.$store.state.token;
- },
- loggedIn() {
- return this.$store.state.loggedIn;
- }
+ ...mapState({
+ config: (state) => state.config,
+ albums: (state) => state.albums.tinyDetails,
+ }),
+ ...mapGetters({ loggedIn: 'auth/isLoggedIn', token: 'auth/getToken' }),
},
watch: {
loggedIn() {
@@ -96,7 +104,7 @@ export default {
},
selectedAlbum() {
this.updateDropzoneConfig();
- }
+ },
},
mounted() {
this.dropzoneOptions = {
@@ -119,7 +127,7 @@ export default {
maxFilesize: this.config.maxFileSize,
previewTemplate: this.$refs.template.innerHTML,
dictDefaultMessage: 'Drag & Drop your files or click to browse',
- headers: { Accept: 'application/vnd.lolisafe.json' }
+ headers: { Accept: 'application/vnd.lolisafe.json' },
};
this.showDropzone = true;
if (this.loggedIn) this.getAlbums();
@@ -129,8 +137,11 @@ export default {
Get all available albums so the user can upload directly to one (or several soon™) of them.
*/
async getAlbums() {
- const response = await this.$axios.$get(`albums/dropdown`);
- this.albums = response.albums;
+ try {
+ await this.$store.dispatch('albums/getTinyDetails');
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
this.updateDropzoneConfig();
},
@@ -143,14 +154,14 @@ export default {
this.$refs.el.setOption('headers', {
Accept: 'application/vnd.lolisafe.json',
Authorization: this.token ? `Bearer ${this.token}` : '',
- albumId: this.selectedAlbum ? this.selectedAlbum : null
+ albumId: this.selectedAlbum ? this.selectedAlbum : null,
});
},
/*
Dropzone stuff
*/
- dropzoneFilesAdded(files) {
+ dropzoneFilesAdded() {
this.alreadyAddedFiles = true;
},
dropzoneSuccess(file, response) {
@@ -159,8 +170,9 @@ export default {
dropzoneError(file, message, xhr) {
this.$store.dispatch('alert', {
text: 'There was an error uploading this file. Check the console.',
- error: true
+ error: true,
});
+ // eslint-disable-next-line no-console
console.error(file, message, xhr);
},
async dropzoneChunksUploaded(file, done) {
@@ -170,12 +182,11 @@ export default {
original: file.name,
size: file.size,
type: file.type,
- count: file.upload.totalChunkCount
- }]
+ count: file.upload.totalChunkCount,
+ }],
});
this.processResult(file, data);
- this.$forceUpdate();
return done();
},
@@ -194,8 +205,8 @@ export default {
this.$clipboard(response.url);
});
*/
- }
- }
+ },
+ },
};
</script>
<style lang="scss" scoped>