mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-30 05:23:08 +02:00
parent
705d40ab37
commit
3f71b14637
22 changed files with 249 additions and 214 deletions
14
migration/1595075960584-blurhash.ts
Normal file
14
migration/1595075960584-blurhash.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class blurhash1595075960584 implements MigrationInterface {
|
||||||
|
name = 'blurhash1595075960584'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" ADD "blurhash" character varying(128)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "blurhash"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
migration/1595077605646-blurhash-for-avatar-banner.ts
Normal file
20
migration/1595077605646-blurhash-for-avatar-banner.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class blurhashForAvatarBanner1595077605646 implements MigrationInterface {
|
||||||
|
name = 'blurhashForAvatarBanner1595077605646'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarColor"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerColor"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "bannerColor" character varying(32)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "avatarColor" character varying(32)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -112,6 +112,7 @@
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.713.0",
|
"aws-sdk": "2.713.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
|
"blurhash": "1.1.3",
|
||||||
"bull": "3.15.0",
|
"bull": "3.15.0",
|
||||||
"cafy": "15.2.1",
|
"cafy": "15.2.1",
|
||||||
"cbor": "5.0.2",
|
"cbor": "5.0.2",
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
|
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
|
||||||
<span class="inner" :style="icon"></span>
|
<img class="inner" :src="url"/>
|
||||||
</span>
|
</span>
|
||||||
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
|
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
|
||||||
<span class="inner" :style="icon"></span>
|
<img class="inner" :src="url"/>
|
||||||
</span>
|
|
||||||
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
|
|
||||||
<span class="inner" :style="icon"></span>
|
|
||||||
</router-link>
|
|
||||||
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
|
|
||||||
<span class="inner" :style="icon"></span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -45,22 +39,6 @@ export default Vue.extend({
|
||||||
? getStaticImageUrl(this.user.avatarUrl)
|
? getStaticImageUrl(this.user.avatarUrl)
|
||||||
: this.user.avatarUrl;
|
: this.user.avatarUrl;
|
||||||
},
|
},
|
||||||
icon(): any {
|
|
||||||
return {
|
|
||||||
backgroundColor: this.user.avatarColor,
|
|
||||||
backgroundImage: `url(${this.url})`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'user.avatarColor'() {
|
|
||||||
this.$el.style.color = this.user.avatarColor;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.user.avatarColor) {
|
|
||||||
this.$el.style.color = this.user.avatarColor;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick(e) {
|
onClick(e) {
|
||||||
|
@ -102,15 +80,17 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
background-position: center center;
|
position: absolute;
|
||||||
background-size: cover;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,36 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`">
|
<div class="zdjebgpv" ref="thumbnail">
|
||||||
<img
|
<img-with-blurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
|
||||||
:src="file.url"
|
|
||||||
:alt="file.name"
|
|
||||||
:title="file.name"
|
|
||||||
@load="onThumbnailLoaded"
|
|
||||||
v-if="detail && is === 'image'"/>
|
|
||||||
<video
|
|
||||||
:src="file.url"
|
|
||||||
ref="volumectrl"
|
|
||||||
preload="metadata"
|
|
||||||
controls
|
|
||||||
v-else-if="detail && is === 'video'"/>
|
|
||||||
<img :src="file.thumbnailUrl" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
|
|
||||||
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
|
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
|
||||||
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
|
<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="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
|
||||||
|
|
||||||
<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
|
<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
|
||||||
<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
|
<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
|
||||||
<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
|
<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
|
||||||
<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
|
<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
|
||||||
<fa :icon="faFile" class="icon" v-else/>
|
<fa :icon="faFile" class="icon" v-else/>
|
||||||
|
<fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
|
||||||
<fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -47,8 +26,12 @@ import {
|
||||||
faFileArchive,
|
faFileArchive,
|
||||||
faFilm
|
faFilm
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import ImgWithBlurhash from './img-with-blurhash.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
ImgWithBlurhash
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
file: {
|
file: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -59,11 +42,6 @@ export default Vue.extend({
|
||||||
required: false,
|
required: false,
|
||||||
default: 'cover'
|
default: 'cover'
|
||||||
},
|
},
|
||||||
detail: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -108,20 +86,12 @@ export default Vue.extend({
|
||||||
? (this.is === 'image' || this.is === 'video')
|
? (this.is === 'image' || this.is === 'video')
|
||||||
: false;
|
: false;
|
||||||
},
|
},
|
||||||
background(): string {
|
|
||||||
return this.file.properties.avgColor || 'transparent';
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
|
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
|
||||||
if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
|
if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onThumbnailLoaded() {
|
|
||||||
if (this.file.properties.avgColor) {
|
|
||||||
this.$refs.thumbnail.style.backgroundColor = 'transparent';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
volumechange() {
|
volumechange() {
|
||||||
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
|
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
|
||||||
this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
|
this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
|
||||||
|
@ -132,14 +102,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.zdjebgpv {
|
.zdjebgpv {
|
||||||
display: flex;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> img,
|
|
||||||
> .icon {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon-sub {
|
> .icon-sub {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
|
@ -153,37 +117,10 @@ export default Vue.extend({
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.detail) {
|
|
||||||
> img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
|
pointer-events: none;
|
||||||
height: 65%;
|
height: 65%;
|
||||||
width: 65%;
|
width: 65%;
|
||||||
}
|
}
|
||||||
|
|
||||||
> video,
|
|
||||||
> audio {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.detail {
|
|
||||||
> .icon {
|
|
||||||
height: 100px;
|
|
||||||
width: 100px;
|
|
||||||
margin: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:not(.icon) {
|
|
||||||
max-height: 300px;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -126,17 +126,6 @@ export default Vue.extend({
|
||||||
this.browser.isDragSource = false;
|
this.browser.isDragSource = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
onThumbnailLoaded() {
|
|
||||||
if (this.file.properties.avgColor) {
|
|
||||||
anime({
|
|
||||||
targets: this.$refs.thumbnail,
|
|
||||||
backgroundColor: 'transparent', // TODO fade
|
|
||||||
duration: 100,
|
|
||||||
easing: 'linear'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
rename() {
|
rename() {
|
||||||
this.$root.dialog({
|
this.$root.dialog({
|
||||||
title: this.$t('renameFile'),
|
title: this.$t('renameFile'),
|
||||||
|
@ -332,7 +321,6 @@ export default Vue.extend({
|
||||||
width: 128px;
|
width: 128px;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: var(--driveFileIcon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .name {
|
> .name {
|
||||||
|
|
78
src/client/components/img-with-blurhash.vue
Normal file
78
src/client/components/img-with-blurhash.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="xubzgfgb" :title="title">
|
||||||
|
<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/>
|
||||||
|
<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { decode } from 'blurhash';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
hash: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 64
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.draw();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
draw() {
|
||||||
|
const pixels = decode(this.hash, this.size, this.size);
|
||||||
|
const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
|
||||||
|
const imageData = ctx!.createImageData(this.size, this.size);
|
||||||
|
imageData.data.set(pixels);
|
||||||
|
ctx!.putImageData(imageData, 0, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.xubzgfgb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> canvas,
|
||||||
|
> img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,19 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="hide" @click="hide = false">
|
<div class="qjewsnkg" v-if="hide" @click="hide = false">
|
||||||
|
<img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/>
|
||||||
|
<div class="text">
|
||||||
<div>
|
<div>
|
||||||
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
||||||
<span>{{ $t('clickToShow') }}</span>
|
<span>{{ $t('clickToShow') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else>
|
</div>
|
||||||
|
<div class="gqnyydlz" v-else>
|
||||||
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
|
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
|
||||||
<a
|
<a
|
||||||
:href="image.url"
|
:href="image.url"
|
||||||
:style="style"
|
|
||||||
:title="image.name"
|
:title="image.name"
|
||||||
@click.prevent="onClick"
|
@click.prevent="onClick"
|
||||||
>
|
>
|
||||||
<div v-if="image.type === 'image/gif'">GIF</div>
|
<img-with-blurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name"/>
|
||||||
|
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -23,8 +26,12 @@ import Vue from 'vue';
|
||||||
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||||
import ImageViewer from './image-viewer.vue';
|
import ImageViewer from './image-viewer.vue';
|
||||||
|
import ImgWithBlurhash from './img-with-blurhash.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
ImgWithBlurhash
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -42,23 +49,18 @@ export default Vue.extend({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style(): any {
|
url(): any {
|
||||||
let url = `url(${
|
let url = this.$store.state.device.disableShowingAnimatedImages
|
||||||
this.$store.state.device.disableShowingAnimatedImages
|
|
||||||
? getStaticImageUrl(this.image.thumbnailUrl)
|
? getStaticImageUrl(this.image.thumbnailUrl)
|
||||||
: this.image.thumbnailUrl
|
: this.image.thumbnailUrl;
|
||||||
})`;
|
|
||||||
|
|
||||||
if (this.$store.state.device.loadRemoteMedia) {
|
if (this.$store.state.device.loadRemoteMedia) {
|
||||||
url = null;
|
url = null;
|
||||||
} else if (this.raw || this.$store.state.device.loadRawImages) {
|
} else if (this.raw || this.$store.state.device.loadRawImages) {
|
||||||
url = `url(${this.image.url})`;
|
url = this.image.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return url;
|
||||||
'background-color': this.image.properties.avgColor || 'transparent',
|
|
||||||
'background-image': url
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -82,7 +84,38 @@ export default Vue.extend({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.gqnyydlzavusgskkfvwvjiattxdzsqlf {
|
.qjewsnkg {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> .bg {
|
||||||
|
filter: brightness(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gqnyydlz {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> i {
|
> i {
|
||||||
|
@ -110,7 +143,7 @@ export default Vue.extend({
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
> div {
|
> .gif {
|
||||||
background-color: var(--fg);
|
background-color: var(--fg);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--accentLighten);
|
color: var(--accentLighten);
|
||||||
|
@ -126,22 +159,4 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.qjewsnkgzzxlxtzncydssfbgjibiehcy {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: #111;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: table-cell;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-count="1"] {
|
&[data-count="1"] {
|
||||||
|
|
|
@ -10,8 +10,7 @@
|
||||||
<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
|
<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
|
||||||
<div class="file" v-if="message.file">
|
<div class="file" v-if="message.file">
|
||||||
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
|
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
|
||||||
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"
|
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
|
||||||
:style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/>
|
|
||||||
<p v-else>{{ message.file.name }}</p>
|
<p v-else>{{ message.file.name }}</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<section class="oyyftmcf">
|
<section class="oyyftmcf">
|
||||||
<mk-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/>
|
<mk-file-thumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/>
|
||||||
</section>
|
</section>
|
||||||
</x-container>
|
</x-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -123,10 +123,6 @@ a {
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import * as fileType from 'file-type';
|
||||||
import isSvg from 'is-svg';
|
import isSvg from 'is-svg';
|
||||||
import * as probeImageSize from 'probe-image-size';
|
import * as probeImageSize from 'probe-image-size';
|
||||||
import * as sharp from 'sharp';
|
import * as sharp from 'sharp';
|
||||||
|
import { encode } from 'blurhash';
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ export type FileInfo = {
|
||||||
};
|
};
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
avgColor?: number[];
|
blurhash?: string;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,12 +72,11 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// average color
|
let blurhash: string | undefined;
|
||||||
let avgColor: number[] | undefined;
|
|
||||||
|
|
||||||
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
|
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
|
||||||
avgColor = await calcAvgColor(path).catch(e => {
|
blurhash = await getBlurhash(path).catch(e => {
|
||||||
warnings.push(`calcAvgColor failed: ${e}`);
|
warnings.push(`getBlurhash failed: ${e}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
||||||
type,
|
type,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
avgColor,
|
blurhash,
|
||||||
warnings,
|
warnings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -173,18 +173,15 @@ async function detectImageSize(path: string): Promise<{
|
||||||
/**
|
/**
|
||||||
* Calculate average color of image
|
* Calculate average color of image
|
||||||
*/
|
*/
|
||||||
async function calcAvgColor(path: string): Promise<number[]> {
|
function getBlurhash(path: string): Promise<string> {
|
||||||
const img = sharp(path);
|
return new Promise((resolve, reject) => {
|
||||||
|
sharp(path)
|
||||||
const info = await (img as any).stats();
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
if (info.isOpaque) {
|
.resize(64, 64, { fit: 'inside' })
|
||||||
const r = Math.round(info.channels[0].mean);
|
.toBuffer((err, buffer, { width, height }) => {
|
||||||
const g = Math.round(info.channels[1].mean);
|
if (err) return reject(err);
|
||||||
const b = Math.round(info.channels[2].mean);
|
resolve(encode(new Uint8ClampedArray(buffer), width, height, 7, 7));
|
||||||
|
});
|
||||||
return [r, g, b];
|
});
|
||||||
} else {
|
|
||||||
return [255, 255, 255];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,12 @@ export class DriveFile {
|
||||||
})
|
})
|
||||||
public comment: string | null;
|
public comment: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128, nullable: true,
|
||||||
|
comment: 'The BlurHash string.'
|
||||||
|
})
|
||||||
|
public blurhash: string | null;
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: {},
|
default: {},
|
||||||
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
|
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
|
||||||
|
|
|
@ -106,14 +106,14 @@ export class User {
|
||||||
public bannerUrl: string | null;
|
public bannerUrl: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 32, nullable: true,
|
length: 128, nullable: true,
|
||||||
})
|
})
|
||||||
public avatarColor: string | null;
|
public avatarBlurhash: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 32, nullable: true,
|
length: 128, nullable: true,
|
||||||
})
|
})
|
||||||
public bannerColor: string | null;
|
public bannerBlurhash: string | null;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -115,6 +115,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||||
md5: file.md5,
|
md5: file.md5,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
isSensitive: file.isSensitive,
|
isSensitive: file.isSensitive,
|
||||||
|
blurhash: file.blurhash,
|
||||||
properties: file.properties,
|
properties: file.properties,
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
|
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
|
||||||
thumbnailUrl: this.getPublicUrl(file, true, meta),
|
thumbnailUrl: this.getPublicUrl(file, true, meta),
|
||||||
|
|
|
@ -165,7 +165,8 @@ export class UserRepository extends Repository<User> {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
host: user.host,
|
host: user.host,
|
||||||
avatarUrl: user.avatarUrl ? user.avatarUrl : config.url + '/avatar/' + user.id,
|
avatarUrl: user.avatarUrl ? user.avatarUrl : config.url + '/avatar/' + user.id,
|
||||||
avatarColor: user.avatarColor,
|
avatarBlurhash: user.avatarBlurhash,
|
||||||
|
avatarColor: null, // 後方互換性のため
|
||||||
isAdmin: user.isAdmin || falsy,
|
isAdmin: user.isAdmin || falsy,
|
||||||
isModerator: user.isModerator || falsy,
|
isModerator: user.isModerator || falsy,
|
||||||
isBot: user.isBot || falsy,
|
isBot: user.isBot || falsy,
|
||||||
|
@ -196,7 +197,8 @@ export class UserRepository extends Repository<User> {
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||||
bannerUrl: user.bannerUrl,
|
bannerUrl: user.bannerUrl,
|
||||||
bannerColor: user.bannerColor,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
bannerColor: null, // 後方互換性のため
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isModerator: user.isModerator || falsy,
|
isModerator: user.isModerator || falsy,
|
||||||
isSilenced: user.isSilenced || falsy,
|
isSilenced: user.isSilenced || falsy,
|
||||||
|
@ -331,7 +333,7 @@ export const packedUserSchema = {
|
||||||
format: 'url',
|
format: 'url',
|
||||||
nullable: true as const, optional: false as const,
|
nullable: true as const, optional: false as const,
|
||||||
},
|
},
|
||||||
avatarColor: {
|
avatarBlurhash: {
|
||||||
type: 'any' as const,
|
type: 'any' as const,
|
||||||
nullable: true as const, optional: false as const,
|
nullable: true as const, optional: false as const,
|
||||||
},
|
},
|
||||||
|
@ -340,7 +342,7 @@ export const packedUserSchema = {
|
||||||
format: 'url',
|
format: 'url',
|
||||||
nullable: true as const, optional: true as const,
|
nullable: true as const, optional: true as const,
|
||||||
},
|
},
|
||||||
bannerColor: {
|
bannerBlurhash: {
|
||||||
type: 'any' as const,
|
type: 'any' as const,
|
||||||
nullable: true as const, optional: true as const,
|
nullable: true as const, optional: true as const,
|
||||||
},
|
},
|
||||||
|
|
|
@ -226,24 +226,24 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||||
const bannerId = banner ? banner.id : null;
|
const bannerId = banner ? banner.id : null;
|
||||||
const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
|
const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
|
||||||
const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
|
const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
|
||||||
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
|
const avatarBlurhash = avatar ? avatar.blurhash : null;
|
||||||
const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null;
|
const bannerBlurhash = banner ? banner.blurhash : null;
|
||||||
|
|
||||||
await Users.update(user!.id, {
|
await Users.update(user!.id, {
|
||||||
avatarId,
|
avatarId,
|
||||||
bannerId,
|
bannerId,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
bannerUrl,
|
bannerUrl,
|
||||||
avatarColor,
|
avatarBlurhash,
|
||||||
bannerColor
|
bannerBlurhash
|
||||||
});
|
});
|
||||||
|
|
||||||
user!.avatarId = avatarId;
|
user!.avatarId = avatarId;
|
||||||
user!.bannerId = bannerId;
|
user!.bannerId = bannerId;
|
||||||
user!.avatarUrl = avatarUrl;
|
user!.avatarUrl = avatarUrl;
|
||||||
user!.bannerUrl = bannerUrl;
|
user!.bannerUrl = bannerUrl;
|
||||||
user!.avatarColor = avatarColor;
|
user!.avatarBlurhash = avatarBlurhash;
|
||||||
user!.bannerColor = bannerColor;
|
user!.bannerBlurhash = bannerBlurhash;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region カスタム絵文字取得
|
//#region カスタム絵文字取得
|
||||||
|
@ -341,13 +341,13 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
updates.avatarId = avatar.id;
|
updates.avatarId = avatar.id;
|
||||||
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
|
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
|
||||||
updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
|
updates.avatarBlurhash = avatar.blurhash;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (banner) {
|
if (banner) {
|
||||||
updates.bannerId = banner.id;
|
updates.bannerId = banner.id;
|
||||||
updates.bannerUrl = DriveFiles.getPublicUrl(banner);
|
updates.bannerUrl = DriveFiles.getPublicUrl(banner);
|
||||||
updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null;
|
updates.bannerBlurhash = banner.blurhash;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
|
|
|
@ -210,8 +210,8 @@ export default define(meta, async (ps, user, token) => {
|
||||||
|
|
||||||
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
|
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
|
||||||
|
|
||||||
if (avatar.properties.avgColor) {
|
if (avatar.blurhash) {
|
||||||
updates.avatarColor = avatar.properties.avgColor;
|
updates.avatarBlurhash = avatar.blurhash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,8 +223,8 @@ export default define(meta, async (ps, user, token) => {
|
||||||
|
|
||||||
updates.bannerUrl = DriveFiles.getPublicUrl(banner, false);
|
updates.bannerUrl = DriveFiles.getPublicUrl(banner, false);
|
||||||
|
|
||||||
if (banner.properties.avgColor) {
|
if (banner.blurhash) {
|
||||||
updates.bannerColor = banner.properties.avgColor;
|
updates.bannerBlurhash = banner.blurhash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -327,7 +327,6 @@ export default async function(
|
||||||
const properties: {
|
const properties: {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
avgColor?: string;
|
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (info.width) {
|
if (info.width) {
|
||||||
|
@ -335,10 +334,6 @@ export default async function(
|
||||||
properties['height'] = info.height;
|
properties['height'] = info.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.avgColor) {
|
|
||||||
properties['avgColor'] = `rgb(${info.avgColor.join(',')})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = user ? await UserProfiles.findOne(user.id) : null;
|
const profile = user ? await UserProfiles.findOne(user.id) : null;
|
||||||
|
|
||||||
const folder = await fetchFolder();
|
const folder = await fetchFolder();
|
||||||
|
@ -351,6 +346,7 @@ export default async function(
|
||||||
file.folderId = folder !== null ? folder.id : null;
|
file.folderId = folder !== null ? folder.id : null;
|
||||||
file.comment = comment;
|
file.comment = comment;
|
||||||
file.properties = properties;
|
file.properties = properties;
|
||||||
|
file.blurhash = info.blurhash || null;
|
||||||
file.isLink = isLink;
|
file.isLink = isLink;
|
||||||
file.isSensitive = user
|
file.isSensitive = user
|
||||||
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: undefined,
|
width: undefined,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
avgColor: undefined
|
blurhash: null
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: 512,
|
width: 512,
|
||||||
height: 512,
|
height: 512,
|
||||||
avgColor: [ 181, 99, 106 ]
|
blurhash: '' // TODO
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
avgColor: [ 249, 253, 250 ]
|
blurhash: '' // TODO
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
avgColor: [ 249, 253, 250 ]
|
blurhash: '' // TODO
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
avgColor: [ 255, 255, 255 ]
|
blurhash: '' // TODO
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
avgColor: [ 255, 255, 255 ]
|
blurhash: '' // TODO
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
avgColor: [ 255, 255, 255 ]
|
blurhash: '' // TODO
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ describe('Get file info', () => {
|
||||||
},
|
},
|
||||||
width: 25000,
|
width: 25000,
|
||||||
height: 25000,
|
height: 25000,
|
||||||
avgColor: undefined
|
blurhash: '' // TODO
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1669,6 +1669,11 @@ bluebird@^3.1.1, bluebird@^3.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
|
blurhash@1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||||
|
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||||
|
|
||||||
bn.js@^4.0.0:
|
bn.js@^4.0.0:
|
||||||
version "4.11.8"
|
version "4.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||||
|
|
Loading…
Reference in a new issue