update
This commit is contained in:
parent
d4b17a16e8
commit
f0d93755e9
|
@ -390,6 +390,7 @@ import { GetterService } from './GetterService.js';
|
||||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||||
import * as ep___admin_accounts_present_points from './endpoints/admin/accounts/present-points.js';
|
import * as ep___admin_accounts_present_points from './endpoints/admin/accounts/present-points.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
import * as ep___emoji_speedtest from './endpoints/admin/emoji/speedtest.js';
|
||||||
const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
|
const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
|
||||||
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
|
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
|
||||||
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
|
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
|
||||||
|
@ -411,6 +412,7 @@ const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-de
|
||||||
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
||||||
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
|
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
|
||||||
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
|
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
|
||||||
|
const $emoji_speedtest: Provider = { provide: 'ep:emoji/speedtest', useClass: ep___emoji_speedtest.default };
|
||||||
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
||||||
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
||||||
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
||||||
|
@ -799,6 +801,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_unsetUserAvatar,
|
$admin_unsetUserAvatar,
|
||||||
|
$emoji_speedtest,
|
||||||
$admin_unsetUserBanner,
|
$admin_unsetUserBanner,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
|
@ -1183,6 +1186,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_unsetUserAvatar,
|
$admin_unsetUserAvatar,
|
||||||
$admin_unsetUserBanner,
|
$admin_unsetUserBanner,
|
||||||
|
$emoji_speedtest,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
$admin_drive_files,
|
$admin_drive_files,
|
||||||
|
|
|
@ -50,6 +50,7 @@ import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-c
|
||||||
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
||||||
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
||||||
import * as ep___admin_emoji_updateRequest from './endpoints/admin/emoji/update-request.js';
|
import * as ep___admin_emoji_updateRequest from './endpoints/admin/emoji/update-request.js';
|
||||||
|
import * as ep___emoji_speedtest from './endpoints/admin/emoji/speedtest.js';
|
||||||
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
||||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||||
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
||||||
|
@ -432,6 +433,7 @@ const eps = [
|
||||||
['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
|
['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
|
||||||
['admin/emoji/update', ep___admin_emoji_update],
|
['admin/emoji/update', ep___admin_emoji_update],
|
||||||
['admin/emoji/update-request', ep___admin_emoji_updateRequest],
|
['admin/emoji/update-request', ep___admin_emoji_updateRequest],
|
||||||
|
['emoji/speedtest', ep___emoji_speedtest],
|
||||||
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
|
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
|
||||||
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
|
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
|
||||||
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
|
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
requireCredential: true,
|
||||||
|
requireRolePolicy: 'canManageCustomEmojis',
|
||||||
|
kind: 'write:admin:emoji',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['url'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
private customEmojiService: CustomEmojiService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const response = await fetch(ps.url, {
|
||||||
|
'headers': {
|
||||||
|
'accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
'pragma': 'no-cache',
|
||||||
|
'priority': 'u=1, i',
|
||||||
|
},
|
||||||
|
'method': 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
|
||||||
|
if (!metadata.pages) {
|
||||||
|
throw new Error('Invalid image format or no animation frames found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameRate = metadata.delay && metadata.delay.length > 0
|
||||||
|
? 1000 / metadata.delay[0]
|
||||||
|
: 30; // Fallback to 30 FPS if no delay information is present
|
||||||
|
|
||||||
|
const colorsPerFrame: number[] = [];
|
||||||
|
for (let i = 0; i < metadata.pages; i++) {
|
||||||
|
const { data, info } = await sharp(buffer, { page: i }).raw().toBuffer({ resolveWithObject: true });
|
||||||
|
const uniqueColors = new Set<string>();
|
||||||
|
for (let y = 0; y < info.height; y++) {
|
||||||
|
for (let x = 0; x < info.width; x++) {
|
||||||
|
const offset = (y * info.width + x) * info.channels;
|
||||||
|
const color = `${data[offset]}-${data[offset + 1]}-${data[offset + 2]}`;
|
||||||
|
uniqueColors.add(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colorsPerFrame.push(uniqueColors.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorChanges = colorsPerFrame.map((colorCount, index, arr) => {
|
||||||
|
if (index === 0) return 0;
|
||||||
|
return Math.abs(colorCount - arr[index - 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const averageColorChangePerSecond = colorChanges.reduce((sum, change) => sum + change, 0) / colorsPerFrame.length;
|
||||||
|
console.log('Average color change per second:', 10 < averageColorChangePerSecond);
|
||||||
|
return Boolean(10 < averageColorChangePerSecond);
|
||||||
|
// You can store or use this information as needed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@caed0/webp-conv": "^1.1.0",
|
||||||
"@discordapp/twemoji": "15.0.3",
|
"@discordapp/twemoji": "15.0.3",
|
||||||
"@github/webauthn-json": "2.1.1",
|
"@github/webauthn-json": "2.1.1",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
"@vitejs/plugin-vue": "5.0.4",
|
"@vitejs/plugin-vue": "5.0.4",
|
||||||
"@vue/compiler-sfc": "3.4.26",
|
"@vue/compiler-sfc": "3.4.26",
|
||||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.4",
|
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.4",
|
||||||
|
"apng-js": "^1.1.1",
|
||||||
"astring": "1.8.6",
|
"astring": "1.8.6",
|
||||||
"broadcast-channel": "7.0.0",
|
"broadcast-channel": "7.0.0",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
|
@ -47,10 +49,14 @@
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
|
"gif-frames": "^1.0.1",
|
||||||
|
"gifshot": "^0.4.5",
|
||||||
|
"gifuct-js": "^2.1.2",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
"insert-text-at-cursor": "0.3.0",
|
"insert-text-at-cursor": "0.3.0",
|
||||||
"is-file-animated": "1.0.2",
|
"is-file-animated": "1.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
|
"libwebpjs": "^0.0.1",
|
||||||
"matter-js": "0.19.0",
|
"matter-js": "0.19.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"misskey-bubble-game": "workspace:*",
|
"misskey-bubble-game": "workspace:*",
|
||||||
|
@ -75,7 +81,9 @@
|
||||||
"vite": "5.2.11",
|
"vite": "5.2.11",
|
||||||
"vue": "3.4.26",
|
"vue": "3.4.26",
|
||||||
"vuedraggable": "next",
|
"vuedraggable": "next",
|
||||||
"wavesurfer.js": "^7.7.14"
|
"wavesurfer.js": "^7.7.14",
|
||||||
|
"webm-wasm": "^0.4.1",
|
||||||
|
"webp-hero": "^0.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
|
|
|
@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<MkWindow
|
<MkWindow
|
||||||
ref="windowEl"
|
ref="windowEl"
|
||||||
:initialWidth="400"
|
:initialWidth="600"
|
||||||
:initialHeight="500"
|
:initialHeight="600"
|
||||||
:canResize="false"
|
:canResize="true"
|
||||||
@close="windowEl.close()"
|
@close="windowEl.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="$emit('closed')"
|
||||||
>
|
>
|
||||||
|
@ -18,7 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MkSpacer :marginMin="20" :marginMax="28">
|
<MkSpacer :marginMin="20" :marginMax="28">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m" style="display: flex; flex-direction: row">
|
||||||
|
<div>
|
||||||
<div v-if="imgUrl != null" :class="$style.imgs">
|
<div v-if="imgUrl != null" :class="$style.imgs">
|
||||||
<div style="background: #000;" :class="$style.imgContainer">
|
<div style="background: #000;" :class="$style.imgContainer">
|
||||||
<img :src="imgUrl" :class="$style.img"/>
|
<img :src="imgUrl" :class="$style.img"/>
|
||||||
|
@ -74,7 +75,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
{{ i18n.ts.isNotifyIsHome }}
|
{{ i18n.ts.isNotifyIsHome }}
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="imgUrl">
|
||||||
|
<MkInput v-model="text">
|
||||||
|
<template #label>テスト文章</template>
|
||||||
|
</MkInput><br/>
|
||||||
|
<MkNoteSimple :note="{isHidden:false,replyId:null,renoteId:null,files:[],user: $i,text:text,cw:null, emojis: {[name]: imgUrl}}"/>
|
||||||
|
<p v-if="speed ">基準より眩しい可能性があります。</p>
|
||||||
|
<p v-if="!speed">問題は見つかりませんでした。</p>
|
||||||
|
<p>※上記の物は問題がないことを保証するものではありません。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<div :class="$style.footerButtons">
|
<div :class="$style.footerButtons">
|
||||||
<MkButton v-if="!isRequest" danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
<MkButton v-if="!isRequest" danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||||
|
@ -87,9 +99,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { DriveFile } from 'misskey-js/built/entities.js';
|
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
@ -102,12 +113,14 @@ import { customEmojiCategories } from '@/custom-emojis.js';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import { selectFile, selectFiles } from '@/scripts/select-file.js';
|
import { selectFile, selectFiles } from '@/scripts/select-file.js';
|
||||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emoji?: any,
|
emoji?: any,
|
||||||
isRequest: boolean,
|
isRequest: boolean,
|
||||||
}>();
|
}>();
|
||||||
|
const text = ref<string>('テスト文章');
|
||||||
|
const speed = ref<boolean>(false);
|
||||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||||
|
@ -132,6 +145,12 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const colorChanges = ref<number | null>(null);
|
||||||
|
|
||||||
|
watch(colorChanges, (value) => {
|
||||||
|
console.log(value);
|
||||||
|
});
|
||||||
|
|
||||||
async function changeImage(ev) {
|
async function changeImage(ev) {
|
||||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||||
|
@ -222,6 +241,12 @@ async function del() {
|
||||||
windowEl.value.close();
|
windowEl.value.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(imgUrl, async (value) => {
|
||||||
|
speed.value = await misskeyApi('emoji/speedtest', {
|
||||||
|
url: value,
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -243,6 +268,12 @@ async function del() {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
.preview {
|
||||||
|
display: block;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.roleItem {
|
.roleItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -34,7 +34,7 @@ import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||||
import MkCwButton from '@/components/MkCwButton.vue';
|
import MkCwButton from '@/components/MkCwButton.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import {misskeyApi} from "@/scripts/misskey-api.js";
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: Misskey.entities.Note & {
|
note: Misskey.entities.Note & {
|
||||||
|
|
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
|
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
|
||||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||||
|
|
||||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { VNode, h, SetupContext, provide } from 'vue';
|
import { VNode, h, SetupContext, provide } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { ID, Instance } from 'misskey-js/built/entities.js';
|
||||||
import MkUrl from '@/components/global/MkUrl.vue';
|
import MkUrl from '@/components/global/MkUrl.vue';
|
||||||
import MkTime from '@/components/global/MkTime.vue';
|
import MkTime from '@/components/global/MkTime.vue';
|
||||||
import MkLink from '@/components/MkLink.vue';
|
import MkLink from '@/components/MkLink.vue';
|
||||||
|
@ -23,7 +24,6 @@ import { defaultStore } from '@/store';
|
||||||
import { mixEmoji } from '@/scripts/emojiKitchen/emojiMixer';
|
import { mixEmoji } from '@/scripts/emojiKitchen/emojiMixer';
|
||||||
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
|
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
|
||||||
import { uhoize as doUhoize } from '@/scripts/uhoize.js';
|
import { uhoize as doUhoize } from '@/scripts/uhoize.js';
|
||||||
import {ID, Instance} from "misskey-js/built/entities.js";
|
|
||||||
import { safeParseFloat } from '@/scripts/safe-parse.js';
|
import { safeParseFloat } from '@/scripts/safe-parse.js';
|
||||||
|
|
||||||
const QUOTE_STYLE = `
|
const QUOTE_STYLE = `
|
||||||
|
@ -73,7 +73,7 @@ type MfmProps = {
|
||||||
emojiUrls?: Record<string, string>;
|
emojiUrls?: Record<string, string>;
|
||||||
rootScale?: number;
|
rootScale?: number;
|
||||||
nyaize?: boolean | 'respect';
|
nyaize?: boolean | 'respect';
|
||||||
uhoize: boolean | 'respect';
|
uhoize?: boolean | 'respect';
|
||||||
parsedNodes?: mfm.MfmNode[] | null;
|
parsedNodes?: mfm.MfmNode[] | null;
|
||||||
enableEmojiMenu?: boolean;
|
enableEmojiMenu?: boolean;
|
||||||
enableEmojiMenuReaction?: boolean;
|
enableEmojiMenuReaction?: boolean;
|
||||||
|
@ -246,7 +246,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
const radius = parseFloat(token.props.args.rad ?? '6');
|
const radius = parseFloat(token.props.args.rad ?? '6');
|
||||||
return h('span', {
|
return h('span', {
|
||||||
class: '_mfm_blur_',
|
class: '_mfm_blur_',
|
||||||
style: `--blur-px: ${radius}px;`
|
style: `--blur-px: ${radius}px;`,
|
||||||
}, genEl(token.children, scale));
|
}, genEl(token.children, scale));
|
||||||
}
|
}
|
||||||
case 'rainbow': {
|
case 'rainbow': {
|
||||||
|
@ -363,7 +363,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
name: emoji1 + emoji2,
|
name: emoji1 + emoji2,
|
||||||
normal: props.plain,
|
normal: props.plain,
|
||||||
url: mixedEmojiUrl
|
url: mixedEmojiUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case 'unixtime': {
|
case 'unixtime': {
|
||||||
|
@ -474,7 +474,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
|
|
||||||
case 'emojiCode': {
|
case 'emojiCode': {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (props.author?.host == null) {
|
if (props.author?.host == null && !props.emojiUrls) {
|
||||||
return [h(MkCustomEmoji, {
|
return [h(MkCustomEmoji, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
name: token.props.name,
|
name: token.props.name,
|
||||||
|
@ -487,6 +487,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
})];
|
})];
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
console.log(props.emojiUrls, props.emojiUrls[token.props.name], token.props.name);
|
||||||
if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
|
if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
|
||||||
return [h('span', `:${token.props.name}:`)];
|
return [h('span', `:${token.props.name}:`)];
|
||||||
} else {
|
} else {
|
||||||
|
@ -495,7 +496,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
name: token.props.name,
|
name: token.props.name,
|
||||||
url: props.emojiUrls && props.emojiUrls[token.props.name],
|
url: props.emojiUrls && props.emojiUrls[token.props.name],
|
||||||
normal: props.plain,
|
normal: props.plain,
|
||||||
host: props.author.host,
|
host: props.author.host ? props.author.host : null,
|
||||||
useOriginalSize: scale >= 2.5,
|
useOriginalSize: scale >= 2.5,
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
const imageUrl = ref<string>('path_to_your_image.gif'); // または '.apng'
|
||||||
|
|
||||||
|
// 1秒あたりの色変化数を保持するリアクティブ変数
|
||||||
|
const colorChangesPerSecond = ref<number | null>(null);
|
||||||
|
|
||||||
|
// コンポーネントがマウントされたときに画像を取得して解析する
|
||||||
|
onMounted(() => {
|
||||||
|
fetchImage(imageUrl.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 画像を取得する関数
|
||||||
|
async function fetchImage(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
if (url.endsWith('.gif')) {
|
||||||
|
analyzeGif(bytes);
|
||||||
|
} else if (url.endsWith('.apng')) {
|
||||||
|
analyzeApng(bytes);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching the image:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GIFの解析関数
|
||||||
|
function analyzeGif(bytes: Uint8Array) {
|
||||||
|
const frames = extractGifFrames(bytes);
|
||||||
|
calculateColorChanges(frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// APNGの解析関数
|
||||||
|
function analyzeApng(bytes: Uint8Array) {
|
||||||
|
const frames = extractApngFrames(bytes);
|
||||||
|
calculateColorChanges(frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GIFフレーム抽出関数
|
||||||
|
function extractGifFrames(bytes: Uint8Array) {
|
||||||
|
const frames = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < bytes.length) {
|
||||||
|
// GIFのヘッダーとロジカルスクリーンディスクリプタをスキップ
|
||||||
|
if (i === 0) i += 13;
|
||||||
|
// グローバルカラーテーブルのスキップ
|
||||||
|
if (i === 13) i += (bytes[10] & 0x80 ? 3 * (2 ** ((bytes[10] & 0x07) + 1)) : 0);
|
||||||
|
|
||||||
|
// イメージディスクリプタを探す
|
||||||
|
if (bytes[i] === 0x2C) {
|
||||||
|
const imageLeft = bytes[i + 1] + (bytes[i + 2] << 8);
|
||||||
|
const imageTop = bytes[i + 3] + (bytes[i + 4] << 8);
|
||||||
|
const imageWidth = bytes[i + 5] + (bytes[i + 6] << 8);
|
||||||
|
const imageHeight = bytes[i + 7] + (bytes[i + 8] << 8);
|
||||||
|
const localColorTableFlag = bytes[i + 9] & 0x80;
|
||||||
|
const localColorTableSize = 2 ** ((bytes[i + 9] & 0x07) + 1);
|
||||||
|
|
||||||
|
i += 10;
|
||||||
|
|
||||||
|
if (localColorTableFlag) {
|
||||||
|
i += 3 * localColorTableSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (bytes[i] !== 0x00) {
|
||||||
|
const blockSize = bytes[i];
|
||||||
|
i += blockSize + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
const frame = extractPixelColor(bytes, imageLeft, imageTop, imageWidth, imageHeight);
|
||||||
|
frames.push(frame);
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// APNGフレーム抽出関数
|
||||||
|
function extractApngFrames(bytes: Uint8Array) {
|
||||||
|
const frames = [];
|
||||||
|
let i = 8; // PNGシグネチャをスキップ
|
||||||
|
|
||||||
|
while (i < bytes.length) {
|
||||||
|
const length = (bytes[i] << 24) + (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3];
|
||||||
|
const type = String.fromCharCode(bytes[i + 4], bytes[i + 5], bytes[i + 6], bytes[i + 7]);
|
||||||
|
|
||||||
|
if (type === 'IDAT' || type === 'fdAT') {
|
||||||
|
const imageLeft = 0;
|
||||||
|
const imageTop = 0;
|
||||||
|
const imageWidth = 0;
|
||||||
|
const imageHeight = 0;
|
||||||
|
|
||||||
|
const frame = extractPixelColor(bytes, imageLeft, imageTop, imageWidth, imageHeight);
|
||||||
|
frames.push(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += length + 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中央ピクセルの色を抽出する関数
|
||||||
|
function extractPixelColor(bytes: Uint8Array, left: number, top: number, width: number, height: number) {
|
||||||
|
const centerX = left + Math.floor(width / 2);
|
||||||
|
const centerY = top + Math.floor(height / 2);
|
||||||
|
const index = (centerY * width + centerX) * 4;
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: bytes[index],
|
||||||
|
g: bytes[index + 1],
|
||||||
|
b: bytes[index + 2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 色の変化を計算する関数
|
||||||
|
function calculateColorChanges(frames: { r: number; g: number; b: number }[]) {
|
||||||
|
let colorChangeCount = 0;
|
||||||
|
for (let i = 1; i < frames.length; i++) {
|
||||||
|
const prevFrame = frames[i - 1];
|
||||||
|
const currFrame = frames[i];
|
||||||
|
if (prevFrame.r !== currFrame.r || prevFrame.g !== currFrame.g || prevFrame.b !== currFrame.b) {
|
||||||
|
colorChangeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仮にFPSが30と仮定し、1秒あたりの色変化回数を計算
|
||||||
|
const fps = 30;
|
||||||
|
const durationInSeconds = frames.length / fps;
|
||||||
|
colorChangesPerSecond.value = colorChangeCount / durationInSeconds;
|
||||||
|
}
|
2474
pnpm-lock.yaml
2474
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue