Merge pull request 'MFM Play Animation Button' (#10100) from Freeplay/calckey:mfm-warn into develop
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/10100
This commit is contained in:
commit
8470fb4a85
|
@ -1193,6 +1193,10 @@ _nsfw:
|
||||||
ignore: "Don't hide NSFW media"
|
ignore: "Don't hide NSFW media"
|
||||||
force: "Hide all media"
|
force: "Hide all media"
|
||||||
_mfm:
|
_mfm:
|
||||||
|
play: "Play MFM"
|
||||||
|
stop: "Stop MFM"
|
||||||
|
warn: "MFM may contain rapidly moving or flashy animations"
|
||||||
|
alwaysPlay: "Always autoplay all animated MFM"
|
||||||
cheatSheet: "MFM Cheatsheet"
|
cheatSheet: "MFM Cheatsheet"
|
||||||
intro: "MFM is a markup language used on Misskey, Calckey, Akkoma, and more that\
|
intro: "MFM is a markup language used on Misskey, Calckey, Akkoma, and more that\
|
||||||
\ can be used in many places. Here you can view a list of all available MFM syntax."
|
\ can be used in many places. Here you can view a list of all available MFM syntax."
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<div class="wrmlmaau">
|
<div class="wrmlmaau">
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="content"
|
||||||
:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
|
:class="{ collapsed, isLong, showContent: note.cw && !showContent, disableAnim: disableMfm }"
|
||||||
>
|
>
|
||||||
<XCwButton
|
<XCwButton
|
||||||
ref="cwButton"
|
ref="cwButton"
|
||||||
|
@ -121,6 +121,17 @@
|
||||||
></XShowMoreButton>
|
></XShowMoreButton>
|
||||||
<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
|
<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
|
||||||
</div>
|
</div>
|
||||||
|
<MkButton
|
||||||
|
v-if="hasMfm && defaultStore.state.animatedMfm"
|
||||||
|
@click.stop="toggleMfm"
|
||||||
|
>
|
||||||
|
<template v-if="disableMfm">
|
||||||
|
<i class="ph-play ph-bold"></i> {{ i18n.ts._mfm.play }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<i class="ph-stop ph-bold"></i> {{ i18n.ts._mfm.stop }}
|
||||||
|
</template>
|
||||||
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -128,14 +139,18 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import * as misskey from "calckey-js";
|
import * as misskey from "calckey-js";
|
||||||
import * as mfm from "mfm-js";
|
import * as mfm from "mfm-js";
|
||||||
|
import * as os from "@/os";
|
||||||
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
||||||
import XMediaList from "@/components/MkMediaList.vue";
|
import XMediaList from "@/components/MkMediaList.vue";
|
||||||
import XPoll from "@/components/MkPoll.vue";
|
import XPoll from "@/components/MkPoll.vue";
|
||||||
import MkUrlPreview from "@/components/MkUrlPreview.vue";
|
import MkUrlPreview from "@/components/MkUrlPreview.vue";
|
||||||
import XShowMoreButton from "@/components/MkShowMoreButton.vue";
|
import XShowMoreButton from "@/components/MkShowMoreButton.vue";
|
||||||
import XCwButton from "@/components/MkCwButton.vue";
|
import XCwButton from "@/components/MkCwButton.vue";
|
||||||
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
|
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
|
||||||
|
import { extractMfmWithAnimation } from "@/scripts/extract-mfm";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -164,6 +179,30 @@ const urls = props.note.text
|
||||||
|
|
||||||
let showContent = $ref(false);
|
let showContent = $ref(false);
|
||||||
|
|
||||||
|
const mfms = props.note.text ? extractMfmWithAnimation(mfm.parse(props.note.text)) : null;
|
||||||
|
|
||||||
|
const hasMfm = $ref(mfms.length > 0);
|
||||||
|
|
||||||
|
let disableMfm = $ref(hasMfm && defaultStore.state.animatedMfm);
|
||||||
|
|
||||||
|
async function toggleMfm() {
|
||||||
|
if (disableMfm) {
|
||||||
|
if (!defaultStore.state.animatedMfmWarnShown) {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: "warning",
|
||||||
|
text: i18n.ts._mfm.warn,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
defaultStore.set("animatedMfmWarnShown", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
disableMfm = false;
|
||||||
|
} else {
|
||||||
|
disableMfm = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function focusFooter(ev) {
|
function focusFooter(ev) {
|
||||||
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
|
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
|
||||||
emit("focusfooter");
|
emit("focusfooter");
|
||||||
|
@ -195,6 +234,7 @@ function focusFooter(ev) {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrmlmaau {
|
.wrmlmaau {
|
||||||
.content {
|
.content {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
@ -286,6 +326,13 @@ function focusFooter(ev) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disableAnim :deep(span) {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> :deep(button) {
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -41,16 +41,26 @@
|
||||||
{{ i18n.ts.next }}</MkButton
|
{{ i18n.ts.next }}</MkButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<Transition name="fade">
|
||||||
|
<section v-if="tutorial === 0" key="1" class="_content">
|
||||||
<h2 class="_title title">
|
<h2 class="_title title">
|
||||||
<i class="ph-info ph-bold ph-lg"></i>
|
<i class="ph-info ph-bold ph-lg"></i>
|
||||||
{{ i18n.ts._tutorial.title }}
|
{{ i18n.ts._tutorial.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<Transition name="fade">
|
|
||||||
<div v-if="tutorial === 0" key="1" class="_content">
|
|
||||||
<h3>{{ i18n.ts._tutorial.step1_1 }}</h3>
|
<h3>{{ i18n.ts._tutorial.step1_1 }}</h3>
|
||||||
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
|
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
|
||||||
</div>
|
<FormSwitch v-model="autoplayMfm" class="_formBlock">
|
||||||
<div
|
{{ i18n.ts._mfm.alwaysPlay }}
|
||||||
|
<template #caption>
|
||||||
|
<i class="ph-warning ph-bold ph-lg" style="color: var(--warn)"></i>
|
||||||
|
{{ i18n.ts._mfm.warn }}
|
||||||
|
</template>
|
||||||
|
</FormSwitch>
|
||||||
|
<FormSwitch v-model="reduceAnimation" class="_formBlock">
|
||||||
|
{{ i18n.ts.reduceUiAnimation }}
|
||||||
|
</FormSwitch>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
v-else-if="tutorial === 1"
|
v-else-if="tutorial === 1"
|
||||||
key="2"
|
key="2"
|
||||||
class="_content"
|
class="_content"
|
||||||
|
@ -60,8 +70,8 @@
|
||||||
<br />
|
<br />
|
||||||
<XSettings :save-button="true" />
|
<XSettings :save-button="true" />
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</section>
|
||||||
<div
|
<section
|
||||||
v-else-if="tutorial === 2"
|
v-else-if="tutorial === 2"
|
||||||
key="3"
|
key="3"
|
||||||
class="_content"
|
class="_content"
|
||||||
|
@ -74,8 +84,8 @@
|
||||||
><i class="ph-check ph-bold ph-lg"></i>
|
><i class="ph-check ph-bold ph-lg"></i>
|
||||||
{{ i18n.ts.next }}</MkButton
|
{{ i18n.ts.next }}</MkButton
|
||||||
>
|
>
|
||||||
</div>
|
</section>
|
||||||
<div
|
<section
|
||||||
v-else-if="tutorial === 3"
|
v-else-if="tutorial === 3"
|
||||||
key="4"
|
key="4"
|
||||||
class="_content"
|
class="_content"
|
||||||
|
@ -90,8 +100,8 @@
|
||||||
</I18n>
|
</I18n>
|
||||||
<br />
|
<br />
|
||||||
<XPostForm class="post-form _block" />
|
<XPostForm class="post-form _block" />
|
||||||
</div>
|
</section>
|
||||||
<div
|
<section
|
||||||
v-else-if="tutorial === 4"
|
v-else-if="tutorial === 4"
|
||||||
key="5"
|
key="5"
|
||||||
class="_content"
|
class="_content"
|
||||||
|
@ -160,8 +170,8 @@
|
||||||
</I18n>
|
</I18n>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</section>
|
||||||
<div
|
<section
|
||||||
v-else-if="tutorial === 5"
|
v-else-if="tutorial === 5"
|
||||||
key="6"
|
key="6"
|
||||||
class="_content"
|
class="_content"
|
||||||
|
@ -187,7 +197,7 @@
|
||||||
@click="installPwa"
|
@click="installPwa"
|
||||||
>{{ i18n.ts.pwa }}</MkButton
|
>{{ i18n.ts.pwa }}</MkButton
|
||||||
>
|
>
|
||||||
</div>
|
</section>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -196,7 +206,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import { reactive, computed } from "vue";
|
||||||
import XSettings from "@/pages/settings/profile.vue";
|
import XSettings from "@/pages/settings/profile.vue";
|
||||||
import XModalWindow from "@/components/MkModalWindow.vue";
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
|
@ -204,6 +214,7 @@ import XFeaturedUsers from "@/pages/explore.users.vue";
|
||||||
import XPostForm from "@/components/MkPostForm.vue";
|
import XPostForm from "@/components/MkPostForm.vue";
|
||||||
import MkSparkle from "@/components/MkSparkle.vue";
|
import MkSparkle from "@/components/MkSparkle.vue";
|
||||||
import MkPushNotificationAllowButton from "@/components/MkPushNotificationAllowButton.vue";
|
import MkPushNotificationAllowButton from "@/components/MkPushNotificationAllowButton.vue";
|
||||||
|
import FormSwitch from "@/components/form/switch.vue";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { $i } from "@/account";
|
import { $i } from "@/account";
|
||||||
|
@ -251,6 +262,21 @@ const tutorial = computed({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const autoplayMfm = computed(
|
||||||
|
defaultStore.makeGetterSetter(
|
||||||
|
"animatedMfm",
|
||||||
|
(v) => !v,
|
||||||
|
(v) => !v
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const reduceAnimation = computed(
|
||||||
|
defaultStore.makeGetterSetter(
|
||||||
|
"animation",
|
||||||
|
(v) => !v,
|
||||||
|
(v) => !v
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
function installPwa(ev: MouseEvent) {
|
function installPwa(ev: MouseEvent) {
|
||||||
const pwaInstall = document.getElementsByTagName("pwa-install")[0];
|
const pwaInstall = document.getElementsByTagName("pwa-install")[0];
|
||||||
pwaInstall.showDialog();
|
pwaInstall.showDialog();
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="vrtktovh _formBlock">
|
<section class="vrtktovh _formBlock">
|
||||||
<div class="label"><slot name="label"></slot></div>
|
<h3 class="label"><slot name="label"></slot></h3>
|
||||||
<div class="main _formRoot">
|
<div class="main _formRoot">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup></script>
|
||||||
|
@ -29,6 +29,7 @@
|
||||||
> .label {
|
> .label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: 1.5em 0 16px 0;
|
margin: 1.5em 0 16px 0;
|
||||||
|
font-size: 1em;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -102,35 +102,22 @@ export default defineComponent({
|
||||||
switch (token.props.name) {
|
switch (token.props.name) {
|
||||||
case "tada": {
|
case "tada": {
|
||||||
const speed = validTime(token.props.args.speed) || "1s";
|
const speed = validTime(token.props.args.speed) || "1s";
|
||||||
style = `font-size: 150%;${
|
style = `font-size: 150%; animation: tada ${speed} linear infinite both;`;
|
||||||
defaultStore.state.animatedMfm
|
|
||||||
? `animation: tada ${speed} linear infinite both;`
|
|
||||||
: ""
|
|
||||||
}`;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "jelly": {
|
case "jelly": {
|
||||||
const speed = validTime(token.props.args.speed) || "1s";
|
const speed = validTime(token.props.args.speed) || "1s";
|
||||||
style =
|
style = `animation: mfm-rubberBand ${speed} linear infinite both;`;
|
||||||
defaultStore.state.animatedMfm && !reducedMotion()
|
|
||||||
? `animation: mfm-rubberBand ${speed} linear infinite both;`
|
|
||||||
: "";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "twitch": {
|
case "twitch": {
|
||||||
const speed = validTime(token.props.args.speed) || "0.5s";
|
const speed = validTime(token.props.args.speed) || "0.5s";
|
||||||
style =
|
style = `animation: mfm-twitch ${speed} ease infinite;`;
|
||||||
defaultStore.state.animatedMfm && !reducedMotion()
|
|
||||||
? `animation: mfm-twitch ${speed} ease infinite;`
|
|
||||||
: "";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "shake": {
|
case "shake": {
|
||||||
const speed = validTime(token.props.args.speed) || "0.5s";
|
const speed = validTime(token.props.args.speed) || "0.5s";
|
||||||
style =
|
style = `animation: mfm-shake ${speed} ease infinite;`;
|
||||||
defaultStore.state.animatedMfm && !reducedMotion()
|
|
||||||
? `animation: mfm-shake ${speed} ease infinite;`
|
|
||||||
: "";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "spin": {
|
case "spin": {
|
||||||
|
@ -145,38 +132,26 @@ export default defineComponent({
|
||||||
? "mfm-spinY"
|
? "mfm-spinY"
|
||||||
: "mfm-spin";
|
: "mfm-spin";
|
||||||
const speed = validTime(token.props.args.speed) || "1.5s";
|
const speed = validTime(token.props.args.speed) || "1.5s";
|
||||||
style =
|
style = `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};`;
|
||||||
defaultStore.state.animatedMfm && !reducedMotion()
|
|
||||||
? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};`
|
|
||||||
: "";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "jump": {
|
case "jump": {
|
||||||
const speed = validTime(token.props.args.speed) || "0.75s";
|
const speed = validTime(token.props.args.speed) || "0.75s";
|
||||||
style =
|
style = `animation: mfm-jump ${speed} linear infinite;`;
|
||||||
defaultStore.state.animatedMfm && !reducedMotion()
|
|
||||||
? `animation: mfm-jump ${speed} linear infinite;`
|
|
||||||
: "";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "bounce": {
|
case "bounce": {
|
||||||
const speed = validTime(token.props.args.speed) || "0.75s";
|
const speed = validTime(token.props.args.speed) || "0.75s";
|
||||||
style =
|
style = `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;`;
|
||||||
defaultStore.state.animatedMfm && !reducedMotion()
|
|
||||||
? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;`
|
|
||||||
: "";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "rainbow": {
|
case "rainbow": {
|
||||||
const speed = validTime(token.props.args.speed) || "1s";
|
const speed = validTime(token.props.args.speed) || "1s";
|
||||||
style =
|
style = `animation: mfm-rainbow ${speed} linear infinite;`;
|
||||||
defaultStore.state.animatedMfm && !reducedMotion()
|
|
||||||
? `animation: mfm-rainbow ${speed} linear infinite;`
|
|
||||||
: "";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sparkle": {
|
case "sparkle": {
|
||||||
if (!(defaultStore.state.animatedMfm || reducedMotion())) {
|
if (reducedMotion()) {
|
||||||
return genEl(token.children);
|
return genEl(token.children);
|
||||||
}
|
}
|
||||||
return h(MkSparkle, {}, genEl(token.children));
|
return h(MkSparkle, {}, genEl(token.children));
|
||||||
|
|
|
@ -92,9 +92,13 @@
|
||||||
<FormSwitch v-model="showAds" class="_formBlock">{{
|
<FormSwitch v-model="showAds" class="_formBlock">{{
|
||||||
i18n.ts.showAds
|
i18n.ts.showAds
|
||||||
}}</FormSwitch>
|
}}</FormSwitch>
|
||||||
<FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{
|
<FormSwitch v-model="autoplayMfm" class="_formBlock">
|
||||||
i18n.ts.disableAnimatedMfm
|
{{ i18n.ts._mfm.alwaysPlay }}
|
||||||
}}</FormSwitch>
|
<template #caption>
|
||||||
|
<i class="ph-warning ph-bold ph-lg" style="color: var(--warn)"></i>
|
||||||
|
{{ i18n.ts._mfm.warn }}
|
||||||
|
</template>
|
||||||
|
</FormSwitch>
|
||||||
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{
|
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{
|
||||||
i18n.ts.reduceUiAnimation
|
i18n.ts.reduceUiAnimation
|
||||||
}}</FormSwitch>
|
}}</FormSwitch>
|
||||||
|
@ -261,7 +265,7 @@ const showGapBetweenNotesInTimeline = computed(
|
||||||
defaultStore.makeGetterSetter("showGapBetweenNotesInTimeline")
|
defaultStore.makeGetterSetter("showGapBetweenNotesInTimeline")
|
||||||
);
|
);
|
||||||
const showAds = computed(defaultStore.makeGetterSetter("showAds"));
|
const showAds = computed(defaultStore.makeGetterSetter("showAds"));
|
||||||
const disableAnimatedMfm = computed(
|
const autoplayMfm = computed(
|
||||||
defaultStore.makeGetterSetter(
|
defaultStore.makeGetterSetter(
|
||||||
"animatedMfm",
|
"animatedMfm",
|
||||||
(v) => !v,
|
(v) => !v,
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import * as mfm from "mfm-js";
|
||||||
|
|
||||||
|
const animatedMfm = ["tada", "jelly", "twitch", "shake", "spin", "jump", "bounce", "rainbow"];
|
||||||
|
|
||||||
|
export function extractMfmWithAnimation(
|
||||||
|
nodes: mfm.MfmNode[],
|
||||||
|
): string[] {
|
||||||
|
const mfmNodes = mfm.extract(nodes, (node) => {
|
||||||
|
return (
|
||||||
|
node.type === "fn" && animatedMfm.indexOf(node.props.name) > -1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const mfms = mfmNodes.map((x) => x.props.fn);
|
||||||
|
|
||||||
|
return mfms;
|
||||||
|
}
|
|
@ -158,6 +158,10 @@ export const defaultStore = markRaw(
|
||||||
where: "device",
|
where: "device",
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
animatedMfmWarnShown: {
|
||||||
|
where: "device",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
loadRawImages: {
|
loadRawImages: {
|
||||||
where: "device",
|
where: "device",
|
||||||
default: false,
|
default: false,
|
||||||
|
|
Loading…
Reference in New Issue