(add) note embed page
This commit is contained in:
parent
e1f00e613d
commit
10fbea1224
|
@ -489,7 +489,7 @@ export class ClientServerService {
|
|||
|
||||
// Note Embed
|
||||
fastify.get<{ Params: { note: string; } }>('/notes/:note/embed', async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
reply.removeHeader('X-Frame-Options');
|
||||
|
||||
const note = await this.notesRepository.findOneBy({
|
||||
id: request.params.note,
|
||||
|
|
|
@ -11,7 +11,7 @@ html {
|
|||
width: 100vw;
|
||||
height: 100vh;
|
||||
cursor: wait;
|
||||
background-color: var(--bg);
|
||||
background-color: var(--bg, #fff);
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
|
|
@ -4,16 +4,19 @@ block vars
|
|||
- const user = note.user;
|
||||
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||
- const url = `${config.url}/notes/${note.id}`;
|
||||
- const userUrl = (user) => user.host ? `${config.url}/@${user.username}@${user.host}` : `${config.url}/@${user.username}`;
|
||||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const displayUser = isRenote ? note.renote.user: note.user;
|
||||
- const mainNote = isRenote ? note.renote : note;
|
||||
- const displayMedia = mainNote.files.filter((item) => item.type.startsWith('image') || item.type.startsWith('video')).slice(0, 4);
|
||||
- const noteEmoji = Object.entries(note.emojis || {}).map((e) => { return { name: e[0], url: e[1], host: note.user.host }; }).concat(Object.entries(note.renote?.emojis || {}).map((e) => { return { name: e[0], url: e[1], host: note.renote?.user.host }; }))
|
||||
|
||||
block meta
|
||||
if !user.host
|
||||
link(rel='alternate' href=url type='application/activity+json')
|
||||
if note.uri
|
||||
link(rel='alternate' href=note.uri type='application/activity+json')
|
||||
|
||||
script(type='application/json') !{JSON.stringify(note)}
|
||||
script(id='remote_custom_emojis' type='application/json') !{JSON.stringify(noteEmoji)}
|
||||
|
||||
block content
|
||||
div#note
|
||||
|
@ -22,22 +25,24 @@ block content
|
|||
div.wrapper
|
||||
if isRenote
|
||||
div#renote
|
||||
a.avatar(href=`${config.url}/@${note.user.username}` target="_blank" rel="noopener noreferrer")
|
||||
a.avatar(href=userUrl(note.user) target="_blank" rel="noopener noreferrer")
|
||||
img(src=note.user.avatarUrl)
|
||||
|
||||
i.ti.ti-repeat
|
||||
|
||||
span(data-mi-i18n='renotedBy' data-mi-i18n-ctx=`{"user": "${note.user.name || note.user.username}"}`)
|
||||
a(href=`${config.url}/@${note.user.username}` target="_blank" rel="noopener noreferrer")
|
||||
a(href=userUrl(note.user) target="_blank" rel="noopener noreferrer")
|
||||
b(data-mi-i18n-target='user')
|
||||
|
||||
div.author
|
||||
a.avatar(href=`${config.url}/@${displayUser.username}` target="_blank" rel="noopener noreferrer")
|
||||
a.avatar(href=userUrl(displayUser) target="_blank" rel="noopener noreferrer")
|
||||
img(src=displayUser.avatarUrl)
|
||||
|
||||
div.user-info
|
||||
a.user-name(href=`${config.url}/@${displayUser.username}` target="_blank" rel="noopener noreferrer") #{displayUser.name || displayUser.username}
|
||||
a.user-name(href=userUrl(displayUser) target="_blank" rel="noopener noreferrer") #{displayUser.name || displayUser.username}
|
||||
div.user-id @#{displayUser.username}
|
||||
if displayUser.host
|
||||
span(style="opacity: .5;") @#{displayUser.host}
|
||||
|
||||
div#instance-info
|
||||
a.click-anime(href=config.url target='_blank')
|
||||
|
@ -45,9 +50,77 @@ block content
|
|||
span.sr-only(data-mi-i18n='aboutX' data-mi-i18n-ctx=`{"x": "${instanceName}"}`)
|
||||
|
||||
main
|
||||
div.mfm !{isRenote ? note.renote.text : note.text}
|
||||
if (mainNote.cw != null)
|
||||
div.cw-meta
|
||||
span.mfm.cw-summary #{mainNote.cw}
|
||||
button._button#cw-button
|
||||
span#cw-info-show
|
||||
b(data-mi-i18n="showMore")
|
||||
span.cw-info-wrapper
|
||||
if(mainNote.text)
|
||||
span.cw-info(data-mi-i18n='_cw.chars' data-mi-i18n-ctx=`{"count": ${mainNote.text.length}}`)
|
||||
if(mainNote.files && mainNote.files.length !== 0)
|
||||
span.cw-info(data-mi-i18n='_cw.files' data-mi-i18n-ctx=`{"count": ${mainNote.files.length}}`)
|
||||
if(mainNote.poll != null)
|
||||
span.cw-info(data-mi-i18n='poll')
|
||||
span.hide#cw-info-hide
|
||||
b(data-mi-i18n="hide")
|
||||
div#note-body.mfm.hide #{mainNote.text}
|
||||
|
||||
else
|
||||
div#note-body.mfm #{mainNote.text}
|
||||
|
||||
if (!isRenote && note.renote)
|
||||
div#quote.mfm !{note.renote.text}
|
||||
|
||||
hr
|
||||
pre(style='white-space: pre-wrap;') !{JSON.stringify(note, null, 2)}
|
||||
div#quote
|
||||
div.quote-avatar
|
||||
a.avatar(href=userUrl(note.renote.user) target="_blank" rel="noopener noreferrer")
|
||||
img(src=note.renote.user.avatarUrl)
|
||||
|
||||
div.quote-body
|
||||
div.meta
|
||||
div.author
|
||||
a(href=userUrl(note.renote.user) target="_blank" rel="noopener noreferrer")
|
||||
b #{note.renote.user.name || note.renote.user.username}
|
||||
span @#{note.renote.user.username}
|
||||
if note.renote.user.host
|
||||
span(style="opacity: .5;") @#{note.renote.user.host}
|
||||
|
||||
a.time(href=`${config.url}/notes/${note.renote.id}` target="_blank" rel="noopener noreferrer")
|
||||
time.locale-string(datetime=note.renote.createdAt data-mi-date-mode="relative")
|
||||
|
||||
if (note.renote.cw != null)
|
||||
div.cw-meta
|
||||
span.mfm.cw-summary #{note.renote.cw}
|
||||
button._button#quote-cw-button
|
||||
span#quote-cw-info-show
|
||||
b(data-mi-i18n="showMore")
|
||||
span.cw-info-wrapper
|
||||
if(mainNote.text)
|
||||
span.cw-info(data-mi-i18n='_cw.chars' data-mi-i18n-ctx=`{"count": ${mainNote.text.length}}`)
|
||||
if(mainNote.files && mainNote.files.length !== 0)
|
||||
span.cw-info(data-mi-i18n='_cw.files' data-mi-i18n-ctx=`{"count": ${mainNote.files.length}}`)
|
||||
if(mainNote.poll != null)
|
||||
span.cw-info(data-mi-i18n='poll')
|
||||
span.hide#quote-cw-info-hide
|
||||
b(data-mi-i18n="hide")
|
||||
div#quote-note-body.mfm.hide #{mainNote.text}
|
||||
else
|
||||
div#quote-note-body.mfm #{note.renote.text}
|
||||
footer
|
||||
div.info
|
||||
a(href=`${config.url}/notes/${mainNote.id}`, target="_blank", rel="noopener noreferrer")
|
||||
time.locale-string(datetime=mainNote.createdAt data-mi-date-mode="detailed")
|
||||
div.reactions
|
||||
each val, key in mainNote.reactions
|
||||
span.reaction-item
|
||||
span.emoji #{key}
|
||||
span.count #{val}
|
||||
|
||||
a._button.button(href=`${config.url}/notes/${mainNote.id}`, target="_blank", rel="noopener noreferrer")
|
||||
i.ti.ti-arrow-back-up
|
||||
a._button.button(href=`${config.url}/notes/${mainNote.id}`, target="_blank", rel="noopener noreferrer")
|
||||
i.ti.ti-repeat
|
||||
a._button.button(href=`${config.url}/notes/${mainNote.id}`, target="_blank", rel="noopener noreferrer")
|
||||
i.ti.ti-plus
|
||||
a._button.button(href=`${config.url}/notes/${mainNote.id}`, target="_blank", rel="noopener noreferrer")
|
||||
i.ti.ti-dots
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Misskey 埋め込みのカスタマイズについて
|
||||
|
||||
URLパラメータで、埋め込みの形式をカスタマイズできます。
|
||||
|
||||
## 共通
|
||||
|
||||
- `rounded` … 外枠を丸める。`0`で無効(デフォルト…`1`)
|
||||
- `theme` … プリインストールテーマのID、もしくは `light` `dark` を指定可能。指定なしの場合は、最後に使用したテーマもしくはデバイスのカラーモードに応じたデフォルトテーマを適用。(デフォルト:指定なし)
|
||||
- `mfm` … `animated` を指定可能。指定すると、動きのあるMFMが有効になります。(デフォルト…指定なし)
|
|
@ -16,18 +16,171 @@ html,body {
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.custom-emoji {
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
height: 2em;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
vertical-align: center;
|
||||
transition: transform .2s ease;
|
||||
|
||||
&.custom-emoji>img,svg {
|
||||
height: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.unicode-emoji>img,svg {
|
||||
height: 1.25em;
|
||||
vertical-align: -.25em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.mfm {
|
||||
white-space: pre-wrap;
|
||||
|
||||
._mfm_blur_ {
|
||||
filter: blur(6px);
|
||||
transition: filter 0.3s;
|
||||
|
||||
&:hover {
|
||||
filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
.mfm-x2 {
|
||||
--mfm-zoom-size: 200%;
|
||||
}
|
||||
|
||||
.mfm-x3 {
|
||||
--mfm-zoom-size: 400%;
|
||||
}
|
||||
|
||||
.mfm-x4 {
|
||||
--mfm-zoom-size: 600%;
|
||||
}
|
||||
|
||||
.mfm-x2, .mfm-x3, .mfm-x4 {
|
||||
font-size: var(--mfm-zoom-size);
|
||||
|
||||
.mfm-x2, .mfm-x3, .mfm-x4 {
|
||||
/* only half effective */
|
||||
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
|
||||
|
||||
.mfm-x2, .mfm-x3, .mfm-x4 {
|
||||
/* disabled */
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mfm-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes mfm-spinX {
|
||||
0% { transform: perspective(128px) rotateX(0deg); }
|
||||
100% { transform: perspective(128px) rotateX(360deg); }
|
||||
}
|
||||
|
||||
@keyframes mfm-spinY {
|
||||
0% { transform: perspective(128px) rotateY(0deg); }
|
||||
100% { transform: perspective(128px) rotateY(360deg); }
|
||||
}
|
||||
|
||||
@keyframes mfm-jump {
|
||||
0% { transform: translateY(0); }
|
||||
25% { transform: translateY(-16px); }
|
||||
50% { transform: translateY(0); }
|
||||
75% { transform: translateY(-8px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes mfm-bounce {
|
||||
0% { transform: translateY(0) scale(1, 1); }
|
||||
25% { transform: translateY(-16px) scale(1, 1); }
|
||||
50% { transform: translateY(0) scale(1, 1); }
|
||||
75% { transform: translateY(0) scale(1.5, 0.75); }
|
||||
100% { transform: translateY(0) scale(1, 1); }
|
||||
}
|
||||
|
||||
// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
|
||||
// let css = '';
|
||||
// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
|
||||
@keyframes mfm-twitch {
|
||||
0% { transform: translate(7px, -2px) }
|
||||
5% { transform: translate(-3px, 1px) }
|
||||
10% { transform: translate(-7px, -1px) }
|
||||
15% { transform: translate(0px, -1px) }
|
||||
20% { transform: translate(-8px, 6px) }
|
||||
25% { transform: translate(-4px, -3px) }
|
||||
30% { transform: translate(-4px, -6px) }
|
||||
35% { transform: translate(-8px, -8px) }
|
||||
40% { transform: translate(4px, 6px) }
|
||||
45% { transform: translate(-3px, 1px) }
|
||||
50% { transform: translate(2px, -10px) }
|
||||
55% { transform: translate(-7px, 0px) }
|
||||
60% { transform: translate(-2px, 4px) }
|
||||
65% { transform: translate(3px, -8px) }
|
||||
70% { transform: translate(6px, 7px) }
|
||||
75% { transform: translate(-7px, -2px) }
|
||||
80% { transform: translate(-7px, -8px) }
|
||||
85% { transform: translate(9px, 3px) }
|
||||
90% { transform: translate(-3px, -2px) }
|
||||
95% { transform: translate(-10px, 2px) }
|
||||
100% { transform: translate(-2px, -6px) }
|
||||
}
|
||||
|
||||
// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
|
||||
// let css = '';
|
||||
// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
|
||||
@keyframes mfm-shake {
|
||||
0% { transform: translate(-3px, -1px) rotate(-8deg) }
|
||||
5% { transform: translate(0px, -1px) rotate(-10deg) }
|
||||
10% { transform: translate(1px, -3px) rotate(0deg) }
|
||||
15% { transform: translate(1px, 1px) rotate(11deg) }
|
||||
20% { transform: translate(-2px, 1px) rotate(1deg) }
|
||||
25% { transform: translate(-1px, -2px) rotate(-2deg) }
|
||||
30% { transform: translate(-1px, 2px) rotate(-3deg) }
|
||||
35% { transform: translate(2px, 1px) rotate(6deg) }
|
||||
40% { transform: translate(-2px, -3px) rotate(-9deg) }
|
||||
45% { transform: translate(0px, -1px) rotate(-12deg) }
|
||||
50% { transform: translate(1px, 2px) rotate(10deg) }
|
||||
55% { transform: translate(0px, -3px) rotate(8deg) }
|
||||
60% { transform: translate(1px, -1px) rotate(8deg) }
|
||||
65% { transform: translate(0px, -1px) rotate(-7deg) }
|
||||
70% { transform: translate(-1px, -3px) rotate(6deg) }
|
||||
75% { transform: translate(0px, -2px) rotate(4deg) }
|
||||
80% { transform: translate(-2px, -1px) rotate(3deg) }
|
||||
85% { transform: translate(1px, -3px) rotate(-10deg) }
|
||||
90% { transform: translate(1px, 0px) rotate(3deg) }
|
||||
95% { transform: translate(-2px, 0px) rotate(-3deg) }
|
||||
100% { transform: translate(2px, 1px) rotate(2deg) }
|
||||
}
|
||||
|
||||
@keyframes mfm-rubberBand {
|
||||
from { transform: scale3d(1, 1, 1); }
|
||||
30% { transform: scale3d(1.25, 0.75, 1); }
|
||||
40% { transform: scale3d(0.75, 1.25, 1); }
|
||||
50% { transform: scale3d(1.15, 0.85, 1); }
|
||||
65% { transform: scale3d(0.95, 1.05, 1); }
|
||||
75% { transform: scale3d(1.05, 0.95, 1); }
|
||||
to { transform: scale3d(1, 1, 1); }
|
||||
}
|
||||
|
||||
@keyframes mfm-rainbow {
|
||||
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
|
||||
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
|
||||
}
|
||||
}
|
||||
|
||||
#error {
|
||||
padding: 0 0 3.5rem;
|
||||
text-align: center;
|
||||
|
@ -52,74 +205,4 @@ html,body {
|
|||
p {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#note {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.wrapper {
|
||||
#renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
color: var(--renote);
|
||||
|
||||
>.avatar {
|
||||
display: block;
|
||||
margin-right: 1rem;
|
||||
|
||||
>img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
>.avatar {
|
||||
display: block;
|
||||
margin-right: 1rem;
|
||||
|
||||
>img {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
>.user-info {
|
||||
>.user-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#instance-info {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
font-size: 1.05rem;
|
||||
|
||||
#quote {
|
||||
font-size: .95rem;
|
||||
padding: .75rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: var(--radius);
|
||||
border: dashed 1px var(--renote);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,25 @@
|
|||
import JSON5 from 'json5';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { version, lang, updateLocale } from '@/config';
|
||||
import { updateI18n } from '@/i18n';
|
||||
import { version, lang, updateLocale, url } from '@/config';
|
||||
import { embedInitI18n } from './scripts/embed-i18n';
|
||||
import '@/style.scss';
|
||||
import './embed.scss';
|
||||
import 'iframe-resizer/js/iframeResizer.contentWindow';
|
||||
import { embedInitLinkAnime } from './scripts/link-anime';
|
||||
import { parseMfm } from './scripts/parse-mfm';
|
||||
import { renderNotFound } from './scripts/render-not-found';
|
||||
import { parseEmoji } from './scripts/parse-emoji';
|
||||
import { applyTheme } from './scripts/theme';
|
||||
import lightTheme from '@/themes/_light.json5';
|
||||
import darkTheme from '@/themes/_dark.json5';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
|
||||
console.info(`Misskey (Embed Sandbox) v${version}`);
|
||||
|
||||
const supportedEmbedEntity: string[] = [
|
||||
'notes'
|
||||
];
|
||||
|
||||
if (_DEV_) {
|
||||
console.warn('Development mode!!!');
|
||||
|
||||
|
@ -47,7 +57,7 @@ if (localeOutdated) {
|
|||
miLocalStorage.setItem('locale', newLocale);
|
||||
miLocalStorage.setItem('localeVersion', version);
|
||||
updateLocale(parsedNewLocale);
|
||||
updateI18n(parsedNewLocale);
|
||||
embedInitI18n();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
@ -60,23 +70,85 @@ const html = document.documentElement;
|
|||
html.setAttribute('lang', lang);
|
||||
//#endregion
|
||||
|
||||
embedInitI18n();
|
||||
//#region ページのパスをパース
|
||||
// パス構造: /{entityName}/{id}/embed
|
||||
const path = location.pathname;
|
||||
if (!path.includes('/embed')) {
|
||||
location.href = url;
|
||||
throw new Error('Embed script was loaded on non-embed page. Force redirect to the top page.');
|
||||
}
|
||||
const pageMetaValues:string[] = path.split('/').filter((e) => e != '' && e != 'embed');
|
||||
const pageMeta: { entityName: string; id: string; } = {
|
||||
entityName: pageMetaValues[0],
|
||||
id: pageMetaValues[1],
|
||||
};
|
||||
const URLParams = new URLSearchParams(location.search);
|
||||
const enableAnimatedMfm = URLParams.get("mfm") === 'animated';
|
||||
const disableRootRound = URLParams.get("rounded") === "0";
|
||||
//#endregion
|
||||
|
||||
document.querySelectorAll(".mfm").forEach((e) => {
|
||||
e.innerHTML = parseMfm(e.innerHTML).outerHTML;
|
||||
});
|
||||
const rootEl = document.getElementById("container");
|
||||
if (disableRootRound && rootEl) {
|
||||
rootEl.style.borderRadius = "0";
|
||||
}
|
||||
|
||||
//#region ロード画面解除
|
||||
const splash = document.getElementById('splash');
|
||||
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
|
||||
if (splash) splash.addEventListener('transitionend', () => {
|
||||
splash.remove();
|
||||
});
|
||||
//#region テーマ適用
|
||||
const themePreferences = localStorage.getItem('theme');
|
||||
const getTheme = async () => {
|
||||
// テーマが指定されている場合
|
||||
if (URLParams.has("theme")) {
|
||||
switch (URLParams.get("theme")) {
|
||||
case 'light':
|
||||
return lightTheme;
|
||||
case 'dark':
|
||||
return darkTheme;
|
||||
default:
|
||||
return await import(`../themes/${URLParams.get("theme")}.json5`).catch((_) => {
|
||||
return isDeviceDarkmode() ? darkTheme : lightTheme;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return isDeviceDarkmode() ? darkTheme : lightTheme;
|
||||
}
|
||||
};
|
||||
|
||||
if (splash) {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.pointerEvents = 'none';
|
||||
if (!themePreferences || URLParams.has("theme")) {
|
||||
applyTheme(await getTheme(), false);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
embedInitLinkAnime();
|
||||
//埋め込みページごとのスクリプト読み込み
|
||||
if (!supportedEmbedEntity.includes(pageMeta.entityName)) {
|
||||
renderNotFound();
|
||||
afterPageInitialization();
|
||||
} else {
|
||||
import(`./pages/${pageMeta.entityName}.ts`).then(() => {
|
||||
afterPageInitialization();
|
||||
});
|
||||
}
|
||||
|
||||
function afterPageInitialization() {
|
||||
embedInitI18n();
|
||||
|
||||
//@ts-ignore
|
||||
document.querySelectorAll(".mfm").forEach((e: HTMLElement) => {
|
||||
e.innerHTML = parseMfm(e.innerText, enableAnimatedMfm).outerHTML;
|
||||
});
|
||||
|
||||
parseEmoji();
|
||||
|
||||
//#region ロード画面解除
|
||||
const splash = document.getElementById('splash');
|
||||
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
|
||||
if (splash) splash.addEventListener('transitionend', () => {
|
||||
splash.remove();
|
||||
});
|
||||
|
||||
if (splash) {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.pointerEvents = 'none';
|
||||
}
|
||||
//#endregion
|
||||
|
||||
embedInitLinkAnime();
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
#note {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.wrapper {
|
||||
#renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
color: var(--renote);
|
||||
|
||||
>.avatar {
|
||||
display: block;
|
||||
margin-right: 1rem;
|
||||
|
||||
>img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
>.avatar {
|
||||
display: block;
|
||||
margin-right: 1rem;
|
||||
|
||||
>img {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
>.user-info {
|
||||
>.user-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#instance-info {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
#note-body {
|
||||
position: relative;
|
||||
font-size: 1.05rem;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#quote {
|
||||
font-size: .95rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: var(--radius);
|
||||
border: dashed 1px var(--renote);
|
||||
display: flex;
|
||||
|
||||
.quote-avatar {
|
||||
margin-right: .5rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-body {
|
||||
flex-grow: 1;
|
||||
|
||||
>.meta {
|
||||
display: flex;
|
||||
|
||||
>.author>a {
|
||||
display: inline-block;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
>a.time {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cw-meta {
|
||||
margin-bottom: .5em;
|
||||
|
||||
>.cw-summary>div {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#cw-button,
|
||||
#quote-cw-button {
|
||||
display: inline-block;
|
||||
padding: .25em .5em;
|
||||
font-size: 0.75em;
|
||||
color: var(--cwFg);
|
||||
background: var(--cwBg);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background: var(--cwHoverBg);
|
||||
}
|
||||
|
||||
.cw-info-wrapper {
|
||||
>:first-child {
|
||||
margin-left: 4px;
|
||||
&:before {
|
||||
content: '(';
|
||||
}
|
||||
}
|
||||
|
||||
>:after {
|
||||
content: ' / ';
|
||||
}
|
||||
|
||||
>:last-child:after {
|
||||
content: ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
.info {
|
||||
margin: 1rem 0;
|
||||
opacity: .7;
|
||||
}
|
||||
.reactions {
|
||||
.reaction-item {
|
||||
border-radius: 4px;
|
||||
padding: 0 6px;
|
||||
margin: 2px;
|
||||
height: 32px;
|
||||
background-color: var(--fgOnAccent);
|
||||
display: inline-block;
|
||||
|
||||
img {
|
||||
height: 1.25rem;
|
||||
vertical-align: -.25rem;
|
||||
}
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.button {
|
||||
margin-right: 2rem;
|
||||
padding: .5rem;
|
||||
font-size: 1.2rem;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { embedI18n as i18n } from '../scripts/embed-i18n';
|
||||
import { dateTimeFormat } from '@/scripts/intl-const';
|
||||
import '../notes.scss';
|
||||
|
||||
//CWボタン
|
||||
document.getElementById("cw-button")?.addEventListener("click", () => {
|
||||
document.getElementById("note-body")?.classList.toggle("hide");
|
||||
document.getElementById("cw-info-show")?.classList.toggle("hide");
|
||||
document.getElementById("cw-info-hide")?.classList.toggle("hide");
|
||||
});
|
||||
document.getElementById("quote-cw-button")?.addEventListener("click", () => {
|
||||
document.getElementById("quote-note-body")?.classList.toggle("hide");
|
||||
document.getElementById("quote-cw-info-show")?.classList.toggle("hide");
|
||||
document.getElementById("quote-cw-info-hide")?.classList.toggle("hide");
|
||||
});
|
||||
|
||||
//時刻(タイムゾーン関連がややこしいのでJSでレンダリング)
|
||||
document.querySelectorAll("time.locale-string").forEach((el) => {
|
||||
const dateTimeRaw = el.getAttribute("datetime");
|
||||
const mode = el.getAttribute("data-mi-date-mode");
|
||||
if (dateTimeRaw !== null) {
|
||||
const _time = new Date(dateTimeRaw).getTime();
|
||||
|
||||
const invalid = Number.isNaN(_time);
|
||||
const absolute:string = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||
let now = new Date().getTime();
|
||||
|
||||
const relative = () => {
|
||||
if (invalid) return i18n.ts._ago.invalid;
|
||||
|
||||
const ago = (now - _time) / 1000/*ms*/;
|
||||
|
||||
return (
|
||||
ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) :
|
||||
ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) :
|
||||
ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) :
|
||||
ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) :
|
||||
ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) :
|
||||
ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
|
||||
ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
|
||||
ago >= -1 ? i18n.ts._ago.justNow :
|
||||
i18n.ts._ago.future);
|
||||
};
|
||||
|
||||
switch (mode) {
|
||||
case 'absolute':
|
||||
el.innerHTML = absolute;
|
||||
break;
|
||||
case 'detailed':
|
||||
el.innerHTML = `${absolute} (${relative()})`;
|
||||
break;
|
||||
case 'relative':
|
||||
default:
|
||||
el.innerHTML = relative();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { };
|
|
@ -1,4 +1,5 @@
|
|||
import { i18n } from "@/i18n";
|
||||
import { locale } from "@/config";
|
||||
import { I18n } from "@/scripts/i18n";
|
||||
|
||||
/**
|
||||
* 非vueページ向け翻訳適用関数
|
||||
|
@ -8,6 +9,10 @@ import { i18n } from "@/i18n";
|
|||
* <span data-mi-i18n="翻訳key(必須)" data-mi-i18n-ctx=" *JSON Objectで動的な値を指定(任意)* "></span>
|
||||
* ```
|
||||
*/
|
||||
const i18n = new I18n(locale);
|
||||
|
||||
export const embedI18n = i18n;
|
||||
|
||||
export function embedInitI18n() {
|
||||
const els: NodeListOf<HTMLElement> = document.querySelectorAll("[data-mi-i18n]");
|
||||
els.forEach((tag: HTMLElement) => {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { char2twemojiFilePath } from '@/scripts/emoji-base';
|
||||
|
||||
const char2path = char2twemojiFilePath;
|
||||
|
||||
const remoteCustomEmojiEl = document.getElementById("remote_custom_emojis");
|
||||
let remoteCustomEmoji: { name: string; url: string; host?: string; }[] = [];
|
||||
if (remoteCustomEmojiEl) {
|
||||
remoteCustomEmoji = JSON.parse(remoteCustomEmojiEl.innerHTML);
|
||||
}
|
||||
|
||||
function getCustomEmojiName(ceNameRaw: string) {
|
||||
return (ceNameRaw.startsWith(":") ? ceNameRaw.substr(1, ceNameRaw.length - 2) : ceNameRaw).replace('@.', '')
|
||||
}
|
||||
|
||||
function getCustomEmojiUrl(ceName: string) {
|
||||
const remote = remoteCustomEmoji.find((e) => e.name === ceName);
|
||||
if (remote) {
|
||||
return remote.url;
|
||||
}
|
||||
return `/emoji/${ceName}.webp`;
|
||||
}
|
||||
|
||||
export function parseEmoji() {
|
||||
document.querySelectorAll(".emoji").forEach((el) => {
|
||||
let src: (string | null) = null;
|
||||
if (el.innerHTML.startsWith(":")) {
|
||||
console.log(getCustomEmojiName(el.innerHTML));
|
||||
src = getCustomEmojiUrl(getCustomEmojiName(el.innerHTML));
|
||||
} else {
|
||||
src = char2path(el.innerHTML);
|
||||
}
|
||||
if (src) {
|
||||
const emojiEl = document.createElement("img");
|
||||
emojiEl.src = src;
|
||||
emojiEl.title = getCustomEmojiName(el.innerHTML);
|
||||
el.innerHTML = emojiEl.outerHTML;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
import * as mfm from 'mfm-js';
|
||||
import { toUnicode } from 'punycode';
|
||||
import { host as localHost } from '@/config';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import Prism from 'prismjs';
|
||||
import 'prismjs/themes/prism-okaidia.css';
|
||||
|
||||
const QUOTE_STYLE = `
|
||||
display: block;
|
||||
|
@ -18,11 +21,16 @@ interface MfmFn extends mfm.MfmFn {
|
|||
}
|
||||
};
|
||||
|
||||
export function parseMfm(text: string): HTMLDivElement {
|
||||
export function parseMfm(text: string, useAnim: boolean = false): HTMLDivElement {
|
||||
const ast = mfm.parse(text);
|
||||
|
||||
const el = document.createElement("div");
|
||||
|
||||
const validTime = (t: string | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
return t.match(/^[0-9.]+s$/) ? t : null;
|
||||
};
|
||||
|
||||
function genEl(ast: (MfmFn | mfm.MfmNode)[]) {
|
||||
return ast.map((token: (MfmFn | mfm.MfmNode)) => {
|
||||
switch (token.type) {
|
||||
|
@ -69,6 +77,49 @@ export function parseMfm(text: string): HTMLDivElement {
|
|||
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
|
||||
let style;
|
||||
switch (token.props.name) {
|
||||
case 'tada': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
|
||||
break;
|
||||
}
|
||||
case 'jelly': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
|
||||
break;
|
||||
}
|
||||
case 'twitch': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.5s';
|
||||
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
|
||||
break;
|
||||
}
|
||||
case 'shake': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.5s';
|
||||
style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
|
||||
break;
|
||||
}
|
||||
case 'spin': {
|
||||
const direction =
|
||||
token.props.args.left ? 'reverse' :
|
||||
token.props.args.alternate ? 'alternate' :
|
||||
'normal';
|
||||
const anime =
|
||||
token.props.args.x ? 'mfm-spinX' :
|
||||
token.props.args.y ? 'mfm-spinY' :
|
||||
'mfm-spin';
|
||||
const speed = validTime(token.props.args.speed) ?? '1.5s';
|
||||
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
||||
break;
|
||||
}
|
||||
case 'jump': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.75s';
|
||||
style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
|
||||
break;
|
||||
}
|
||||
case 'bounce': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.75s';
|
||||
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
|
||||
break;
|
||||
}
|
||||
case 'flip': {
|
||||
const transform =
|
||||
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
|
||||
|
@ -122,6 +173,12 @@ export function parseMfm(text: string): HTMLDivElement {
|
|||
})
|
||||
return [el];
|
||||
}
|
||||
case 'rainbow': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
|
||||
break;
|
||||
}
|
||||
// TODO: Sparkle の プレーンHTML実装
|
||||
case 'rotate': {
|
||||
const degrees = parseFloat(token.props.args.deg ?? '90');
|
||||
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
|
||||
|
@ -151,6 +208,9 @@ export function parseMfm(text: string): HTMLDivElement {
|
|||
style = `background-color: #${color};`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
style = '';
|
||||
break;
|
||||
}
|
||||
if (style == null) {
|
||||
const el = document.createElement("span");
|
||||
|
@ -240,14 +300,19 @@ export function parseMfm(text: string): HTMLDivElement {
|
|||
el.style.overflowX = 'scroll';
|
||||
el.style.width = '100%';
|
||||
const elc = document.createElement('code');
|
||||
elc.innerText = token.props.code;
|
||||
const prismLang = Prism.languages[token.props.lang] ? token.props.lang : 'js';
|
||||
elc.innerHTML = Prism.highlight(token.props.code, Prism.languages[prismLang], prismLang);
|
||||
el.classList.add(`language-${prismLang}`);
|
||||
elc.classList.add(`language-${prismLang}`);
|
||||
el.appendChild(elc);
|
||||
return [el];
|
||||
}
|
||||
|
||||
case 'inlineCode': {
|
||||
const el = document.createElement('code');
|
||||
el.innerText = token.props.code;
|
||||
const prismLang = 'js';
|
||||
el.innerHTML = Prism.highlight(token.props.code, Prism.languages[prismLang], prismLang);
|
||||
el.classList.add(`language-${prismLang}`);
|
||||
return [el];
|
||||
}
|
||||
|
||||
|
@ -262,14 +327,14 @@ export function parseMfm(text: string): HTMLDivElement {
|
|||
|
||||
case 'emojiCode': {
|
||||
const el = document.createElement('span');
|
||||
el.classList.add('custom-emoji', 'needs-replacing');
|
||||
el.classList.add('emoji', 'custom-emoji');
|
||||
el.innerText = `:${token.props.name}:`;
|
||||
return [el];
|
||||
}
|
||||
|
||||
case 'unicodeEmoji': {
|
||||
const el = document.createElement('span');
|
||||
el.classList.add('emoji');
|
||||
el.classList.add('emoji', 'unicode-emoji');
|
||||
el.innerText = token.props.emoji;
|
||||
return [el];
|
||||
}
|
||||
|
@ -325,5 +390,11 @@ export function parseMfm(text: string): HTMLDivElement {
|
|||
el.appendChild(element as HTMLElement);
|
||||
});
|
||||
|
||||
el.innerHTML = sanitizeHtml(el.innerHTML, {
|
||||
allowedAttributes: {
|
||||
'*': ['style', 'class']
|
||||
}
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { instanceName } from "@/config";
|
||||
import { embedI18n as i18n } from '../scripts/embed-i18n';
|
||||
|
||||
export function renderNotFound() {
|
||||
const el = document.getElementById("container");
|
||||
if (el) {
|
||||
el.innerHTML = `<div id="error">
|
||||
<div>
|
||||
<div id="instance-info">
|
||||
<a class="click-anime" href="http://localhost:3000" target="_blank">
|
||||
<img src="/static-assets/splash.png" class="_anime_bounce_standBy">
|
||||
<span class="sr-only">${i18n.t('aboutX', {x: instanceName || 'Misskey'})}</span>
|
||||
</a>
|
||||
</div>
|
||||
<img class="main" src="https://xn--931a.moe/assets/not-found.jpg">
|
||||
<h2>${i18n.ts.notFound}</h2>
|
||||
<p>${i18n.ts.notFoundDescription}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
author: string;
|
||||
desc?: string;
|
||||
base?: 'dark' | 'light';
|
||||
props: Record<string, string>;
|
||||
};
|
||||
|
||||
import lightTheme from '@/themes/_light.json5';
|
||||
import darkTheme from '@/themes/_dark.json5';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
|
||||
export const getBuiltinThemes = () => Promise.all(
|
||||
[
|
||||
'l-light',
|
||||
'l-coffee',
|
||||
'l-apricot',
|
||||
'l-rainy',
|
||||
'l-botanical',
|
||||
'l-vivid',
|
||||
'l-cherry',
|
||||
'l-sushi',
|
||||
'l-u0',
|
||||
|
||||
'd-dark',
|
||||
'd-persimmon',
|
||||
'd-astro',
|
||||
'd-future',
|
||||
'd-botanical',
|
||||
'd-green-lime',
|
||||
'd-green-orange',
|
||||
'd-cherry',
|
||||
'd-ice',
|
||||
'd-u0',
|
||||
].map(name => import(`../../themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
|
||||
let timeout = null;
|
||||
|
||||
export function applyTheme(theme: Theme, persist = true) {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
|
||||
document.documentElement.classList.add('_themeChanging_');
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('_themeChanging_');
|
||||
}, 1000);
|
||||
|
||||
const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
|
||||
|
||||
// Deep copy
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme.base) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
|
||||
}
|
||||
|
||||
const props = compile(_theme);
|
||||
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', props['htmlThemeColor']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(props)) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty('color-schema', colorSchema);
|
||||
|
||||
if (persist) {
|
||||
miLocalStorage.setItem('theme', JSON.stringify(props));
|
||||
miLocalStorage.setItem('colorSchema', colorSchema);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function compile(theme: Theme): Record<string, string> {
|
||||
function getColor(val: string): tinycolor.Instance {
|
||||
// ref (prop)
|
||||
if (val[0] === '@') {
|
||||
return getColor(theme.props[val.substr(1)]);
|
||||
}
|
||||
|
||||
// ref (const)
|
||||
else if (val[0] === '$') {
|
||||
return getColor(theme.props[val]);
|
||||
}
|
||||
|
||||
// func
|
||||
else if (val[0] === ':') {
|
||||
const parts = val.split('<');
|
||||
const func = parts.shift().substr(1);
|
||||
const arg = parseFloat(parts.shift());
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
}
|
||||
|
||||
const props = {};
|
||||
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function genValue(c: tinycolor.Instance): string {
|
||||
return c.toRgbString();
|
||||
}
|
||||
|
||||
export function validateTheme(theme: Record<string, any>): boolean {
|
||||
if (theme.id == null || typeof theme.id !== 'string') return false;
|
||||
if (theme.name == null || typeof theme.name !== 'string') return false;
|
||||
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
|
||||
if (theme.props == null || typeof theme.props !== 'object') return false;
|
||||
return true;
|
||||
}
|
Loading…
Reference in New Issue