Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
a4b7a9db03 | |||
280eeb9d75 | |||
3f71b14637 | |||
705d40ab37 | |||
b39850de01 | |||
b9c5e95b85 | |||
0c1de7b1b6 | |||
0a4499fd03 | |||
b663a47331 | |||
eb275a62a6 | |||
e18caa3396 | |||
eb15d31ebf | |||
e7f1ab2d01 | |||
9d3beb3174 | |||
b6c3399abe | |||
0a28573845 | |||
937df577f1 | |||
e54fd6c2cb | |||
3caea9d33e | |||
b9e9631195 |
57
CHANGELOG.md
57
CHANGELOG.md
@ -1,6 +1,63 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
Next (2020/7/)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
- サウンドを追加 [b9e9631](https://github.com/syuilo/misskey/commit/b9e9631195a8ca5ed1386daeacdc835456d52975)
|
||||
|
||||
### 🐛Fixes
|
||||
-
|
||||
|
||||
12.41.2 (2020/7/12)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
- モーダルにぼかし効果を使用するオプション [aae9bc4c](https://github.com/syuilo/misskey/commit/aae9bc4cf4c583b4d675391fe3da2fa53b7f18e0)
|
||||
- スタイルの調整 [eb84445](https://github.com/syuilo/misskey/commit/eb84445796039b93d124fa615e96c08fedcd9bf9), [dab7282](https://github.com/syuilo/misskey/commit/dab728278ca577622c575d1968eb6a22c7b444b9), [35a8c379](https://github.com/syuilo/misskey/commit/35a8c37922193317b3f6397562c762f9a9169b91)
|
||||
|
||||
### 🐛Fixes
|
||||
- Deckのタイムラインを追加した直後のタイムライン種別の選択がキャンセルできない問題を修正 [#6535](https://github.com/syuilo/misskey/pull/6535)
|
||||
- ノート詳細 /notes/:id ページの直リンを踏むと Not Found になる問題を修正 [364bd9a](https://github.com/syuilo/misskey/commit/364bd9ae74226c46ccdad810884bce11b2bef156)
|
||||
- Deckでメインカラムの「投稿があります」をクリックしても上に行かない問題を修正 [5b5b64d](https://github.com/syuilo/misskey/commit/5b5b64d2514cf445aa81a6750ac4185f4e7dd8cd)
|
||||
- 翻訳の修正 [7555ab0](https://github.com/syuilo/misskey/commit/7555ab097a6aab68851782b641a33fb3fdf2f101), [426c2fa](https://github.com/syuilo/misskey/commit/426c2fa5d152610516337cc5a53810e136d573db)
|
||||
|
||||
12.41.1 (2020/7/12)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
- ResizeObserver Polyfillを削除 [c89abda](https://github.com/syuilo/misskey/commit/c89abda3fb55857bb81c4f2163a4a0396a04fc27)
|
||||
* Misskey Webのパフォーマンスが劇的に改善されました
|
||||
- スタイルの調整 [7cbe95a](https://github.com/syuilo/misskey/commit/7cbe95a1cf67f2536a6332bbccc7129afcd92f73), [320352b](https://github.com/syuilo/misskey/commit/320352bf4ba56ddd67c9c6bc0816dab94c53191b)
|
||||
|
||||
### 🐛Fixes
|
||||
- サイドバーのホームを押すことでのトップへのスクロールが動作しなくなっている問題を修正 [3c66990](https://github.com/syuilo/misskey/commit/3c669902632570bb1354f6b53253037f183718b5)
|
||||
|
||||
12.41.0 (2020/7/12)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
- デッキの実装 [#6504](https://github.com/syuilo/misskey/pull/6504), [065ec8e](https://github.com/syuilo/misskey/commit/065ec8e17080887814b1912233d38e412b2811d2), [debc008](https://github.com/syuilo/misskey/commit/debc0086fab6c131cf37f00e8b03fbe5d6f09c64)
|
||||
- テーマエディターの実装 [#6482](https://github.com/syuilo/misskey/pull/6482)
|
||||
- プラグインシステムの実装 [#6479](https://github.com/syuilo/misskey/pull/6479)
|
||||
- ウィジェットの位置を固定するオプションを追加 [3799708](https://github.com/syuilo/misskey/commit/3799708daf52c221c03ff0b1c11d8b888b22d32f)
|
||||
- ウィジェットの位置を固定しない場合、Twitterのようにstickyに画面追従するように [c25cf7f](https://github.com/syuilo/misskey/commit/c25cf7f89a1d3d7e55331396bbc3f44920a38de5)
|
||||
- サウンドを追加 (syuilo/pirori) [d4b4b61](https://github.com/syuilo/misskey/commit/d4b4b61535ee4f5f759ba3342b55e978e43f1c7b)
|
||||
- タイムライン上でTwitterの埋め込みプレビューを表示できるように [#6496](https://github.com/syuilo/misskey/pull/6496)
|
||||
- デザインや挙動の調整 [#6495](https://github.com/syuilo/misskey/pull/6495), [752669b](https://github.com/syuilo/misskey/commit/752669bf5ea83b81ddcabb804e795a24debe6dc0), [#6497](https://github.com/syuilo/misskey/pull/6497), [ade11aa](https://github.com/syuilo/misskey/commit/ade11aa447f0102c9202955e01c59fcb501f794e), [27a17b4](https://github.com/syuilo/misskey/commit/27a17b467d72aea81774c04b8ca3e01ed6874b24), [4fd0636](https://github.com/syuilo/misskey/commit/4fd06369d355f032b5eb245dfd98faadee2289f9), [ca2e53b](https://github.com/syuilo/misskey/commit/ca2e53bd6e3de50f2fdf62da16734873be37fcc4), [8ff2694](https://github.com/syuilo/misskey/commit/8ff2694cadd3ab3d51f96fc2ea3bbfde29475660), [11f8d74](https://github.com/syuilo/misskey/commit/11f8d742eb53e8b815abc8ed1c34627dcbaa9e2f)
|
||||
- ソースコードのリファクタ [a591a33](https://github.com/syuilo/misskey/commit/a591a334ed6fd7f8ed936bf7e7edfcce08de035a)
|
||||
|
||||
### 🐛Fixes
|
||||
- 依存パッケージの更新 [#6491](https://github.com/syuilo/misskey/pull/6491), [#6516](https://github.com/syuilo/misskey/pull/6516), [d327bb8](https://github.com/syuilo/misskey/commit/d327bb8ff1b8765e92d6815d244e74f0793f6157)
|
||||
- サーバーへのファイルダウンロードのタイムアウトを11秒から60秒に緩和 [#6503](https://github.com/syuilo/misskey/pull/6503)
|
||||
- 非ログイン時にキーボードショートカットで投稿フォームが開けてしまう問題を修正 [#6508](https://github.com/syuilo/misskey/pull/6508)
|
||||
- キャッシュされてないリモートファイルのURLが相対URLで返ってくる問題を修正 [#6514](https://github.com/syuilo/misskey/pull/6514)
|
||||
* リモートファイルをキャッシュしない設定のインスタンスにおいてサードパーティークライアントでリモートの画像が表示できない問題が修正されます
|
||||
- Mastodon v2.5.0未満からのActivityが受け取れない問題の修正 [#6518](https://github.com/syuilo/misskey/pull/6518)
|
||||
- music.youtube.comのURLプレビューの修正 [#6496](https://github.com/syuilo/misskey/pull/6496)
|
||||
- URLプレビューの翻訳を修正 [#6496](https://github.com/syuilo/misskey/pull/6496)
|
||||
- ノートの表示幅が狭いとTwitterウィジェットがはみ出すのをなんとか修正 [#6496](https://github.com/syuilo/misskey/pull/6496)
|
||||
- HiDPi環境でMisskey v12 Roomの家具を選択できない問題を修正 [#6507](https://github.com/syuilo/misskey/pull/6507)
|
||||
- Safariでの検索インプット・検索ボタンのデザインが適用されないのを修正 [#6484](https://github.com/syuilo/misskey/pull/6484)
|
||||
- フォロワーではないリモートユーザーに削除通知が配信されない問題を修正 [#6475](https://github.com/syuilo/misskey/pull/6475)
|
||||
|
||||
12.40.0 (2020/7/5)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
|
@ -442,7 +442,7 @@ remote: "リモート"
|
||||
total: "合計"
|
||||
weekOverWeekChanges: "前週比"
|
||||
dayOverDayChanges: "前日比"
|
||||
accessibility: "アクセシビリティ"
|
||||
appearance: "アピアランス"
|
||||
clinetSettings: "クライアント設定"
|
||||
accountSettings: "アカウント設定"
|
||||
promotion: "プロモーション"
|
||||
@ -529,6 +529,12 @@ pluginInstallWarn: "信頼できないプラグインはインストールしな
|
||||
deck: "デッキ"
|
||||
undeck: "デッキ解除"
|
||||
useBlurEffectForModal: "モーダルにぼかし効果を使用"
|
||||
generateAccessToken: "アクセストークンの発行"
|
||||
permission: "権限"
|
||||
enableAll: "全て有効にする"
|
||||
disableAll: "全て無効にする"
|
||||
tokenRequested: "アカウントへのアクセス許可"
|
||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
|
||||
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
|
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)`);
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.41.2",
|
||||
"version": "12.42.0",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -47,7 +47,7 @@
|
||||
"@koa/multer": "3.0.0",
|
||||
"@koa/router": "9.0.1",
|
||||
"@sinonjs/fake-timers": "6.0.1",
|
||||
"@syuilo/aiscript": "0.7.2",
|
||||
"@syuilo/aiscript": "0.8.0",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.14.0",
|
||||
"@types/cbor": "5.0.0",
|
||||
@ -112,6 +112,7 @@
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.713.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.3",
|
||||
"bull": "3.15.0",
|
||||
"cafy": "15.2.1",
|
||||
"cbor": "5.0.2",
|
||||
|
@ -375,7 +375,8 @@ export default Vue.extend({
|
||||
$left-widgets-hide-threshold: 1600px;
|
||||
$right-widgets-hide-threshold: 1090px;
|
||||
|
||||
min-height: 100vh;
|
||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
min-height: calc(var(--vh, 1vh) * 100);
|
||||
box-sizing: border-box;
|
||||
padding-top: $header-height;
|
||||
|
||||
@ -544,17 +545,14 @@ export default Vue.extend({
|
||||
|
||||
> .content {
|
||||
> * {
|
||||
min-height: calc(100vh - #{$header-height});
|
||||
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
|
||||
box-sizing: border-box;
|
||||
padding: var(--margin);
|
||||
|
||||
&.full {
|
||||
padding: 0 var(--margin);
|
||||
}
|
||||
|
||||
&.naked {
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -597,7 +595,8 @@ export default Vue.extend({
|
||||
&.fixed {
|
||||
position: sticky;
|
||||
overflow: auto;
|
||||
height: calc(100vh - #{$header-height});
|
||||
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
height: calc((var(--vh, 1vh) * 100) - #{$header-height});
|
||||
top: $header-height;
|
||||
}
|
||||
|
||||
@ -620,7 +619,8 @@ export default Vue.extend({
|
||||
> .container {
|
||||
position: sticky;
|
||||
height: min-content;
|
||||
min-height: calc(100vh - #{$header-height});
|
||||
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
|
||||
padding: var(--margin) 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
BIN
src/client/assets/sounds/syuilo/pirori-square-wet.mp3
Normal file
BIN
src/client/assets/sounds/syuilo/pirori-square-wet.mp3
Normal file
Binary file not shown.
BIN
src/client/assets/sounds/syuilo/pirori-wet.mp3
Normal file
BIN
src/client/assets/sounds/syuilo/pirori-wet.mp3
Normal file
Binary file not shown.
@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
|
||||
<span class="inner" :style="icon"></span>
|
||||
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
|
||||
<img class="inner" :src="url"/>
|
||||
</span>
|
||||
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
|
||||
<span class="inner" :style="icon"></span>
|
||||
</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 class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
|
||||
<img class="inner" :src="url"/>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
@ -45,22 +39,6 @@ export default Vue.extend({
|
||||
? getStaticImageUrl(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: {
|
||||
onClick(e) {
|
||||
@ -102,15 +80,17 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
.inner {
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 100%;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,36 +1,15 @@
|
||||
<template>
|
||||
<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`">
|
||||
<img
|
||||
: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"/>
|
||||
<div class="zdjebgpv" ref="thumbnail">
|
||||
<img-with-blurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
|
||||
<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'"/>
|
||||
<fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -47,8 +26,12 @@ import {
|
||||
faFileArchive,
|
||||
faFilm
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import ImgWithBlurhash from './img-with-blurhash.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
ImgWithBlurhash
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
@ -59,11 +42,6 @@ export default Vue.extend({
|
||||
required: false,
|
||||
default: 'cover'
|
||||
},
|
||||
detail: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -108,20 +86,12 @@ export default Vue.extend({
|
||||
? (this.is === 'image' || this.is === 'video')
|
||||
: false;
|
||||
},
|
||||
background(): string {
|
||||
return this.file.properties.avgColor || '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.$refs.thumbnail.style.backgroundColor = 'transparent';
|
||||
}
|
||||
},
|
||||
volumechange() {
|
||||
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
|
||||
this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
|
||||
@ -132,14 +102,8 @@ export default Vue.extend({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zdjebgpv {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
> img,
|
||||
> .icon {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .icon-sub {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
@ -153,37 +117,10 @@ export default Vue.extend({
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&:not(.detail) {
|
||||
> img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
> .icon {
|
||||
pointer-events: none;
|
||||
height: 65%;
|
||||
width: 65%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -126,17 +126,6 @@ export default Vue.extend({
|
||||
this.browser.isDragSource = false;
|
||||
},
|
||||
|
||||
onThumbnailLoaded() {
|
||||
if (this.file.properties.avgColor) {
|
||||
anime({
|
||||
targets: this.$refs.thumbnail,
|
||||
backgroundColor: 'transparent', // TODO fade
|
||||
duration: 100,
|
||||
easing: 'linear'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
rename() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('renameFile'),
|
||||
@ -332,7 +321,6 @@ export default Vue.extend({
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin: auto;
|
||||
color: var(--driveFileIcon);
|
||||
}
|
||||
|
||||
> .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>
|
||||
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="hide" @click="hide = false">
|
||||
<div>
|
||||
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
||||
<span>{{ $t('clickToShow') }}</span>
|
||||
<div class="qjewsnkg" v-if="hide" @click="hide = false">
|
||||
<img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/>
|
||||
<div class="text">
|
||||
<div>
|
||||
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
||||
<span>{{ $t('clickToShow') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else>
|
||||
<div class="gqnyydlz" v-else>
|
||||
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
|
||||
<a
|
||||
:href="image.url"
|
||||
:style="style"
|
||||
:title="image.name"
|
||||
@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>
|
||||
</div>
|
||||
</template>
|
||||
@ -23,8 +26,12 @@ import Vue from 'vue';
|
||||
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
import ImageViewer from './image-viewer.vue';
|
||||
import ImgWithBlurhash from './img-with-blurhash.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
ImgWithBlurhash
|
||||
},
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
@ -42,23 +49,18 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
style(): any {
|
||||
let url = `url(${
|
||||
this.$store.state.device.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(this.image.thumbnailUrl)
|
||||
: this.image.thumbnailUrl
|
||||
})`;
|
||||
url(): any {
|
||||
let url = this.$store.state.device.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(this.image.thumbnailUrl)
|
||||
: this.image.thumbnailUrl;
|
||||
|
||||
if (this.$store.state.device.loadRemoteMedia) {
|
||||
url = null;
|
||||
} else if (this.raw || this.$store.state.device.loadRawImages) {
|
||||
url = `url(${this.image.url})`;
|
||||
url = this.image.url;
|
||||
}
|
||||
|
||||
return {
|
||||
'background-color': this.image.properties.avgColor || 'transparent',
|
||||
'background-image': url
|
||||
};
|
||||
return url;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -82,7 +84,38 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<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;
|
||||
|
||||
> i {
|
||||
@ -110,7 +143,7 @@ export default Vue.extend({
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
> div {
|
||||
> .gif {
|
||||
background-color: var(--fg);
|
||||
border-radius: 6px;
|
||||
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>
|
||||
|
@ -114,7 +114,7 @@ export default Vue.extend({
|
||||
|
||||
> * {
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&[data-count="1"] {
|
||||
|
@ -353,7 +353,8 @@ export default Vue.extend({
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
width: $nav-width;
|
||||
height: 100vh;
|
||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--navBg);
|
||||
|
@ -61,6 +61,7 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
tick() {
|
||||
// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
|
||||
this.now = new Date();
|
||||
|
||||
this.tickId = setTimeout(() => {
|
||||
|
115
src/client/components/token-generate-window.vue
Normal file
115
src/client/components/token-generate-window.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false">
|
||||
<template #header>{{ title || $t('generateAccessToken') }}</template>
|
||||
<div class="ugkkpisj">
|
||||
<div>
|
||||
<mk-info warn v-if="information">{{ information }}</mk-info>
|
||||
</div>
|
||||
<div>
|
||||
<mk-input v-model="name">{{ $t('name') }}</mk-input>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-bottom: 16px;"><b>{{ $t('permission') }}</b></div>
|
||||
<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button>
|
||||
<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button>
|
||||
<mk-switch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</mk-switch>
|
||||
</div>
|
||||
</div>
|
||||
</x-window>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { kinds } from '../../misc/api-permissions';
|
||||
import XWindow from './window.vue';
|
||||
import MkInput from './ui/input.vue';
|
||||
import MkTextarea from './ui/textarea.vue';
|
||||
import MkSwitch from './ui/switch.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
import MkInfo from './ui/info.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XWindow,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
MkButton,
|
||||
MkInfo,
|
||||
},
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
information: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
initialName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
initialPermissions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: this.initialName,
|
||||
permissions: {},
|
||||
kinds
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.initialPermissions) {
|
||||
for (const kind of this.initialPermissions) {
|
||||
Vue.set(this.permissions, kind, true);
|
||||
}
|
||||
} else {
|
||||
for (const kind of this.kinds) {
|
||||
Vue.set(this.permissions, kind, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.$emit('ok', {
|
||||
name: this.name,
|
||||
permissions: Object.keys(this.permissions).filter(p => this.permissions[p])
|
||||
});
|
||||
this.$refs.window.close();
|
||||
},
|
||||
|
||||
disableAll() {
|
||||
for (const p in this.permissions) {
|
||||
this.permissions[p] = false;
|
||||
}
|
||||
},
|
||||
|
||||
enableAll() {
|
||||
for (const p in this.permissions) {
|
||||
this.permissions[p] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ugkkpisj {
|
||||
> div {
|
||||
padding: 24px;
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -49,6 +49,7 @@ import { search } from './scripts/search';
|
||||
import DeckColumnCore from './components/deck/column-core.vue';
|
||||
import DeckColumn from './components/deck/column.vue';
|
||||
import XSidebar from './components/sidebar.vue';
|
||||
import { getScrollContainer } from './scripts/scroll';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -108,6 +109,8 @@ export default Vue.extend({
|
||||
|
||||
created() {
|
||||
document.documentElement.style.overflowY = 'hidden';
|
||||
document.documentElement.style.scrollBehavior = 'auto';
|
||||
window.addEventListener('wheel', this.onWheel);
|
||||
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
@ -119,6 +122,12 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
onWheel(e) {
|
||||
if (getScrollContainer(e.target) == null) {
|
||||
document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
|
||||
}
|
||||
},
|
||||
|
||||
showNav() {
|
||||
this.$refs.nav.show();
|
||||
},
|
||||
@ -211,7 +220,8 @@ export default Vue.extend({
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
padding: $deckMargin 0 $deckMargin $deckMargin;
|
||||
|
@ -9,6 +9,8 @@ import PortalVue from 'portal-vue';
|
||||
import VAnimateCss from 'v-animate-css';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { AiScript } from '@syuilo/aiscript';
|
||||
import { deserialize } from '@syuilo/aiscript/built/serializer';
|
||||
|
||||
import VueHotkey from './scripts/hotkey';
|
||||
import App from './app.vue';
|
||||
@ -26,7 +28,6 @@ import createStore from './store';
|
||||
import { clientDb, get, count } from './db';
|
||||
import { setI18nContexts } from './scripts/set-i18n-contexts';
|
||||
import { createPluginEnv } from './scripts/aiscript/api';
|
||||
import { AiScript } from '@syuilo/aiscript';
|
||||
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueHotkey);
|
||||
@ -59,6 +60,16 @@ if (localStorage.getItem('theme') == null) {
|
||||
applyTheme(lightTheme);
|
||||
}
|
||||
|
||||
//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
// TODO: いつの日にか消したい
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
window.addEventListener('resize', () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Detect the user language
|
||||
let lang = localStorage.getItem('lang');
|
||||
|
||||
@ -103,9 +114,13 @@ document.body.innerHTML = '<div id="app"></div>';
|
||||
|
||||
const store = createStore();
|
||||
|
||||
// 他のタブと永続化されたstateを同期
|
||||
window.addEventListener('storage', e => {
|
||||
if (e.key === 'vuex') {
|
||||
store.replaceState(JSON.parse(localStorage['vuex']));
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
...JSON.parse(e.newValue)
|
||||
});
|
||||
} else if (e.key === 'i') {
|
||||
location.reload();
|
||||
}
|
||||
@ -253,7 +268,7 @@ os.init(async () => {
|
||||
|
||||
store.commit('initPlugin', { plugin, aiscript });
|
||||
|
||||
aiscript.exec(plugin.ast);
|
||||
aiscript.exec(deserialize(plugin.ast));
|
||||
}
|
||||
|
||||
if (store.getters.isSignedIn) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="naked full">
|
||||
<div class="full">
|
||||
<portal to="header">
|
||||
<button @click="menu" class="_button _jmoebdiw_">
|
||||
<fa :icon="faCloud" style="margin-right: 8px;"/>
|
||||
|
@ -10,8 +10,7 @@
|
||||
<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
|
||||
<div class="file" v-if="message.file">
|
||||
<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"
|
||||
:style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/>
|
||||
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
|
||||
<p v-else>{{ message.file.name }}</p>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-messaging-room naked"
|
||||
<div class="mk-messaging-room"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@drop.prevent.stop="onDrop"
|
||||
>
|
||||
@ -41,6 +41,7 @@ import XList from '../../components/date-separated-list.vue';
|
||||
import XMessage from './messaging-room.message.vue';
|
||||
import XForm from './messaging-room.form.vue';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import { isBottom, onScrollBottom } from '../../scripts/scroll';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -91,8 +92,6 @@ export default Vue.extend({
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
|
||||
document.removeEventListener('visibilitychange', this.onVisibilitychange);
|
||||
|
||||
this.ilObserver.disconnect();
|
||||
@ -118,8 +117,6 @@ export default Vue.extend({
|
||||
this.connection.on('read', this.onRead);
|
||||
this.connection.on('deleted', this.onDeleted);
|
||||
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
|
||||
document.addEventListener('visibilitychange', this.onVisibilitychange);
|
||||
|
||||
this.fetchMessages().then(() => {
|
||||
@ -198,7 +195,7 @@ export default Vue.extend({
|
||||
onMessage(message) {
|
||||
this.$root.sound('chat');
|
||||
|
||||
const isBottom = this.isBottom();
|
||||
const _isBottom = isBottom(this.$el, 64);
|
||||
|
||||
this.messages.push(message);
|
||||
if (message.userId != this.$store.state.i.id && !document.hidden) {
|
||||
@ -207,7 +204,7 @@ export default Vue.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (isBottom) {
|
||||
if (_isBottom) {
|
||||
// Scroll to bottom
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
@ -244,17 +241,6 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
isBottom() {
|
||||
const asobi = 64;
|
||||
const current = this.isNaked
|
||||
? window.scrollY + window.innerHeight
|
||||
: this.$el.scrollTop + this.$el.offsetHeight;
|
||||
const max = this.isNaked
|
||||
? document.body.offsetHeight
|
||||
: this.$el.scrollHeight;
|
||||
return current > (max - asobi);
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
window.scroll(0, document.body.offsetHeight);
|
||||
},
|
||||
@ -267,6 +253,10 @@ export default Vue.extend({
|
||||
notifyNewMessage() {
|
||||
this.showIndicator = true;
|
||||
|
||||
onScrollBottom(this.$el, () => {
|
||||
this.showIndicator = false;
|
||||
});
|
||||
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
@ -274,14 +264,6 @@ export default Vue.extend({
|
||||
}, 4000);
|
||||
},
|
||||
|
||||
onScroll() {
|
||||
const el = this.isNaked ? window.document.documentElement : this.$el;
|
||||
const current = el.scrollTop + el.clientHeight;
|
||||
if (current > el.scrollHeight - 1) {
|
||||
this.showIndicator = false;
|
||||
}
|
||||
},
|
||||
|
||||
onVisibilitychange() {
|
||||
if (document.hidden) return;
|
||||
for (const message of this.messages) {
|
||||
|
@ -2,9 +2,7 @@
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faKey"/> API</div>
|
||||
<div class="_content">
|
||||
<mk-input :value="$store.state.i.token" readonly>
|
||||
<span>{{ $t('token') }}</span>
|
||||
</mk-input>
|
||||
<mk-button @click="generateToken">{{ $t('generateAccessToken') }}</mk-button>
|
||||
<mk-button @click="regenerateToken"><fa :icon="faSyncAlt"/> {{ $t('regenerate') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
@ -26,6 +24,22 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async generateToken() {
|
||||
this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
|
||||
}).$on('ok', async ({ name, permissions }) => {
|
||||
const { token } = await this.$root.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
title: this.$t('token'),
|
||||
text: token
|
||||
});
|
||||
});
|
||||
},
|
||||
regenerateToken() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('password'),
|
||||
|
@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</x-container>
|
||||
</template>
|
||||
|
@ -68,14 +68,8 @@
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div>
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('appearance') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="autoReload">
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
|
||||
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
|
||||
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
|
||||
<mk-switch v-model="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</mk-switch>
|
||||
@ -83,6 +77,25 @@
|
||||
{{ $t('useOsNativeEmojis') }}
|
||||
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('fontSize') }}</div>
|
||||
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="autoReload">
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
|
||||
<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch>
|
||||
<mk-switch v-model="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</mk-switch>
|
||||
<mk-switch v-model="fixedWidgetsPosition">{{ $t('fixedWidgetsPosition') }}</mk-switch>
|
||||
@ -95,13 +108,6 @@
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
</mk-select>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('fontSize') }}</div>
|
||||
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<mk-button @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</mk-button>
|
||||
@ -134,6 +140,8 @@ const sounds = [
|
||||
'syuilo/poi1',
|
||||
'syuilo/poi2',
|
||||
'syuilo/pirori',
|
||||
'syuilo/pirori-wet',
|
||||
'syuilo/pirori-square-wet',
|
||||
'aisha/1',
|
||||
'aisha/2',
|
||||
'aisha/3',
|
||||
|
@ -30,7 +30,10 @@
|
||||
<div>{{ $t('description') }}:</div>
|
||||
<div>{{ selectedPlugin.description }}</div>
|
||||
</div>
|
||||
<mk-button @click="uninstall()" style="margin-top: 8px;"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
|
||||
<div style="margin-top: 8px;">
|
||||
<mk-button @click="config()" inline v-if="selectedPlugin.config"><fa :icon="faCog"/> {{ $t('settings') }}</mk-button>
|
||||
<mk-button @click="uninstall()" inline><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
|
||||
</div>
|
||||
</template>
|
||||
</details>
|
||||
</div>
|
||||
@ -39,12 +42,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { AiScript, parse } from '@syuilo/aiscript';
|
||||
import { serialize } from '@syuilo/aiscript/built/serializer';
|
||||
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkTextarea from '../../components/ui/textarea.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkInfo from '../../components/ui/info.vue';
|
||||
import { AiScript, parse } from '@syuilo/aiscript';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -58,7 +62,7 @@ export default Vue.extend({
|
||||
return {
|
||||
script: '',
|
||||
selectedPluginId: null,
|
||||
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload
|
||||
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
|
||||
}
|
||||
},
|
||||
|
||||
@ -70,7 +74,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
install() {
|
||||
async install() {
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(this.script);
|
||||
@ -82,7 +86,6 @@ export default Vue.extend({
|
||||
return;
|
||||
}
|
||||
const meta = AiScript.collectMetadata(ast);
|
||||
console.log(meta);
|
||||
if (meta == null) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
@ -98,7 +101,7 @@ export default Vue.extend({
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { id, name, version, author, description } = data;
|
||||
const { id, name, version, author, description, permissions, config } = data;
|
||||
if (id == null || name == null || version == null || author == null) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
@ -106,16 +109,40 @@ export default Vue.extend({
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = permissions == null || permissions.length === 0 ? null : await new Promise(async (res, rej) => {
|
||||
this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
|
||||
title: this.$t('tokenRequested'),
|
||||
information: this.$t('pluginTokenRequestedDescription'),
|
||||
initialName: name,
|
||||
initialPermissions: permissions
|
||||
}).$on('ok', async ({ name, permissions }) => {
|
||||
const { token } = await this.$root.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
|
||||
res(token);
|
||||
});
|
||||
});
|
||||
|
||||
this.$store.commit('deviceUser/installPlugin', {
|
||||
meta: {
|
||||
id, name, version, author, description
|
||||
id, name, version, author, description, permissions, config
|
||||
},
|
||||
ast
|
||||
token,
|
||||
ast: serialize(ast)
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
@ -124,6 +151,29 @@ export default Vue.extend({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
|
||||
async config() {
|
||||
const config = this.selectedPlugin.config;
|
||||
for (const key in this.selectedPlugin.configData) {
|
||||
config[key].default = this.selectedPlugin.configData[key];
|
||||
}
|
||||
|
||||
const { canceled, result } = await this.$root.form(this.selectedPlugin.name, config);
|
||||
if (canceled) return;
|
||||
|
||||
this.$store.commit('deviceUser/configPlugin', {
|
||||
id: this.selectedPluginId,
|
||||
config: result
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { utils, values } from '@syuilo/aiscript';
|
||||
import { jsToVal } from '@syuilo/aiscript/built/interpreter/util';
|
||||
|
||||
export function createAiScriptEnv(vm, opts) {
|
||||
let apiRequests = 0;
|
||||
@ -26,7 +27,7 @@ export function createAiScriptEnv(vm, opts) {
|
||||
if (token) utils.assertString(token);
|
||||
apiRequests++;
|
||||
if (apiRequests > 16) return values.NULL;
|
||||
const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : null);
|
||||
const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
|
||||
return utils.jsToVal(res);
|
||||
}),
|
||||
'Mk:save': values.FN_NATIVE(([key, value]) => {
|
||||
@ -42,8 +43,14 @@ export function createAiScriptEnv(vm, opts) {
|
||||
}
|
||||
|
||||
export function createPluginEnv(vm, opts) {
|
||||
const config = new Map();
|
||||
for (const key in opts.plugin.config) {
|
||||
const val = opts.plugin.configData[key] || opts.plugin.config[key].default;
|
||||
config.set(key, jsToVal(val));
|
||||
}
|
||||
|
||||
return {
|
||||
...createAiScriptEnv(vm, opts),
|
||||
...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }),
|
||||
'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
@ -53,5 +60,6 @@ export function createPluginEnv(vm, opts) {
|
||||
'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Plugin:config': values.OBJ(config),
|
||||
};
|
||||
}
|
||||
|
@ -26,6 +26,19 @@ export function onScrollTop(el: Element, cb) {
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
|
||||
export function onScrollBottom(el: Element, cb) {
|
||||
const container = getScrollContainer(el) || window;
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
const pos = getScrollPosition(el);
|
||||
if (pos + el.clientHeight > el.scrollHeight - 1) {
|
||||
cb();
|
||||
container.removeEventListener('scroll', onscroll);
|
||||
}
|
||||
};
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
|
||||
export function scroll(el: Element, top: number) {
|
||||
const container = getScrollContainer(el);
|
||||
if (container == null) {
|
||||
@ -34,3 +47,14 @@ export function scroll(el: Element, top: number) {
|
||||
container.scrollTop = top;
|
||||
}
|
||||
}
|
||||
|
||||
export function isBottom(el: Element, asobi = 0) {
|
||||
const container = getScrollContainer(el);
|
||||
const current = container
|
||||
? el.scrollTop + el.offsetHeight
|
||||
: window.scrollY + window.innerHeight;
|
||||
const max = container
|
||||
? el.scrollHeight
|
||||
: document.body.offsetHeight;
|
||||
return current >= (max - asobi);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export class StickySidebar {
|
||||
|
||||
if (this.isTop) {
|
||||
this.isTop = false;
|
||||
this.spacer.style.marginTop = `${scrollTop}px`;
|
||||
this.spacer.style.marginTop = `${this.lastScrollTop}px`;
|
||||
}
|
||||
} else { // upscroll
|
||||
const overflow = this.el.clientHeight - window.innerHeight;
|
||||
|
@ -302,6 +302,7 @@ export default () => new Vuex.Store({
|
||||
},
|
||||
|
||||
mergeMe(ctx, me) {
|
||||
// TODO: プロパティ一つ一つに対してコミットが発生するのはアレなので良い感じにする
|
||||
for (const [key, value] of Object.entries(me)) {
|
||||
ctx.commit('updateIKeyValue', { key, value });
|
||||
}
|
||||
@ -586,13 +587,11 @@ export default () => new Vuex.Store({
|
||||
},
|
||||
//#endregion
|
||||
|
||||
installPlugin(state, { meta, ast }) {
|
||||
installPlugin(state, { meta, ast, token }) {
|
||||
state.plugins.push({
|
||||
id: meta.id,
|
||||
name: meta.name,
|
||||
version: meta.version,
|
||||
author: meta.author,
|
||||
description: meta.description,
|
||||
...meta,
|
||||
configData: {},
|
||||
token: token,
|
||||
ast: ast
|
||||
});
|
||||
},
|
||||
@ -600,6 +599,10 @@ export default () => new Vuex.Store({
|
||||
uninstallPlugin(state, id) {
|
||||
state.plugins = state.plugins.filter(x => x.id != id);
|
||||
},
|
||||
|
||||
configPlugin(state, { id, config }) {
|
||||
state.plugins.find(p => p.id === id).configData = config;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -123,10 +123,6 @@ a {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
* {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
|
@ -8,20 +8,18 @@ APIを使い始めるには、まずアクセストークンを取得する必
|
||||
|
||||
## アクセストークンの取得
|
||||
基本的に、APIはリクエストにはアクセストークンが必要となります。
|
||||
あなたの作ろうとしているアプリケーションが、あなた専用のものなのか、それとも不特定多数の人に使ってもらうものなのかによって、アクセストークンの取得手順は異なります。
|
||||
APIにリクエストするのが自分自身なのか、不特定の利用者に使ってもらうアプリケーションなのかによって取得手順は異なります。
|
||||
|
||||
* あなた専用の場合: [「自分のアカウントのアクセストークンを取得する」](#自分のアカウントのアクセストークンを取得する)に進む
|
||||
* 皆に使ってもらう場合: [「アプリケーションとしてアクセストークンを取得する」](#アプリケーションとしてアクセストークンを取得する)に進む
|
||||
* 前者の場合: [「自分自身のアクセストークンを手動発行する」](#自分自身のアクセストークンを手動発行する)に進む
|
||||
* 後者の場合: [「アプリケーション利用者にアクセストークンの発行をリクエストする」](#アプリケーション利用者にアクセストークンの発行をリクエストする)に進む
|
||||
|
||||
### 自分のアカウントのアクセストークンを取得する
|
||||
「設定 > API」で、自分のアクセストークンを取得できます。
|
||||
|
||||
> この方法で入手したアクセストークンは強力なので、第三者に教えないでください(アプリなどにも入力しないでください)。
|
||||
### 自分自身のアクセストークンを手動発行する
|
||||
「設定 > API」で、自分のアクセストークンを発行できます。
|
||||
|
||||
[「APIの使い方」へ進む](#APIの使い方)
|
||||
|
||||
### アプリケーションとしてアクセストークンを取得する
|
||||
アプリケーションを使ってもらうには、ユーザーのアクセストークンを以下の手順で取得する必要があります。
|
||||
### アプリケーション利用者にアクセストークンの発行をリクエストする
|
||||
アプリケーション利用者のアクセストークンを取得するには、以下の手順で発行をリクエストします。
|
||||
|
||||
#### Step 1
|
||||
|
||||
@ -48,7 +46,7 @@ UUIDを生成する。以後これをセッションIDと呼びます。
|
||||
* どのような権限があるかは[APIリファレンス](/api-doc)で確認できます
|
||||
|
||||
#### Step 3
|
||||
ユーザーが連携を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
|
||||
ユーザーが発行を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
|
||||
|
||||
レスポンスに含まれるプロパティ:
|
||||
* `token` ... ユーザーのアクセストークン
|
||||
|
@ -6,6 +6,7 @@ import * as fileType from 'file-type';
|
||||
import isSvg from 'is-svg';
|
||||
import * as probeImageSize from 'probe-image-size';
|
||||
import * as sharp from 'sharp';
|
||||
import { encode } from 'blurhash';
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
@ -18,7 +19,7 @@ export type FileInfo = {
|
||||
};
|
||||
width?: number;
|
||||
height?: number;
|
||||
avgColor?: number[];
|
||||
blurhash?: string;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
@ -71,12 +72,11 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
// average color
|
||||
let avgColor: number[] | undefined;
|
||||
let blurhash: string | undefined;
|
||||
|
||||
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
|
||||
avgColor = await calcAvgColor(path).catch(e => {
|
||||
warnings.push(`calcAvgColor failed: ${e}`);
|
||||
blurhash = await getBlurhash(path).catch(e => {
|
||||
warnings.push(`getBlurhash failed: ${e}`);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
@ -87,7 +87,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
|
||||
type,
|
||||
width,
|
||||
height,
|
||||
avgColor,
|
||||
blurhash,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
@ -173,18 +173,15 @@ async function detectImageSize(path: string): Promise<{
|
||||
/**
|
||||
* Calculate average color of image
|
||||
*/
|
||||
async function calcAvgColor(path: string): Promise<number[]> {
|
||||
const img = sharp(path);
|
||||
|
||||
const info = await (img as any).stats();
|
||||
|
||||
if (info.isOpaque) {
|
||||
const r = Math.round(info.channels[0].mean);
|
||||
const g = Math.round(info.channels[1].mean);
|
||||
const b = Math.round(info.channels[2].mean);
|
||||
|
||||
return [r, g, b];
|
||||
} else {
|
||||
return [255, 255, 255];
|
||||
}
|
||||
function getBlurhash(path: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sharp(path)
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.resize(64, 64, { fit: 'inside' })
|
||||
.toBuffer((err, buffer, { width, height }) => {
|
||||
if (err) return reject(err);
|
||||
resolve(encode(new Uint8ClampedArray(buffer), width, height, 7, 7));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -67,6 +67,12 @@ export class DriveFile {
|
||||
})
|
||||
public comment: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The BlurHash string.'
|
||||
})
|
||||
public blurhash: string | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
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;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: true,
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public avatarColor: string | null;
|
||||
public avatarBlurhash: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: true,
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public bannerColor: string | null;
|
||||
public bannerBlurhash: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
@ -115,6 +115,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
md5: file.md5,
|
||||
size: file.size,
|
||||
isSensitive: file.isSensitive,
|
||||
blurhash: file.blurhash,
|
||||
properties: file.properties,
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
|
||||
thumbnailUrl: this.getPublicUrl(file, true, meta),
|
||||
|
@ -165,7 +165,8 @@ export class UserRepository extends Repository<User> {
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarUrl ? user.avatarUrl : config.url + '/avatar/' + user.id,
|
||||
avatarColor: user.avatarColor,
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarColor: null, // 後方互換性のため
|
||||
isAdmin: user.isAdmin || falsy,
|
||||
isModerator: user.isModerator || falsy,
|
||||
isBot: user.isBot || falsy,
|
||||
@ -196,7 +197,8 @@ export class UserRepository extends Repository<User> {
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerColor: user.bannerColor,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
bannerColor: null, // 後方互換性のため
|
||||
isLocked: user.isLocked,
|
||||
isModerator: user.isModerator || falsy,
|
||||
isSilenced: user.isSilenced || falsy,
|
||||
@ -331,7 +333,7 @@ export const packedUserSchema = {
|
||||
format: 'url',
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
avatarColor: {
|
||||
avatarBlurhash: {
|
||||
type: 'any' as const,
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
@ -340,7 +342,7 @@ export const packedUserSchema = {
|
||||
format: 'url',
|
||||
nullable: true as const, optional: true as const,
|
||||
},
|
||||
bannerColor: {
|
||||
bannerBlurhash: {
|
||||
type: 'any' as const,
|
||||
nullable: true as const, optional: true as const,
|
||||
},
|
||||
|
@ -47,7 +47,15 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
|
||||
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
|
||||
if (authUser == null) {
|
||||
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
|
||||
try {
|
||||
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
|
||||
} catch (e) {
|
||||
// 対象が4xxならスキップ
|
||||
if (e.statusCode >= 400 && e.statusCode < 500) {
|
||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
||||
}
|
||||
throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
|
||||
}
|
||||
}
|
||||
|
||||
// それでもわからなければ終了
|
||||
|
@ -226,24 +226,24 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
const bannerId = banner ? banner.id : null;
|
||||
const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
|
||||
const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
|
||||
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
|
||||
const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null;
|
||||
const avatarBlurhash = avatar ? avatar.blurhash : null;
|
||||
const bannerBlurhash = banner ? banner.blurhash : null;
|
||||
|
||||
await Users.update(user!.id, {
|
||||
avatarId,
|
||||
bannerId,
|
||||
avatarUrl,
|
||||
bannerUrl,
|
||||
avatarColor,
|
||||
bannerColor
|
||||
avatarBlurhash,
|
||||
bannerBlurhash
|
||||
});
|
||||
|
||||
user!.avatarId = avatarId;
|
||||
user!.bannerId = bannerId;
|
||||
user!.avatarUrl = avatarUrl;
|
||||
user!.bannerUrl = bannerUrl;
|
||||
user!.avatarColor = avatarColor;
|
||||
user!.bannerColor = bannerColor;
|
||||
user!.avatarBlurhash = avatarBlurhash;
|
||||
user!.bannerBlurhash = bannerBlurhash;
|
||||
//#endregion
|
||||
|
||||
//#region カスタム絵文字取得
|
||||
@ -341,13 +341,13 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||
if (avatar) {
|
||||
updates.avatarId = avatar.id;
|
||||
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
|
||||
updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
|
||||
updates.avatarBlurhash = avatar.blurhash;
|
||||
}
|
||||
|
||||
if (banner) {
|
||||
updates.bannerId = banner.id;
|
||||
updates.bannerUrl = DriveFiles.getPublicUrl(banner);
|
||||
updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null;
|
||||
updates.bannerBlurhash = banner.blurhash;
|
||||
}
|
||||
|
||||
// Update user
|
||||
|
@ -210,8 +210,8 @@ export default define(meta, async (ps, user, token) => {
|
||||
|
||||
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
|
||||
|
||||
if (avatar.properties.avgColor) {
|
||||
updates.avatarColor = avatar.properties.avgColor;
|
||||
if (avatar.blurhash) {
|
||||
updates.avatarBlurhash = avatar.blurhash;
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,8 +223,8 @@ export default define(meta, async (ps, user, token) => {
|
||||
|
||||
updates.bannerUrl = DriveFiles.getPublicUrl(banner, false);
|
||||
|
||||
if (banner.properties.avgColor) {
|
||||
updates.bannerColor = banner.properties.avgColor;
|
||||
if (banner.blurhash) {
|
||||
updates.bannerBlurhash = banner.blurhash;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ export const meta = {
|
||||
|
||||
params: {
|
||||
session: {
|
||||
validator: $.str
|
||||
validator: $.nullable.str
|
||||
},
|
||||
|
||||
name: {
|
||||
@ -52,4 +52,8 @@ export default define(meta, async (ps, user) => {
|
||||
iconUrl: ps.iconUrl,
|
||||
permission: ps.permission,
|
||||
});
|
||||
|
||||
return {
|
||||
token: accessToken
|
||||
};
|
||||
});
|
||||
|
@ -78,7 +78,7 @@ router.post('/miauth/:session/check', async ctx => {
|
||||
session: ctx.params.session
|
||||
});
|
||||
|
||||
if (token && !token.fetched) {
|
||||
if (token && token.session != null && !token.fetched) {
|
||||
AccessTokens.update(token.id, {
|
||||
fetched: true
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import endpoints from '../endpoints';
|
||||
import * as locale from '../../../../locales/';
|
||||
import { kinds as kindsList } from '../kinds';
|
||||
import { kinds as kindsList } from '../../../misc/api-permissions';
|
||||
|
||||
export interface IKindInfo {
|
||||
endpoints: string[];
|
||||
|
@ -327,7 +327,6 @@ export default async function(
|
||||
const properties: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
avgColor?: string;
|
||||
} = {};
|
||||
|
||||
if (info.width) {
|
||||
@ -335,10 +334,6 @@ export default async function(
|
||||
properties['height'] = info.height;
|
||||
}
|
||||
|
||||
if (info.avgColor) {
|
||||
properties['avgColor'] = `rgb(${info.avgColor.join(',')})`;
|
||||
}
|
||||
|
||||
const profile = user ? await UserProfiles.findOne(user.id) : null;
|
||||
|
||||
const folder = await fetchFolder();
|
||||
@ -351,6 +346,7 @@ export default async function(
|
||||
file.folderId = folder !== null ? folder.id : null;
|
||||
file.comment = comment;
|
||||
file.properties = properties;
|
||||
file.blurhash = info.blurhash || null;
|
||||
file.isLink = isLink;
|
||||
file.isSensitive = user
|
||||
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
||||
|
@ -26,7 +26,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
avgColor: undefined
|
||||
blurhash: null
|
||||
});
|
||||
}));
|
||||
|
||||
@ -43,7 +43,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
avgColor: [ 181, 99, 106 ]
|
||||
blurhash: '' // TODO
|
||||
});
|
||||
}));
|
||||
|
||||
@ -60,7 +60,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 249, 253, 250 ]
|
||||
blurhash: '' // TODO
|
||||
});
|
||||
}));
|
||||
|
||||
@ -77,7 +77,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 249, 253, 250 ]
|
||||
blurhash: '' // TODO
|
||||
});
|
||||
}));
|
||||
|
||||
@ -94,7 +94,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 255, 255, 255 ]
|
||||
blurhash: '' // TODO
|
||||
});
|
||||
}));
|
||||
|
||||
@ -111,7 +111,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 255, 255, 255 ]
|
||||
blurhash: '' // TODO
|
||||
});
|
||||
}));
|
||||
|
||||
@ -129,7 +129,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 255, 255, 255 ]
|
||||
blurhash: '' // TODO
|
||||
});
|
||||
}));
|
||||
|
||||
@ -146,7 +146,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 25000,
|
||||
height: 25000,
|
||||
avgColor: undefined
|
||||
blurhash: '' // TODO
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
13
yarn.lock
13
yarn.lock
@ -192,10 +192,10 @@
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1.7.0"
|
||||
|
||||
"@syuilo/aiscript@0.7.2":
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.7.2.tgz#2f30adb14ffa9f1180af83c059927ab306b175a5"
|
||||
integrity sha512-l8HVTJTq9KLzDqGswOIGlBepkacudUp70EScrLjL7nEL2NKcti7Ui5fwZCrmxazxgGz6NrVNX5UBIOFFyrwr0A==
|
||||
"@syuilo/aiscript@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.8.0.tgz#3a895ddd9f5bd5afa1648acb5fd3e6f94f434cbb"
|
||||
integrity sha512-mrZ3awYf1R81D+OWZctRFiAWUt6xL3A5ovBn2OD8+1hZyX3T7S+awqrhYVLoQPhd/cijz1RT6PE8AEUtuR1J8Q==
|
||||
dependencies:
|
||||
autobind-decorator "2.4.0"
|
||||
chalk "4.0.0"
|
||||
@ -1669,6 +1669,11 @@ bluebird@^3.1.1, bluebird@^3.4.1:
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
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:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
|
Reference in New Issue
Block a user