feat: Blurhash integration

Resolve #6559
This commit is contained in:
syuilo 2020-07-19 00:24:07 +09:00
parent 0388d5beaf
commit 0c9a52940f
22 changed files with 249 additions and 214 deletions

View 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"`);
}
}

View 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)`);
}
}

View File

@ -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",

View File

@ -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>

View File

@ -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) { > .icon {
> img { pointer-events: none;
height: 100%; height: 65%;
width: 100%; width: 65%;
object-fit: cover;
}
> .icon {
height: 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>

View File

@ -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 {

View 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>

View File

@ -1,19 +1,22 @@
<template> <template>
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="hide" @click="hide = false"> <div class="qjewsnkg" v-if="hide" @click="hide = false">
<div> <img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> <div class="text">
<span>{{ $t('clickToShow') }}</span> <div>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<span>{{ $t('clickToShow') }}</span>
</div>
</div> </div>
</div> </div>
<div class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else> <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>

View File

@ -114,7 +114,7 @@ export default Vue.extend({
> * { > * {
overflow: hidden; overflow: hidden;
border-radius: 4px; border-radius: 6px;
} }
&[data-count="1"] { &[data-count="1"] {

View File

@ -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>

View File

@ -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>

View File

@ -123,10 +123,6 @@ a {
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
* {
cursor: pointer;
}
} }
hr { hr {

View File

@ -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];
}
} }

View File

@ -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.'

View File

@ -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,

View File

@ -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),

View File

@ -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,
}, },

View File

@ -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

View File

@ -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;
} }
} }

View File

@ -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 :

View File

@ -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
}); });
})); }));
}); });

View File

@ -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"