mirror of
https://github.com/misskey-dev/misskey
synced 2024-12-13 22:29:05 +09:00
埋め込みコード生成機能
This commit is contained in:
parent
05ca36f400
commit
3bd055b045
50
locales/index.d.ts
vendored
50
locales/index.d.ts
vendored
@ -4988,6 +4988,14 @@ export interface Locale extends ILocale {
|
|||||||
* {x}から
|
* {x}から
|
||||||
*/
|
*/
|
||||||
"fromX": ParameterizedString<"x">;
|
"fromX": ParameterizedString<"x">;
|
||||||
|
/**
|
||||||
|
* 埋め込みコードをコピー
|
||||||
|
*/
|
||||||
|
"copyEmbedCode": string;
|
||||||
|
/**
|
||||||
|
* このユーザーのノート
|
||||||
|
*/
|
||||||
|
"noteOfThisUser": string;
|
||||||
"_delivery": {
|
"_delivery": {
|
||||||
/**
|
/**
|
||||||
* 配信状態
|
* 配信状態
|
||||||
@ -10070,6 +10078,48 @@ export interface Locale extends ILocale {
|
|||||||
*/
|
*/
|
||||||
"loop": string;
|
"loop": string;
|
||||||
};
|
};
|
||||||
|
"_embedCodeGen": {
|
||||||
|
/**
|
||||||
|
* 埋め込みコードをカスタマイズ
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* ヘッダーを表示
|
||||||
|
*/
|
||||||
|
"header": string;
|
||||||
|
/**
|
||||||
|
* 自動で続きを読み込む(非推奨)
|
||||||
|
*/
|
||||||
|
"autoload": string;
|
||||||
|
/**
|
||||||
|
* 高さの最大値
|
||||||
|
*/
|
||||||
|
"maxHeight": string;
|
||||||
|
/**
|
||||||
|
* 0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。
|
||||||
|
*/
|
||||||
|
"maxHeightDescription": string;
|
||||||
|
/**
|
||||||
|
* 高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。
|
||||||
|
*/
|
||||||
|
"maxHeightWarn": string;
|
||||||
|
/**
|
||||||
|
* 角丸にする
|
||||||
|
*/
|
||||||
|
"rounded": string;
|
||||||
|
/**
|
||||||
|
* 外枠に枠線をつける
|
||||||
|
*/
|
||||||
|
"border": string;
|
||||||
|
/**
|
||||||
|
* プレビューに反映
|
||||||
|
*/
|
||||||
|
"applyToPreview": string;
|
||||||
|
/**
|
||||||
|
* プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。
|
||||||
|
*/
|
||||||
|
"previewIsNotActual": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
@ -1243,6 +1243,8 @@ noDescription: "説明文はありません"
|
|||||||
alwaysConfirmFollow: "フォローの際常に確認する"
|
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||||
inquiry: "お問い合わせ"
|
inquiry: "お問い合わせ"
|
||||||
fromX: "{x}から"
|
fromX: "{x}から"
|
||||||
|
copyEmbedCode: "埋め込みコードをコピー"
|
||||||
|
noteOfThisUser: "このユーザーのノート"
|
||||||
|
|
||||||
_delivery:
|
_delivery:
|
||||||
status: "配信状態"
|
status: "配信状態"
|
||||||
@ -2685,3 +2687,15 @@ _mediaControls:
|
|||||||
pip: "ピクチャインピクチャ"
|
pip: "ピクチャインピクチャ"
|
||||||
playbackRate: "再生速度"
|
playbackRate: "再生速度"
|
||||||
loop: "ループ再生"
|
loop: "ループ再生"
|
||||||
|
|
||||||
|
_embedCodeGen:
|
||||||
|
title: "埋め込みコードをカスタマイズ"
|
||||||
|
header: "ヘッダーを表示"
|
||||||
|
autoload: "自動で続きを読み込む(非推奨)"
|
||||||
|
maxHeight: "高さの最大値"
|
||||||
|
maxHeightDescription: "0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。"
|
||||||
|
maxHeightWarn: "高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。"
|
||||||
|
rounded: "角丸にする"
|
||||||
|
border: "外枠に枠線をつける"
|
||||||
|
applyToPreview: "プレビューに反映"
|
||||||
|
previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。"
|
||||||
|
322
packages/frontend/src/components/MkEmbedCodeGenDialog.vue
Normal file
322
packages/frontend/src/components/MkEmbedCodeGenDialog.vue
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="dialogEl"
|
||||||
|
:width="1000"
|
||||||
|
:height="600"
|
||||||
|
:scroll="false"
|
||||||
|
:withOkButton="true"
|
||||||
|
@close="cancel()"
|
||||||
|
@ok="ok()"
|
||||||
|
@closed="$emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header>{{ i18n.ts._embedCodeGen.title }}</template>
|
||||||
|
|
||||||
|
<div :class="$style.embedCodeGenRoot">
|
||||||
|
<div :class="$style.embedCodeGenWrapper">
|
||||||
|
<div
|
||||||
|
:class="$style.embedCodeGenPreviewRoot"
|
||||||
|
>
|
||||||
|
<MkLoading :class="$style.embedCodeGenPreviewSpinner" v-if="iframeLoading"/>
|
||||||
|
<div :class="$style.embedCodeGenPreviewWrapper">
|
||||||
|
<div :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
|
||||||
|
<div :class="$style.embedCodeGenPreviewResizerRoot" ref="resizerRootEl">
|
||||||
|
<div
|
||||||
|
:class="$style.embedCodeGenPreviewResizer"
|
||||||
|
:style="{ transform: iframeStyle }"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
ref="iframeEl"
|
||||||
|
:src="embedPreviewUrl"
|
||||||
|
:class="$style.embedCodeGenPreviewIframe"
|
||||||
|
:style="{ height: `${iframeHeight}px` }"
|
||||||
|
@load="iframeOnLoad"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.embedCodeGenSettings" class="_gaps">
|
||||||
|
<MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
|
||||||
|
<template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
|
||||||
|
<template #suffix>px</template>
|
||||||
|
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkSelect v-model="colorMode">
|
||||||
|
<template #label>{{ i18n.ts.theme }}</template>
|
||||||
|
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
|
||||||
|
<option value="light">{{ i18n.ts.light }}</option>
|
||||||
|
<option value="dark">{{ i18n.ts.dark }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
||||||
|
<MkSwitch v-if="isEmbedWithScrollbar" v-model="autoload">{{ i18n.ts._embedCodeGen.autoload }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
|
||||||
|
<MkInfo v-if="typeof maxHeight === 'number' && maxHeight <= 0" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
|
||||||
|
<MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
|
||||||
|
<div>
|
||||||
|
<MkButton @click="applyToPreview" :disabled="iframeLoading">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
|
||||||
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { url } from '@/config.js';
|
||||||
|
import copy from '@/scripts/copy-to-clipboard.js';
|
||||||
|
import { normalizeEmbedParams, embedRouteWithScrollbar, getEmbedCode } from '@/scripts/get-embed-code.js';
|
||||||
|
import type { EmbeddableEntity, EmbedParams } from '@/scripts/get-embed-code.js';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'ok', url: string, code: string): void;
|
||||||
|
(ev: 'cancel'): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
entity: EmbeddableEntity;
|
||||||
|
idOrUsername: string;
|
||||||
|
params?: EmbedParams;
|
||||||
|
doCopy?: boolean;
|
||||||
|
}>(), {
|
||||||
|
doCopy: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
//#region Modalの制御
|
||||||
|
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('cancel');
|
||||||
|
dialogEl.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok() {
|
||||||
|
const _idOrUsername = props.entity === 'user-timeline' ? '@' + props.idOrUsername : props.idOrUsername;
|
||||||
|
const generatedUrl = `${url}/embed/${props.entity}/${_idOrUsername}?${new URLSearchParams(normalizeEmbedParams(paramsForUrl.value)).toString()}`;
|
||||||
|
const generatedCode = getEmbedCode(`/embed/${props.entity}/${_idOrUsername}`, paramsForUrl.value);
|
||||||
|
if (props.doCopy) {
|
||||||
|
copy(generatedCode);
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
emit('ok', generatedUrl, generatedCode);
|
||||||
|
dialogEl.value?.close();
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 埋め込みURL生成・カスタマイズ
|
||||||
|
|
||||||
|
// 本URL生成用params
|
||||||
|
const paramsForUrl = computed<EmbedParams>(() => ({
|
||||||
|
header: header.value === true ? undefined : header.value,
|
||||||
|
autoload: autoload.value === true ? undefined : autoload.value,
|
||||||
|
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
|
||||||
|
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
|
||||||
|
rounded: rounded.value === true ? undefined : rounded.value,
|
||||||
|
border: border.value === true ? undefined : border.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// プレビュー用params(手動で更新を掛けるのでref)
|
||||||
|
const paramsForPreview = ref<EmbedParams>(props.params ?? {});
|
||||||
|
|
||||||
|
const embedPreviewUrl = computed(() => {
|
||||||
|
const _idOrUsername = props.entity === 'user-timeline' ? '@' + props.idOrUsername : props.idOrUsername;
|
||||||
|
const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value));
|
||||||
|
if (paramClass.has('maxHeight')) {
|
||||||
|
const maxHeight = parseInt(paramClass.get('maxHeight')!);
|
||||||
|
paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限
|
||||||
|
}
|
||||||
|
return `${url}/embed/${props.entity}/${_idOrUsername}?${paramClass.toString()}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
|
||||||
|
const header = ref(props.params?.header ?? true);
|
||||||
|
const autoload = ref(props.params?.autoload ?? false);
|
||||||
|
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
|
||||||
|
|
||||||
|
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
|
||||||
|
const rounded = ref(props.params?.rounded ?? true);
|
||||||
|
const border = ref(props.params?.border ?? true);
|
||||||
|
|
||||||
|
function applyToPreview() {
|
||||||
|
const currentPreviewUrl = embedPreviewUrl.value;
|
||||||
|
|
||||||
|
paramsForPreview.value = {
|
||||||
|
header: header.value,
|
||||||
|
autoload: false, // プレビューはスクロールできないので常にfalse
|
||||||
|
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
|
||||||
|
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
|
||||||
|
rounded: rounded.value,
|
||||||
|
border: border.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (currentPreviewUrl === embedPreviewUrl.value) {
|
||||||
|
// URLが変わらなくてもリロード
|
||||||
|
iframeEl.value?.contentWindow?.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region プレビューのリサイズ
|
||||||
|
const resizerRootEl = shallowRef<HTMLDivElement>();
|
||||||
|
const iframeLoading = ref(true);
|
||||||
|
const iframeEl = shallowRef<HTMLIFrameElement>();
|
||||||
|
const iframeHeight = ref(0);
|
||||||
|
const iframeScale = ref(1);
|
||||||
|
const iframeStyle = computed(() => {
|
||||||
|
return `translate(-50%, -50%) scale(${iframeScale.value})`;
|
||||||
|
});
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
calcScale();
|
||||||
|
});
|
||||||
|
|
||||||
|
function iframeOnLoad() {
|
||||||
|
iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => {
|
||||||
|
iframeLoading.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
iframeHeight.value = 0;
|
||||||
|
iframeScale.value = 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function windowEventHandler(event: MessageEvent) {
|
||||||
|
if (event.source !== iframeEl.value?.contentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.data.type === 'misskey:embed:ready') {
|
||||||
|
iframeEl.value!.contentWindow?.postMessage({
|
||||||
|
type: 'misskey:embedParent:registerIframeId',
|
||||||
|
payload: {
|
||||||
|
iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (event.data.type === 'misskey:embed:changeHeight') {
|
||||||
|
iframeHeight.value = event.data.payload.height;
|
||||||
|
nextTick(() => {
|
||||||
|
calcScale();
|
||||||
|
iframeLoading.value = false; // 初回の高さ変更まで待つ
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function calcScale() {
|
||||||
|
if (!resizerRootEl.value) return;
|
||||||
|
const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
|
||||||
|
const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ(プレビューの文字は28px)
|
||||||
|
const iframeWidth = 500;
|
||||||
|
const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしない
|
||||||
|
iframeScale.value = scale;
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('message', windowEventHandler);
|
||||||
|
if (!resizerRootEl.value) return;
|
||||||
|
resizeObserver.observe(resizerRootEl.value);
|
||||||
|
});
|
||||||
|
onDeactivated(() => {
|
||||||
|
window.removeEventListener('message', windowEventHandler);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('message', windowEventHandler);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.embedCodeGenRoot {
|
||||||
|
container-type: inline-size;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenWrapper {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenPreviewRoot {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--bg);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenPreviewWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenPreviewTitle {
|
||||||
|
width: fit-content;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
background-color: var(--panel);
|
||||||
|
border-right: 1px solid var(--divider);
|
||||||
|
border-bottom: 1px solid var(--divider);
|
||||||
|
border-bottom-right-radius: var(--radius);
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenPreviewSpinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenPreviewResizerRoot {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenPreviewResizer {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenPreviewIframe {
|
||||||
|
border: none;
|
||||||
|
width: 500px;
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedCodeGenSettings {
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 800px) {
|
||||||
|
.embedCodeGenWrapper {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
103
packages/frontend/src/scripts/get-embed-code.ts
Normal file
103
packages/frontend/src/scripts/get-embed-code.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { url } from '@/config.js';
|
||||||
|
import { MOBILE_THRESHOLD } from '@/const.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import copy from '@/scripts/copy-to-clipboard.js';
|
||||||
|
import MkEmbedCodeGenDialog from '@/components/MkEmbedCodeGenDialog.vue';
|
||||||
|
|
||||||
|
// 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる)
|
||||||
|
const embeddableEntities = [
|
||||||
|
'notes',
|
||||||
|
'user-timeline',
|
||||||
|
'clip',
|
||||||
|
'tag',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type EmbeddableEntity = typeof embeddableEntities[number];
|
||||||
|
|
||||||
|
// 内部でスクロールがあるページ
|
||||||
|
export const embedRouteWithScrollbar: EmbeddableEntity[] = [
|
||||||
|
'clip',
|
||||||
|
'tag',
|
||||||
|
'user-timeline'
|
||||||
|
];
|
||||||
|
|
||||||
|
export type EmbedParams = {
|
||||||
|
maxHeight?: number;
|
||||||
|
colorMode?: 'light' | 'dark';
|
||||||
|
rounded?: boolean;
|
||||||
|
border?: boolean;
|
||||||
|
autoload?: boolean;
|
||||||
|
header?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeEmbedParams(params: EmbedParams): Record<string, string> {
|
||||||
|
// paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す
|
||||||
|
const normalizedParams: Record<string, string> = {};
|
||||||
|
for (const key in params) {
|
||||||
|
if (params[key] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (typeof params[key]) {
|
||||||
|
case 'number':
|
||||||
|
normalizedParams[key] = params[key].toString();
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
normalizedParams[key] = params[key] ? 'true' : 'false';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
normalizedParams[key] = params[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalizedParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 埋め込みコードを生成(iframe IDの発番もやる)
|
||||||
|
*/
|
||||||
|
export function getEmbedCode(path: string, params?: EmbedParams): string {
|
||||||
|
const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく
|
||||||
|
|
||||||
|
let paramString = '';
|
||||||
|
if (params) {
|
||||||
|
const searchParams = new URLSearchParams(normalizeEmbedParams(params));
|
||||||
|
paramString = '?' + searchParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframeCode = [
|
||||||
|
`<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`,
|
||||||
|
`<script defer src="${url}/embed.js"></script>`,
|
||||||
|
];
|
||||||
|
return iframeCode.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 埋め込みコードを生成してコピーする(カスタマイズ機能つき)
|
||||||
|
*
|
||||||
|
* カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください
|
||||||
|
*/
|
||||||
|
export function copyEmbedCode(entity: EmbeddableEntity, idOrUsername: string, params?: EmbedParams) {
|
||||||
|
const _params = { ...params };
|
||||||
|
|
||||||
|
if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) {
|
||||||
|
_params.maxHeight = 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー
|
||||||
|
if (window.innerWidth < MOBILE_THRESHOLD) {
|
||||||
|
const _idOrUsername = entity === 'user-timeline' ? `@${idOrUsername}` : idOrUsername;
|
||||||
|
copy(getEmbedCode(`/embed/${entity}/${_idOrUsername}`, _params));
|
||||||
|
os.success();
|
||||||
|
} else {
|
||||||
|
os.popup(MkEmbedCodeGenDialog, {
|
||||||
|
entity,
|
||||||
|
idOrUsername,
|
||||||
|
params: _params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js';
|
|||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { isSupportShare } from '@/scripts/navigator.js';
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
|
import { copyEmbedCode } from '@/scripts/get-embed-code.js';
|
||||||
|
|
||||||
export async function getNoteClipMenu(props: {
|
export async function getNoteClipMenu(props: {
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
@ -321,6 +322,13 @@ export function getNoteMenu(props: {
|
|||||||
text: i18n.ts.share,
|
text: i18n.ts.share,
|
||||||
action: share,
|
action: share,
|
||||||
}] : []),
|
}] : []),
|
||||||
|
(!appearNote.url && !appearNote.uri) ? {
|
||||||
|
icon: 'ti ti-code',
|
||||||
|
text: i18n.ts.copyEmbedCode,
|
||||||
|
action: () => {
|
||||||
|
copyEmbedCode('notes', appearNote.id);
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
|
$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
|
||||||
icon: 'ti ti-language-hiragana',
|
icon: 'ti ti-language-hiragana',
|
||||||
text: i18n.ts.translate,
|
text: i18n.ts.translate,
|
||||||
|
@ -16,6 +16,7 @@ import { $i, iAmModerator } from '@/account.js';
|
|||||||
import { IRouter } from '@/nirax.js';
|
import { IRouter } from '@/nirax.js';
|
||||||
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
|
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
|
||||||
import { mainRouter } from '@/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
import { copyEmbedCode } from '@/scripts/get-embed-code.js';
|
||||||
|
|
||||||
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
|
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
|
||||||
const meId = $i ? $i.id : null;
|
const meId = $i ? $i.id : null;
|
||||||
@ -177,7 +178,17 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||||||
if (user.url == null) return;
|
if (user.url == null) return;
|
||||||
window.open(user.url, '_blank', 'noopener');
|
window.open(user.url, '_blank', 'noopener');
|
||||||
},
|
},
|
||||||
}] : []), {
|
}] : [{
|
||||||
|
icon: 'ti ti-code',
|
||||||
|
text: i18n.ts.copyEmbedCode,
|
||||||
|
type: 'parent' as const,
|
||||||
|
children: [{
|
||||||
|
text: i18n.ts.noteOfThisUser,
|
||||||
|
action: () => {
|
||||||
|
copyEmbedCode('user-timeline', user.username);
|
||||||
|
},
|
||||||
|
}], // TODO: ユーザーカードの埋め込みなど
|
||||||
|
}]), {
|
||||||
icon: 'ti ti-share',
|
icon: 'ti ti-share',
|
||||||
text: i18n.ts.copyProfileUrl,
|
text: i18n.ts.copyProfileUrl,
|
||||||
action: () => {
|
action: () => {
|
||||||
|
@ -11,6 +11,7 @@ import { globalEvents } from '@/events.js';
|
|||||||
import lightTheme from '@/themes/_light.json5';
|
import lightTheme from '@/themes/_light.json5';
|
||||||
import darkTheme from '@/themes/_dark.json5';
|
import darkTheme from '@/themes/_dark.json5';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { isEmbedPage } from '@/scripts/embed-page.js';
|
||||||
|
|
||||||
export type Theme = {
|
export type Theme = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -95,7 +96,9 @@ export function applyTheme(theme: Theme, persist = true) {
|
|||||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.style.setProperty('color-scheme', colorScheme);
|
if (!isEmbedPage()) {
|
||||||
|
document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||||
|
}
|
||||||
|
|
||||||
if (persist) {
|
if (persist) {
|
||||||
miLocalStorage.setItem('theme', JSON.stringify(props));
|
miLocalStorage.setItem('theme', JSON.stringify(props));
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
html.embed {
|
html.embed {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
color-scheme: light dark;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,20 +75,22 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.f-1 {
|
&:not(.embed) {
|
||||||
font-size: 15px;
|
&.f-1 {
|
||||||
}
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
&.f-2 {
|
&.f-2 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.f-3 {
|
&.f-3 {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.useSystemFont {
|
&.useSystemFont {
|
||||||
font-family: system-ui;
|
font-family: system-ui;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user