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:
Kainoa Kanter 2023-05-13 03:02:38 +00:00
commit 8470fb4a85
8 changed files with 136 additions and 59 deletions

View File

@ -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."

View File

@ -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>

View File

@ -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();

View File

@ -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;

View File

@ -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));

View File

@ -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,

View File

@ -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;
}

View File

@ -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,