diff --git a/src/client/app/common/views/components/drive-file-thumbnail.vue b/src/client/app/common/views/components/drive-file-thumbnail.vue new file mode 100644 index 0000000000..c924aa434d --- /dev/null +++ b/src/client/app/common/views/components/drive-file-thumbnail.vue @@ -0,0 +1,174 @@ +<template> +<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`"> + <img + :src="file.url" + :alt="file.name" + :title="file.name" + v-if="detail && is === 'image'"/> + <video + :src="file.url" + ref="volumectrl" + preload="metadata" + controls + v-else-if="detail && is === 'video'"/> + <img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/> + <fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/> + <fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/> + + <audio + :src="file.url" + ref="volumectrl" + preload="metadata" + controls + v-else-if="detail && is === 'audio'"/> + <fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/> + + <fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/> + <fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/> + <fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/> + <fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/> + <fa :icon="faFile" class="icon" v-else/> + + <fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import anime from 'animejs'; +import i18n from '../../../i18n'; +import { + faFile, + faFileAlt, + faFileImage, + faMusic, + faFileVideo, + faFileCsv, + faFilePdf, + faFileArchive, + faFilm + } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + props: { + file: { + type: Object, + required: true + }, + fit: { + type: String, + required: true + }, + detail: { + type: Boolean, + required: false, + default: false + } + }, + data() { + return { + isContextmenuShowing: false, + isDragging: false, + + faFile, + faFileAlt, + faFileImage, + faMusic, + faFileVideo, + faFileCsv, + faFilePdf, + faFileArchive, + faFilm + }; + }, + computed: { + is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' { + if (this.file.type.startsWith('image/')) return 'image'; + if (this.file.type.startsWith('video/')) return 'video'; + if (this.file.type === 'audio/midi') return 'midi'; + if (this.file.type.startsWith('audio/')) return 'audio'; + if (this.file.type.endsWith('/csv')) return 'csv'; + if (this.file.type.endsWith('/pdf')) return 'pdf'; + if (this.file.type.startsWith('text/')) return 'textfile'; + if ([ + "application/zip", + "application/x-cpio", + "application/x-bzip", + "application/x-bzip2", + "application/java-archive", + "application/x-rar-compressed", + "application/x-tar", + "application/gzip", + "application/x-7z-compressed" + ].some(e => e === this.file.type)) return 'archive'; + return 'unknown'; + }, + isThumbnailAvailable(): boolean { + return this.file.thumbnailUrl.endsWith('?thumbnail') ? (this.is === 'image' || this.is === 'video') : true; + }, + background(): string { + return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 + ? `rgb(${this.file.properties.avgColor.join(',')})` + : 'transparent'; + } + }, + mounted() { + const audioTag = this.$refs.volumectrl as HTMLAudioElement; + if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume; + }, + methods: { + onThumbnailLoaded() { + if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) { + anime({ + targets: this.$refs.thumbnail, + backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`, + duration: 100, + easing: 'linear' + }); + } + }, + volumechange() { + const audioTag = this.$refs.volumectrl as HTMLAudioElement; + this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.zdjebgpv + display flex + + > img, + > .icon + pointer-events none + + > img + height 100% + width 100% + margin auto + object-fit cover + + > .icon + height 65% + width 65% + margin auto + + > video, + > audio + width 100% + + > .icon-sub + position absolute + width 30% + height auto + margin 0 + right 4% + bottom 4% + + &.detail + > .icon + height 100px + margin 16px auto + +</style> diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue index b9d202f555..975b1d6b52 100644 --- a/src/client/app/desktop/views/components/drive.file.vue +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -21,9 +21,9 @@ <img src="/assets/label-red.svg"/> <p>{{ $t('nsfw') }}</p> </div> - <div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`"> - <img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded"/> - </div> + + <x-file-thumbnail class="thumbnail" :file="file" fit="contain"/> + <p class="name"> <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> @@ -34,15 +34,18 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; -import anime from 'animejs'; import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; import updateAvatar from '../../api/update-avatar'; import updateBanner from '../../api/update-banner'; import { appendQuery } from '../../../../../prelude/url'; +import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; export default Vue.extend({ i18n: i18n('desktop/views/components/drive.file.vue'), props: ['file'], + components: { + XFileThumbnail + }, data() { return { isContextmenuShowing: false, @@ -58,11 +61,6 @@ export default Vue.extend({ }, title(): string { return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`; - }, - background(): string { - return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 - ? `rgb(${this.file.properties.avgColor.join(',')})` - : 'transparent'; } }, methods: { @@ -256,6 +254,9 @@ export default Vue.extend({ > .name color var(--primaryForeground) + > .thumbnail + color var(--primaryForeground) + &[data-is-contextmenu-showing] &:after content "" @@ -321,18 +322,7 @@ export default Vue.extend({ width 128px height 128px margin auto - - > img - display block - position absolute - top 0 - left 0 - right 0 - bottom 0 - margin auto - max-width 128px - max-height 128px - pointer-events none + color var(--driveFileIcon) > .name display block diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue index d7432437e0..be6d8594a0 100644 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ b/src/client/app/mobile/views/components/drive.file-detail.vue @@ -1,11 +1,7 @@ <template> <div class="pyvicwrksnfyhpfgkjwqknuururpaztw"> <div class="preview"> - <img v-if="kind == 'image'" ref="img" - :src="file.url" - :alt="file.name" - :title="file.name" - :style="style"> + <x-file-thumbnail class="preview" :file="file" fit="cover" :detail="true"/> <template v-if="kind != 'image'"><fa icon="file"/></template> <footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height"> <span class="size"> @@ -62,11 +58,16 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import { gcd } from '../../../../../prelude/math'; import { appendQuery } from '../../../../../prelude/url'; +import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; export default Vue.extend({ i18n: i18n('mobile/views/components/drive.file-detail.vue'), props: ['file'], + components: { + XFileThumbnail + }, + data() { return { gcd, @@ -147,8 +148,7 @@ export default Vue.extend({ padding 8px background var(--bg) - > img - display block + > .preview max-width 100% max-height 300px margin 0 auto diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue index 28baa528c0..ff6dee2a72 100644 --- a/src/client/app/mobile/views/components/drive.file.vue +++ b/src/client/app/mobile/views/components/drive.file.vue @@ -1,7 +1,7 @@ <template> <a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> <div class="container"> - <div class="thumbnail" :style="thumbnail"></div> + <x-file-thumbnail class="thumbnail" :file="file" fit="cover"/> <div class="body"> <p class="name"> <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> @@ -26,9 +26,14 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; +import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; + export default Vue.extend({ i18n: i18n('mobile/views/components/drive.file.vue'), props: ['file'], + components: { + XFileThumbnail + }, data() { return { isSelected: false @@ -37,12 +42,6 @@ export default Vue.extend({ computed: { browser(): any { return this.$parent; - }, - thumbnail(): any { - return { - 'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent', - 'background-image': `url(${this.file.thumbnailUrl})` - }; } }, created() { @@ -74,9 +73,12 @@ export default Vue.extend({ pointer-events none > .container + display grid max-width 500px margin 0 auto padding 16px + grid-template-columns 64px 1fr + grid-column-gap 10px &:after content "" @@ -84,18 +86,13 @@ export default Vue.extend({ clear both > .thumbnail - display block - float left width 64px height 64px - background-size cover - background-position center center + color var(--driveFileIcon) > .body display block - float left - width calc(100% - 74px) - margin-left 10px + word-break break-all > .name display block @@ -154,6 +151,6 @@ export default Vue.extend({ background var(--primary) &, * - color #fff !important + color var(--primaryForeground) !important </style> diff --git a/src/client/themes/dark.json5 b/src/client/themes/dark.json5 index 13c55999e5..5f44f8570e 100644 --- a/src/client/themes/dark.json5 +++ b/src/client/themes/dark.json5 @@ -153,6 +153,8 @@ messagingRoomMessageBg: '$secondary', messagingRoomMessageFg: '#fff', + driveFileIcon: '$text', + formButtonBorder: 'rgba(255, 255, 255, 0.1)', formButtonHoverBg: ':alpha<0.2<$primary', formButtonHoverBorder: ':alpha<0.5<$primary', diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5 index 65bd3b1216..4617f6aabe 100644 --- a/src/client/themes/light.json5 +++ b/src/client/themes/light.json5 @@ -153,6 +153,8 @@ messagingRoomMessageBg: '#eee', messagingRoomMessageFg: '#333', + driveFileIcon: '$text', + formButtonBorder: 'rgba(0, 0, 0, 0.1)', formButtonHoverBg: ':alpha<0.12<$primary', formButtonHoverBorder: ':alpha<0.3<$primary', @@ -179,7 +181,7 @@ desktopTimelineSrcHover: ':darken<7<$text', desktopWindowTitle: '$text', desktopWindowShadow: 'rgba(0, 0, 0, 0.2)', - desktopDriveBg: '#fff', + desktopDriveBg: '@bg', desktopDriveFolderBg: ':lighten<31<$primary', desktopDriveFolderHoverBg: ':lighten<27<$primary', desktopDriveFolderActiveBg: ':lighten<25<$primary',