summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-06-05 19:47:08 +0900
committerGitHub <noreply@github.com>2023-06-05 19:47:08 +0900
commit407a965c1d78db9b13ec89a7be910b3c120aafcf (patch)
tree33e00f7a00c4e33b2c95a6e2aba85cea7b9f05f6 /packages/frontend/src/components
parentMerge pull request #10833 from misskey-dev/develop (diff)
parentMerge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff)
downloadmisskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.tar.gz
misskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.tar.bz2
misskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.zip
Merge pull request #10932 from misskey-dev/develop
Release: 13.13.0
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkAbuseReportWindow.vue10
-rw-r--r--packages/frontend/src/components/MkAccountMoved.vue4
-rw-r--r--packages/frontend/src/components/MkAchievements.vue9
-rw-r--r--packages/frontend/src/components/MkAnalogClock.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkAnalogClock.vue34
-rw-r--r--packages/frontend/src/components/MkAnimBg.vue243
-rw-r--r--packages/frontend/src/components/MkAsUi.vue12
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue16
-rw-r--r--packages/frontend/src/components/MkAvatars.vue2
-rw-r--r--packages/frontend/src/components/MkButton.vue19
-rw-r--r--packages/frontend/src/components/MkChannelFollowButton.vue22
-rw-r--r--packages/frontend/src/components/MkChannelList.vue3
-rw-r--r--packages/frontend/src/components/MkChart.vue34
-rw-r--r--packages/frontend/src/components/MkChartTooltip.vue2
-rw-r--r--packages/frontend/src/components/MkClickerGame.vue6
-rw-r--r--packages/frontend/src/components/MkContainer.vue36
-rw-r--r--packages/frontend/src/components/MkContextMenu.vue8
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue2
-rw-r--r--packages/frontend/src/components/MkDateSeparatedList.vue2
-rw-r--r--packages/frontend/src/components/MkDialog.vue12
-rw-r--r--packages/frontend/src/components/MkDigitalClock.stories.impl.ts32
-rw-r--r--packages/frontend/src/components/MkDigitalClock.vue21
-rw-r--r--packages/frontend/src/components/MkDrive.file.vue178
-rw-r--r--packages/frontend/src/components/MkDrive.folder.vue84
-rw-r--r--packages/frontend/src/components/MkDrive.navFolder.vue18
-rw-r--r--packages/frontend/src/components/MkDrive.vue286
-rw-r--r--packages/frontend/src/components/MkDriveFileThumbnail.vue52
-rw-r--r--packages/frontend/src/components/MkDriveSelectDialog.vue6
-rw-r--r--packages/frontend/src/components/MkDriveWindow.vue8
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue51
-rw-r--r--packages/frontend/src/components/MkEmojiPickerDialog.vue30
-rw-r--r--packages/frontend/src/components/MkEmojiPickerWindow.vue11
-rw-r--r--packages/frontend/src/components/MkFileCaptionEditWindow.vue6
-rw-r--r--packages/frontend/src/components/MkFoldableSection.vue195
-rw-r--r--packages/frontend/src/components/MkFolder.vue22
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue33
-rw-r--r--packages/frontend/src/components/MkForgotPassword.vue55
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue90
-rw-r--r--packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts4
-rw-r--r--packages/frontend/src/components/MkGalleryPostPreview.vue17
-rw-r--r--packages/frontend/src/components/MkImageViewer.vue78
-rw-r--r--packages/frontend/src/components/MkImgWithBlurhash.vue204
-rw-r--r--packages/frontend/src/components/MkKeyValue.vue26
-rw-r--r--packages/frontend/src/components/MkLaunchPad.vue2
-rw-r--r--packages/frontend/src/components/MkMediaBanner.vue95
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue99
-rw-r--r--packages/frontend/src/components/MkMediaList.vue76
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue111
-rw-r--r--packages/frontend/src/components/MkMention.vue2
-rw-r--r--packages/frontend/src/components/MkMenu.child.vue2
-rw-r--r--packages/frontend/src/components/MkMenu.vue4
-rw-r--r--packages/frontend/src/components/MkModal.vue62
-rw-r--r--packages/frontend/src/components/MkModalPageWindow.vue182
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue2
-rw-r--r--packages/frontend/src/components/MkNote.vue25
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue616
-rw-r--r--packages/frontend/src/components/MkNotePreview.vue2
-rw-r--r--packages/frontend/src/components/MkNoteSimple.vue2
-rw-r--r--packages/frontend/src/components/MkNotes.vue2
-rw-r--r--packages/frontend/src/components/MkNotification.vue23
-rw-r--r--packages/frontend/src/components/MkNotificationSettingWindow.vue6
-rw-r--r--packages/frontend/src/components/MkNotifications.vue10
-rw-r--r--packages/frontend/src/components/MkObjectView.value.vue64
-rw-r--r--packages/frontend/src/components/MkObjectView.vue8
-rw-r--r--packages/frontend/src/components/MkOmit.vue24
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue16
-rw-r--r--packages/frontend/src/components/MkPagination.vue8
-rw-r--r--packages/frontend/src/components/MkPoll.vue114
-rw-r--r--packages/frontend/src/components/MkPollEditor.vue2
-rw-r--r--packages/frontend/src/components/MkPopupMenu.vue4
-rw-r--r--packages/frontend/src/components/MkPostForm.vue14
-rw-r--r--packages/frontend/src/components/MkPostFormAttaches.vue105
-rw-r--r--packages/frontend/src/components/MkPostFormDialog.vue4
-rw-r--r--packages/frontend/src/components/MkPushNotificationAllowButton.vue38
-rw-r--r--packages/frontend/src/components/MkRadios.vue36
-rw-r--r--packages/frontend/src/components/MkReactedUsersDialog.vue4
-rw-r--r--packages/frontend/src/components/MkReactionIcon.vue4
-rw-r--r--packages/frontend/src/components/MkReactionTooltip.vue4
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.details.vue4
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.reaction.vue19
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.vue12
-rw-r--r--packages/frontend/src/components/MkRenotedUsersDialog.vue4
-rw-r--r--packages/frontend/src/components/MkRetentionLineChart.vue4
-rw-r--r--packages/frontend/src/components/MkRippleEffect.vue16
-rw-r--r--packages/frontend/src/components/MkRolePreview.vue13
-rw-r--r--packages/frontend/src/components/MkSample.vue118
-rw-r--r--packages/frontend/src/components/MkSignin.vue34
-rw-r--r--packages/frontend/src/components/MkSigninDialog.vue4
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue10
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.vue8
-rw-r--r--packages/frontend/src/components/MkSignupDialog.vue10
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue10
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue21
-rw-r--r--packages/frontend/src/components/MkTab.vue12
-rw-r--r--packages/frontend/src/components/MkTagCloud.vue24
-rw-r--r--packages/frontend/src/components/MkTextarea.vue342
-rw-r--r--packages/frontend/src/components/MkTimeline.vue42
-rw-r--r--packages/frontend/src/components/MkToast.vue10
-rw-r--r--packages/frontend/src/components/MkTokenGenerateWindow.vue8
-rw-r--r--packages/frontend/src/components/MkTooltip.vue19
-rw-r--r--packages/frontend/src/components/MkUpdated.vue2
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue41
-rw-r--r--packages/frontend/src/components/MkUrlPreviewPopup.vue8
-rw-r--r--packages/frontend/src/components/MkUserInfo.vue4
-rw-r--r--packages/frontend/src/components/MkUserOnlineIndicator.vue10
-rw-r--r--packages/frontend/src/components/MkUserPopup.vue19
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue14
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue4
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.vue4
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue8
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.vue76
-rw-r--r--packages/frontend/src/components/MkUsersTooltip.vue14
-rw-r--r--packages/frontend/src/components/MkVisibilityPicker.vue2
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue6
-rw-r--r--packages/frontend/src/components/MkWaitingDialog.vue2
-rw-r--r--packages/frontend/src/components/MkWidgets.vue38
-rw-r--r--packages/frontend/src/components/MkWindow.vue10
-rw-r--r--packages/frontend/src/components/MkYouTubePlayer.vue2
-rw-r--r--packages/frontend/src/components/form/link.vue116
-rw-r--r--packages/frontend/src/components/form/slot.vue38
-rw-r--r--packages/frontend/src/components/form/suspense.vue132
-rw-r--r--packages/frontend/src/components/global/MkA.vue12
-rw-r--r--packages/frontend/src/components/global/MkAcct.vue2
-rw-r--r--packages/frontend/src/components/global/MkAd.vue12
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue3
-rw-r--r--packages/frontend/src/components/global/MkCondensedLine.vue9
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue4
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts367
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue171
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue4
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue4
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue9
-rw-r--r--packages/frontend/src/components/global/MkTime.vue1
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/components/global/MkUserName.vue2
-rw-r--r--packages/frontend/src/components/global/i18n.ts58
-rw-r--r--packages/frontend/src/components/index.ts2
-rw-r--r--packages/frontend/src/components/mfm.ts390
-rw-r--r--packages/frontend/src/components/page/block.type.ts29
-rw-r--r--packages/frontend/src/components/page/page.block.vue55
-rw-r--r--packages/frontend/src/components/page/page.button.vue66
-rw-r--r--packages/frontend/src/components/page/page.canvas.vue48
-rw-r--r--packages/frontend/src/components/page/page.counter.vue51
-rw-r--r--packages/frontend/src/components/page/page.if.vue31
-rw-r--r--packages/frontend/src/components/page/page.image.vue12
-rw-r--r--packages/frontend/src/components/page/page.note.vue48
-rw-r--r--packages/frontend/src/components/page/page.number-input.vue54
-rw-r--r--packages/frontend/src/components/page/page.post.vue111
-rw-r--r--packages/frontend/src/components/page/page.radio-button.vue44
-rw-r--r--packages/frontend/src/components/page/page.section.vue82
-rw-r--r--packages/frontend/src/components/page/page.switch.vue54
-rw-r--r--packages/frontend/src/components/page/page.text-input.vue54
-rw-r--r--packages/frontend/src/components/page/page.text.vue74
-rw-r--r--packages/frontend/src/components/page/page.textarea-input.vue45
-rw-r--r--packages/frontend/src/components/page/page.textarea.vue39
-rw-r--r--packages/frontend/src/components/page/page.vue59
157 files changed, 2983 insertions, 4078 deletions
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index 9f2bf99338..48236782d9 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -1,5 +1,5 @@
<template>
-<MkWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
+<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')">
<template #header>
<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
@@ -8,8 +8,8 @@
</template>
</I18n>
</template>
- <MkSpacer :margin-min="20" :margin-max="28">
- <div class="dpvffvvy _gaps_m">
+ <MkSpacer :marginMin="20" :marginMax="28">
+ <div class="_gaps_m" :class="$style.root">
<div class="">
<MkTextarea v-model="comment">
<template #label>{{ i18n.ts.details }}</template>
@@ -60,8 +60,8 @@ function send() {
}
</script>
-<style lang="scss" scoped>
-.dpvffvvy {
+<style lang="scss" module>
+.root {
--root-margin: 16px;
}
</style>
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index b02bfdc2b8..bc07b9ba5f 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -7,11 +7,11 @@
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
+import { UserLite } from 'misskey-js/built/entities';
import MkMention from './MkMention.vue';
import { i18n } from '@/i18n';
import { host as localHost } from '@/config';
-import { ref } from 'vue';
-import { UserLite } from 'misskey-js/built/entities';
import { api } from '@/os';
const user = ref<UserLite>();
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index d30037dcf9..3fdb261dac 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -3,7 +3,14 @@
<div v-if="achievements" :class="$style.root">
<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
<div :class="$style.icon">
- <div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
+ <div
+ :class="[$style.iconFrame, {
+ [$style.iconFrame_bronze]: ACHIEVEMENT_BADGES[achievement.name].frame === 'bronze',
+ [$style.iconFrame_silver]: ACHIEVEMENT_BADGES[achievement.name].frame === 'silver',
+ [$style.iconFrame_gold]: ACHIEVEMENT_BADGES[achievement.name].frame === 'gold',
+ [$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum',
+ }]"
+ >
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div>
diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
index e7fbb47284..0aebdccf4f 100644
--- a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
+++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
+import isChromatic from 'chromatic/isChromatic';
import MkAnalogClock from './MkAnalogClock.vue';
-import isChromatic from 'chromatic';
export const Default = {
render(args) {
return {
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index f12020f810..05caffe7d0 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -39,6 +39,7 @@
-->
<line
+ ref="sLine"
:class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]"
:x1="5 - (0 * (sHandLengthRatio * handsTailLength))"
:y1="5 + (1 * (sHandLengthRatio * handsTailLength))"
@@ -73,9 +74,10 @@
</template>
<script lang="ts" setup>
-import { computed, onMounted, onBeforeUnmount } from 'vue';
+import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import tinycolor from 'tinycolor2';
import { globalEvents } from '@/events.js';
+import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
const angleDiff = (a: number, b: number) => {
@@ -145,6 +147,7 @@ let mAngle = $ref<number>(0);
let sAngle = $ref<number>(0);
let disableSAnimate = $ref(false);
let sOneRound = false;
+const sLine = ref<SVGPathElement>();
function tick() {
const now = props.now();
@@ -160,17 +163,21 @@ function tick() {
}
hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6);
mAngle = Math.PI * (m + s / 60) / 30;
- if (sOneRound) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
+ if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
sAngle = Math.PI * 60 / 30;
- window.setTimeout(() => {
+ defaultIdlingRenderScheduler.delete(tick);
+ sLine.value.addEventListener('transitionend', () => {
disableSAnimate = true;
- window.setTimeout(() => {
+ requestAnimationFrame(() => {
sAngle = 0;
- window.setTimeout(() => {
+ requestAnimationFrame(() => {
disableSAnimate = false;
- }, 100);
- }, 100);
- }, 700);
+ if (enabled) {
+ defaultIdlingRenderScheduler.add(tick);
+ }
+ });
+ });
+ }, { once: true });
} else {
sAngle = Math.PI * s / 30;
}
@@ -194,20 +201,13 @@ function calcColors() {
calcColors();
onMounted(() => {
- const update = () => {
- if (enabled) {
- tick();
- window.setTimeout(update, 1000);
- }
- };
- update();
-
+ defaultIdlingRenderScheduler.add(tick);
globalEvents.on('themeChanged', calcColors);
});
onBeforeUnmount(() => {
enabled = false;
-
+ defaultIdlingRenderScheduler.delete(tick);
globalEvents.off('themeChanged', calcColors);
});
</script>
diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue
new file mode 100644
index 0000000000..575ea7c5e3
--- /dev/null
+++ b/packages/frontend/src/components/MkAnimBg.vue
@@ -0,0 +1,243 @@
+<template>
+<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, shallowRef } from 'vue';
+import isChromatic from 'chromatic/isChromatic';
+
+const canvasEl = shallowRef<HTMLCanvasElement>();
+
+const props = withDefaults(defineProps<{
+ scale?: number;
+ focus?: number;
+}>(), {
+ scale: 1.0,
+ focus: 1.0,
+});
+
+function loadShader(gl, type, source) {
+ const shader = gl.createShader(type);
+
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ alert(
+ `falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
+ );
+ gl.deleteShader(shader);
+ return null;
+ }
+
+ return shader;
+}
+
+function initShaderProgram(gl, vsSource, fsSource) {
+ const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
+ const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
+
+ const shaderProgram = gl.createProgram();
+ gl.attachShader(shaderProgram, vertexShader);
+ gl.attachShader(shaderProgram, fragmentShader);
+ gl.linkProgram(shaderProgram);
+
+ if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
+ alert(
+ `failed to init shader: ${gl.getProgramInfoLog(
+ shaderProgram,
+ )}`,
+ );
+ return null;
+ }
+
+ return shaderProgram;
+}
+
+let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
+
+onMounted(() => {
+ const canvas = canvasEl.value!;
+ canvas.width = canvas.offsetWidth;
+ canvas.height = canvas.offsetHeight;
+
+ const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
+ if (gl == null) return;
+
+ gl.clearColor(0.0, 0.0, 0.0, 0.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ const positionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+
+ const shaderProgram = initShaderProgram(gl, `
+ attribute vec2 vertex;
+
+ uniform vec2 u_scale;
+
+ varying vec2 v_pos;
+
+ void main() {
+ gl_Position = vec4(vertex, 0.0, 1.0);
+ v_pos = vertex / u_scale;
+ }
+ `, `
+ precision mediump float;
+
+ vec3 mod289(vec3 x) {
+ return x - floor(x * (1.0 / 289.0)) * 289.0;
+ }
+
+ vec2 mod289(vec2 x) {
+ return x - floor(x * (1.0 / 289.0)) * 289.0;
+ }
+
+ vec3 permute(vec3 x) {
+ return mod289(((x*34.0)+1.0)*x);
+ }
+
+ float snoise(vec2 v) {
+ const vec4 C = vec4(0.211324865405187,
+ 0.366025403784439,
+ -0.577350269189626,
+ 0.024390243902439);
+
+ vec2 i = floor(v + dot(v, C.yy) );
+ vec2 x0 = v - i + dot(i, C.xx);
+
+ vec2 i1;
+ i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
+ vec4 x12 = x0.xyxy + C.xxzz;
+ x12.xy -= i1;
+
+ i = mod289(i);
+ vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ + i.x + vec3(0.0, i1.x, 1.0 ));
+
+ vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
+ m = m*m ;
+ m = m*m ;
+
+ vec3 x = 2.0 * fract(p * C.www) - 1.0;
+ vec3 h = abs(x) - 0.5;
+ vec3 ox = floor(x + 0.5);
+ vec3 a0 = x - ox;
+
+ m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
+
+ vec3 g;
+ g.x = a0.x * x0.x + h.x * x0.y;
+ g.yz = a0.yz * x12.xz + h.yz * x12.yw;
+ return 130.0 * dot(m, g);
+ }
+
+ uniform float u_time;
+ uniform vec2 u_resolution;
+ uniform float u_spread;
+ uniform float u_speed;
+ uniform float u_warp;
+ uniform float u_focus;
+ uniform float u_itensity;
+
+ varying vec2 v_pos;
+
+ float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
+ float SPREAD = 0.7 * u_spread;
+ float SPEED = 0.00055 * u_speed;
+ float WARP = 1.5 * u_warp;
+ float FOCUS = 1.15 * u_focus;
+
+ vec2 dist = _pos - _origin;
+
+ float distortion = snoise( vec2(
+ _pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
+ _pos.y * 1.192 * WARP + u_time * SPEED * 0.3
+ ) ) * 0.5 + 0.5;
+
+ float feather = 0.01 + SPREAD * pow( distortion, FOCUS );
+
+ return 1.0 - smoothstep(
+ _radius - ( _radius * feather ),
+ _radius + ( _radius * feather ),
+ dot( dist, dist ) * 4.0
+ );
+ }
+
+ void main() {
+ vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 );
+ vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 );
+ vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 );
+
+ float ratio = u_resolution.x / u_resolution.y;
+
+ vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
+
+ vec3 color = vec3( 0.0 );
+
+ float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
+ float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
+ float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
+
+ float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
+ float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
+ float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 );
+
+ color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix );
+ color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
+ color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
+
+ color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
+
+ vec3 inverted = vec3( 1.0 ) - color;
+ gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
+ }
+ `);
+
+ gl.useProgram(shaderProgram);
+ const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
+ const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
+ const u_spread = gl.getUniformLocation(shaderProgram, 'u_spread');
+ const u_speed = gl.getUniformLocation(shaderProgram, 'u_speed');
+ const u_warp = gl.getUniformLocation(shaderProgram, 'u_warp');
+ const u_focus = gl.getUniformLocation(shaderProgram, 'u_focus');
+ const u_itensity = gl.getUniformLocation(shaderProgram, 'u_itensity');
+ const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
+ gl.uniform2fv(u_resolution, [canvas.width, canvas.height]);
+ gl.uniform1f(u_spread, 1.0);
+ gl.uniform1f(u_speed, 1.0);
+ gl.uniform1f(u_warp, 1.0);
+ gl.uniform1f(u_focus, props.focus);
+ gl.uniform1f(u_itensity, 0.5);
+ gl.uniform2fv(u_scale, [props.scale, props.scale]);
+
+ const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
+ gl.enableVertexAttribArray(vertex);
+ gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
+
+ const vertices = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
+
+ if (isChromatic()) {
+ gl!.uniform1f(u_time, 0);
+ gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
+ } else {
+ function render(timeStamp) {
+ gl!.uniform1f(u_time, timeStamp);
+ gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
+
+ handle = window.requestAnimationFrame(render);
+ }
+
+ handle = window.requestAnimationFrame(render);
+ }
+});
+
+onUnmounted(() => {
+ if (handle) {
+ window.cancelAnimationFrame(handle);
+ }
+});
+</script>
+
+<style lang="scss" module>
+</style>
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 6ade5316c6..8bfcfa6aa6 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -11,29 +11,29 @@
<div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
</div>
- <MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
+ <MkSwitch v-else-if="c.type === 'switch'" :modelValue="valueForSwitch" @update:modelValue="onSwitchUpdate">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkSwitch>
- <MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput">
+ <MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default" @update:modelValue="c.onInput">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkTextarea>
- <MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput">
+ <MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onInput">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
- <MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput">
+ <MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default" type="number" @update:modelValue="c.onInput">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
- <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange">
+ <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onChange">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
</MkSelect>
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
- <MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
+ <MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
<template #label>{{ c.title }}</template>
<template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 663c57623d..fd892d8174 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -10,7 +10,7 @@
</li>
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
</ol>
- <ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]">
+ <ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
<span class="name">{{ hashtag }}</span>
</li>
@@ -42,7 +42,7 @@ import { acct } from '@/filters/user';
import * as os from '@/os';
import { MFM_TAGS } from '@/scripts/mfm-tags';
import { defaultStore } from '@/store';
-import { emojilist } from '@/scripts/emojilist';
+import { emojilist, getEmojiName } from '@/scripts/emojilist';
import { i18n } from '@/i18n';
import { miLocalStorage } from '@/local-storage';
import { customEmojis } from '@/custom-emojis';
@@ -71,14 +71,14 @@ const emojiDb = computed(() => {
url: char2path(x.char),
}));
- for (const x of lib) {
- if (x.keywords) {
- for (const k of x.keywords) {
+ for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+ for (const [emoji, keywords] of Object.entries(index)) {
+ for (const k of keywords) {
unicodeEmojiDB.push({
- emoji: x.char,
+ emoji: emoji,
name: k,
- aliasOf: x.name,
- url: char2path(x.char),
+ aliasOf: getEmojiName(emoji)!,
+ url: char2path(emoji),
});
}
}
diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue
index 995a72e511..630620fc08 100644
--- a/packages/frontend/src/components/MkAvatars.vue
+++ b/packages/frontend/src/components/MkAvatars.vue
@@ -1,7 +1,7 @@
<template>
<div>
<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
- <MkAvatar :user="user" style="width:32px;height:32px;" indicator link preview/>
+ <MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/>
</div>
</div>
</template>
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 0ddee34f0a..16e44ec618 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -2,23 +2,23 @@
<button
v-if="!link"
ref="el" class="_button"
- :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]"
+ :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
:type="type"
@click="emit('click', $event)"
@mousedown="onMousedown"
>
- <div ref="ripples" :class="$style.ripples"></div>
+ <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content">
<slot></slot>
</div>
</button>
<MkA
v-else class="_button"
- :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]"
+ :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
:to="to"
@mousedown="onMousedown"
>
- <div ref="ripples" :class="$style.ripples"></div>
+ <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content">
<slot></slot>
</div>
@@ -26,9 +26,7 @@
</template>
<script lang="ts" setup>
-import { nextTick, onMounted, useCssModule } from 'vue';
-
-const $style = useCssModule();
+import { nextTick, onMounted } from 'vue';
const props = defineProps<{
type?: 'button' | 'submit' | 'reset';
@@ -44,6 +42,7 @@ const props = defineProps<{
full?: boolean;
small?: boolean;
large?: boolean;
+ transparent?: boolean;
asLike?: boolean;
}>();
@@ -80,7 +79,7 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect();
const ripple = document.createElement('div');
- ripple.classList.add($style.ripple);
+ ripple.classList.add(ripples!.dataset.childrenClass!);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
@@ -194,6 +193,10 @@ function onMousedown(evt: MouseEvent): void {
}
}
+ &.transparent {
+ background: transparent;
+ }
+
&.gradate {
font-weight: bold;
color: var(--fgOnAccent) !important;
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 9e275d6172..7b7bef4787 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -1,20 +1,20 @@
<template>
<button
- class="hdcaacmi _button"
- :class="{ wait, active: isFollowing, full }"
+ class="_button"
+ :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full }]"
:disabled="wait"
@click="onClick"
>
<template v-if="!wait">
<template v-if="isFollowing">
- <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
</template>
<template v-else>
- <span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
</template>
</template>
<template v-else>
- <span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
</template>
</button>
</template>
@@ -57,8 +57,8 @@ async function onClick() {
}
</script>
-<style lang="scss" scoped>
-.hdcaacmi {
+<style lang="scss" module>
+.root {
position: relative;
display: inline-block;
font-weight: bold;
@@ -103,7 +103,7 @@ async function onClick() {
}
&.active {
- color: #fff;
+ color: var(--fgOnAccent);
background: var(--accent);
&:hover {
@@ -121,9 +121,9 @@ async function onClick() {
cursor: wait !important;
opacity: 0.7;
}
+}
- > span {
- margin-right: 6px;
- }
+.text {
+ margin-right: 6px;
}
</style>
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index 408eab7399..4050520eb9 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -26,6 +26,3 @@ const props = withDefaults(defineProps<{
extractor: (item) => item,
});
</script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index 06d5b9949a..00ff98774b 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -1,8 +1,8 @@
<template>
-<div class="cbbedffa">
+<div :class="$style.root">
<canvas ref="chartEl"></canvas>
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
- <div v-if="fetching" class="fetching">
+ <div v-if="fetching" :class="$style.fetching">
<MkLoading/>
</div>
</div>
@@ -817,22 +817,22 @@ onMounted(() => {
/* eslint-enable id-denylist */
</script>
-<style lang="scss" scoped>
-.cbbedffa {
+<style lang="scss" module>
+.root {
position: relative;
+}
- > .fetching {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- -webkit-backdrop-filter: var(--blur, blur(12px));
- backdrop-filter: var(--blur, blur(12px));
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: wait;
- }
+.fetching {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ -webkit-backdrop-filter: var(--blur, blur(12px));
+ backdrop-filter: var(--blur, blur(12px));
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: wait;
}
</style>
diff --git a/packages/frontend/src/components/MkChartTooltip.vue b/packages/frontend/src/components/MkChartTooltip.vue
index 7cfe535edd..fe5b78754d 100644
--- a/packages/frontend/src/components/MkChartTooltip.vue
+++ b/packages/frontend/src/components/MkChartTooltip.vue
@@ -1,5 +1,5 @@
<template>
-<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')">
+<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :maxWidth="340" :direction="'top'" :innerMargin="16" @closed="emit('closed')">
<div v-if="title || series">
<div v-if="title" :class="$style.title">{{ title }}</div>
<template v-if="series">
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index da6439fd2c..a6ab5aded4 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -3,7 +3,7 @@
<div v-if="game.ready" :class="$style.game">
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
- <button v-click-anime class="_button" :class="$style.button" @click="onClick">
+ <button v-click-anime class="_button" @click="onClick">
<img src="/client-assets/cookie.png" :class="$style.img">
</button>
</div>
@@ -84,10 +84,6 @@ onUnmounted(() => {
margin-bottom: 6px;
}
-.button {
-
-}
-
.img {
max-width: 90px;
}
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index d03331a6eb..af1c57b349 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -1,12 +1,12 @@
<template>
-<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]">
+<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.scrollable]: scrollable }]">
<header v-if="showHeader" ref="headerEl" :class="$style.header">
<div :class="$style.title">
<span :class="$style.titleIcon"><slot name="icon"></slot></span>
<slot name="header"></slot>
</div>
<div :class="$style.headerSub">
- <slot name="func" :button-style-class="$style.headerButton"></slot>
+ <slot name="func" :buttonStyleClass="$style.headerButton"></slot>
<button v-if="foldable" :class="$style.headerButton" class="_button" @click="() => showBody = !showBody">
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
<template v-else><i class="ti ti-chevron-down"></i></template>
@@ -14,14 +14,14 @@
</div>
</header>
<Transition
- :enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
@enter="enter"
- @after-enter="afterEnter"
+ @afterEnter="afterEnter"
@leave="leave"
- @after-leave="afterLeave"
+ @afterLeave="afterLeave"
>
<div v-show="showBody" ref="contentEl" :class="[$style.content, { [$style.omitted]: omitted }]">
<slot></slot>
@@ -34,7 +34,7 @@
</template>
<script lang="ts" setup>
-import { onMounted, ref, shallowRef, watch } from 'vue';
+import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -83,13 +83,19 @@ function afterLeave(el) {
const calcOmit = () => {
if (omitted.value || ignoreOmit.value || props.maxHeight == null) return;
+ if (!contentEl.value) return;
const height = contentEl.value.offsetHeight;
omitted.value = height > props.maxHeight;
};
+const omitObserver = new ResizeObserver((entries, observer) => {
+ calcOmit();
+});
+
onMounted(() => {
watch(showBody, v => {
- const headerHeight = props.showHeader ? headerEl.value.offsetHeight : 0;
+ if (!rootEl.value) return;
+ const headerHeight = props.showHeader ? headerEl.value?.offsetHeight ?? 0 : 0;
rootEl.value.style.minHeight = `${headerHeight}px`;
if (v) {
rootEl.value.style.flexBasis = 'auto';
@@ -100,13 +106,15 @@ onMounted(() => {
immediate: true,
});
- rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
+ if (rootEl.value) rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
calcOmit();
- new ResizeObserver((entries, observer) => {
- calcOmit();
- }).observe(contentEl.value);
+ if (contentEl.value) omitObserver.observe(contentEl.value);
+});
+
+onUnmounted(() => {
+ omitObserver.disconnect();
});
</script>
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index b81c806b0c..fb11834f4d 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -1,10 +1,10 @@
<template>
<Transition
appear
- :enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
>
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 043a614e46..82363499b7 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -4,7 +4,7 @@
:width="800"
:height="500"
:scroll="false"
- :with-ok-button="true"
+ :withOkButton="true"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index d6303f9675..6942a0e6c3 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -36,7 +36,7 @@ export default defineComponent({
},
setup(props, { slots, expose }) {
- const $style = useCssModule();
+ const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 9f5404ce15..4d5df0bba4 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -1,10 +1,18 @@
<template>
-<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
+<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
<div :class="$style.root">
<div v-if="icon" :class="$style.icon">
<i :class="icon"></i>
</div>
- <div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
+ <div
+ v-else-if="!input && !select"
+ :class="[$style.icon, {
+ [$style.type_success]: type === 'success',
+ [$style.type_error]: type === 'error',
+ [$style.type_warning]: type === 'warning',
+ [$style.type_info]: type === 'info',
+ }]"
+ >
<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>
diff --git a/packages/frontend/src/components/MkDigitalClock.stories.impl.ts b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts
new file mode 100644
index 0000000000..344f6de47c
--- /dev/null
+++ b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts
@@ -0,0 +1,32 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import isChromatic from 'chromatic/isChromatic';
+import MkDigitalClock from './MkDigitalClock.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkDigitalClock,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkDigitalClock v-bind="props" />',
+ };
+ },
+ args: {
+ now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined,
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkDigitalClock>;
diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue
index 278dc8a5e7..aea20f2489 100644
--- a/packages/frontend/src/components/MkDigitalClock.vue
+++ b/packages/frontend/src/components/MkDigitalClock.vue
@@ -11,19 +11,21 @@
</template>
<script lang="ts" setup>
-import { onUnmounted, ref, watch } from 'vue';
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
const props = withDefaults(defineProps<{
showS?: boolean;
showMs?: boolean;
offset?: number;
+ now?: () => Date;
}>(), {
showS: true,
showMs: false,
offset: 0 - new Date().getTimezoneOffset(),
+ now: () => new Date(),
});
-let intervalId;
const hh = ref('');
const mm = ref('');
const ss = ref('');
@@ -39,9 +41,9 @@ watch(showColon, (v) => {
}
});
-const tick = () => {
- const now = new Date();
- now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset));
+const tick = (): void => {
+ const now = props.now();
+ now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset);
hh.value = now.getHours().toString().padStart(2, '0');
mm.value = now.getMinutes().toString().padStart(2, '0');
ss.value = now.getSeconds().toString().padStart(2, '0');
@@ -52,13 +54,12 @@ const tick = () => {
tick();
-watch(() => props.showMs, () => {
- if (intervalId) window.clearInterval(intervalId);
- intervalId = window.setInterval(tick, props.showMs ? 10 : 1000);
-}, { immediate: true });
+onMounted(() => {
+ defaultIdlingRenderScheduler.add(tick);
+});
onUnmounted(() => {
- window.clearInterval(intervalId);
+ defaultIdlingRenderScheduler.delete(tick);
});
</script>
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index ab408b5008..f0641161be 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -1,7 +1,6 @@
<template>
<div
- class="ncvczrfv"
- :class="{ isSelected }"
+ :class="[$style.root, { [$style.isSelected]: isSelected }]"
draggable="true"
:title="title"
@click="onClick"
@@ -9,25 +8,27 @@
@dragstart="onDragstart"
@dragend="onDragend"
>
- <div v-if="$i?.avatarId == file.id" class="label">
- <img src="/client-assets/label.svg"/>
- <p>{{ i18n.ts.avatar }}</p>
- </div>
- <div v-if="$i?.bannerId == file.id" class="label">
- <img src="/client-assets/label.svg"/>
- <p>{{ i18n.ts.banner }}</p>
- </div>
- <div v-if="file.isSensitive" class="label red">
- <img src="/client-assets/label-red.svg"/>
- <p>{{ i18n.ts.nsfw }}</p>
- </div>
+ <div style="pointer-events: none;">
+ <div v-if="$i?.avatarId == file.id" :class="[$style.label]">
+ <img :class="$style.labelImg" src="/client-assets/label.svg"/>
+ <p :class="$style.labelText">{{ i18n.ts.avatar }}</p>
+ </div>
+ <div v-if="$i?.bannerId == file.id" :class="[$style.label]">
+ <img :class="$style.labelImg" src="/client-assets/label.svg"/>
+ <p :class="$style.labelText">{{ i18n.ts.banner }}</p>
+ </div>
+ <div v-if="file.isSensitive" :class="[$style.label, $style.red]">
+ <img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
+ <p :class="$style.labelText">{{ i18n.ts.nsfw }}</p>
+ </div>
- <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+ <MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/>
- <p class="name">
- <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
- <span v-if="file.name.lastIndexOf('.') != -1" class="ext">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
- </p>
+ <p :class="$style.name">
+ <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
+ <span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
+ </p>
+ </div>
</div>
</template>
@@ -88,20 +89,13 @@ function onDragend() {
}
</script>
-<style lang="scss" scoped>
-.ncvczrfv {
+<style lang="scss" module>
+.root {
position: relative;
padding: 8px 0 0 0;
min-height: 180px;
border-radius: 8px;
-
- &, * {
- cursor: pointer;
- }
-
- > * {
- pointer-events: none;
- }
+ cursor: pointer;
&:hover {
background: rgba(#000, 0.05);
@@ -165,82 +159,78 @@ function onDragend() {
color: #fff;
}
}
+}
+
+.label {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
- > .label {
+ &:before,
+ &:after {
+ content: "";
+ display: block;
position: absolute;
+ z-index: 1;
+ background: #0c7ac9;
+ }
+
+ &:before {
top: 0;
+ left: 57px;
+ width: 28px;
+ height: 8px;
+ }
+
+ &:after {
+ top: 57px;
left: 0;
- pointer-events: none;
+ width: 8px;
+ height: 28px;
+ }
+ &.red {
&:before,
&:after {
- content: "";
- display: block;
- position: absolute;
- z-index: 1;
- background: #0c7ac9;
- }
-
- &:before {
- top: 0;
- left: 57px;
- width: 28px;
- height: 8px;
- }
-
- &:after {
- top: 57px;
- left: 0;
- width: 8px;
- height: 28px;
- }
-
- &.red {
- &:before,
- &:after {
- background: #c12113;
- }
- }
-
- > img {
- position: absolute;
- z-index: 2;
- top: 0;
- left: 0;
- }
-
- > p {
- position: absolute;
- z-index: 3;
- top: 19px;
- left: -28px;
- width: 120px;
- margin: 0;
- text-align: center;
- line-height: 28px;
- color: #fff;
- transform: rotate(-45deg);
+ background: #c12113;
}
}
+}
- > .thumbnail {
- width: 110px;
- height: 110px;
- margin: auto;
- }
+.labelImg {
+ position: absolute;
+ z-index: 2;
+ top: 0;
+ left: 0;
+}
- > .name {
- display: block;
- margin: 4px 0 0 0;
- font-size: 0.8em;
- text-align: center;
- word-break: break-all;
- color: var(--fg);
- overflow: hidden;
+.labelText {
+ position: absolute;
+ z-index: 3;
+ top: 19px;
+ left: -28px;
+ width: 120px;
+ margin: 0;
+ text-align: center;
+ line-height: 28px;
+ color: #fff;
+ transform: rotate(-45deg);
+}
- > .ext {
- opacity: 0.5;
- }
- }
+.thumbnail {
+ width: 110px;
+ height: 110px;
+ margin: auto;
+}
+
+.name {
+ display: block;
+ margin: 4px 0 0 0;
+ font-size: 0.8em;
+ text-align: center;
+ word-break: break-all;
+ color: var(--fg);
+ overflow: hidden;
}
</style>
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 156013b9aa..1969342402 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -1,7 +1,6 @@
<template>
<div
- class="rghtznwe"
- :class="{ draghover }"
+ :class="[$style.root, { [$style.draghover]: draghover }]"
draggable="true"
:title="title"
@click="onClick"
@@ -15,15 +14,15 @@
@dragstart="onDragstart"
@dragend="onDragend"
>
- <p class="name">
- <template v-if="hover"><i class="ti ti-folder ti-fw"></i></template>
- <template v-if="!hover"><i class="ti ti-folder ti-fw"></i></template>
+ <p :class="$style.name">
+ <template v-if="hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
+ <template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
{{ folder.name }}
</p>
- <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
+ <p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
{{ i18n.ts.uploadFolder }}
</p>
- <button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
+ <button v-if="selectMode" class="_button" :class="[$style.checkbox, { [$style.checked]: isSelected }]" @click.prevent.stop="checkboxClicked"></button>
</div>
</template>
@@ -267,35 +266,14 @@ function onContextmenu(ev: MouseEvent) {
}
</script>
-<style lang="scss" scoped>
-.rghtznwe {
+<style lang="scss" module>
+.root {
position: relative;
padding: 8px;
height: 64px;
background: var(--driveFolderBg);
border-radius: 4px;
-
- &, * {
- cursor: pointer;
- }
-
- *:not(.checkbox) {
- pointer-events: none;
- }
-
- > .checkbox {
- position: absolute;
- bottom: 8px;
- right: 8px;
- width: 16px;
- height: 16px;
- background: #fff;
- border: solid 1px #000;
-
- &.checked {
- background: var(--accent);
- }
- }
+ cursor: pointer;
&.draghover {
&:after {
@@ -310,24 +288,38 @@ function onContextmenu(ev: MouseEvent) {
border-radius: 4px;
}
}
+}
- > .name {
- margin: 0;
- font-size: 0.9em;
- color: var(--desktopDriveFolderFg);
+.checkbox {
+ position: absolute;
+ bottom: 8px;
+ right: 8px;
+ width: 16px;
+ height: 16px;
+ background: #fff;
+ border: solid 1px #000;
- > i {
- margin-right: 4px;
- margin-left: 2px;
- text-align: left;
- }
+ &.checked {
+ background: var(--accent);
}
+}
- > .upload {
- margin: 4px 4px;
- font-size: 0.8em;
- text-align: right;
- color: var(--desktopDriveFolderFg);
- }
+.name {
+ margin: 0;
+ font-size: 0.9em;
+ color: var(--desktopDriveFolderFg);
+}
+
+.icon {
+ margin-right: 4px;
+ margin-left: 2px;
+ text-align: left;
+}
+
+.upload {
+ margin: 4px 4px;
+ font-size: 0.8em;
+ text-align: right;
+ color: var(--desktopDriveFolderFg);
}
</style>
diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue
index dbbfef5f05..3349603d3b 100644
--- a/packages/frontend/src/components/MkDrive.navFolder.vue
+++ b/packages/frontend/src/components/MkDrive.navFolder.vue
@@ -1,13 +1,13 @@
<template>
-<div class="drylbebk"
- :class="{ draghover }"
+<div
+ :class="[$style.root, { [$style.draghover]: draghover }]"
@click="onClick"
@dragover.prevent.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@drop.stop="onDrop"
>
- <i v-if="folder == null" class="ti ti-cloud"></i>
+ <i v-if="folder == null" class="ti ti-cloud" style="margin-right: 4px;"></i>
<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
</div>
</template>
@@ -130,18 +130,10 @@ function onDrop(ev: DragEvent) {
}
</script>
-<style lang="scss" scoped>
-.drylbebk {
- > * {
- pointer-events: none;
- }
-
+<style lang="scss" module>
+.root {
&.draghover {
background: #eee;
}
-
- > i {
- margin-right: 4px;
- }
}
</style>
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index bfec57d6a0..52aef450d9 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -1,89 +1,90 @@
<template>
-<div class="yfudmmck">
- <nav>
- <div class="path" @contextmenu.prevent.stop="() => {}">
+<div :class="$style.root">
+ <nav :class="$style.nav">
+ <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
<XNavFolder
- :class="{ current: folder == null }"
- :parent-folder="folder"
+ :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
+ :parentFolder="folder"
@move="move"
@upload="upload"
- @remove-file="removeFile"
- @remove-folder="removeFolder"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
/>
<template v-for="f in hierarchyFolders">
- <span class="separator"><i class="ti ti-chevron-right"></i></span>
+ <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
<XNavFolder
:folder="f"
- :parent-folder="folder"
+ :parentFolder="folder"
+ :class="[$style.navPathItem]"
@move="move"
@upload="upload"
- @remove-file="removeFile"
- @remove-folder="removeFolder"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
/>
</template>
- <span v-if="folder != null" class="separator"><i class="ti ti-chevron-right"></i></span>
- <span v-if="folder != null" class="folder current">{{ folder.name }}</span>
+ <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
+ <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
</div>
- <button class="menu _button" @click="showMenu"><i class="ti ti-dots"></i></button>
+ <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
</nav>
<div
- ref="main" class="main"
- :class="{ uploading: uploadings.length > 0, fetching }"
+ ref="main"
+ :class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
@dragover.prevent.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
@contextmenu.stop="onContextmenu"
>
- <div ref="contents" class="contents">
- <div v-show="folders.length > 0" ref="foldersContainer" class="folders">
+ <div ref="contents">
+ <div v-show="folders.length > 0" ref="foldersContainer" :class="$style.folders">
<XFolder
v-for="(f, i) in folders"
:key="f.id"
v-anim="i"
- class="folder"
+ :class="$style.folder"
:folder="f"
- :select-mode="select === 'folder'"
- :is-selected="selectedFolders.some(x => x.id === f.id)"
+ :selectMode="select === 'folder'"
+ :isSelected="selectedFolders.some(x => x.id === f.id)"
@chosen="chooseFolder"
@move="move"
@upload="upload"
- @remove-file="removeFile"
- @remove-folder="removeFolder"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
@dragstart="isDragSource = true"
@dragend="isDragSource = false"
/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
- <div v-for="(n, i) in 16" :key="i" class="padding"></div>
+ <div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton>
</div>
- <div v-show="files.length > 0" ref="filesContainer" class="files">
+ <div v-show="files.length > 0" ref="filesContainer" :class="$style.files">
<XFile
v-for="(file, i) in files"
:key="file.id"
v-anim="i"
- class="file"
+ :class="$style.file"
:file="file"
- :select-mode="select === 'file'"
- :is-selected="selectedFiles.some(x => x.id === file.id)"
+ :selectMode="select === 'file'"
+ :isSelected="selectedFiles.some(x => x.id === file.id)"
@chosen="chooseFile"
@dragstart="isDragSource = true"
@dragend="isDragSource = false"
/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
- <div v-for="(n, i) in 16" :key="i" class="padding"></div>
+ <div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
</div>
- <div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
- <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
- <p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
- <p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p>
+ <div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
+ <div v-if="draghover">{{ i18n.t('empty-draghover') }}</div>
+ <div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div>
+ <div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
</div>
</div>
<MkLoading v-if="fetching"/>
</div>
- <div v-if="draghover" class="dropzone"></div>
- <input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
+ <div v-if="draghover" :class="$style.dropzone"></div>
+ <input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
</div>
</template>
@@ -95,7 +96,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue';
import XFolder from '@/components/MkDrive.folder.vue';
import XFile from '@/components/MkDrive.file.vue';
import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { uploadFile, uploads } from '@/scripts/upload';
@@ -131,7 +132,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = uploads;
-const connection = stream.useChannel('drive');
+const connection = useStream().useChannel('drive');
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
// ドロップされようとしているか
@@ -658,147 +659,116 @@ onBeforeUnmount(() => {
});
</script>
-<style lang="scss" scoped>
-.yfudmmck {
+<style lang="scss" module>
+.root {
display: flex;
flex-direction: column;
height: 100%;
+}
- > nav {
- display: flex;
- z-index: 2;
- width: 100%;
- padding: 0 8px;
- box-sizing: border-box;
- overflow: auto;
- font-size: 0.9em;
- box-shadow: 0 1px 0 var(--divider);
-
- &, * {
- user-select: none;
- }
-
- > .path {
- display: inline-block;
- vertical-align: bottom;
- line-height: 42px;
- white-space: nowrap;
-
- > * {
- display: inline-block;
- margin: 0;
- padding: 0 8px;
- line-height: 42px;
- cursor: pointer;
-
- * {
- pointer-events: none;
- }
-
- &:hover {
- text-decoration: underline;
- }
-
- &.current {
- font-weight: bold;
- cursor: default;
-
- &:hover {
- text-decoration: none;
- }
- }
+.nav {
+ display: flex;
+ z-index: 2;
+ width: 100%;
+ padding: 0 8px;
+ box-sizing: border-box;
+ overflow: auto;
+ font-size: 0.9em;
+ box-shadow: 0 1px 0 var(--divider);
+ user-select: none;
+}
- &.separator {
- margin: 0;
- padding: 0;
- opacity: 0.5;
- cursor: default;
+.navPath {
+ display: inline-block;
+ vertical-align: bottom;
+ line-height: 42px;
+ white-space: nowrap;
+}
- > i {
- margin: 0;
- }
- }
- }
- }
+.navPathItem {
+ display: inline-block;
+ margin: 0;
+ padding: 0 8px;
+ line-height: 42px;
+ cursor: pointer;
- > .menu {
- margin-left: auto;
- padding: 0 12px;
- }
+ &:hover {
+ text-decoration: underline;
}
- > .main {
- flex: 1;
- overflow: auto;
- padding: var(--margin);
+ &.navCurrent {
+ font-weight: bold;
+ cursor: default;
- &, * {
- user-select: none;
+ &:hover {
+ text-decoration: none;
}
+ }
- &.fetching {
- cursor: wait !important;
-
- * {
- pointer-events: none;
- }
-
- > .contents {
- opacity: 0.5;
- }
- }
+ &.navSeparator {
+ margin: 0;
+ padding: 0;
+ opacity: 0.5;
+ cursor: default;
+ }
+}
- &.uploading {
- height: calc(100% - 38px - 100px);
- }
+.navMenu {
+ margin-left: auto;
+ padding: 0 12px;
+}
- > .contents {
+.main {
+ flex: 1;
+ overflow: auto;
+ padding: var(--margin);
+ user-select: none;
- > .folders,
- > .files {
- display: flex;
- flex-wrap: wrap;
+ &.fetching {
+ cursor: wait !important;
+ opacity: 0.5;
+ pointer-events: none;
+ }
- > .folder,
- > .file {
- flex-grow: 1;
- width: 128px;
- margin: 4px;
- box-sizing: border-box;
- }
+ &.uploading {
+ height: calc(100% - 38px - 100px);
+ }
+}
- > .padding {
- flex-grow: 1;
- pointer-events: none;
- width: 128px + 8px;
- }
- }
+.folders,
+.files {
+ display: flex;
+ flex-wrap: wrap;
+}
- > .empty {
- padding: 16px;
- text-align: center;
- pointer-events: none;
- opacity: 0.5;
+.folder,
+.file {
+ flex-grow: 1;
+ width: 128px;
+ margin: 4px;
+ box-sizing: border-box;
+}
- > p {
- margin: 0;
- }
- }
- }
- }
+.padding {
+ flex-grow: 1;
+ pointer-events: none;
+ width: 128px + 8px;
+}
- > .dropzone {
- position: absolute;
- left: 0;
- top: 38px;
- width: 100%;
- height: calc(100% - 38px);
- border: dashed 2px var(--focus);
- pointer-events: none;
- }
+.empty {
+ padding: 16px;
+ text-align: center;
+ pointer-events: none;
+ opacity: 0.5;
+}
- > input {
- display: none;
- }
+.dropzone {
+ position: absolute;
+ left: 0;
+ top: 38px;
+ width: 100%;
+ height: calc(100% - 38px);
+ border: dashed 2px var(--focus);
+ pointer-events: none;
}
</style>
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index 33379ed5ca..490aed6e04 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -1,16 +1,16 @@
<template>
-<div ref="thumbnail" class="zdjebgpv">
+<div ref="thumbnail" :class="$style.root">
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
- <i v-else-if="is === 'image'" class="ti ti-photo icon"></i>
- <i v-else-if="is === 'video'" class="ti ti-video icon"></i>
- <i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music icon"></i>
- <i v-else-if="is === 'csv'" class="ti ti-file-text icon"></i>
- <i v-else-if="is === 'pdf'" class="ti ti-file-text icon"></i>
- <i v-else-if="is === 'textfile'" class="ti ti-file-text icon"></i>
- <i v-else-if="is === 'archive'" class="ti ti-file-zip icon"></i>
- <i v-else class="ti ti-file icon"></i>
+ <i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
+ <i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
+ <i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i>
+ <i v-else-if="is === 'csv'" class="ti ti-file-text" :class="$style.icon"></i>
+ <i v-else-if="is === 'pdf'" class="ti ti-file-text" :class="$style.icon"></i>
+ <i v-else-if="is === 'textfile'" class="ti ti-file-text" :class="$style.icon"></i>
+ <i v-else-if="is === 'archive'" class="ti ti-file-zip" :class="$style.icon"></i>
+ <i v-else class="ti ti-file" :class="$style.icon"></i>
- <i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video icon-sub"></i>
+ <i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video" :class="$style.iconSub"></i>
</div>
</template>
@@ -53,28 +53,28 @@ const isThumbnailAvailable = computed(() => {
});
</script>
-<style lang="scss" scoped>
-.zdjebgpv {
+<style lang="scss" module>
+.root {
position: relative;
display: flex;
background: var(--panel);
border-radius: 8px;
overflow: clip;
+}
- > .icon-sub {
- position: absolute;
- width: 30%;
- height: auto;
- margin: 0;
- right: 4%;
- bottom: 4%;
- }
+.iconSub {
+ position: absolute;
+ width: 30%;
+ height: auto;
+ margin: 0;
+ right: 4%;
+ bottom: 4%;
+}
- > .icon {
- pointer-events: none;
- margin: auto;
- font-size: 32px;
- color: #777;
- }
+.icon {
+ pointer-events: none;
+ margin: auto;
+ font-size: 32px;
+ color: #777;
}
</style>
diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue
index 8d2b19c013..da873cb90b 100644
--- a/packages/frontend/src/components/MkDriveSelectDialog.vue
+++ b/packages/frontend/src/components/MkDriveSelectDialog.vue
@@ -3,8 +3,8 @@
ref="dialog"
:width="800"
:height="500"
- :with-ok-button="true"
- :ok-button-disabled="(type === 'file') && (selected.length === 0)"
+ :withOkButton="true"
+ :okButtonDisabled="(type === 'file') && (selected.length === 0)"
@click="cancel()"
@close="cancel()"
@ok="ok()"
@@ -14,7 +14,7 @@
{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template>
- <XDrive :multiple="multiple" :select="type" @change-selection="onChangeSelection" @selected="ok()"/>
+ <XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
</MkModalWindow>
</template>
diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue
index 8b2abc15a3..64ccbec9c3 100644
--- a/packages/frontend/src/components/MkDriveWindow.vue
+++ b/packages/frontend/src/components/MkDriveWindow.vue
@@ -1,15 +1,15 @@
<template>
<MkWindow
ref="window"
- :initial-width="800"
- :initial-height="500"
- :can-resize="true"
+ :initialWidth="800"
+ :initialHeight="500"
+ :canResize="true"
@closed="emit('closed')"
>
<template #header>
{{ i18n.ts.drive }}
</template>
- <XDrive :initial-folder="initialFolder"/>
+ <XDrive :initialFolder="initialFolder"/>
</MkWindow>
</template>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 9eaf16374b..cf856fd31f 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -1,7 +1,8 @@
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
- <div ref="emojisEl" class="emojis">
+ <!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
+ <div ref="emojisEl" class="emojis" tabindex="-1">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
@@ -69,8 +70,8 @@
<XSection
v-for="category in customEmojiCategories"
:key="`custom:${category}`"
- :initial-shown="false"
- :emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).map(e => `:${e.name}:`))"
+ :initialShown="false"
+ :emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
@chosen="chosen"
>
{{ category || i18n.ts.other }}
@@ -101,7 +102,8 @@ import { isTouchUsing } from '@/scripts/touch';
import { deviceKind } from '@/scripts/device-kind';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
-import { customEmojiCategories, customEmojis } from '@/custom-emojis';
+import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis';
+import { $i } from '@/account';
const props = withDefaults(defineProps<{
showPinned?: boolean;
@@ -222,7 +224,6 @@ watch(q, () => {
if (newQ.includes(' ')) { // AND検索
const keywords = newQ.split(' ');
- // 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
@@ -231,11 +232,12 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
- // 名前またはエイリアスにキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
- matches.add(emoji);
- if (matches.size >= max) break;
+ for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
}
}
} else {
@@ -247,13 +249,14 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
- matches.add(emoji);
- if (matches.size >= max) break;
+ for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+ for (const emoji of emojis) {
+ if (index[emoji.char].some(k => k.startsWith(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
}
}
- if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
@@ -263,10 +266,12 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
- matches.add(emoji);
- if (matches.size >= max) break;
+ for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+ for (const emoji of emojis) {
+ if (index[emoji.char].some(k => k.includes(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
}
}
}
@@ -274,10 +279,14 @@ watch(q, () => {
return matches;
};
- searchResultCustom.value = Array.from(searchCustom());
+ searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable);
searchResultUnicode.value = Array.from(searchUnicode());
});
+function filterAvailable(emoji: Misskey.entities.CustomEmoji): boolean {
+ return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
+}
+
function focus() {
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
searchEl.value?.focus({
@@ -347,7 +356,7 @@ function done(query?: string): boolean | void {
if (query == null || typeof query !== 'string') return;
const q2 = query.replace(/:/g, '');
- const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2);
+ const exactMatchCustom = customEmojisMap.get(q2);
if (exactMatchCustom) {
chosen(exactMatchCustom);
return true;
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index c568d4ed5c..cfb65e3b63 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -2,10 +2,10 @@
<MkModal
ref="modal"
v-slot="{ type, maxHeight }"
- :z-priority="'middle'"
- :prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
- :transparent-bg="true"
- :manual-showing="manualShowing"
+ :zPriority="'middle'"
+ :preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+ :transparentBg="true"
+ :manualShowing="manualShowing"
:src="src"
@click="modal?.close()"
@opening="opening"
@@ -14,11 +14,11 @@
>
<MkEmojiPicker
ref="picker"
- class="ryghynhb _popup _shadow"
- :class="{ drawer: type === 'drawer' }"
- :show-pinned="showPinned"
- :as-reaction-picker="asReactionPicker"
- :as-drawer="type === 'drawer'"
+ class="_popup _shadow"
+ :class="{ [$style.drawer]: type === 'drawer' }"
+ :showPinned="showPinned"
+ :asReactionPicker="asReactionPicker"
+ :asDrawer="type === 'drawer'"
:max-height="maxHeight"
@chosen="chosen"
/>
@@ -67,12 +67,10 @@ function opening() {
}
</script>
-<style lang="scss" scoped>
-.ryghynhb {
- &.drawer {
- border-radius: 24px;
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 0;
- }
+<style lang="scss" module>
+.drawer {
+ border-radius: 24px;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
}
</style>
diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue
index 84970410e9..9fecfd6082 100644
--- a/packages/frontend/src/components/MkEmojiPickerWindow.vue
+++ b/packages/frontend/src/components/MkEmojiPickerWindow.vue
@@ -1,13 +1,14 @@
<template>
-<MkWindow ref="window"
- :initial-width="300"
- :initial-height="290"
- :can-resize="true"
+<MkWindow
+ ref="window"
+ :initialWidth="300"
+ :initialHeight="290"
+ :canResize="true"
:mini="true"
:front="true"
@closed="emit('closed')"
>
- <MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" as-window :class="$style.picker" @chosen="chosen"/>
+ <MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
</MkWindow>
</template>
diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index 95eef45df0..61b87bda78 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -3,14 +3,14 @@
ref="dialog"
:width="400"
:height="450"
- :with-ok-button="true"
- :ok-button-disabled="false"
+ :withOkButton="true"
+ :okButtonDisabled="false"
@ok="ok()"
@close="dialog.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.describeFile }}</template>
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<MkDriveFileThumbnail :file="file" fit="contain" style="height: 100px; margin-bottom: 16px;"/>
<MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription">
<template #label>{{ i18n.ts.caption }}</template>
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 475e01c8d4..5dd07fc7da 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -1,9 +1,9 @@
<template>
-<div class="ssazuxis">
- <header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
- <div class="title"><div><slot name="header"></slot></div></div>
- <div class="divider"></div>
- <button class="_button">
+<div ref="el" :class="$style.root">
+ <header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
+ <div :class="$style.title"><div><slot name="header"></slot></div></div>
+ <div :class="$style.divider"></div>
+ <button class="_button" :class="$style.button">
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
<template v-else><i class="ti ti-chevron-down"></i></template>
</button>
@@ -11,9 +11,9 @@
<Transition
:name="defaultStore.state.animation ? 'folder-toggle' : ''"
@enter="enter"
- @after-enter="afterEnter"
+ @afterEnter="afterEnter"
@leave="leave"
- @after-leave="afterLeave"
+ @afterLeave="afterLeave"
>
<div v-show="showBody">
<slot></slot>
@@ -22,84 +22,71 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, ref, shallowRef, watch } from 'vue';
import tinycolor from 'tinycolor2';
import { miLocalStorage } from '@/local-storage';
import { defaultStore } from '@/store';
const miLocalStoragePrefix = 'ui:folder:' as const;
-export default defineComponent({
- props: {
- expanded: {
- type: Boolean,
- required: false,
- default: true,
- },
- persistKey: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- defaultStore,
- bg: null,
- showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
- };
- },
- watch: {
- showBody() {
- if (this.persistKey) {
- miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
- }
- },
- },
- mounted() {
- function getParentBg(el: Element | null): string {
- if (el == null || el.tagName === 'BODY') return 'var(--bg)';
- const bg = el.style.background || el.style.backgroundColor;
- if (bg) {
- return bg;
- } else {
- return getParentBg(el.parentElement);
- }
- }
- const rawBg = getParentBg(this.$el);
- const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
- bg.setAlpha(0.85);
- this.bg = bg.toRgbString();
- },
- methods: {
- toggleContent(show: boolean) {
- this.showBody = show;
- },
+const props = withDefaults(defineProps<{
+ expanded?: boolean;
+ persistKey?: string;
+}>(), {
+ expanded: true,
+});
+
+const el = shallowRef<HTMLDivElement>();
+const bg = ref<string | null>(null);
+const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
+
+watch(showBody, () => {
+ if (props.persistKey) {
+ miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f');
+ }
+});
+
+function enter(el: Element) {
+ const elementHeight = el.getBoundingClientRect().height;
+ el.style.height = 0;
+ el.offsetHeight; // reflow
+ el.style.height = elementHeight + 'px';
+}
+
+function afterEnter(el: Element) {
+ el.style.height = null;
+}
+
+function leave(el: Element) {
+ const elementHeight = el.getBoundingClientRect().height;
+ el.style.height = elementHeight + 'px';
+ el.offsetHeight; // reflow
+ el.style.height = 0;
+}
+
+function afterLeave(el: Element) {
+ el.style.height = null;
+}
- enter(el) {
- const elementHeight = el.getBoundingClientRect().height;
- el.style.height = 0;
- el.offsetHeight; // reflow
- el.style.height = elementHeight + 'px';
- },
- afterEnter(el) {
- el.style.height = null;
- },
- leave(el) {
- const elementHeight = el.getBoundingClientRect().height;
- el.style.height = elementHeight + 'px';
- el.offsetHeight; // reflow
- el.style.height = 0;
- },
- afterLeave(el) {
- el.style.height = null;
- },
- },
+onMounted(() => {
+ function getParentBg(el: HTMLElement | null): string {
+ if (el == null || el.tagName === 'BODY') return 'var(--bg)';
+ const bg = el.style.background || el.style.backgroundColor;
+ if (bg) {
+ return bg;
+ } else {
+ return getParentBg(el.parentElement);
+ }
+ }
+ const rawBg = getParentBg(el.value);
+ const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+ _bg.setAlpha(0.85);
+ bg.value = _bg.toRgbString();
});
</script>
-<style lang="scss" scoped>
+<style lang="scss" module>
.folder-toggle-enter-active, .folder-toggle-leave-active {
overflow-y: clip;
transition: opacity 0.5s, height 0.5s !important;
@@ -111,45 +98,41 @@ export default defineComponent({
opacity: 0;
}
-.ssazuxis {
+.root {
position: relative;
+}
- > header {
- display: flex;
- position: relative;
- z-index: 10;
- position: sticky;
- top: var(--stickyTop, 0px);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(20px));
+.header {
+ display: flex;
+ position: relative;
+ z-index: 10;
+ position: sticky;
+ top: var(--stickyTop, 0px);
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(20px));
+}
- > .title {
- display: grid;
- place-content: center;
- margin: 0;
- padding: 12px 16px 12px 0;
- }
+.title {
+ display: grid;
+ place-content: center;
+ margin: 0;
+ padding: 12px 16px 12px 0;
+}
- > .divider {
- flex: 1;
- margin: auto;
- height: 1px;
- background: var(--divider);
- }
+.divider {
+ flex: 1;
+ margin: auto;
+ height: 1px;
+ background: var(--divider);
+}
- > button {
- padding: 12px 0 12px 16px;
- }
- }
+.button {
+ padding: 12px 0 12px 16px;
}
@container (max-width: 500px) {
- .ssazuxis {
- > header {
- > .title {
- padding: 8px 10px 8px 0;
- }
- }
+ .title {
+ padding: 8px 10px 8px 0;
}
}
</style>
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 10eee6aab1..70f0cc5cda 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -5,8 +5,8 @@
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
<div :class="$style.headerText">
- <div :class="$style.headerTextMain">
- <slot name="label"></slot>
+ <div>
+ <MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
</div>
<div :class="$style.headerTextSub">
<slot name="caption"></slot>
@@ -22,18 +22,18 @@
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened">
<Transition
- :enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
@enter="enter"
- @after-enter="afterEnter"
+ @afterEnter="afterEnter"
@leave="leave"
- @after-leave="afterLeave"
+ @afterLeave="afterLeave"
>
<KeepAlive>
<div v-show="opened">
- <MkSpacer :margin-min="14" :margin-max="22">
+ <MkSpacer :marginMin="14" :marginMax="22">
<slot></slot>
</MkSpacer>
</div>
@@ -185,10 +185,6 @@ onMounted(() => {
padding-right: 12px;
}
-.headerTextMain {
-
-}
-
.headerTextSub {
color: var(--fgTransparentWeak);
font-size: .85em;
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index beee21c647..b732fbb2b9 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -1,30 +1,30 @@
<template>
<button
- class="kpoogebi _button"
- :class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
+ class="_button"
+ :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
:disabled="wait"
@click="onClick"
>
<template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
- <span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
<!-- つまりリモートフォローの場合。 -->
- <span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template>
<template v-else-if="isFollowing">
- <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
- <span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
- <span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
</template>
</template>
<template v-else>
- <span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template>
</button>
</template>
@@ -33,7 +33,7 @@
import { onBeforeUnmount, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
import { i18n } from '@/i18n';
import { claimAchievement } from '@/scripts/achievements';
import { $i } from '@/account';
@@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{
let isFollowing = $ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
let wait = $ref(false);
-const connection = stream.useChannel('main');
+const connection = useStream().useChannel('main');
if (props.user.isFollowing == null) {
os.api('users/show', {
@@ -126,13 +126,12 @@ onBeforeUnmount(() => {
});
</script>
-<style lang="scss" scoped>
-.kpoogebi {
+<style lang="scss" module>
+.root {
position: relative;
display: inline-block;
font-weight: bold;
- color: var(--accent);
- background: transparent;
+ color: var(--fgOnWhite);
border: solid 1px var(--accent);
padding: 0;
height: 31px;
@@ -196,9 +195,9 @@ onBeforeUnmount(() => {
cursor: wait !important;
opacity: 0.7;
}
+}
- > span {
- margin-right: 6px;
- }
+.text {
+ margin-right: 6px;
}
</style>
diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue
index 0befa7e3ae..1264c42331 100644
--- a/packages/frontend/src/components/MkForgotPassword.vue
+++ b/packages/frontend/src/components/MkForgotPassword.vue
@@ -8,27 +8,28 @@
>
<template #header>{{ i18n.ts.forgotPassword }}</template>
- <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
- <div class="main _gaps_m">
- <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
- <template #label>{{ i18n.ts.username }}</template>
- <template #prefix>@</template>
- </MkInput>
+ <MkSpacer :marginMin="20" :marginMax="28">
+ <form v-if="instance.enableEmail" @submit.prevent="onSubmit">
+ <div class="_gaps_m">
+ <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
+ <template #label>{{ i18n.ts.username }}</template>
+ <template #prefix>@</template>
+ </MkInput>
- <MkInput v-model="email" type="email" :spellcheck="false" required>
- <template #label>{{ i18n.ts.emailAddress }}</template>
- <template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
- </MkInput>
+ <MkInput v-model="email" type="email" :spellcheck="false" required>
+ <template #label>{{ i18n.ts.emailAddress }}</template>
+ <template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
+ </MkInput>
- <MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
- </div>
- <div class="sub">
- <MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
+ <MkButton type="submit" rounded :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
+
+ <MkInfo>{{ i18n.ts._forgotPassword.ifNoEmail }}</MkInfo>
+ </div>
+ </form>
+ <div v-else>
+ {{ i18n.ts._forgotPassword.contactAdmin }}
</div>
- </form>
- <div v-else class="bafecedb">
- {{ i18n.ts._forgotPassword.contactAdmin }}
- </div>
+ </MkSpacer>
</MkModalWindow>
</template>
@@ -37,6 +38,7 @@ import { } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
@@ -62,20 +64,3 @@ async function onSubmit() {
dialog.close();
}
</script>
-
-<style lang="scss" scoped>
-.bafeceda {
- > .main {
- padding: 24px;
- }
-
- > .sub {
- border-top: solid 0.5px var(--divider);
- padding: 24px;
- }
-}
-
-.bafecedb {
- padding: 24px;
-}
-</style>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 979df2e7c1..6d2b391e6d 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -2,9 +2,9 @@
<MkModalWindow
ref="dialog"
:width="450"
- :can-close="false"
- :with-ok-button="true"
- :ok-button-disabled="false"
+ :canClose="false"
+ :withOkButton="true"
+ :okButtonDisabled="false"
@click="cancel()"
@ok="ok()"
@close="cancel()"
@@ -14,7 +14,7 @@
{{ title }}
</template>
- <MkSpacer :margin-min="20" :margin-max="32">
+ <MkSpacer :marginMin="20" :marginMax="32">
<div class="_gaps_m">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
@@ -41,7 +41,7 @@
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
</MkRadios>
- <MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
+ <MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkRange>
@@ -54,8 +54,8 @@
</MkModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { reactive, shallowRef } from 'vue';
import MkInput from './MkInput.vue';
import MkTextarea from './MkTextarea.vue';
import MkSwitch from './MkSwitch.vue';
@@ -66,58 +66,36 @@ import MkRadios from './MkRadios.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkModalWindow,
- MkInput,
- MkTextarea,
- MkSwitch,
- MkSelect,
- MkRange,
- MkButton,
- MkRadios,
- },
+const props = defineProps<{
+ title: string;
+ form: any;
+}>();
- props: {
- title: {
- type: String,
- required: true,
- },
- form: {
- type: Object,
- required: true,
- },
- },
+const emit = defineEmits<{
+ (ev: 'done', v: {
+ canceled?: boolean;
+ result?: any;
+ }): void;
+}>();
- emits: ['done'],
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const values = reactive({});
- data() {
- return {
- values: {},
- i18n,
- };
- },
+for (const item in props.form) {
+ values[item] = props.form[item].default ?? null;
+}
- created() {
- for (const item in this.form) {
- this.values[item] = this.form[item].default ?? null;
- }
- },
+function ok() {
+ emit('done', {
+ result: values,
+ });
+ dialog.value.close();
+}
- methods: {
- ok() {
- this.$emit('done', {
- result: this.values,
- });
- this.$refs.dialog.close();
- },
-
- cancel() {
- this.$emit('done', {
- canceled: true,
- });
- this.$refs.dialog.close();
- },
- },
-});
+function cancel() {
+ emit('done', {
+ canceled: true,
+ });
+ dialog.value.close();
+}
</script>
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts
index 57b3e75513..72ac0a58f9 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts
+++ b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts
@@ -44,6 +44,10 @@ export const Default = {
],
parameters: {
layout: 'centered',
+ chromatic: {
+ // FIXME: flaky
+ disableSnapshot: true,
+ },
},
} satisfies StoryObj<typeof MkGalleryPostPreview>;
export const Hover = {
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 4f8f7b945a..3a39ad963b 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -5,16 +5,13 @@
<ImgWithBlurhash
class="img layered"
:transition="safe ? null : {
- enterActiveClass: $style.transition_toggle_enterActive,
+ duration: 500,
leaveActiveClass: $style.transition_toggle_leaveActive,
- enterFromClass: $style.transition_toggle_enterFrom,
leaveToClass: $style.transition_toggle_leaveTo,
- enterToClass: $style.transition_toggle_enterTo,
- leaveFromClass: $style.transition_toggle_leaveFrom,
}"
:src="post.files[0].thumbnailUrl"
:hash="post.files[0].blurhash"
- :force-blurhash="!show"
+ :forceBlurhash="!show"
/>
</Transition>
</div>
@@ -53,24 +50,16 @@ function leaveHover(): void {
</script>
<style lang="scss" module>
-.transition_toggle_enterActive,
.transition_toggle_leaveActive {
- transition: opacity 0.5s;
+ transition: opacity .5s;
position: absolute;
top: 0;
left: 0;
}
-.transition_toggle_enterFrom,
.transition_toggle_leaveTo {
opacity: 0;
}
-
-.transition_toggle_enterTo,
-.transition_toggle_leaveFrom {
- transition: none;
- opacity: 1;
-}
</style>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkImageViewer.vue b/packages/frontend/src/components/MkImageViewer.vue
deleted file mode 100644
index a90e27e502..0000000000
--- a/packages/frontend/src/components/MkImageViewer.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<template>
-<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
- <div class="xubzgfga">
- <header>{{ image.name }}</header>
- <img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
- <footer>
- <span>{{ image.type }}</span>
- <span>{{ bytes(image.size) }}</span>
- <span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
- </footer>
- </div>
-</MkModal>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import * as misskey from 'misskey-js';
-import bytes from '@/filters/bytes';
-import number from '@/filters/number';
-import MkModal from '@/components/MkModal.vue';
-
-const props = withDefaults(defineProps<{
- image: misskey.entities.DriveFile;
-}>(), {
-});
-
-const emit = defineEmits<{
- (ev: 'closed'): void;
-}>();
-
-const modal = $shallowRef<InstanceType<typeof MkModal>>();
-</script>
-
-<style lang="scss" scoped>
-.xubzgfga {
- margin: auto;
- display: flex;
- flex-direction: column;
- height: 100%;
-
- > header,
- > footer {
- align-self: center;
- display: inline-block;
- padding: 6px 9px;
- font-size: 90%;
- background: rgba(0, 0, 0, 0.5);
- border-radius: 6px;
- color: #fff;
- }
-
- > header {
- margin-bottom: 8px;
- opacity: 0.9;
- }
-
- > img {
- display: block;
- flex: 1;
- min-height: 0;
- object-fit: contain;
- width: 100%;
- cursor: zoom-out;
- image-orientation: from-image;
- }
-
- > footer {
- margin-top: 8px;
- opacity: 0.8;
-
- > span + span {
- margin-left: 0.5em;
- padding-left: 0.5em;
- border-left: solid 1px rgba(255, 255, 255, 0.5);
- }
- }
-}
-</style>
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 6406a35060..672a28f6d0 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -1,30 +1,60 @@
<template>
-<div :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
- <img v-if="!loaded && src && !forceBlurhash" :class="$style.loader" :src="src" @load="onLoad"/>
- <Transition
- mode="in-out"
- :enter-active-class="defaultStore.state.animation && (props.transition?.enterActiveClass ?? $style['transition_toggle_enterActive']) || undefined"
- :leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_toggle_leaveActive']) || undefined"
- :enter-from-class="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
- :leave-to-class="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
- :enter-to-class="defaultStore.state.animation && (props.transition?.enterToClass ?? $style['transition_toggle_enterTo']) || undefined"
- :leave-from-class="defaultStore.state.animation && (props.transition?.leaveFromClass ?? $style['transition_toggle_leaveFrom']) || undefined"
+<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
+ <TransitionGroup
+ :duration="defaultStore.state.animation && props.transition?.duration || undefined"
+ :enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
+ :leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
+ :enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
+ :leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
+ :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
+ :leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
>
- <canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="width" :height="height" :title="title ?? undefined"/>
- <img v-else :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined"/>
- </Transition>
+ <canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
+ <img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
+ </TransitionGroup>
</div>
</template>
+<script lang="ts">
+import { $ref } from 'vue/macros';
+import DrawBlurhash from '@/workers/draw-blurhash?worker';
+import TestWebGL2 from '@/workers/test-webgl2?worker';
+import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
+import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
+
+const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
+ // テスト環境で Web Worker インスタンスは作成できない
+ if (import.meta.env.MODE === 'test') {
+ resolve(null);
+ return;
+ }
+ const testWorker = new TestWebGL2();
+ testWorker.addEventListener('message', event => {
+ if (event.data.result) {
+ const workers = new WorkerMultiDispatch(
+ () => new DrawBlurhash(),
+ Math.min(navigator.hardwareConcurrency - 1, 4),
+ );
+ resolve(workers);
+ if (_DEV_) console.log('WebGL2 in worker is supported!');
+ } else {
+ resolve(null);
+ if (_DEV_) console.log('WebGL2 in worker is not supported...');
+ }
+ testWorker.terminate();
+ });
+});
+</script>
+
<script lang="ts" setup>
-import { onMounted, shallowRef, useCssModule, watch } from 'vue';
-import { decode } from 'blurhash';
+import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { render } from 'buraha';
import { defaultStore } from '@/store';
-const $style = useCssModule();
-
const props = withDefaults(defineProps<{
transition?: {
+ duration?: number | { enter: number; leave: number; };
enterActiveClass?: string;
leaveActiveClass?: string;
enterFromClass?: string;
@@ -51,67 +81,141 @@ const props = withDefaults(defineProps<{
forceBlurhash: false,
});
+const viewId = uuid();
const canvas = shallowRef<HTMLCanvasElement>();
+const root = shallowRef<HTMLDivElement>();
+const img = shallowRef<HTMLImageElement>();
let loaded = $ref(false);
-let width = $ref(props.width);
-let height = $ref(props.height);
+let canvasWidth = $ref(64);
+let canvasHeight = $ref(64);
+let imgWidth = $ref(props.width);
+let imgHeight = $ref(props.height);
+let bitmapTmp = $ref<CanvasImageSource | undefined>();
+const hide = computed(() => !loaded || props.forceBlurhash);
-function onLoad() {
- loaded = true;
+function waitForDecode() {
+ if (props.src != null && props.src !== '') {
+ nextTick()
+ .then(() => img.value?.decode())
+ .then(() => {
+ loaded = true;
+ }, error => {
+ console.error('Error occured during decoding image', img.value, error);
+ throw Error(error);
+ });
+ } else {
+ loaded = false;
+ }
}
-watch([() => props.width, () => props.height], () => {
+watch([() => props.width, () => props.height, root], () => {
const ratio = props.width / props.height;
if (ratio > 1) {
- width = Math.round(64 * ratio);
- height = 64;
+ canvasWidth = Math.round(64 * ratio);
+ canvasHeight = 64;
} else {
- width = 64;
- height = Math.round(64 / ratio);
+ canvasWidth = 64;
+ canvasHeight = Math.round(64 / ratio);
}
+
+ const clientWidth = root.value?.clientWidth ?? 300;
+ imgWidth = clientWidth;
+ imgHeight = Math.round(clientWidth / ratio);
}, {
immediate: true,
});
-function draw() {
- if (props.hash == null || !canvas.value) return;
- const pixels = decode(props.hash, width, height);
+function drawImage(bitmap: CanvasImageSource) {
+ // canvasがない(mountedされていない)場合はTmpに保存しておく
+ if (!canvas.value) {
+ bitmapTmp = bitmap;
+ return;
+ }
+
+ // canvasがあれば描画する
+ bitmapTmp = undefined;
+ const ctx = canvas.value.getContext('2d');
+ if (!ctx) return;
+ ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight);
+}
+
+async function draw() {
+ if (!canvas.value || props.hash == null) return;
+
const ctx = canvas.value.getContext('2d');
- const imageData = ctx!.createImageData(width, height);
- imageData.data.set(pixels);
- ctx!.putImageData(imageData, 0, 0);
+ if (!ctx) return;
+
+ // avgColorでお茶をにごす
+ ctx.beginPath();
+ ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ const workers = await workerPromise;
+ if (workers) {
+ workers.postMessage(
+ {
+ id: viewId,
+ hash: props.hash,
+ width: canvasWidth,
+ height: canvasHeight,
+ },
+ undefined,
+ );
+ } else {
+ try {
+ const work = document.createElement('canvas');
+ work.width = canvasWidth;
+ work.height = canvasHeight;
+ render(props.hash, work);
+ ctx.drawImage(work, 0, 0, canvasWidth, canvasHeight);
+ } catch (error) {
+ console.error('Error occured during drawing blurhash', error);
+ }
+ }
}
-watch([() => props.hash, canvas], () => {
+function workerOnMessage(event: MessageEvent) {
+ if (event.data.id !== viewId) return;
+ drawImage(event.data.bitmap as ImageBitmap);
+}
+
+workerPromise.then(worker => {
+ if (worker) {
+ worker.addListener(workerOnMessage);
+ }
+
draw();
});
-onMounted(() => {
+watch(() => props.src, () => {
+ waitForDecode();
+});
+
+watch(() => props.hash, () => {
draw();
});
-</script>
-<style lang="scss" module>
-.transition_toggle_enterActive,
-.transition_toggle_leaveActive {
- position: absolute;
- top: 0;
- left: 0;
-}
+onMounted(() => {
+ // drawImageがmountedより先に呼ばれている場合はここで描画する
+ if (bitmapTmp) {
+ drawImage(bitmapTmp);
+ }
+ waitForDecode();
+});
-.transition_toggle_enterTo,
-.transition_toggle_leaveFrom {
- opacity: 0;
-}
+onUnmounted(() => {
+ workerPromise.then(worker => {
+ worker?.removeListener(workerOnMessage);
+ });
+});
+</script>
-.loader {
+<style lang="scss" module>
+.transition_leaveActive {
position: absolute;
top: 0;
left: 0;
- width: 0;
- height: 0;
}
-
.root {
position: relative;
width: 100%;
diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue
index ff69c79641..4b6a775635 100644
--- a/packages/frontend/src/components/MkKeyValue.vue
+++ b/packages/frontend/src/components/MkKeyValue.vue
@@ -1,9 +1,9 @@
<template>
-<div class="alqyeyti" :class="{ oneline }">
- <div class="key">
+<div :class="[$style.root, { [$style.oneline]: oneline }]">
+ <div :class="$style.key">
<slot name="key"></slot>
</div>
- <div class="value">
+ <div :class="$style.value">
<slot name="value"></slot>
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button>
</div>
@@ -30,24 +30,18 @@ const copy_ = () => {
};
</script>
-<style lang="scss" scoped>
-.alqyeyti {
- > .key {
- font-size: 0.85em;
- padding: 0 0 0.25em 0;
- opacity: 0.75;
- }
-
+<style lang="scss" module>
+.root {
&.oneline {
display: flex;
- > .key {
+ .key {
width: 30%;
font-size: 1em;
padding: 0 8px 0 0;
}
- > .value {
+ .value {
width: 70%;
white-space: nowrap;
overflow: hidden;
@@ -55,4 +49,10 @@ const copy_ = () => {
}
}
}
+
+.key {
+ font-size: 0.85em;
+ padding: 0 0 0.25em 0;
+ opacity: 0.75;
+}
</style>
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 80e5cc8270..9262778612 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :prefer-type="preferedModalType" :anchor="anchor" :transparent-bg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
<div class="main">
<template v-for="item in items">
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 5ca4c50518..5902d6fd25 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -1,27 +1,27 @@
<template>
-<div class="mk-media-banner">
- <div v-if="media.isSensitive && hide" class="sensitive" @click="hide = false">
- <span class="icon"><i class="ti ti-alert-triangle"></i></span>
+<div :class="$style.root">
+ <div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
+ <span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
- <div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
- <VuePlyr :options="{ volume: 0.5 }">
- <audio controls preload="metadata">
- <source
- :src="media.url"
- :type="media.type"
- />
- </audio>
- </VuePlyr>
+ <div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :class="$style.audio">
+ <audio
+ ref="audioEl"
+ :src="media.url"
+ :title="media.name"
+ controls
+ preload="metadata"
+ @volumechange="volumechange"
+ />
</div>
<a
- v-else class="download"
+ v-else :class="$style.download"
:href="media.url"
:title="media.name"
:download="media.name"
>
- <span class="icon"><i class="ti ti-download"></i></span>
+ <span style="font-size: 1.6em;"><i class="ti ti-download"></i></span>
<b>{{ media.name }}</b>
</a>
</div>
@@ -30,9 +30,7 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as misskey from 'misskey-js';
-import VuePlyr from 'vue-plyr';
import { soundConfigStore } from '@/scripts/sound';
-import 'vue-plyr/dist/vue-plyr.css';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
@@ -52,55 +50,34 @@ onMounted(() => {
});
</script>
-<style lang="scss" scoped>
-.mk-media-banner {
+<style lang="scss" module>
+.root {
width: 100%;
border-radius: 4px;
margin-top: 4px;
- // overflow: clip;
-
- --plyr-color-main: var(--accent);
- --plyr-audio-controls-background: var(--bg);
- --plyr-audio-controls-color: var(--accentLighten);
-
- > .download,
- > .sensitive {
- display: flex;
- align-items: center;
- font-size: 12px;
- padding: 8px 12px;
- white-space: nowrap;
-
- > * {
- display: block;
- }
-
- > b {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- > *:not(:last-child) {
- margin-right: .2em;
- }
+ overflow: clip;
+}
- > .icon {
- font-size: 1.6em;
- }
- }
+.download,
+.sensitive {
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+ padding: 8px 12px;
+ white-space: nowrap;
+}
- > .download {
- background: var(--noteAttachedFile);
- }
+.download {
+ background: var(--noteAttachedFile);
+}
- > .sensitive {
- background: #111;
- color: #fff;
- }
+.sensitive {
+ background: #111;
+ color: #fff;
+}
- > .audio {
- border-radius: 8px;
- // overflow: clip;
- }
+.audio {
+ border-radius: 8px;
+ overflow: clip;
}
</style>
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 42dc9e79ff..b29871c363 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -1,29 +1,39 @@
<template>
-<div v-if="hide" :class="$style.hidden" @click="hide = false">
- <ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :width="image.properties.width" :height="image.properties.height" :force-blurhash="defaultStore.state.enableDataSaverMode"/>
- <div :class="$style.hiddenText">
- <div :class="$style.hiddenTextWrapper">
- <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
- <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
- <span style="display: block;">{{ i18n.ts.clickToShow }}</span>
- </div>
- </div>
-</div>
-<div v-else :class="$style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'">
+<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
<a
:class="$style.imageContainer"
:href="image.url"
:title="image.name"
>
- <ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
+ <ImgWithBlurhash
+ :hash="image.blurhash"
+ :src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
+ :forceBlurhash="hide"
+ :cover="hide"
+ :alt="image.comment || image.name"
+ :title="image.comment || image.name"
+ :width="image.properties.width"
+ :height="image.properties.height"
+ :style="hide ? 'filter: brightness(0.5);' : null"
+ />
</a>
- <div :class="$style.indicators">
- <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
- <div v-if="image.comment" :class="$style.indicator">ALT</div>
- <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
- </div>
- <button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
- <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
+ <template v-if="hide">
+ <div :class="$style.hiddenText">
+ <div :class="$style.hiddenTextWrapper">
+ <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
+ <span style="display: block;">{{ i18n.ts.clickToShow }}</span>
+ </div>
+ </div>
+ </template>
+ <template v-else>
+ <div :class="$style.indicators">
+ <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
+ <div v-if="image.comment" :class="$style.indicator">ALT</div>
+ <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
+ </div>
+ <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
+ </template>
</div>
</template>
@@ -53,6 +63,12 @@ const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
: props.image.thumbnailUrl,
);
+function onclick() {
+ if (hide) {
+ hide = false;
+ }
+}
+
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
watch(() => props.image, () => {
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
@@ -62,10 +78,17 @@ watch(() => props.image, () => {
});
function showMenu(ev: MouseEvent) {
- os.popupMenu([...(iAmModerator ? [{
- text: i18n.ts.markAsSensitive,
+ os.popupMenu([{
+ text: i18n.ts.hide,
icon: 'ti ti-eye-off',
action: () => {
+ hide = true;
+ },
+ }, ...(iAmModerator ? [{
+ text: i18n.ts.markAsSensitive,
+ icon: 'ti ti-eye-exclamation',
+ danger: true,
+ action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
},
}] : [])], ev.currentTarget ?? ev.target);
@@ -105,34 +128,20 @@ function showMenu(ev: MouseEvent) {
background-size: 16px 16px;
}
-.hide {
- display: block;
- position: absolute;
- border-radius: 6px;
- background-color: var(--accentedBg);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
- color: var(--accent);
- font-size: 0.8em;
- padding: 6px 8px;
- text-align: center;
- top: 12px;
- right: 12px;
-}
-
.menu {
display: block;
position: absolute;
- border-radius: 6px;
+ border-radius: 999px;
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: #fff;
font-size: 0.8em;
- padding: 6px 8px;
+ width: 32px;
+ height: 32px;
text-align: center;
- bottom: 12px;
- right: 12px;
+ bottom: 10px;
+ right: 10px;
}
.imageContainer {
@@ -149,12 +158,10 @@ function showMenu(ev: MouseEvent) {
.indicators {
display: inline-flex;
position: absolute;
- top: 12px;
- left: 12px;
- text-align: center;
+ top: 10px;
+ left: 10px;
pointer-events: none;
opacity: .5;
- font-size: 14px;
gap: 6px;
}
@@ -165,7 +172,7 @@ function showMenu(ev: MouseEvent) {
color: var(--accentLighten);
display: inline-block;
font-weight: bold;
- font-size: 12px;
- padding: 2px 6px;
+ font-size: 0.8em;
+ padding: 2px 5px;
}
</style>
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index e456ff3eec..a0a2450054 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -6,7 +6,11 @@
ref="gallery"
:class="[
$style.medias,
- count <= 4 ? $style['n' + count] : $style.nMany,
+ count === 1 ? [$style.n1, {
+ [$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9',
+ [$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1',
+ [$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3',
+ }] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
]"
>
<template v-for="media in mediaList.filter(media => previewable(media))">
@@ -19,7 +23,7 @@
</template>
<script lang="ts" setup>
-import { onMounted, ref, useCssModule, watch } from 'vue';
+import { onMounted, watch, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@@ -36,13 +40,42 @@ const props = defineProps<{
raw?: boolean;
}>();
-const $style = useCssModule();
-
-const gallery = ref<HTMLDivElement>();
+const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
+function calcAspectRatio() {
+ if (!gallery.value) return;
+
+ let img = props.mediaList[0];
+
+ if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
+ gallery.value.style.aspectRatio = '';
+ return;
+ }
+
+ // アスペクト比上限設定では、横長の場合は高さを縮小させる
+ const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
+
+ switch (defaultStore.state.mediaListWithOneImageAppearance) {
+ case '16_9':
+ gallery.value.style.aspectRatio = ratioMax(16 / 9);
+ break;
+ case '1_1':
+ gallery.value.style.aspectRatio = ratioMax(1);
+ break;
+ case '2_3':
+ gallery.value.style.aspectRatio = ratioMax(2 / 3);
+ break;
+ default:
+ gallery.value.style.aspectRatio = '';
+ break;
+ }
+}
+
+watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio());
+
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList
@@ -64,7 +97,7 @@ onMounted(() => {
return item;
}),
gallery: gallery.value,
- mainClass: $style.pswp,
+ mainClass: 'pswp',
children: '.image',
thumbSelector: '.image',
loop: false,
@@ -162,12 +195,37 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
display: grid;
grid-gap: 8px;
- // for webkit
height: 100%;
+ width: 100%;
&.n1 {
- aspect-ratio: 16/9;
grid-template-rows: 1fr;
+
+ // default (expand)
+ min-height: 64px;
+ max-height: clamp(
+ 64px,
+ 50cqh,
+ min(360px, 50vh)
+ );
+
+ &.n116_9 {
+ min-height: none;
+ max-height: none;
+ aspect-ratio: 16 / 9; // fallback
+ }
+
+ &.n11_1{
+ min-height: none;
+ max-height: none;
+ aspect-ratio: 1 / 1; // fallback
+ }
+
+ &.n12_3 {
+ min-height: none;
+ max-height: none;
+ aspect-ratio: 2 / 3; // fallback
+ }
}
&.n2 {
@@ -211,7 +269,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
border-radius: 8px;
}
-.pswp {
+:global(.pswp) {
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
--pswp-bg: var(--modalBg) !important;
}
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index a4b76300e6..40bae90b5e 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -1,26 +1,28 @@
<template>
-<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false">
+<div v-if="hide" :class="$style.hidden" @click="hide = false">
<!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること -->
- <div>
- <b v-if="video.isSensitive"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
- <b v-else><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
+ <div :class="$style.sensitive">
+ <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
-<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
- <VuePlyr :options="{ volume: 0.5 }">
- <video
- controls
- :data-poster="video.thumbnailUrl"
+<div v-else :class="$style.visible">
+ <video
+ :class="$style.video"
+ :poster="video.thumbnailUrl"
+ :title="video.comment"
+ :alt="video.comment"
+ preload="none"
+ controls
+ @contextmenu.stop
+ >
+ <source
+ :src="video.url"
+ :type="video.type"
>
- <source
- size="720"
- :src="video.url"
- :type="video.type"
- />
- </video>
- </VuePlyr>
- <i class="ti ti-eye-off" @click="hide = true"></i>
+ </video>
+ <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
</div>
</template>
@@ -28,9 +30,7 @@
import { ref } from 'vue';
import * as misskey from 'misskey-js';
import bytes from '@/filters/bytes';
-import VuePlyr from 'vue-plyr';
import { defaultStore } from '@/store';
-import 'vue-plyr/dist/vue-plyr.css';
import { i18n } from '@/i18n';
const props = defineProps<{
@@ -40,56 +40,49 @@ const props = defineProps<{
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
</script>
-<style lang="scss" scoped>
-.kkjnbbplepmiyuadieoenjgutgcmtsvu {
+<style lang="scss" module>
+.visible {
position: relative;
+}
- --plyr-color-main: var(--accent);
-
- > i {
- display: block;
- position: absolute;
- border-radius: 6px;
- background-color: var(--fg);
- color: var(--accentLighten);
- font-size: 14px;
- opacity: .5;
- padding: 3px 6px;
- text-align: center;
- cursor: pointer;
- top: 12px;
- right: 12px;
- }
-
- > video {
- display: flex;
- justify-content: center;
- align-items: center;
+.hide {
+ display: block;
+ position: absolute;
+ border-radius: 6px;
+ background-color: var(--fg);
+ color: var(--accentLighten);
+ font-size: 14px;
+ opacity: .5;
+ padding: 3px 6px;
+ text-align: center;
+ cursor: pointer;
+ top: 12px;
+ right: 12px;
+}
- font-size: 3.5em;
- overflow: hidden;
- background-position: center;
- background-size: cover;
- width: 100%;
- height: 100%;
- }
+.video {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 3.5em;
+ overflow: hidden;
+ background-position: center;
+ background-size: cover;
+ width: 100%;
+ height: 100%;
}
-.icozogqfvdetwohsdglrbswgrejoxbdj {
+.hidden {
display: flex;
justify-content: center;
align-items: center;
background: #111;
color: #fff;
+}
- > div {
- display: table-cell;
- text-align: center;
- font-size: 12px;
-
- > b {
- display: block;
- }
- }
+.sensitive {
+ display: table-cell;
+ text-align: center;
+ font-size: 12px;
}
</style>
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 481c3710ca..bb256c394b 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -2,7 +2,7 @@
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<span>
- <span :class="$style.username">@{{ username }}</span>
+ <span>@{{ username }}</span>
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span>
</span>
</MkA>
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index e0935efbe7..4fedfe7014 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -1,6 +1,6 @@
<template>
<div ref="el" :class="$style.root">
- <MkMenu :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
+ <MkMenu :items="items" :align="align" :width="width" :asDrawer="false" @close="onChildClosed"/>
</div>
</template>
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index e513a65a32..7dd6a8c88f 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -49,8 +49,8 @@
<span>{{ i18n.ts.none }}</span>
</span>
</div>
- <div v-if="childMenu" :class="$style.child">
- <XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
+ <div v-if="childMenu">
+ <XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
</div>
</div>
</template>
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 99df9e8150..bb5c6c7aab 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -1,11 +1,31 @@
<template>
<Transition
:name="transitionName"
- :enter-active-class="$style['transition_' + transitionName + '_enterActive']"
- :leave-active-class="$style['transition_' + transitionName + '_leaveActive']"
- :enter-from-class="$style['transition_' + transitionName + '_enterFrom']"
- :leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
- :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"
+ :enterActiveClass="normalizeClass({
+ [$style.transition_modalDrawer_enterActive]: transitionName === 'modal-drawer',
+ [$style.transition_modalPopup_enterActive]: transitionName === 'modal-popup',
+ [$style.transition_modal_enterActive]: transitionName === 'modal',
+ [$style.transition_send_enterActive]: transitionName === 'send',
+ })"
+ :leaveActiveClass="normalizeClass({
+ [$style.transition_modalDrawer_leaveActive]: transitionName === 'modal-drawer',
+ [$style.transition_modalPopup_leaveActive]: transitionName === 'modal-popup',
+ [$style.transition_modal_leaveActive]: transitionName === 'modal',
+ [$style.transition_send_leaveActive]: transitionName === 'send',
+ })"
+ :enterFromClass="normalizeClass({
+ [$style.transition_modalDrawer_enterFrom]: transitionName === 'modal-drawer',
+ [$style.transition_modalPopup_enterFrom]: transitionName === 'modal-popup',
+ [$style.transition_modal_enterFrom]: transitionName === 'modal',
+ [$style.transition_send_enterFrom]: transitionName === 'send',
+ })"
+ :leaveToClass="normalizeClass({
+ [$style.transition_modalDrawer_leaveTo]: transitionName === 'modal-drawer',
+ [$style.transition_modalPopup_leaveTo]: transitionName === 'modal-popup',
+ [$style.transition_modal_leaveTo]: transitionName === 'modal',
+ [$style.transition_send_leaveTo]: transitionName === 'send',
+ })"
+ :duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
>
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
@@ -17,7 +37,7 @@
</template>
<script lang="ts" setup>
-import { nextTick, onMounted, watch, provide } from 'vue';
+import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { defaultStore } from '@/store';
@@ -38,7 +58,7 @@ type ModalTypes = 'popup' | 'dialog' | 'drawer';
const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
anchor?: { x: string; y: string; };
- src?: HTMLElement;
+ src?: HTMLElement | null;
preferType?: ModalTypes | 'auto';
zPriority?: 'low' | 'middle' | 'high';
noOverlap?: boolean;
@@ -264,6 +284,10 @@ const onOpened = () => {
}, { passive: true });
};
+const alignObserver = new ResizeObserver((entries, observer) => {
+ align();
+});
+
onMounted(() => {
watch(() => props.src, async () => {
if (props.src) {
@@ -278,12 +302,14 @@ onMounted(() => {
}, { immediate: true });
nextTick(() => {
- new ResizeObserver((entries, observer) => {
- align();
- }).observe(content!);
+ alignObserver.observe(content!);
});
});
+onUnmounted(() => {
+ alignObserver.disconnect();
+});
+
defineExpose({
close,
});
@@ -339,8 +365,8 @@ defineExpose({
}
}
-.transition_modal-popup_enterActive,
-.transition_modal-popup_leaveActive {
+.transition_modalPopup_enterActive,
+.transition_modalPopup_leaveActive {
> .bg {
transition: opacity 0.1s !important;
}
@@ -350,8 +376,8 @@ defineExpose({
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
}
}
-.transition_modal-popup_enterFrom,
-.transition_modal-popup_leaveTo {
+.transition_modalPopup_enterFrom,
+.transition_modalPopup_leaveTo {
> .bg {
opacity: 0;
}
@@ -364,7 +390,7 @@ defineExpose({
}
}
-.transition_modal-drawer_enterActive {
+.transition_modalDrawer_enterActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -373,7 +399,7 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
-.transition_modal-drawer_leaveActive {
+.transition_modalDrawer_leaveActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -382,8 +408,8 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
-.transition_modal-drawer_enterFrom,
-.transition_modal-drawer_leaveTo {
+.transition_modalDrawer_enterFrom,
+.transition_modalDrawer_leaveTo {
> .bg {
opacity: 0;
}
diff --git a/packages/frontend/src/components/MkModalPageWindow.vue b/packages/frontend/src/components/MkModalPageWindow.vue
deleted file mode 100644
index b38865f525..0000000000
--- a/packages/frontend/src/components/MkModalPageWindow.vue
+++ /dev/null
@@ -1,182 +0,0 @@
-<template>
-<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
- <div ref="rootEl" class="hrmcaedk" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
- <div class="header" @contextmenu="onContextmenu">
- <button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ti ti-arrow-left"></i></button>
- <span v-else style="display: inline-block; width: 20px"></span>
- <span v-if="pageMetadata?.value" class="title">
- <i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
- <span>{{ pageMetadata?.value.title }}</span>
- </span>
- <button class="_button" @click="$refs.modal.close()"><i class="ti ti-x"></i></button>
- </div>
- <div class="body" style="container-type: inline-size;">
- <MkStickyContainer>
- <template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template>
- <RouterView :router="router"/>
- </MkStickyContainer>
- </div>
- </div>
-</MkModal>
-</template>
-
-<script lang="ts" setup>
-import { ComputedRef, provide } from 'vue';
-import MkModal from '@/components/MkModal.vue';
-import { popout as _popout } from '@/scripts/popout';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { url } from '@/config';
-import * as os from '@/os';
-import { mainRouter, routes } from '@/router';
-import { i18n } from '@/i18n';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
-import { Router } from '@/nirax';
-
-const props = defineProps<{
- initialPath: string;
-}>();
-
-defineEmits<{
- (ev: 'closed'): void;
- (ev: 'click'): void;
-}>();
-
-const router = new Router(routes, props.initialPath);
-
-router.addListener('push', ctx => {
-
-});
-
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
-let rootEl = $ref();
-let modal = $shallowRef<InstanceType<typeof MkModal>>();
-let path = $ref(props.initialPath);
-let width = $ref(860);
-let height = $ref(660);
-const history = [];
-
-provide('router', router);
-provideMetadataReceiver((info) => {
- pageMetadata = info;
-});
-provide('shouldOmitHeaderTitle', true);
-provide('shouldHeaderThin', true);
-
-const pageUrl = $computed(() => url + path);
-const contextmenu = $computed(() => {
- return [{
- type: 'label',
- text: path,
- }, {
- icon: 'ti ti-player-eject',
- text: i18n.ts.showInPage,
- action: expand,
- }, {
- icon: 'ti ti-window-maximize',
- text: i18n.ts.popout,
- action: popout,
- }, null, {
- icon: 'ti ti-external-link',
- text: i18n.ts.openInNewTab,
- action: () => {
- window.open(pageUrl, '_blank');
- modal.close();
- },
- }, {
- icon: 'ti ti-link',
- text: i18n.ts.copyLink,
- action: () => {
- copyToClipboard(pageUrl);
- },
- }];
-});
-
-function navigate(path, record = true) {
- if (record) history.push(router.getCurrentPath());
- router.push(path);
-}
-
-function back() {
- navigate(history.pop(), false);
-}
-
-function expand() {
- mainRouter.push(path);
- modal.close();
-}
-
-function popout() {
- _popout(path, rootEl);
- modal.close();
-}
-
-function onContextmenu(ev: MouseEvent) {
- os.contextMenu(contextmenu, ev);
-}
-</script>
-
-<style lang="scss" scoped>
-.hrmcaedk {
- margin: auto;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- contain: content;
- border-radius: var(--radius);
-
- --root-margin: 24px;
-
- @media (max-width: 500px) {
- --root-margin: 16px;
- }
-
- > .header {
- $height: 52px;
- $height-narrow: 42px;
- display: flex;
- flex-shrink: 0;
- height: $height;
- line-height: $height;
- font-weight: bold;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- background: var(--windowHeader);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
-
- > button {
- height: $height;
- width: $height;
-
- &:hover {
- color: var(--fgHighlighted);
- }
- }
-
- @media (max-width: 500px) {
- height: $height-narrow;
- line-height: $height-narrow;
- padding-left: 16px;
-
- > button {
- height: $height-narrow;
- width: $height-narrow;
- }
- }
-
- > .title {
- flex: 1;
-
- > .icon {
- margin-right: 0.5em;
- }
- }
- }
-
- > .body {
- overflow: auto;
- background: var(--bg);
- }
-}
-</style>
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index 63c55b904a..08569b4d6e 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
+<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')">
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
<div ref="headerEl" :class="$style.header">
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index d95f8de311..7c9ddadbf8 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -44,8 +44,8 @@
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
<div :class="$style.main">
- <MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/>
- <MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/>
+ <MkNoteHeader :note="appearNote" :mini="true"/>
+ <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
@@ -55,17 +55,17 @@
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
- <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
+ <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
- <div v-else :class="$style.translated">
+ <div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
- <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
+ <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div>
</div>
</div>
- <div v-if="appearNote.files.length > 0" :class="$style.files">
- <MkMediaList :media-list="appearNote.files"/>
+ <div v-if="appearNote.files.length > 0">
+ <MkMediaList :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
@@ -79,7 +79,7 @@
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div>
- <MkReactionsViewer :note="appearNote" :max-number="16">
+ <MkReactionsViewer :note="appearNote" :maxNumber="16">
<template #more>
<button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
{{ i18n.ts.more }}
@@ -205,8 +205,11 @@ const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const isLong = (appearNote.cw == null && appearNote.text != null && (
+ (appearNote.text.includes('$[x2')) ||
(appearNote.text.includes('$[x3')) ||
(appearNote.text.includes('$[x4')) ||
+ (appearNote.text.includes('$[scale')) ||
+ (appearNote.text.includes('$[position')) ||
(appearNote.text.split('\n').length > 9) ||
(appearNote.text.length > 500) ||
(appearNote.files.length >= 5) ||
@@ -274,7 +277,7 @@ function renote(viaKeyboard = false) {
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
-
+
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
@@ -305,7 +308,7 @@ function renote(viaKeyboard = false) {
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
-
+
os.api('notes/create', {
renoteId: appearNote.id,
}).then(() => {
@@ -379,6 +382,8 @@ function undoReact(note): void {
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
+ // 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
+ if (el.tagName === 'AUDIO') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 0d6d329d98..a65039277b 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -4,25 +4,25 @@
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
- class="lxwezrsl"
- :tabindex="!isDeleted ? '-1' : null"
- :class="{ renote: isRenote }"
+ :class="$style.root"
>
- <MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
- <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
- <div v-if="isRenote" class="renote">
- <MkAvatar class="avatar" :user="note.user" link preview/>
- <i class="ti ti-repeat"></i>
- <I18n :src="i18n.ts.renotedBy" tag="span">
- <template #user>
- <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
- <MkUserName :user="note.user"/>
- </MkA>
- </template>
- </I18n>
- <div class="info">
- <button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
- <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
+ <MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
+ <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
+ <div v-if="isRenote" :class="$style.renote">
+ <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
+ <i class="ti ti-repeat" style="margin-right: 4px;"></i>
+ <span :class="$style.renoteText">
+ <I18n :src="i18n.ts.renotedBy" tag="span">
+ <template #user>
+ <MkA v-user-preview="note.userId" :class="$style.renoteName" :to="userPage(note.user)">
+ <MkUserName :user="note.user"/>
+ </MkA>
+ </template>
+ </I18n>
+ </span>
+ <div :class="$style.renoteInfo">
+ <button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
+ <i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
<MkTime :time="note.createdAt"/>
</button>
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
@@ -33,16 +33,16 @@
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
</div>
</div>
- <article class="article" @contextmenu.stop="onContextmenu">
- <header class="header">
- <MkAvatar class="avatar" :user="appearNote.user" indicator link preview/>
- <div class="body">
- <div class="top">
- <MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)">
+ <article :class="$style.note" @contextmenu.stop="onContextmenu">
+ <header :class="$style.noteHeader">
+ <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
+ <div :class="$style.noteHeaderBody">
+ <div>
+ <MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
<MkUserName :nowrap="false" :user="appearNote.user"/>
</MkA>
- <span v-if="appearNote.user.isBot" class="is-bot">bot</span>
- <div class="info">
+ <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
+ <div :class="$style.noteHeaderInfo">
<span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
@@ -51,84 +51,81 @@
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
</div>
</div>
- <div class="username"><MkAcct :user="appearNote.user"/></div>
- <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
+ <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
+ <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
</div>
</header>
- <div class="main">
- <div class="body">
- <p v-if="appearNote.cw != null" class="cw">
- <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
- <MkCwButton v-model="showContent" :note="appearNote"/>
- </p>
- <div v-show="appearNote.cw == null || showContent" class="content">
- <div class="text">
- <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
- <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
- <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
- <a v-if="appearNote.renote != null" class="rp">RN:</a>
- <div v-if="translating || translation" class="translation">
- <MkLoading v-if="translating" mini/>
- <div v-else class="translated">
- <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
- <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
- </div>
- </div>
+ <div :class="$style.noteContent">
+ <p v-if="appearNote.cw != null" :class="$style.cw">
+ <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
+ <MkCwButton v-model="showContent" :note="appearNote"/>
+ </p>
+ <div v-show="appearNote.cw == null || showContent">
+ <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
+ <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
+ <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
+ <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
+ <div v-if="translating || translation" :class="$style.translation">
+ <MkLoading v-if="translating" mini/>
+ <div v-else>
+ <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
+ <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div>
- <div v-if="appearNote.files.length > 0" class="files">
- <MkMediaList :media-list="appearNote.files"/>
- </div>
- <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
- <div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div>
</div>
- <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
- </div>
- <footer class="footer">
- <div class="info">
- <MkA class="created-at" :to="notePage(appearNote)">
- <MkTime :time="appearNote.createdAt" mode="detail"/>
- </MkA>
+ <div v-if="appearNote.files.length > 0">
+ <MkMediaList :mediaList="appearNote.files"/>
</div>
- <MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
- <button class="button _button" @click="reply()">
- <i class="ti ti-arrow-back-up"></i>
- <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
- </button>
- <button
- v-if="canRenote"
- ref="renoteButton"
- class="button _button"
- @mousedown="renote()"
- >
- <i class="ti ti-repeat"></i>
- <p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
- </button>
- <button v-else class="button _button" disabled>
- <i class="ti ti-ban"></i>
- </button>
- <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
- <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
- <i v-else class="ti ti-plus"></i>
- </button>
- <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
- <i class="ti ti-minus"></i>
- </button>
- <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()">
- <i class="ti ti-paperclip"></i>
- </button>
- <button ref="menuButton" class="button _button" @mousedown="menu()">
- <i class="ti ti-dots"></i>
- </button>
- </footer>
+ <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
+ <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
+ </div>
+ <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div>
+ <footer>
+ <div :class="$style.noteFooterInfo">
+ <MkA :to="notePage(appearNote)">
+ <MkTime :time="appearNote.createdAt" mode="detail"/>
+ </MkA>
+ </div>
+ <MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
+ <button class="_button" :class="$style.noteFooterButton" @click="reply()">
+ <i class="ti ti-arrow-back-up"></i>
+ <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p>
+ </button>
+ <button
+ v-if="canRenote"
+ ref="renoteButton"
+ class="_button"
+ :class="$style.noteFooterButton"
+ @mousedown="renote()"
+ >
+ <i class="ti ti-repeat"></i>
+ <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p>
+ </button>
+ <button v-else class="_button" :class="$style.noteFooterButton" disabled>
+ <i class="ti ti-ban"></i>
+ </button>
+ <button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
+ <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
+ <i v-else class="ti ti-plus"></i>
+ </button>
+ <button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
+ <i class="ti ti-minus"></i>
+ </button>
+ <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
+ <i class="ti ti-paperclip"></i>
+ </button>
+ <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
+ <i class="ti ti-dots"></i>
+ </button>
+ </footer>
</article>
- <MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
+ <MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
</div>
-<div v-else class="_panel muted" @click="muted = false">
+<div v-else class="_panel" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
- <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
+ <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/>
</MkA>
</template>
@@ -438,318 +435,249 @@ if (appearNote.replyId) {
}
</script>
-<style lang="scss" scoped>
-.lxwezrsl {
+<style lang="scss" module>
+.root {
position: relative;
transition: box-shadow 0.1s ease;
overflow: clip;
contain: content;
+}
- &:focus-visible {
- outline: none;
-
- &:after {
- content: "";
- pointer-events: none;
- display: block;
- position: absolute;
- z-index: 10;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- margin: auto;
- width: calc(100% - 8px);
- height: calc(100% - 8px);
- border: dashed 1px var(--focus);
- border-radius: var(--radius);
- box-sizing: border-box;
- }
- }
-
- &:hover > .article > .main > .footer > .button {
- opacity: 1;
- }
-
- > .reply-to {
- opacity: 0.7;
- padding-bottom: 0;
- }
+.replyTo {
+ opacity: 0.7;
+ padding-bottom: 0;
+}
- > .reply-to-more {
- opacity: 0.7;
- }
+.replyToMore {
+ opacity: 0.7;
+}
- > .renote {
- display: flex;
- align-items: center;
- padding: 16px 32px 8px 32px;
- line-height: 28px;
- white-space: pre;
- color: var(--renote);
+.renote {
+ display: flex;
+ align-items: center;
+ padding: 16px 32px 8px 32px;
+ line-height: 28px;
+ white-space: pre;
+ color: var(--renote);
+}
- > .avatar {
- flex-shrink: 0;
- display: inline-block;
- width: 28px;
- height: 28px;
- margin: 0 8px 0 0;
- border-radius: 6px;
- }
+.renoteAvatar {
+ flex-shrink: 0;
+ display: inline-block;
+ width: 28px;
+ height: 28px;
+ margin: 0 8px 0 0;
+ border-radius: 6px;
+}
- > i {
- margin-right: 4px;
- }
+.renoteText {
+ overflow: hidden;
+ flex-shrink: 1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
- > span {
- overflow: hidden;
- flex-shrink: 1;
- text-overflow: ellipsis;
- white-space: nowrap;
+.renoteName {
+ font-weight: bold;
+}
- > .name {
- font-weight: bold;
- }
- }
+.renoteInfo {
+ margin-left: auto;
+ font-size: 0.9em;
+}
- > .info {
- margin-left: auto;
- font-size: 0.9em;
+.renoteTime {
+ flex-shrink: 0;
+ color: inherit;
+}
- > .time {
- flex-shrink: 0;
- color: inherit;
+.renote + .note {
+ padding-top: 8px;
+}
- > .dropdownIcon {
- margin-right: 4px;
- }
- }
- }
- }
+.note {
+ padding: 32px;
+ font-size: 1.2em;
- > .renote + .article {
- padding-top: 8px;
+ &:hover > .main > .footer > .button {
+ opacity: 1;
}
+}
- > .article {
- padding: 32px;
- font-size: 1.2em;
-
- > .header {
- display: flex;
- position: relative;
- margin-bottom: 16px;
- align-items: center;
-
- > .avatar {
- display: block;
- flex-shrink: 0;
- width: 58px;
- height: 58px;
- }
-
- > .body {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding-left: 16px;
- font-size: 0.95em;
-
- > .top {
- > .name {
- font-weight: bold;
- line-height: 1.3;
- }
+.noteHeader {
+ display: flex;
+ position: relative;
+ margin-bottom: 16px;
+ align-items: center;
+}
- > .is-bot {
- display: inline-block;
- margin: 0 0.5em;
- padding: 4px 6px;
- font-size: 80%;
- line-height: 1;
- border: solid 0.5px var(--divider);
- border-radius: 4px;
- }
+.noteHeaderAvatar {
+ display: block;
+ flex-shrink: 0;
+ width: 58px;
+ height: 58px;
+}
- > .info {
- float: right;
- }
- }
+.noteHeaderBody {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding-left: 16px;
+ font-size: 0.95em;
+}
- > .username {
- margin-bottom: 2px;
- line-height: 1.3;
- word-wrap: anywhere;
- }
- }
- }
+.noteHeaderName {
+ font-weight: bold;
+ line-height: 1.3;
+}
- > .main {
- > .body {
- container-type: inline-size;
+.isBot {
+ display: inline-block;
+ margin: 0 0.5em;
+ padding: 4px 6px;
+ font-size: 80%;
+ line-height: 1;
+ border: solid 0.5px var(--divider);
+ border-radius: 4px;
+}
- > .cw {
- cursor: default;
- display: block;
- margin: 0;
- padding: 0;
- overflow-wrap: break-word;
+.noteHeaderInfo {
+ float: right;
+}
- > .text {
- margin-right: 8px;
- }
- }
+.noteHeaderUsername {
+ margin-bottom: 2px;
+ line-height: 1.3;
+ word-wrap: anywhere;
+}
- > .content {
- > .text {
- overflow-wrap: break-word;
+.noteContent {
+ container-type: inline-size;
+ overflow-wrap: break-word;
+}
- > .reply {
- color: var(--accent);
- margin-right: 0.5em;
- }
+.cw {
+ cursor: default;
+ display: block;
+ margin: 0;
+ padding: 0;
+ overflow-wrap: break-word;
+}
- > .rp {
- margin-left: 4px;
- font-style: oblique;
- color: var(--renote);
- }
+.noteReplyTarget {
+ color: var(--accent);
+ margin-right: 0.5em;
+}
- > .translation {
- border: solid 0.5px var(--divider);
- border-radius: var(--radius);
- padding: 12px;
- margin-top: 8px;
- }
- }
+.rn {
+ margin-left: 4px;
+ font-style: oblique;
+ color: var(--renote);
+}
- > .url-preview {
- margin-top: 8px;
- }
+.translation {
+ border: solid 0.5px var(--divider);
+ border-radius: var(--radius);
+ padding: 12px;
+ margin-top: 8px;
+}
- > .poll {
- font-size: 80%;
- }
+.poll {
+ font-size: 80%;
+}
- > .renote {
- padding: 8px 0;
+.quote {
+ padding: 8px 0;
+}
- > .note {
- padding: 16px;
- border: dashed 1px var(--renote);
- border-radius: 8px;
- }
- }
- }
+.quoteNote {
+ padding: 16px;
+ border: dashed 1px var(--renote);
+ border-radius: 8px;
+}
- > .channel {
- opacity: 0.7;
- font-size: 80%;
- }
- }
+.channel {
+ opacity: 0.7;
+ font-size: 80%;
+}
- > .footer {
- > .info {
- margin: 16px 0;
- opacity: 0.7;
- font-size: 0.9em;
- }
+.noteFooterInfo {
+ margin: 16px 0;
+ opacity: 0.7;
+ font-size: 0.9em;
+}
- > .button {
- margin: 0;
- padding: 8px;
- opacity: 0.7;
+.noteFooterButton {
+ margin: 0;
+ padding: 8px;
+ opacity: 0.7;
- &:not(:last-child) {
- margin-right: 28px;
- }
+ &:not(:last-child) {
+ margin-right: 28px;
+ }
- &:hover {
- color: var(--fgHighlighted);
- }
+ &:hover {
+ color: var(--fgHighlighted);
+ }
+}
- > .count {
- display: inline;
- margin: 0 0 0 8px;
- opacity: 0.7;
- }
+.noteFooterButtonCount {
+ display: inline;
+ margin: 0 0 0 8px;
+ opacity: 0.7;
- &.reacted {
- color: var(--accent);
- }
- }
- }
- }
+ &.reacted {
+ color: var(--accent);
}
+}
- > .reply {
- border-top: solid 0.5px var(--divider);
- }
+.reply {
+ border-top: solid 0.5px var(--divider);
}
@container (max-width: 500px) {
- .lxwezrsl {
+ .root {
font-size: 0.9em;
}
}
@container (max-width: 450px) {
- .lxwezrsl {
- > .renote {
- padding: 8px 16px 0 16px;
- }
+ .renote {
+ padding: 8px 16px 0 16px;
+ }
- > .article {
- padding: 16px;
+ .note {
+ padding: 16px;
+ }
- > .header {
- > .avatar {
- width: 50px;
- height: 50px;
- }
- }
- }
+ .noteHeaderAvatar {
+ width: 50px;
+ height: 50px;
}
}
@container (max-width: 350px) {
- .lxwezrsl {
- > .article {
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 18px;
- }
- }
- }
- }
+ .noteFooterButton {
+ &:not(:last-child) {
+ margin-right: 18px;
}
}
}
@container (max-width: 300px) {
- .lxwezrsl {
+ .root {
font-size: 0.825em;
+ }
- > .article {
- > .header {
- > .avatar {
- width: 50px;
- height: 50px;
- }
- }
+ .noteHeaderAvatar {
+ width: 50px;
+ height: 50px;
+ }
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 12px;
- }
- }
- }
- }
+ .noteFooterButton {
+ &:not(:last-child) {
+ margin-right: 12px;
}
}
}
diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue
index 6b55c27869..6786f8b256 100644
--- a/packages/frontend/src/components/MkNotePreview.vue
+++ b/packages/frontend/src/components/MkNotePreview.vue
@@ -6,7 +6,7 @@
<MkUserName :user="$i" :nowrap="true"/>
</div>
<div>
- <div :class="$style.content">
+ <div>
<Mfm :text="text.trim()" :author="$i" :i="$i"/>
</div>
</div>
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index bd27a43b61..21be1454a7 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -5,7 +5,7 @@
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div>
<p v-if="note.cw != null" :class="$style.cw">
- <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
+ <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emojiUrls="note.emojis"/>
<MkCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent">
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index a4e949c898..9cc2b7a967 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -15,7 +15,7 @@
:items="notes"
:direction="pagination.reversed ? 'up' : 'down'"
:reversed="pagination.reversed"
- :no-gap="noGap"
+ :noGap="noGap"
:ad="true"
:class="$style.notes"
>
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index efae687e66..d25332b10f 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -5,7 +5,19 @@
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
- <div :class="[$style.subIcon, $style['t_' + notification.type]]">
+ <div
+ :class="[$style.subIcon, {
+ [$style.t_follow]: notification.type === 'follow',
+ [$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted',
+ [$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest',
+ [$style.t_renote]: notification.type === 'renote',
+ [$style.t_reply]: notification.type === 'reply',
+ [$style.t_mention]: notification.type === 'mention',
+ [$style.t_quote]: notification.type === 'quote',
+ [$style.t_pollEnded]: notification.type === 'pollEnded',
+ [$style.t_achievementEarned]: notification.type === 'achievementEarned',
+ }]"
+ >
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
@@ -20,8 +32,8 @@
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
- :custom-emojis="notification.note.emojis"
- :no-style="true"
+ :customEmojis="notification.note.emojis"
+ :noStyle="true"
style="width: 100%; height: 100%;"
/>
</div>
@@ -34,7 +46,7 @@
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header>
- <div :class="$style.content">
+ <div>
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
@@ -243,9 +255,6 @@ useTooltip(reactionRef, (showing) => {
font-size: 0.9em;
}
-.content {
-}
-
.text {
display: flex;
width: 100%;
diff --git a/packages/frontend/src/components/MkNotificationSettingWindow.vue b/packages/frontend/src/components/MkNotificationSettingWindow.vue
index f6d0e5681d..598d3a0551 100644
--- a/packages/frontend/src/components/MkNotificationSettingWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSettingWindow.vue
@@ -3,15 +3,15 @@
ref="dialog"
:width="400"
:height="450"
- :with-ok-button="true"
- :ok-button-disabled="false"
+ :withOkButton="true"
+ :okButtonDisabled="false"
@ok="ok()"
@close="dialog?.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.notificationSetting }}</template>
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<template v-if="showGlobalToggle">
<MkSwitch v-model="useGlobalSetting">
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 1aea95fe0e..70224bffa1 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -8,9 +8,9 @@
</template>
<template #default="{ items: notifications }">
- <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
+ <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
- <XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
+ <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
</MkDateSeparatedList>
</template>
</MkPagination>
@@ -22,7 +22,7 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { notificationTypes } from '@/const';
@@ -45,7 +45,7 @@ const pagination: Paging = {
const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
- stream.send('readNotification');
+ useStream().send('readNotification');
}
if (!isMuted) {
@@ -56,7 +56,7 @@ const onNotification = (notification) => {
let connection;
onMounted(() => {
- connection = stream.useChannel('main');
+ connection = useStream().useChannel('main');
connection.on('notification', onNotification);
});
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index e7fc73bce3..d48e7886eb 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -28,54 +28,38 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent, reactive } from 'vue';
+<script lang="ts" setup>
+import { reactive } from 'vue';
import number from '@/filters/number';
+import XValue from '@/components/MkObjectView.value.vue';
-export default defineComponent({
- name: 'XValue',
+const props = defineProps<{
+ value: any;
+}>();
- props: {
- value: {
- required: true,
- },
- },
+const collapsed = reactive({});
- setup(props) {
- const collapsed = reactive({});
-
- if (isObject(props.value)) {
- for (const key in props.value) {
- collapsed[key] = collapsable(props.value[key]);
- }
- }
-
- function isObject(v): boolean {
- return typeof v === 'object' && !Array.isArray(v) && v !== null;
- }
+if (isObject(props.value)) {
+ for (const key in props.value) {
+ collapsed[key] = collapsable(props.value[key]);
+ }
+}
- function isArray(v): boolean {
- return Array.isArray(v);
- }
+function isObject(v): boolean {
+ return typeof v === 'object' && !Array.isArray(v) && v !== null;
+}
- function isEmpty(v): boolean {
- return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
- }
+function isArray(v): boolean {
+ return Array.isArray(v);
+}
- function collapsable(v): boolean {
- return (isObject(v) || isArray(v)) && !isEmpty(v);
- }
+function isEmpty(v): boolean {
+ return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
+}
- return {
- number,
- collapsed,
- isObject,
- isArray,
- isEmpty,
- collapsable,
- };
- },
-});
+function collapsable(v): boolean {
+ return (isObject(v) || isArray(v)) && !isEmpty(v);
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkObjectView.vue b/packages/frontend/src/components/MkObjectView.vue
index 55578a37f6..8b1ed74142 100644
--- a/packages/frontend/src/components/MkObjectView.vue
+++ b/packages/frontend/src/components/MkObjectView.vue
@@ -1,5 +1,5 @@
<template>
-<div class="zhyxdalp">
+<div>
<XValue :value="value" :collapsed="false"/>
</div>
</template>
@@ -12,9 +12,3 @@ const props = defineProps<{
value: Record<string, unknown>;
}>();
</script>
-
-<style lang="scss" scoped>
-.zhyxdalp {
-
-}
-</style>
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index e2d68d12c3..668f9ff5af 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -8,7 +8,7 @@
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, onUnmounted } from 'vue';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
@@ -21,16 +21,22 @@ let content = $shallowRef<HTMLElement>();
let omitted = $ref(false);
let ignoreOmit = $ref(false);
-onMounted(() => {
- const calcOmit = () => {
- if (omitted || ignoreOmit) return;
- omitted = content.offsetHeight > props.maxHeight;
- };
+const calcOmit = () => {
+ if (omitted || ignoreOmit) return;
+ omitted = content.offsetHeight > props.maxHeight;
+};
+const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit();
- new ResizeObserver((entries, observer) => {
- calcOmit();
- }).observe(content);
+});
+
+onMounted(() => {
+ calcOmit();
+ omitObserver.observe(content);
+});
+
+onUnmounted(() => {
+ omitObserver.disconnect();
});
</script>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 02ce58451d..709b5a52df 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -1,23 +1,23 @@
<template>
<MkWindow
ref="windowEl"
- :initial-width="500"
- :initial-height="500"
- :can-resize="true"
- :close-button="true"
- :buttons-left="buttonsLeft"
- :buttons-right="buttonsRight"
+ :initialWidth="500"
+ :initialHeight="500"
+ :canResize="true"
+ :closeButton="true"
+ :buttonsLeft="buttonsLeft"
+ :buttonsRight="buttonsRight"
:contextmenu="contextmenu"
@closed="$emit('closed')"
>
<template #header>
<template v-if="pageMetadata?.value">
- <i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
+ <i v-if="pageMetadata.value.icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageMetadata.value.title }}</span>
</template>
</template>
- <div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
+ <div :class="$style.root" style="container-type: inline-size;">
<RouterView :key="reloadCount" :router="router"/>
</div>
</MkWindow>
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index cd8af560e4..740094b113 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -1,9 +1,9 @@
<template>
<Transition
- :enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
mode="out-in"
>
<MkLoading v-if="fetching"/>
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 0810061ff9..464e340116 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -1,19 +1,19 @@
<template>
-<div class="tivcixzd" :class="{ done: closed || isVoted }">
- <ul>
- <li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
- <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
- <span>
- <template v-if="choice.isVoted"><i class="ti ti-check"></i></template>
+<div :class="{ [$style.done]: closed || isVoted }">
+ <ul :class="$style.choices">
+ <li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
+ <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
+ <span :class="$style.fg">
+ <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
<Mfm :text="choice.text" :plain="true"/>
- <span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
+ <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
</span>
</li>
</ul>
- <p v-if="!readOnly">
+ <p v-if="!readOnly" :class="$style.info">
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
<span> · </span>
- <a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
+ <a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
<span v-if="remaining > 0"> · {{ timer }}</span>
@@ -86,67 +86,51 @@ const vote = async (id) => {
};
</script>
-<style lang="scss" scoped>
-.tivcixzd {
- > ul {
- display: block;
- margin: 0;
- padding: 0;
- list-style: none;
-
- > li {
- display: block;
- position: relative;
- margin: 4px 0;
- padding: 4px;
- //border: solid 0.5px var(--divider);
- background: var(--accentedBg);
- border-radius: 4px;
- overflow: clip;
- cursor: pointer;
-
- > .backdrop {
- position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- background: var(--accent);
- background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
- transition: width 1s ease;
- }
-
- > span {
- position: relative;
- display: inline-block;
- padding: 3px 5px;
- background: var(--panel);
- border-radius: 3px;
+<style lang="scss" module>
+.choices {
+ display: block;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
- > i {
- margin-right: 4px;
- color: var(--accent);
- }
+.choice {
+ display: block;
+ position: relative;
+ margin: 4px 0;
+ padding: 4px;
+ //border: solid 0.5px var(--divider);
+ background: var(--accentedBg);
+ border-radius: 4px;
+ overflow: clip;
+ cursor: pointer;
+}
- > .votes {
- margin-left: 4px;
- opacity: 0.7;
- }
- }
- }
- }
+.bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background: var(--accent);
+ background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
+ transition: width 1s ease;
+}
- > p {
- color: var(--fg);
+.fg {
+ position: relative;
+ display: inline-block;
+ padding: 3px 5px;
+ background: var(--panel);
+ border-radius: 3px;
+}
- a {
- color: inherit;
- }
- }
+.info {
+ color: var(--fg);
+}
- &.done {
- > ul > li {
- cursor: default;
- }
+.done {
+ .choice {
+ cursor: default;
}
}
</style>
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index 471ec39169..2da9339944 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -5,7 +5,7 @@
</p>
<ul>
<li v-for="(choice, i) in choices" :key="i">
- <MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:model-value="onInput(i, $event)">
+ <MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
</MkInput>
<button class="_button" @click="remove(i)">
<i class="ti ti-x"></i>
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 93b9eb401d..30af365669 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -1,6 +1,6 @@
<template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
- <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
+ <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
</MkModal>
</template>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index c65cb7d6e5..5c65569683 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -22,21 +22,21 @@
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span>
</button>
- <button v-else :class="['_button', $style.headerRightItem, $style.visibility]" disabled>
+ <button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled>
<span><i class="ti ti-device-tv"></i></span>
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
</button>
</template>
- <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" :class="['_button', $style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
+ <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-else><i class="ti ti-rocket-off"></i></span>
</button>
- <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" :class="['_button', $style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance }]" @click="toggleReactionAcceptance">
+ <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ti ti-icons"></i></span>
</button>
- <button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
+ <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div :class="$style.submitInner">
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
@@ -66,7 +66,7 @@
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
- <XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
+ <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
<div v-if="showingOptions" style="padding: 8px 16px;">
@@ -484,8 +484,10 @@ async function toggleReactionAcceptance() {
title: i18n.ts.reactionAcceptance,
items: [
{ value: null, text: i18n.ts.all },
- { value: 'likeOnly' as const, text: i18n.ts.likeOnly },
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
+ { value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
+ { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
+ { value: 'likeOnly' as const, text: i18n.ts.likeOnly },
],
default: reactionAcceptance,
});
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 760c6e5d08..18fa142ebc 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -1,16 +1,16 @@
<template>
-<div v-show="props.modelValue.length != 0" class="skeikyzd">
- <Sortable :model-value="props.modelValue" class="files" item-key="id" :animation="150" :delay="100" :delay-on-touch-only="true" @update:model-value="v => emit('update:modelValue', v)">
+<div v-show="props.modelValue.length != 0" :class="$style.root">
+ <Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
<template #item="{element}">
- <div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
- <MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
- <div v-if="element.isSensitive" class="sensitive">
- <i class="ti ti-alert-triangle icon"></i>
+ <div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
+ <MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
+ <div v-if="element.isSensitive" :class="$style.sensitive">
+ <i class="ti ti-alert-triangle" style="margin: auto;"></i>
</div>
</div>
</template>
</Sortable>
- <p class="remain">{{ 16 - props.modelValue.length }}/16</p>
+ <p :class="$style.remain">{{ 16 - props.modelValue.length }}/16</p>
</div>
</template>
@@ -93,7 +93,7 @@ function showFileMenu(file, ev: MouseEvent) {
action: () => { rename(file); },
}, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
- icon: file.isSensitive ? 'ti ti-eye-off' : 'ti ti-eye',
+ icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye',
action: () => { toggleSensitive(file); },
}, {
text: i18n.ts.describeFile,
@@ -108,60 +108,53 @@ function showFileMenu(file, ev: MouseEvent) {
}
</script>
-<style lang="scss" scoped>
-.skeikyzd {
+<style lang="scss" module>
+.root {
padding: 8px 16px;
position: relative;
+}
- > .files {
- display: flex;
- flex-wrap: wrap;
-
- > .file {
- position: relative;
- width: 64px;
- height: 64px;
- margin-right: 4px;
- border-radius: 4px;
- overflow: hidden;
- cursor: move;
-
- &:hover > .remove {
- display: block;
- }
+.files {
+ display: flex;
+ flex-wrap: wrap;
+}
- > .thumbnail {
- width: 100%;
- height: 100%;
- z-index: 1;
- color: var(--fg);
- }
+.file {
+ position: relative;
+ width: 64px;
+ height: 64px;
+ margin-right: 4px;
+ border-radius: 4px;
+ overflow: hidden;
+ cursor: move;
+}
- > .sensitive {
- display: flex;
- position: absolute;
- width: 64px;
- height: 64px;
- top: 0;
- left: 0;
- z-index: 2;
- background: rgba(17, 17, 17, .7);
- color: #fff;
+.thumbnail {
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ color: var(--fg);
+}
- > .icon {
- margin: auto;
- }
- }
- }
- }
+.sensitive {
+ display: flex;
+ position: absolute;
+ width: 64px;
+ height: 64px;
+ top: 0;
+ left: 0;
+ z-index: 2;
+ background: rgba(17, 17, 17, .7);
+ color: #fff;
+}
- > .remain {
- display: block;
- position: absolute;
- top: 8px;
- right: 8px;
- margin: 0;
- padding: 0;
- }
+.remain {
+ display: block;
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ margin: 0;
+ padding: 0;
+ font-size: 90%;
}
</style>
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index 6326c498d7..98af92c6f8 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -1,6 +1,6 @@
<template>
-<MkModal ref="modal" :prefer-type="'dialog'" @click="modal.close()" @closed="onModalClosed()">
- <MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freeze-after-posted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
+<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()">
+ <MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
</MkModal>
</template>
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index b98c814f24..448084d9ba 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -72,28 +72,28 @@ function subscribe() {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
})
- .then(async subscription => {
- pushSubscription = subscription;
+ .then(async subscription => {
+ pushSubscription = subscription;
- // Register
- pushRegistrationInServer = await api('sw/register', {
- endpoint: subscription.endpoint,
- auth: encode(subscription.getKey('auth')),
- publickey: encode(subscription.getKey('p256dh')),
- });
- }, async err => { // When subscribe failed
+ // Register
+ pushRegistrationInServer = await api('sw/register', {
+ endpoint: subscription.endpoint,
+ auth: encode(subscription.getKey('auth')),
+ publickey: encode(subscription.getKey('p256dh')),
+ });
+ }, async err => { // When subscribe failed
// 通知が許可されていなかったとき
- if (err?.name === 'NotAllowedError') {
- console.info('User denied the notification permission request.');
- return;
- }
+ if (err?.name === 'NotAllowedError') {
+ console.info('User denied the notification permission request.');
+ return;
+ }
- // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
- // 既に存在していることが原因でエラーになった可能性があるので、
- // そのサブスクリプションを解除しておく
- // (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
- await unsubscribe();
- }), null, null);
+ // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
+ // 既に存在していることが原因でエラーになった可能性があるので、
+ // そのサブスクリプションを解除しておく
+ // (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
+ await unsubscribe();
+ }), null, null);
}
async function unsubscribe() {
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index e2240fb4e1..84be10078a 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -1,37 +1,27 @@
<script lang="ts">
-import { VNode, defineComponent, h } from 'vue';
+import { VNode, defineComponent, h, ref, watch } from 'vue';
import MkRadio from './MkRadio.vue';
export default defineComponent({
- components: {
- MkRadio,
- },
props: {
modelValue: {
required: false,
},
},
- data() {
- return {
- value: this.modelValue,
- };
- },
- watch: {
- value() {
- this.$emit('update:modelValue', this.value);
- },
- },
- render() {
- console.log(this.$slots, this.$slots.label && this.$slots.label());
- if (!this.$slots.default) return null;
- let options = this.$slots.default();
- const label = this.$slots.label && this.$slots.label();
- const caption = this.$slots.caption && this.$slots.caption();
+ setup(props, context) {
+ const value = ref(props.modelValue);
+ watch(value, () => {
+ context.emit('update:modelValue', value.value);
+ });
+ if (!context.slots.default) return null;
+ let options = context.slots.default();
+ const label = context.slots.label && context.slots.label();
+ const caption = context.slots.caption && context.slots.caption();
// なぜかFragmentになることがあるため
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
- return h('div', {
+ return () => h('div', {
class: 'novjtcto',
}, [
...(label ? [h('div', {
@@ -42,8 +32,8 @@ export default defineComponent({
}, options.map(option => h(MkRadio, {
key: option.key,
value: option.props?.value,
- modelValue: this.value,
- 'onUpdate:modelValue': value => this.value = value,
+ modelValue: value.value,
+ 'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)),
),
...(caption ? [h('div', {
diff --git a/packages/frontend/src/components/MkReactedUsersDialog.vue b/packages/frontend/src/components/MkReactedUsersDialog.vue
index 0c0cc36692..cd2a359d5c 100644
--- a/packages/frontend/src/components/MkReactedUsersDialog.vue
+++ b/packages/frontend/src/components/MkReactedUsersDialog.vue
@@ -8,7 +8,7 @@
>
<template #header>{{ i18n.ts.reactionsList }}</template>
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<div v-if="note" class="_gaps">
<div v-if="reactions.length === 0" class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@@ -22,7 +22,7 @@
</button>
</div>
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
- <MkUserCardMini :user="user" :with-chart="false"/>
+ <MkUserCardMini :user="user" :withChart="false"/>
</MkA>
</template>
</div>
diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue
index 29b3f9b85b..dfb06f63c4 100644
--- a/packages/frontend/src/components/MkReactionIcon.vue
+++ b/packages/frontend/src/components/MkReactionIcon.vue
@@ -1,6 +1,6 @@
<template>
-<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :no-style="noStyle" :url="emojiUrl"/>
-<MkEmoji v-else :emoji="reaction" :normal="true" :no-style="noStyle"/>
+<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/>
+<MkEmoji v-else :emoji="reaction" :normal="true" :noStyle="noStyle"/>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue
index 4d67dc3da2..34afa72232 100644
--- a/packages/frontend/src/components/MkReactionTooltip.vue
+++ b/packages/frontend/src/components/MkReactionTooltip.vue
@@ -1,7 +1,7 @@
<template>
-<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
+<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
<div :class="$style.root">
- <MkReactionIcon :reaction="reaction" :class="$style.icon" :no-style="true"/>
+ <MkReactionIcon :reaction="reaction" :class="$style.icon" :noStyle="true"/>
<div :class="$style.name">{{ reaction.replace('@.', '') }}</div>
</div>
</MkTooltip>
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index f5e611c62a..99960f5d25 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -1,8 +1,8 @@
<template>
-<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
+<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
<div :class="$style.root">
<div :class="$style.reaction">
- <MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :no-style="true"/>
+ <MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :noStyle="true"/>
<div :class="$style.reactionName">{{ getReactionName(reaction) }}</div>
</div>
<div :class="$style.users">
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 9480af5102..aabebb3abf 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -6,7 +6,7 @@
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]"
@click="toggleReaction()"
>
- <MkReactionIcon :class="$style.icon" :reaction="reaction" :emoji-url="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/>
+ <MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/>
<span :class="$style.count">{{ count }}</span>
</button>
</template>
@@ -22,6 +22,7 @@ import { $i } from '@/account';
import MkReactionEffect from '@/components/MkReactionEffect.vue';
import { claimAchievement } from '@/scripts/achievements';
import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
const props = defineProps<{
reaction: string;
@@ -34,11 +35,19 @@ const buttonEl = shallowRef<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
-const toggleReaction = () => {
+async function toggleReaction() {
if (!canToggle.value) return;
+ // TODO: その絵文字を使う権限があるかどうか確認
+
const oldReaction = props.note.myReaction;
if (oldReaction) {
+ const confirm = await os.confirm({
+ type: 'warning',
+ text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm,
+ });
+ if (confirm.canceled) return;
+
os.api('notes/reactions/delete', {
noteId: props.note.id,
}).then(() => {
@@ -58,9 +67,9 @@ const toggleReaction = () => {
claimAchievement('reactWithoutRead');
}
}
-};
+}
-const anime = () => {
+function anime() {
if (document.hidden) return;
if (!defaultStore.state.animation) return;
@@ -68,7 +77,7 @@ const anime = () => {
const x = rect.left + 16;
const y = rect.top + (buttonEl.value.offsetHeight / 2);
os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end');
-};
+}
watch(() => props.count, (newCount, oldCount) => {
if (oldCount < newCount) anime();
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 3219c8a92c..ce146463ec 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -1,13 +1,13 @@
<template>
<TransitionGroup
- :enter-active-class="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
- :move-class="defaultStore.state.animation ? $style.transition_x_move : ''"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
+ :moveClass="defaultStore.state.animation ? $style.transition_x_move : ''"
tag="div" :class="$style.root"
>
- <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
+ <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/>
<slot v-if="hasMoreReactions" name="more"/>
</TransitionGroup>
</template>
diff --git a/packages/frontend/src/components/MkRenotedUsersDialog.vue b/packages/frontend/src/components/MkRenotedUsersDialog.vue
index 56025535f1..814a68d4da 100644
--- a/packages/frontend/src/components/MkRenotedUsersDialog.vue
+++ b/packages/frontend/src/components/MkRenotedUsersDialog.vue
@@ -8,7 +8,7 @@
>
<template #header>{{ i18n.ts.renotesList }}</template>
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<div v-if="renotes" class="_gaps">
<div v-if="renotes.length === 0" class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@@ -16,7 +16,7 @@
</div>
<template v-else>
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
- <MkUserCardMini :user="user" :with-chart="false"/>
+ <MkUserCardMini :user="user" :withChart="false"/>
</MkA>
</template>
</div>
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index 8bd0279806..9f56189f3e 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -124,7 +124,3 @@ onMounted(async () => {
});
});
</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue
index 9d93211d5f..60c3a47385 100644
--- a/packages/frontend/src/components/MkRippleEffect.vue
+++ b/packages/frontend/src/components/MkRippleEffect.vue
@@ -1,7 +1,7 @@
<template>
-<div class="vswabwbm" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
+<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
- <circle fill="none" cx="64" cy="64">
+ <circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
<animate
attributeName="r"
begin="0s" dur="0.5s"
@@ -22,7 +22,7 @@
/>
</circle>
<g fill="none" fill-rule="evenodd">
- <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
+ <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
<animate
attributeName="r"
begin="0s" dur="0.8s"
@@ -100,17 +100,11 @@ onMounted(() => {
});
</script>
-<style lang="scss" scoped>
-.vswabwbm {
+<style lang="scss" module>
+.root {
pointer-events: none;
position: fixed;
width: 128px;
height: 128px;
-
- > svg {
- > circle {
- stroke: var(--accent);
- }
- }
}
</style>
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index 2f5866f340..9fbe1ec993 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -12,8 +12,10 @@
</template>
</span>
<span :class="$style.name">{{ role.name }}</span>
- <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
- <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
+ <template v-if="detailed">
+ <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
+ <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
+ </template>
</div>
<div :class="$style.description">{{ role.description }}</div>
</MkA>
@@ -23,10 +25,13 @@
import { } from 'vue';
import { i18n } from '@/i18n';
-const props = defineProps<{
+const props = withDefaults(defineProps<{
role: any;
forModeration: boolean;
-}>();
+ detailed: boolean;
+}>(), {
+ detailed: true,
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkSample.vue b/packages/frontend/src/components/MkSample.vue
deleted file mode 100644
index 922b862b47..0000000000
--- a/packages/frontend/src/components/MkSample.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-<div class="">
- <div class="">
- <MkInput v-model="text">
- <template #label>Text</template>
- </MkInput>
- <MkSwitch v-model="flag">
- <span>Switch is now {{ flag ? 'on' : 'off' }}</span>
- </MkSwitch>
- <div style="margin: 32px 0;">
- <MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
- <MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
- <MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
- </div>
- <MkButton inline>This is</MkButton>
- <MkButton inline primary>the button</MkButton>
- </div>
- <div class="" style="pointer-events: none;">
- <Mfm :text="mfm"/>
- </div>
- <div class="">
- <MkButton inline primary @click="openMenu">Open menu</MkButton>
- <MkButton inline primary @click="openDialog">Open dialog</MkButton>
- <MkButton inline primary @click="openForm">Open form</MkButton>
- <MkButton inline primary @click="openDrive">Open drive</MkButton>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
-import MkRadio from '@/components/MkRadio.vue';
-import * as os from '@/os';
-import * as config from '@/config';
-import { $i } from '@/account';
-
-export default defineComponent({
- components: {
- MkButton,
- MkInput,
- MkSwitch,
- MkTextarea,
- MkRadio,
- },
-
- data() {
- return {
- text: '',
- flag: true,
- radio: 'misskey',
- $i,
- mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`,
- };
- },
-
- methods: {
- async openDialog() {
- os.alert({
- type: 'warning',
- title: 'Oh my Aichan',
- text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
- });
- },
-
- async openForm() {
- os.form('Example form', {
- foo: {
- type: 'boolean',
- default: true,
- label: 'This is a boolean property',
- },
- bar: {
- type: 'number',
- default: 300,
- label: 'This is a number property',
- },
- baz: {
- type: 'string',
- default: 'Misskey makes you happy.',
- label: 'This is a string property',
- },
- });
- },
-
- async openDrive() {
- os.selectDriveFile(false);
- },
-
- async selectUser() {
- os.selectUser();
- },
-
- async openMenu(ev) {
- os.popupMenu([{
- type: 'label',
- text: 'Fruits',
- }, {
- text: 'Create some apples',
- action: () => {},
- }, {
- text: 'Read some oranges',
- action: () => {},
- }, {
- text: 'Update some melons',
- action: () => {},
- }, null, {
- text: 'Delete some bananas',
- danger: true,
- action: () => {},
- }], ev.currentTarget ?? ev.target);
- },
- },
-});
-</script>
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index ffc5e82b56..b1a509b9e6 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -1,16 +1,16 @@
<template>
-<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
- <div class="auth _gaps_m">
- <div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
+<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
+ <div class="_gaps_m">
+ <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<div v-if="!totpLogin" class="normal-signin _gaps_m">
- <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
+ <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
- <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :with-password-toggle="true" required data-cy-signin-password>
+ <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :withPasswordToggle="true" required data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
@@ -28,7 +28,7 @@
</div>
<div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
- <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :with-password-toggle="true" required>
+ <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
</MkInput>
@@ -236,18 +236,14 @@ function resetPassword() {
}
</script>
-<style lang="scss" scoped>
-.eppvobhk {
- > .auth {
- > .avatar {
- margin: 0 auto 0 auto;
- width: 64px;
- height: 64px;
- background: #ddd;
- background-position: center;
- background-size: cover;
- border-radius: 100%;
- }
- }
+<style lang="scss" module>
+.avatar {
+ margin: 0 auto 0 auto;
+ width: 64px;
+ height: 64px;
+ background: #ddd;
+ background-position: center;
+ background-size: cover;
+ border-radius: 100%;
}
</style>
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 08e41d6ae5..eb5876e584 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -8,8 +8,8 @@
>
<template #header>{{ i18n.ts.login }}</template>
- <MkSpacer :margin-min="20" :margin-max="28">
- <MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
+ <MkSpacer :marginMin="20" :marginMax="28">
+ <MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
</MkSpacer>
</MkModalWindow>
</template>
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 0e8bdb321e..472269abaf 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -3,13 +3,13 @@
<div :class="$style.banner">
<i class="ti ti-user-edit"></i>
</div>
- <MkSpacer :margin-min="20" :margin-max="32">
+ <MkSpacer :marginMin="20" :marginMax="32">
<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
<template #label>{{ i18n.ts.invitationCode }}</template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
- <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
+ <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
@@ -24,7 +24,7 @@
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
</template>
</MkInput>
- <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
+ <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
<template #prefix><i class="ti ti-mail"></i></template>
<template #caption>
@@ -39,7 +39,7 @@
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
</template>
</MkInput>
- <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
+ <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption>
@@ -48,7 +48,7 @@
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
</template>
</MkInput>
- <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
+ <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption>
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 6da81c3bcb..b6ffba6cc7 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -3,7 +3,7 @@
<div :class="$style.banner">
<i class="ti ti-checklist"></i>
</div>
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<div v-if="instance.disableRegistration">
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
@@ -11,7 +11,7 @@
<div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div>
- <MkFolder v-if="availableServerRules" :default-open="true">
+ <MkFolder v-if="availableServerRules" :defaultOpen="true">
<template #label>{{ i18n.ts.serverRules }}</template>
<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
@@ -22,7 +22,7 @@
<MkSwitch v-model="agreeServerRules" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder>
- <MkFolder v-if="availableTos" :default-open="true">
+ <MkFolder v-if="availableTos" :defaultOpen="true">
<template #label>{{ i18n.ts.termsOfService }}</template>
<template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template>
@@ -31,7 +31,7 @@
<MkSwitch v-model="agreeTos" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder>
- <MkFolder :default-open="true">
+ <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 17f8b86425..d8d002fdb6 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -11,16 +11,16 @@
<div style="overflow-x: clip;">
<Transition
mode="out-in"
- :enter-active-class="$style.transition_x_enterActive"
- :leave-active-class="$style.transition_x_leaveActive"
- :enter-from-class="$style.transition_x_enterFrom"
- :leave-to-class="$style.transition_x_leaveTo"
+ :enterActiveClass="$style.transition_x_enterActive"
+ :leaveActiveClass="$style.transition_x_leaveActive"
+ :enterFromClass="$style.transition_x_enterFrom"
+ :leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="!isAcceptedServerRule">
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
</template>
<template v-else>
- <XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
+ <XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
</template>
</Transition>
</div>
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 1ac7107aa7..3a050889c8 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -1,15 +1,15 @@
<template>
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
- <div :class="$style.body">
+ <div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
- <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
+ <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<details v-if="note.files.length > 0">
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
- <MkMediaList :media-list="note.files"/>
+ <MkMediaList :mediaList="note.files"/>
</details>
<details v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary>
@@ -76,10 +76,6 @@ const collapsed = $ref(
}
}
-.body {
-
-}
-
.reply {
margin-right: 6px;
color: var(--accent);
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 2a8e43c570..72b70416d9 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -23,22 +23,13 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
-export default defineComponent({
- props: {
- def: {
- type: Array,
- required: true,
- },
- grid: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-});
+defineProps<{
+ def: any[];
+ grid?: boolean;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index 6f819bbbd7..7274f9b310 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -7,17 +7,17 @@ export default defineComponent({
required: true,
},
},
- render() {
- const options = this.$slots.default();
+ setup(props, { emit, slots }) {
+ const options = slots.default();
- return h('div', {
+ return () => h('div', {
class: 'pxhvhrfw',
}, options.map(option => withDirectives(h('button', {
- class: ['_button', { active: this.modelValue === option.props.value }],
+ class: ['_button', { active: props.modelValue === option.props.value }],
key: option.key,
- disabled: this.modelValue === option.props.value,
+ disabled: props.modelValue === option.props.value,
onClick: () => {
- this.$emit('update:modelValue', option.props.value);
+ emit('update:modelValue', option.props.value);
},
}, option.children), [
[resolveDirective('click-anime')],
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 4e8d5bab7f..6e4e054aad 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -1,7 +1,7 @@
<template>
-<div ref="rootEl" class="meijqfqm">
- <canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
- <div :id="idForTags" ref="tagsEl" class="tags">
+<div ref="rootEl" :class="$style.root">
+ <canvas :id="idForCanvas" ref="canvasEl" style="display: block;" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
+ <div :id="idForTags" ref="tagsEl" :class="$style.tags">
<ul>
<slot></slot>
</ul>
@@ -70,21 +70,17 @@ defineExpose({
});
</script>
-<style lang="scss" scoped>
-.meijqfqm {
+<style lang="scss" module>
+.root {
position: relative;
overflow: clip;
display: grid;
place-items: center;
+}
- > .canvas {
- display: block;
- }
-
- > .tags {
- position: absolute;
- top: 999px;
- left: 999px;
- }
+.tags {
+ position: absolute;
+ top: 999px;
+ left: 999px;
}
</style>
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 82b631edda..83b2ed2444 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -1,12 +1,12 @@
<template>
-<div class="adhpbeos">
- <div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="input" :class="{ disabled, focused, tall, pre }">
+<div>
+ <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
+ <div :class="{ [$style.disabled]: disabled, [$style.focused]: focused, [$style.tall]: tall, [$style.pre]: pre }" style="position: relative;">
<textarea
ref="inputEl"
v-model="v"
v-adaptive-border
- :class="{ code, _monospace: code }"
+ :class="[$style.textarea, { _monospace: code }]"
:disabled="disabled"
:required="required"
:readonly="readonly"
@@ -20,243 +20,173 @@
@input="onInput"
></textarea>
</div>
- <div class="caption"><slot name="caption"></slot></div>
+ <div :class="$style.caption"><slot name="caption"></slot></div>
- <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
-<script lang="ts">
-import { defineComponent, onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+<script lang="ts" setup>
+import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkButton,
- },
+const props = defineProps<{
+ modelValue: string | null;
+ required?: boolean;
+ readonly?: boolean;
+ disabled?: boolean;
+ pattern?: string;
+ placeholder?: string;
+ autofocus?: boolean;
+ autocomplete?: string;
+ spellcheck?: boolean;
+ debounce?: boolean;
+ manualSave?: boolean;
+ code?: boolean;
+ tall?: boolean;
+ pre?: boolean;
+}>();
- props: {
- modelValue: {
- required: true,
- },
- type: {
- type: String,
- required: false,
- },
- required: {
- type: Boolean,
- required: false,
- },
- readonly: {
- type: Boolean,
- required: false,
- },
- disabled: {
- type: Boolean,
- required: false,
- },
- pattern: {
- type: String,
- required: false,
- },
- placeholder: {
- type: String,
- required: false,
- },
- autofocus: {
- type: Boolean,
- required: false,
- default: false,
- },
- autocomplete: {
- required: false,
- },
- spellcheck: {
- required: false,
- },
- code: {
- type: Boolean,
- required: false,
- },
- tall: {
- type: Boolean,
- required: false,
- default: false,
- },
- pre: {
- type: Boolean,
- required: false,
- default: false,
- },
- debounce: {
- type: Boolean,
- required: false,
- default: false,
- },
- manualSave: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
+const emit = defineEmits<{
+ (ev: 'change', _ev: KeyboardEvent): void;
+ (ev: 'keydown', _ev: KeyboardEvent): void;
+ (ev: 'enter'): void;
+ (ev: 'update:modelValue', value: string): void;
+}>();
- emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+const { modelValue, autofocus } = toRefs(props);
+const v = ref<string>(modelValue.value ?? '');
+const focused = ref(false);
+const changed = ref(false);
+const invalid = ref(false);
+const filled = computed(() => v.value !== '' && v.value != null);
+const inputEl = shallowRef<HTMLTextAreaElement>();
- setup(props, context) {
- const { modelValue, autofocus } = toRefs(props);
- const v = ref(modelValue.value);
- const focused = ref(false);
- const changed = ref(false);
- const invalid = ref(false);
- const filled = computed(() => v.value !== '' && v.value != null);
- const inputEl = ref(null);
+const focus = () => inputEl.value.focus();
+const onInput = (ev) => {
+ changed.value = true;
+ emit('change', ev);
+};
+const onKeydown = (ev: KeyboardEvent) => {
+ if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
- const focus = () => inputEl.value.focus();
- const onInput = (ev) => {
- changed.value = true;
- context.emit('change', ev);
- };
- const onKeydown = (ev: KeyboardEvent) => {
- if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
+ emit('keydown', ev);
- context.emit('keydown', ev);
-
- if (ev.code === 'Enter') {
- context.emit('enter');
- }
- };
-
- const updated = () => {
- changed.value = false;
- context.emit('update:modelValue', v.value);
- };
+ if (ev.code === 'Enter') {
+ emit('enter');
+ }
+};
- const debouncedUpdated = debounce(1000, updated);
+const updated = () => {
+ changed.value = false;
+ emit('update:modelValue', v.value ?? '');
+};
- watch(modelValue, newValue => {
- v.value = newValue;
- });
+const debouncedUpdated = debounce(1000, updated);
- watch(v, newValue => {
- if (!props.manualSave) {
- if (props.debounce) {
- debouncedUpdated();
- } else {
- updated();
- }
- }
+watch(modelValue, newValue => {
+ v.value = newValue;
+});
- invalid.value = inputEl.value.validity.badInput;
- });
+watch(v, newValue => {
+ if (!props.manualSave) {
+ if (props.debounce) {
+ debouncedUpdated();
+ } else {
+ updated();
+ }
+ }
- onMounted(() => {
- nextTick(() => {
- if (autofocus.value) {
- focus();
- }
- });
- });
+ invalid.value = inputEl.value.validity.badInput;
+});
- return {
- v,
- focused,
- invalid,
- changed,
- filled,
- inputEl,
- focus,
- onInput,
- onKeydown,
- updated,
- i18n,
- };
- },
+onMounted(() => {
+ nextTick(() => {
+ if (autofocus.value) {
+ focus();
+ }
+ });
});
</script>
-<style lang="scss" scoped>
-.adhpbeos {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 0;
- user-select: none;
+<style lang="scss" module>
+.label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .caption {
- font-size: 0.85em;
- padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+.caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .input {
- position: relative;
-
- > textarea {
- appearance: none;
- -webkit-appearance: none;
- display: block;
- width: 100%;
- min-width: 100%;
- max-width: 100%;
- min-height: 130px;
- margin: 0;
- padding: 12px;
- font: inherit;
- font-weight: normal;
- font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--panel);
- border-radius: 6px;
- outline: none;
- box-shadow: none;
- box-sizing: border-box;
- transition: border-color 0.1s ease-out;
-
- &:hover {
- border-color: var(--inputBorderHover) !important;
- }
- }
+.textarea {
+ appearance: none;
+ -webkit-appearance: none;
+ display: block;
+ width: 100%;
+ min-width: 100%;
+ max-width: 100%;
+ min-height: 130px;
+ margin: 0;
+ padding: 12px;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 1px var(--panel);
+ border-radius: 6px;
+ outline: none;
+ box-shadow: none;
+ box-sizing: border-box;
+ transition: border-color 0.1s ease-out;
- &.focused {
- > textarea {
- border-color: var(--accent) !important;
- }
- }
+ &:hover {
+ border-color: var(--inputBorderHover) !important;
+ }
+}
- &.disabled {
- opacity: 0.7;
+.focused {
+ > .textarea {
+ border-color: var(--accent) !important;
+ }
+}
- &, * {
- cursor: not-allowed !important;
- }
- }
+.disabled {
+ opacity: 0.7;
+ cursor: not-allowed !important;
- &.tall {
- > textarea {
- min-height: 200px;
- }
- }
+ > .textarea {
+ cursor: not-allowed !important;
+ }
+}
- &.pre {
- > textarea {
- white-space: pre;
- }
- }
+.tall {
+ > .textarea {
+ min-height: 200px;
}
+}
- > .save {
- margin: 8px 0 0 0;
+.pre {
+ > .textarea {
+ white-space: pre;
}
}
+
+.save {
+ margin: 8px 0 0 0;
+}
</style>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index fb0a3a4b67..2595ebc45d 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -1,11 +1,11 @@
<template>
-<MkNotes ref="tlComponent" :no-gap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
+<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
</template>
<script lang="ts" setup>
import { computed, provide, onUnmounted } from 'vue';
import MkNotes from '@/components/MkNotes.vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i } from '@/account';
import { defaultStore } from '@/store';
@@ -46,17 +46,13 @@ const onUserRemoved = () => {
tlComponent.pagingComponent?.reload();
};
-const onChangeFollowing = () => {
- if (!tlComponent.pagingComponent?.backed) {
- tlComponent.pagingComponent?.reload();
- }
-};
-
let endpoint;
let query;
let connection;
let connection2;
+const stream = useStream();
+
if (props.src === 'antenna') {
endpoint = 'antennas/notes';
query = {
@@ -68,23 +64,41 @@ if (props.src === 'antenna') {
connection.on('note', prepend);
} else if (props.src === 'home') {
endpoint = 'notes/timeline';
- connection = stream.useChannel('homeTimeline');
+ query = {
+ withReplies: defaultStore.state.showTimelineReplies,
+ };
+ connection = stream.useChannel('homeTimeline', {
+ withReplies: defaultStore.state.showTimelineReplies,
+ });
connection.on('note', prepend);
connection2 = stream.useChannel('main');
- connection2.on('follow', onChangeFollowing);
- connection2.on('unfollow', onChangeFollowing);
} else if (props.src === 'local') {
endpoint = 'notes/local-timeline';
- connection = stream.useChannel('localTimeline');
+ query = {
+ withReplies: defaultStore.state.showTimelineReplies,
+ };
+ connection = stream.useChannel('localTimeline', {
+ withReplies: defaultStore.state.showTimelineReplies,
+ });
connection.on('note', prepend);
} else if (props.src === 'social') {
endpoint = 'notes/hybrid-timeline';
- connection = stream.useChannel('hybridTimeline');
+ query = {
+ withReplies: defaultStore.state.showTimelineReplies,
+ };
+ connection = stream.useChannel('hybridTimeline', {
+ withReplies: defaultStore.state.showTimelineReplies,
+ });
connection.on('note', prepend);
} else if (props.src === 'global') {
endpoint = 'notes/global-timeline';
- connection = stream.useChannel('globalTimeline');
+ query = {
+ withReplies: defaultStore.state.showTimelineReplies,
+ };
+ connection = stream.useChannel('globalTimeline', {
+ withReplies: defaultStore.state.showTimelineReplies,
+ });
connection.on('note', prepend);
} else if (props.src === 'mentions') {
endpoint = 'notes/mentions';
diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue
index ad53c7f289..e135f56472 100644
--- a/packages/frontend/src/components/MkToast.vue
+++ b/packages/frontend/src/components/MkToast.vue
@@ -1,11 +1,11 @@
<template>
<div>
<Transition
- :enter-active-class="defaultStore.state.animation ? $style.transition_toast_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''"
- appear @after-leave="emit('closed')"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_toast_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''"
+ appear @afterLeave="emit('closed')"
>
<div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }">
<div style="padding: 16px 24px;">
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 56be044405..3ddd81aaee 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -3,16 +3,16 @@
ref="dialog"
:width="400"
:height="450"
- :with-ok-button="true"
- :ok-button-disabled="false"
- :can-close="false"
+ :withOkButton="true"
+ :okButtonDisabled="false"
+ :canClose="false"
@close="dialog.close()"
@closed="$emit('closed')"
@ok="ok()"
>
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<div v-if="information">
<MkInfo warn>{{ information }}</MkInfo>
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index 2d34b090ed..91c9b70a5a 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -1,10 +1,10 @@
<template>
<Transition
- :enter-active-class="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''"
- appear @after-leave="emit('closed')"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''"
+ appear @afterLeave="emit('closed')"
>
<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>
@@ -41,6 +41,9 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
+// タイミングによっては最初から showing = false な場合があり、その場合に closed 扱いにしないと永久にDOMに残ることになる
+if (!props.showing) emit('closed');
+
const el = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex('high');
@@ -66,10 +69,8 @@ onMounted(() => {
setPosition();
const loop = () => {
- loopHandler = window.requestAnimationFrame(() => {
- setPosition();
- loop();
- });
+ setPosition();
+ loopHandler = window.requestAnimationFrame(loop);
};
loop();
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index eed7fa71f6..3a0b2abb4e 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
+<MkModal ref="modal" :zPriority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
<div :class="$style.root">
<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
<div :class="$style.version">✨{{ version }}🚀</div>
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 9c5622b1c5..fcad5b8064 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -22,7 +22,7 @@
</div>
</template>
<template v-else-if="tweetId && tweetExpanded">
- <div ref="twitter" :class="$style.twitter">
+ <div ref="twitter">
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div>
<div :class="$style.action">
@@ -31,7 +31,7 @@
</MkButton>
</div>
</template>
-<div v-else :class="$style.urlPreview">
+<div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
</div>
@@ -41,14 +41,14 @@
<h1 v-else-if="fetching" :class="$style.title"><MkEllipsis/></h1>
<h1 v-else :class="$style.title" :title="title ?? undefined">{{ title }}</h1>
</header>
- <p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.cannotLoad }}</p>
+ <p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.failedToPreviewUrl }}</p>
<p v-else-if="fetching" :class="$style.text"><MkEllipsis/></p>
<p v-else-if="description" :class="$style.text" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
<footer :class="$style.footer">
<img v-if="icon" :class="$style.siteIcon" :src="icon"/>
- <p v-if="unknownUrl" :class="$style.siteName">?</p>
+ <p v-if="unknownUrl" :class="$style.siteName">{{ requestUrl.host }}</p>
<p v-else-if="fetching" :class="$style.siteName"><MkEllipsis/></p>
- <p v-else :class="$style.siteName" :title="sitename ?? undefined">{{ sitename }}</p>
+ <p v-else :class="$style.siteName" :title="sitename ?? requestUrl.host">{{ sitename ?? requestUrl.host }}</p>
</footer>
</article>
</component>
@@ -128,17 +128,33 @@ if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/
requestUrl.hash = '';
-window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => {
- res.json().then((info: SummalyResult) => {
+window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`)
+ .then(res => {
+ if (!res.ok) {
+ fetching = false;
+ unknownUrl = true;
+ return;
+ }
+
+ return res.json();
+ })
+ .then((info: SummalyResult) => {
+ if (info.url == null) {
+ fetching = false;
+ unknownUrl = true;
+ return;
+ }
+
+ fetching = false;
+ unknownUrl = false;
+
title = info.title;
description = info.description;
thumbnail = info.thumbnail;
icon = info.icon;
sitename = info.sitename;
- fetching = false;
player = info.player;
});
-});
function adjustTweetHeight(message: any) {
if (message.origin !== 'https://platform.twitter.com') return;
@@ -194,13 +210,6 @@ onUnmounted(() => {
width: 100%;
}
-.twitter {
-
-}
-
-.urlPreview {
-}
-
.link {
position: relative;
display: block;
diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue
index e244be3e96..36a9e2f73f 100644
--- a/packages/frontend/src/components/MkUrlPreviewPopup.vue
+++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue
@@ -1,6 +1,6 @@
<template>
-<div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
- <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @after-leave="emit('closed')">
+<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
+ <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
</Transition>
</div>
@@ -36,8 +36,8 @@ onMounted(() => {
});
</script>
-<style lang="scss" scoped>
-.fgmtyycl {
+<style lang="scss" module>
+.root {
position: absolute;
width: 500px;
max-width: calc(90vw - 12px);
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index f560ebcd8a..172b517511 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -8,7 +8,7 @@
</div>
<span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
<div :class="$style.description">
- <div v-if="user.description" class="mfm">
+ <div v-if="user.description" :class="$style.mfm">
<Mfm :text="user.description" :author="user" :i="$i"/>
</div>
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
@@ -105,7 +105,7 @@ defineProps<{
.mfm {
display: -webkit-box;
-webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
+ -webkit-box-orient: vertical;
overflow: hidden;
}
diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue
index 251ab5d79a..a2c2b53b08 100644
--- a/packages/frontend/src/components/MkUserOnlineIndicator.vue
+++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue
@@ -1,5 +1,13 @@
<template>
-<div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div>
+<div
+ v-tooltip="text"
+ :class="[$style.root, {
+ [$style.status_online]: user.onlineStatus === 'online',
+ [$style.status_active]: user.onlineStatus === 'active',
+ [$style.status_offline]: user.onlineStatus === 'offline',
+ [$style.status_unknown]: user.onlineStatus === 'unknown',
+ }]"
+></div>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 8ca0355448..c3b777a12e 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -1,10 +1,10 @@
<template>
<Transition
- :enter-active-class="defaultStore.state.animation ? $style.transition_popup_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''"
- appear @after-leave="emit('closed')"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_popup_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''"
+ appear @afterLeave="emit('closed')"
>
<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
<div v-if="user != null">
@@ -22,7 +22,7 @@
<div :class="$style.username"><MkAcct :user="user"/></div>
</div>
<div :class="$style.description">
- <Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
+ <Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/>
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
</div>
<div :class="$style.status">
@@ -192,6 +192,13 @@ onMounted(() => {
border-bottom: solid 1px var(--divider);
}
+.mfm {
+ display: -webkit-box;
+ -webkit-line-clamp: 5;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
.status {
padding: 16px 26px 16px 26px;
}
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index dc78bbf42d..792ff7afd7 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -1,22 +1,22 @@
<template>
<MkModalWindow
ref="dialogEl"
- :with-ok-button="true"
- :ok-button-disabled="selected == null"
+ :withOkButton="true"
+ :okButtonDisabled="selected == null"
@click="cancel()"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.selectUser }}</template>
- <div :class="$style.root">
+ <div>
<div :class="$style.form">
- <FormSplit :min-width="170">
- <MkInput v-model="username" :autofocus="true" @update:model-value="search">
+ <FormSplit :minWidth="170">
+ <MkInput v-model="username" :autofocus="true" @update:modelValue="search">
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
</MkInput>
- <MkInput v-model="host" :datalist="[hostname]" @update:model-value="search">
+ <MkInput v-model="host" :datalist="[hostname]" @update:modelValue="search">
<template #label>{{ i18n.ts.host }}</template>
<template #prefix>@</template>
</MkInput>
@@ -126,8 +126,6 @@ onMounted(() => {
</script>
<style lang="scss" module>
-.root {
-}
.form {
padding: 0 var(--root-margin);
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index a2a195cb09..789f88a8fe 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -2,7 +2,7 @@
<div class="_gaps">
<div style="text-align: center;">{{ i18n.ts._initialAccountSetting.followUsers }}</div>
- <MkFolder :default-open="true">
+ <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.recommended }}</template>
<MkPagination :pagination="pinnedUsers">
@@ -14,7 +14,7 @@
</MkPagination>
</MkFolder>
- <MkFolder :default-open="true">
+ <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.popularUsers }}</template>
<MkPagination :pagination="popularUsers">
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index e9f4f68df8..5cea67ccf5 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -4,6 +4,7 @@
<MkFolder>
<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
+ <template #icon><i class="ti ti-lock"></i></template>
<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
@@ -11,6 +12,7 @@
<MkFolder>
<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
+ <template #icon><i class="ti ti-eye-off"></i></template>
<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
@@ -18,6 +20,7 @@
<MkFolder>
<template #label>{{ i18n.ts.noCrawle }}</template>
+ <template #icon><i class="ti ti-world-x"></i></template>
<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
@@ -25,6 +28,7 @@
<MkFolder>
<template #label>{{ i18n.ts.preventAiLearning }}</template>
+ <template #icon><i class="ti ti-photo-shield"></i></template>
<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index f26ea11214..3107209b97 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -12,11 +12,11 @@
</div>
</FormSlot>
- <MkInput v-model="name" :max="30" manual-save data-cy-user-setup-user-name>
+ <MkInput v-model="name" :max="30" manualSave data-cy-user-setup-user-name>
<template #label>{{ i18n.ts._profile.name }}</template>
</MkInput>
- <MkTextarea v-model="description" :max="500" tall manual-save data-cy-user-setup-user-description>
+ <MkTextarea v-model="description" :max="500" tall manualSave data-cy-user-setup-user-description>
<template #label>{{ i18n.ts._profile.description }}</template>
</MkTextarea>
@@ -37,8 +37,8 @@ import { chooseFileFromPc } from '@/scripts/select-file';
import * as os from '@/os';
import { $i } from '@/account';
-const name = ref('');
-const description = ref('');
+const name = ref($i.name ?? '');
+const description = ref($i.description ?? '');
watch(name, () => {
os.apiWithDialog('i/update', {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 4e80a5c0fb..566441213e 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -7,10 +7,10 @@
@close="close(true)"
@closed="emit('closed')"
>
- <template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
- <template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
- <template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
- <template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
+ <template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template>
+ <template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template>
+ <template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template>
+ <template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template>
<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
@@ -20,65 +20,80 @@
</div>
<Transition
mode="out-in"
- :enter-active-class="$style.transition_x_enterActive"
- :leave-active-class="$style.transition_x_leaveActive"
- :enter-from-class="$style.transition_x_enterFrom"
- :leave-to-class="$style.transition_x_leaveTo"
+ :enterActiveClass="$style.transition_x_enterActive"
+ :leaveActiveClass="$style.transition_x_leaveActive"
+ :enterFromClass="$style.transition_x_enterFrom"
+ :leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="page === 0">
<div :class="$style.centerPage">
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
+ <MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
<div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
+ <MkButton style="margin: 0 auto;" transparent rounded @click="later(true)">{{ i18n.ts.later }}</MkButton>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 1">
<div style="height: 100cqh; overflow: auto;">
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<XProfile/>
- <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ <div class="_buttonsCenter" style="margin-top: 16px;">
+ <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+ <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<XPrivacy/>
- <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ <div class="_buttonsCenter" style="margin-top: 16px;">
+ <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+ <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 3">
<div style="height: 100cqh; overflow: auto;">
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<XFollow/>
</MkSpacer>
<div :class="$style.pageFooter">
- <MkButton primary rounded gradate style="margin: 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ <div class="_buttonsCenter">
+ <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+ <MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
</div>
</div>
</template>
<template v-else-if="page === 4">
<div :class="$style.centerPage">
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
<div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
- <MkPushNotificationAllowButton primary show-only-to-register style="margin: 0 auto;"/>
- <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ <MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
+ <div class="_buttonsCenter" style="margin-top: 16px;">
+ <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+ <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 5">
<div :class="$style.centerPage">
- <MkSpacer :margin-min="20" :margin-max="28">
+ <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
+ <MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
@@ -89,7 +104,10 @@
</template>
</I18n>
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
- <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton>
+ <div class="_buttonsCenter" style="margin-top: 16px;">
+ <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+ <MkButton primary rounded gradate data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton>
+ </div>
</div>
</MkSpacer>
</div>
@@ -106,6 +124,7 @@ import MkButton from '@/components/MkButton.vue';
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
+import MkAnimBg from '@/components/MkAnimBg.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { host } from '@/config';
@@ -137,6 +156,19 @@ async function close(skip: boolean) {
dialog.value.close();
defaultStore.set('accountSetupWizard', -1);
}
+
+async function later(later: boolean) {
+ if (later) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts._initialAccountSetting.laterAreYouSure,
+ });
+ if (canceled) return;
+ }
+
+ dialog.value.close();
+ defaultStore.set('accountSetupWizard', 0);
+}
</script>
<style lang="scss" module>
@@ -183,7 +215,7 @@ async function close(skip: boolean) {
left: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ -webkit-backdrop-filter: blur(15px);
+ backdrop-filter: blur(15px);
}
</style>
diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue
index d0f95fceda..0b80c2edc7 100644
--- a/packages/frontend/src/components/MkUsersTooltip.vue
+++ b/packages/frontend/src/components/MkUsersTooltip.vue
@@ -1,11 +1,11 @@
<template>
-<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
+<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="250" @closed="emit('closed')">
<div :class="$style.root">
<div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar :class="$style.avatar" :user="u"/>
- <MkUserName :class="$style.name" :user="u" :nowrap="true"/>
+ <MkUserName :user="u" :nowrap="true"/>
</div>
- <div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div>
+ <div v-if="users.length < count">+{{ count - users.length }}</div>
</div>
</MkTooltip>
</template>
@@ -43,14 +43,6 @@ const emit = defineEmits<{
}
}
-.name {
-
-}
-
-.omitted {
-
-}
-
.avatar {
width: 24px;
height: 24px;
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index c181d84bc0..c8dbe90944 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" v-slot="{ type }" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
<div :class="[$style.label, $style.item]">
{{ i18n.ts.visibility }}
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 6226768127..9566cc651f 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -39,7 +39,7 @@
<MkTimeline src="local"/>
</div>
</div>
- <div :class="[$style.activeUsersChart, $style.panel]">
+ <div :class="$style.panel">
<XActiveUsersChart/>
</div>
</div>
@@ -220,8 +220,4 @@ function exploreOtherServers() {
height: 350px;
overflow: auto;
}
-
-.activeUsersChart {
-
-}
</style>
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index da98da29d0..1b6ab1f13a 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
+<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
<div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]">
<i v-if="success" :class="[$style.icon, $style.success]" class="ti ti-check"></i>
<MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/>
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index ad1c02a488..30547c7444 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -1,7 +1,7 @@
<template>
<div :class="$style.root">
<template v-if="edit">
- <header :class="$style['edit-header']">
+ <header :class="$style.editHeader">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
@@ -10,26 +10,26 @@
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
</header>
<Sortable
- :model-value="props.widgets"
- item-key="id"
+ :modelValue="props.widgets"
+ itemKey="id"
handle=".handle"
:animation="150"
:group="{ name: 'SortableMkWidgets' }"
- :class="$style['edit-editing']"
- @update:model-value="v => emit('updateWidgets', v)"
+ :class="$style.editEditing"
+ @update:modelValue="v => emit('updateWidgets', v)"
>
<template #item="{element}">
- <div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container>
- <button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
- <button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
+ <div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
+ <button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
+ <button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div class="handle">
- <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/>
+ <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div>
</div>
</template>
</Sortable>
</template>
- <component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
+ <component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
@@ -130,7 +130,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
}
.edit {
- &-header {
+ &Header {
margin: 16px 0;
> * {
@@ -139,17 +139,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
}
}
- &-editing {
+ &Editing {
min-height: 100px;
}
}
-.customize-container {
+.customizeContainer {
position: relative;
cursor: move;
- &-config,
- &-remove {
+ &Config,
+ &Remove {
position: absolute;
z-index: 10000;
top: 8px;
@@ -160,17 +160,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
border-radius: 4px;
}
- &-config {
+ &Config {
right: 8px + 8px + 32px;
}
- &-remove {
+ &Remove {
right: 8px;
}
- &-handle {
+ &Handle {
- &-widget {
+ &Widget {
pointer-events: none;
}
}
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index b662479b2a..dafabf2ba8 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -1,11 +1,11 @@
<template>
<Transition
- :enter-active-class="defaultStore.state.animation ? $style.transition_window_enterActive : ''"
- :leave-active-class="defaultStore.state.animation ? $style.transition_window_leaveActive : ''"
- :enter-from-class="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
- :leave-to-class="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
+ :enterActiveClass="defaultStore.state.animation ? $style.transition_window_enterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.transition_window_leaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
appear
- @after-leave="$emit('closed')"
+ @afterLeave="$emit('closed')"
>
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 4d765fe2f7..0edfd98efc 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -1,5 +1,5 @@
<template>
-<MkWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true">
+<MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true">
<template #header>
<i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i>
<span>{{ title ?? 'YouTube' }}</span>
diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue
index a1775c0bdb..22b5edc3c9 100644
--- a/packages/frontend/src/components/form/link.vue
+++ b/packages/frontend/src/components/form/link.vue
@@ -1,19 +1,19 @@
<template>
-<div class="ffcbddfc" :class="{ inline }">
- <a v-if="external" class="main _button" :href="to" target="_blank">
- <span class="icon"><slot name="icon"></slot></span>
- <span class="text"><slot></slot></span>
- <span class="right">
- <span class="text"><slot name="suffix"></slot></span>
- <i class="ti ti-external-link icon"></i>
+<div :class="[$style.root, { [$style.inline]: inline }]">
+ <a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank">
+ <span :class="$style.icon"><slot name="icon"></slot></span>
+ <span :class="$style.text"><slot></slot></span>
+ <span :class="$style.suffix">
+ <span :class="$style.suffixText"><slot name="suffix"></slot></span>
+ <i class="ti ti-external-link"></i>
</span>
</a>
- <MkA v-else class="main _button" :class="{ active }" :to="to" :behavior="behavior">
- <span class="icon"><slot name="icon"></slot></span>
- <span class="text"><slot></slot></span>
- <span class="right">
- <span class="text"><slot name="suffix"></slot></span>
- <i class="ti ti-chevron-right icon"></i>
+ <MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
+ <span :class="$style.icon"><slot name="icon"></slot></span>
+ <span :class="$style.text"><slot></slot></span>
+ <span :class="$style.suffix">
+ <span :class="$style.suffixText"><slot name="suffix"></slot></span>
+ <i class="ti ti-chevron-right"></i>
</span>
</MkA>
</div>
@@ -26,70 +26,70 @@ const props = defineProps<{
to: string;
active?: boolean;
external?: boolean;
- behavior?: null | 'window' | 'browser' | 'modalWindow';
+ behavior?: null | 'window' | 'browser';
inline?: boolean;
}>();
</script>
-<style lang="scss" scoped>
-.ffcbddfc {
+<style lang="scss" module>
+.root {
display: block;
&.inline {
display: inline-block;
}
+}
- > .main {
- display: flex;
- align-items: center;
- width: 100%;
- box-sizing: border-box;
- padding: 10px 14px;
- background: var(--buttonBg);
- border-radius: 6px;
- font-size: 0.9em;
+.main {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 10px 14px;
+ background: var(--buttonBg);
+ border-radius: 6px;
+ font-size: 0.9em;
- &:hover {
- text-decoration: none;
- background: var(--buttonHoverBg);
- }
+ &:hover {
+ text-decoration: none;
+ background: var(--buttonHoverBg);
+ }
- &.active {
- color: var(--accent);
- background: var(--buttonHoverBg);
- }
+ &.active {
+ color: var(--accent);
+ background: var(--buttonHoverBg);
+ }
+}
- > .icon {
- margin-right: 0.75em;
- flex-shrink: 0;
- text-align: center;
- color: var(--fgTransparentWeak);
+.icon {
+ margin-right: 0.75em;
+ flex-shrink: 0;
+ text-align: center;
+ color: var(--fgTransparentWeak);
- &:empty {
- display: none;
+ &:empty {
+ display: none;
- & + .text {
- padding-left: 4px;
- }
- }
+ & + .text {
+ padding-left: 4px;
}
+ }
+}
- > .text {
- flex-shrink: 1;
- white-space: normal;
- padding-right: 12px;
- text-align: center;
- }
+.text {
+ flex-shrink: 1;
+ white-space: normal;
+ padding-right: 12px;
+ text-align: center;
+}
- > .right {
- margin-left: auto;
- opacity: 0.7;
- white-space: nowrap;
+.suffix {
+ margin-left: auto;
+ opacity: 0.7;
+ white-space: nowrap;
- > .text:not(:empty) {
- margin-right: 0.75em;
- }
- }
+ > .suffixText:not(:empty) {
+ margin-right: 0.75em;
}
}
</style>
diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue
index 79ce8fe51f..809d80620f 100644
--- a/packages/frontend/src/components/form/slot.vue
+++ b/packages/frontend/src/components/form/slot.vue
@@ -1,10 +1,10 @@
<template>
-<div class="adhpbeou">
- <div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="content">
+<div>
+ <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
+ <div>
<slot></slot>
</div>
- <div class="caption"><slot name="caption"></slot></div>
+ <div :class="$style.caption"><slot name="caption"></slot></div>
</div>
</template>
@@ -16,26 +16,24 @@ function focus() {
}
</script>
-<style lang="scss" scoped>
-.adhpbeou {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 0;
- user-select: none;
+<style lang="scss" module>
+.label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .caption {
- font-size: 0.85em;
- padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+.caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
}
</style>
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index 3a44c3da3d..b3d8c22b27 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -1,102 +1,66 @@
<template>
-<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
- <div v-if="pending">
- <MkLoading/>
+<div v-if="pending">
+ <MkLoading/>
+</div>
+<div v-else-if="resolved">
+ <slot :result="result"></slot>
+</div>
+<div v-else>
+ <div :class="$style.error">
+ <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div>
+ <MkButton inline style="margin-top: 16px;" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
</div>
- <div v-else-if="resolved">
- <slot :result="result"></slot>
- </div>
- <div v-else>
- <div class="wszdbhzo">
- <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div>
- <MkButton inline class="retry" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
- </div>
- </div>
-</Transition>
+</div>
</template>
-<script lang="ts">
-import { defineComponent, PropType, ref, watch } from 'vue';
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkButton,
- },
-
- props: {
- p: {
- type: Function as PropType<() => Promise<any>>,
- required: true,
- },
- },
+const props = defineProps<{
+ p: () => Promise<any>;
+}>();
- setup(props, context) {
- const pending = ref(true);
- const resolved = ref(false);
- const rejected = ref(false);
- const result = ref(null);
+const pending = ref(true);
+const resolved = ref(false);
+const rejected = ref(false);
+const result = ref(null);
- const process = () => {
- if (props.p == null) {
- return;
- }
- const promise = props.p();
- pending.value = true;
- resolved.value = false;
- rejected.value = false;
- promise.then((_result) => {
- pending.value = false;
- resolved.value = true;
- result.value = _result;
- });
- promise.catch(() => {
- pending.value = false;
- rejected.value = true;
- });
- };
-
- watch(() => props.p, () => {
- process();
- }, {
- immediate: true,
- });
-
- const retry = () => {
- process();
- };
+const process = () => {
+ if (props.p == null) {
+ return;
+ }
+ const promise = props.p();
+ pending.value = true;
+ resolved.value = false;
+ rejected.value = false;
+ promise.then((_result) => {
+ pending.value = false;
+ resolved.value = true;
+ result.value = _result;
+ });
+ promise.catch(() => {
+ pending.value = false;
+ rejected.value = true;
+ });
+};
- return {
- pending,
- resolved,
- rejected,
- result,
- retry,
- defaultStore,
- i18n,
- };
- },
+watch(() => props.p, () => {
+ process();
+}, {
+ immediate: true,
});
-</script>
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
- transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
- opacity: 0;
-}
+const retry = () => {
+ process();
+};
+</script>
-.wszdbhzo {
+<style lang="scss" module>
+.error {
padding: 16px;
text-align: center;
-
- > .retry {
- margin-top: 16px;
- }
}
</style>
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 40d134dffb..4e608c6efe 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -15,7 +15,7 @@ import { useRouter } from '@/router';
const props = withDefaults(defineProps<{
to: string;
activeClass?: null | string;
- behavior?: null | 'window' | 'browser' | 'modalWindow';
+ behavior?: null | 'window' | 'browser';
}>(), {
activeClass: null,
behavior: null,
@@ -70,14 +70,6 @@ function openWindow() {
os.pageWindow(props.to);
}
-function modalWindow() {
- os.modalPageWindow(props.to);
-}
-
-function popout() {
- popout_(props.to);
-}
-
function nav(ev: MouseEvent) {
if (props.behavior === 'browser') {
location.href = props.to;
@@ -87,8 +79,6 @@ function nav(ev: MouseEvent) {
if (props.behavior) {
if (props.behavior === 'window') {
return openWindow();
- } else if (props.behavior === 'modalWindow') {
- return modalWindow();
}
}
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index 59358aef70..f93659f5ed 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -1,5 +1,5 @@
<template>
-<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :min-scale="2 / 3">
+<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3">
<span>@{{ user.username }}</span>
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
</MkCondensedLine>
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index aa975600f0..8b25ab1b6a 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -1,6 +1,14 @@
<template>
<div v-if="chosen && !shouldHide" :class="$style.root">
- <div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
+ <div
+ v-if="!showMenu"
+ :class="[$style.main, {
+ [$style.form_square]: chosen.place === 'square',
+ [$style.form_horizontal]: chosen.place === 'horizontal',
+ [$style.form_horizontalBig]: chosen.place === 'horizontal-big',
+ [$style.form_vertical]: chosen.place === 'vertical',
+ }]"
+ >
<a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
@@ -122,7 +130,7 @@ function reduceFrequency(): void {
}
}
- &.form_horizontal-big {
+ &.form_horizontalBig {
padding: 8px;
> .link,
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 42abdcbdcc..422b35c9dd 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -1,6 +1,6 @@
<template>
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
- <img :class="$style.inner" :src="url" decoding="async"/>
+ <MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">
@@ -24,6 +24,7 @@
<script lang="ts" setup>
import { watch } from 'vue';
import * as misskey from 'misskey-js';
+import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue
index 1d46ff1ec9..4b2e8e4750 100644
--- a/packages/frontend/src/components/global/MkCondensedLine.vue
+++ b/packages/frontend/src/components/global/MkCondensedLine.vue
@@ -13,13 +13,20 @@ interface Props {
const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => {
+ const results: {
+ container: HTMLSpanElement;
+ transform: string;
+ }[] = [];
for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol];
const container = content.parentElement as HTMLSpanElement;
const contentWidth = content.getBoundingClientRect().width;
const containerWidth = container.getBoundingClientRect().width;
- container.style.transform = `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})`;
+ results.push({ container, transform: `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})` });
+ }
+ for (const result of results) {
+ result.container.style.transform = result.transform;
}
});
</script>
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 0cb31ffcba..e8a7f17cc6 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -7,7 +7,7 @@
import { computed } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy';
import { defaultStore } from '@/store';
-import { customEmojis } from '@/custom-emojis';
+import { customEmojisMap } from '@/custom-emojis';
const props = defineProps<{
name: string;
@@ -26,7 +26,7 @@ const rawUrl = computed(() => {
return props.url;
}
if (isLocal.value) {
- return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
+ return customEmojisMap.get(customEmojiName.value)?.url ?? null;
}
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
});
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
index f6811b6747..685b3b8b8e 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
+import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.ts';
export const Default = {
render(args) {
return {
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
new file mode 100644
index 0000000000..2a50a34390
--- /dev/null
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -0,0 +1,367 @@
+import { VNode, h } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import MkUrl from '@/components/global/MkUrl.vue';
+import MkLink from '@/components/MkLink.vue';
+import MkMention from '@/components/MkMention.vue';
+import MkEmoji from '@/components/global/MkEmoji.vue';
+import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
+import MkCode from '@/components/MkCode.vue';
+import MkGoogle from '@/components/MkGoogle.vue';
+import MkSparkle from '@/components/MkSparkle.vue';
+import MkA from '@/components/global/MkA.vue';
+import { host } from '@/config';
+import { defaultStore } from '@/store';
+
+const QUOTE_STYLE = `
+display: block;
+margin: 8px;
+padding: 6px 0 6px 12px;
+color: var(--fg);
+border-left: solid 3px var(--fg);
+opacity: 0.7;
+`.split('\n').join(' ');
+
+export default function(props: {
+ text: string;
+ plain?: boolean;
+ nowrap?: boolean;
+ author?: Misskey.entities.UserLite;
+ i?: Misskey.entities.UserLite;
+ isNote?: boolean;
+ emojiUrls?: string[];
+ rootScale?: number;
+}) {
+ const isNote = props.isNote !== undefined ? props.isNote : true;
+
+ if (props.text == null || props.text === '') return;
+
+ const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
+
+ const validTime = (t: string | null | undefined) => {
+ if (t == null) return null;
+ return t.match(/^[0-9.]+s$/) ? t : null;
+ };
+
+ const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
+
+ /**
+ * Gen Vue Elements from MFM AST
+ * @param ast MFM AST
+ * @param scale How times large the text is
+ */
+ const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
+ switch (token.type) {
+ case 'text': {
+ const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+
+ if (!props.plain) {
+ const res: (VNode | string)[] = [];
+ for (const t of text.split('\n')) {
+ res.push(h('br'));
+ res.push(t);
+ }
+ res.shift();
+ return res;
+ } else {
+ return [text.replace(/\n/g, ' ')];
+ }
+ }
+
+ case 'bold': {
+ return [h('b', genEl(token.children, scale))];
+ }
+
+ case 'strike': {
+ return [h('del', genEl(token.children, scale))];
+ }
+
+ case 'italic': {
+ return h('i', {
+ style: 'font-style: oblique;',
+ }, genEl(token.children, scale));
+ }
+
+ case 'fn': {
+ // 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)' :
+ token.props.args.v ? 'scaleY(-1)' :
+ 'scaleX(-1)';
+ style = `transform: ${transform};`;
+ break;
+ }
+ case 'x2': {
+ return h('span', {
+ class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
+ }, genEl(token.children, scale * 2));
+ }
+ case 'x3': {
+ return h('span', {
+ class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
+ }, genEl(token.children, scale * 3));
+ }
+ case 'x4': {
+ return h('span', {
+ class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
+ }, genEl(token.children, scale * 4));
+ }
+ case 'font': {
+ const family =
+ token.props.args.serif ? 'serif' :
+ token.props.args.monospace ? 'monospace' :
+ token.props.args.cursive ? 'cursive' :
+ token.props.args.fantasy ? 'fantasy' :
+ token.props.args.emoji ? 'emoji' :
+ token.props.args.math ? 'math' :
+ null;
+ if (family) style = `font-family: ${family};`;
+ break;
+ }
+ case 'blur': {
+ return h('span', {
+ class: '_mfm_blur_',
+ }, genEl(token.children, scale));
+ }
+ case 'rainbow': {
+ const speed = validTime(token.props.args.speed) ?? '1s';
+ style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
+ break;
+ }
+ case 'sparkle': {
+ if (!useAnim) {
+ return genEl(token.children, scale);
+ }
+ return h(MkSparkle, {}, genEl(token.children, scale));
+ }
+ case 'rotate': {
+ const degrees = parseFloat(token.props.args.deg ?? '90');
+ style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
+ break;
+ }
+ case 'position': {
+ if (!defaultStore.state.advancedMfm) break;
+ const x = parseFloat(token.props.args.x ?? '0');
+ const y = parseFloat(token.props.args.y ?? '0');
+ style = `transform: translateX(${x}em) translateY(${y}em);`;
+ break;
+ }
+ case 'scale': {
+ if (!defaultStore.state.advancedMfm) {
+ style = '';
+ break;
+ }
+ const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
+ const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
+ style = `transform: scale(${x}, ${y});`;
+ scale = scale * Math.max(x, y);
+ break;
+ }
+ case 'fg': {
+ let color = token.props.args.color;
+ if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+ style = `color: #${color};`;
+ break;
+ }
+ case 'bg': {
+ let color = token.props.args.color;
+ if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+ style = `background-color: #${color};`;
+ break;
+ }
+ }
+ if (style == null) {
+ return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
+ } else {
+ return h('span', {
+ style: 'display: inline-block; ' + style,
+ }, genEl(token.children, scale));
+ }
+ }
+
+ case 'small': {
+ return [h('small', {
+ style: 'opacity: 0.7;',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'center': {
+ return [h('div', {
+ style: 'text-align:center;',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'url': {
+ return [h(MkUrl, {
+ key: Math.random(),
+ url: token.props.url,
+ rel: 'nofollow noopener',
+ })];
+ }
+
+ case 'link': {
+ return [h(MkLink, {
+ key: Math.random(),
+ url: token.props.url,
+ rel: 'nofollow noopener',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'mention': {
+ return [h(MkMention, {
+ key: Math.random(),
+ host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host,
+ username: token.props.username,
+ })];
+ }
+
+ case 'hashtag': {
+ return [h(MkA, {
+ key: Math.random(),
+ to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
+ style: 'color:var(--hashtag);',
+ }, `#${token.props.hashtag}`)];
+ }
+
+ case 'blockCode': {
+ return [h(MkCode, {
+ key: Math.random(),
+ code: token.props.code,
+ lang: token.props.lang,
+ })];
+ }
+
+ case 'inlineCode': {
+ return [h(MkCode, {
+ key: Math.random(),
+ code: token.props.code,
+ inline: true,
+ })];
+ }
+
+ case 'quote': {
+ if (!props.nowrap) {
+ return [h('div', {
+ style: QUOTE_STYLE,
+ }, genEl(token.children, scale))];
+ } else {
+ return [h('span', {
+ style: QUOTE_STYLE,
+ }, genEl(token.children, scale))];
+ }
+ }
+
+ case 'emojiCode': {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (props.author?.host == null) {
+ return [h(MkCustomEmoji, {
+ key: Math.random(),
+ name: token.props.name,
+ normal: props.plain,
+ host: null,
+ useOriginalSize: scale >= 2.5,
+ })];
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
+ return [h('span', `:${token.props.name}:`)];
+ } else {
+ return [h(MkCustomEmoji, {
+ key: Math.random(),
+ name: token.props.name,
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
+ normal: props.plain,
+ host: props.author.host,
+ useOriginalSize: scale >= 2.5,
+ })];
+ }
+ }
+ }
+
+ case 'unicodeEmoji': {
+ return [h(MkEmoji, {
+ key: Math.random(),
+ emoji: token.props.emoji,
+ })];
+ }
+
+ case 'mathInline': {
+ return [h('code', token.props.formula)];
+ }
+
+ case 'mathBlock': {
+ return [h('code', token.props.formula)];
+ }
+
+ case 'search': {
+ return [h(MkGoogle, {
+ key: Math.random(),
+ q: token.props.query,
+ })];
+ }
+
+ case 'plain': {
+ return [h('span', genEl(token.children, scale))];
+ }
+
+ default: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ console.error('unrecognized ast type:', (token as any).type);
+
+ return [];
+ }
+ }
+ }).flat(Infinity) as (VNode | string)[];
+
+ return h('span', {
+ // https://codeday.me/jp/qa/20190424/690106.html
+ style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
+ }, genEl(ast, props.rootScale ?? 1));
+}
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue
deleted file mode 100644
index 28a0d1c986..0000000000
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-<template>
-<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import MfmCore from '@/components/mfm';
-
-const props = withDefaults(defineProps<{
- text: string;
- plain?: boolean;
- nowrap?: boolean;
- author?: any;
- isNote?: boolean;
-}>(), {
- plain: false,
- nowrap: false,
- author: null,
- isNote: true,
-});
-</script>
-
-<style lang="scss">
-._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%); }
-}
-</style>
-
-<style lang="scss" module>
-.root {
- white-space: pre-wrap;
-
- &.nowrap {
- white-space: pre;
- word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-</style>
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 9e1da64e61..d71343baf9 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -15,8 +15,8 @@
{{ t.title }}
</div>
<Transition
- v-else mode="in-out" @enter="enter" @after-enter="afterEnter" @leave="leave"
- @after-leave="afterLeave"
+ v-else mode="in-out" @enter="enter" @afterEnter="afterEnter" @leave="leave"
+ @afterLeave="afterLeave"
>
<div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div>
</Transition>
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index b91d378b17..0a21d39bca 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -21,7 +21,7 @@
</div>
</div>
</div>
- <XTabs v-if="!narrow || hideTitle" :class="$style.tabs" :tab="tab" :tabs="tabs" :root-el="el" @update:tab="key => emit('update:tab', key)" @tab-click="onTabClick"/>
+ <XTabs v-if="!narrow || hideTitle" :class="$style.tabs" :tab="tab" :tabs="tabs" :rootEl="el" @update:tab="key => emit('update:tab', key)" @tabClick="onTabClick"/>
</template>
<div v-if="(!thin_ && narrow && !hideTitle) || (actions && actions.length > 0)" :class="$style.buttonsRight">
<template v-for="action in actions">
@@ -30,7 +30,7 @@
</div>
</div>
<div v-if="(narrow && !hideTitle) && hasTabs" :class="[$style.lower, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
- <XTabs :class="$style.tabs" :tab="tab" :tabs="tabs" :root-el="el" @update:tab="key => emit('update:tab', key)" @tab-click="onTabClick"/>
+ <XTabs :class="$style.tabs" :tab="tab" :tabs="tabs" :rootEl="el" @update:tab="key => emit('update:tab', key)" @tabClick="onTabClick"/>
</div>
</div>
</template>
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 44c02088da..e5dba54b4e 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -14,6 +14,7 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
+import { $$ } from 'vue/macros';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
const rootEl = $shallowRef<HTMLElement>();
@@ -83,8 +84,8 @@ onMounted(() => {
onUnmounted(() => {
observer.disconnect();
});
-</script>
-
-<style lang="scss" module>
-</style>
+defineExpose({
+ rootEl: $$(rootEl),
+});
+</script>
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 261cc0ee18..dfc3c89798 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -58,7 +58,6 @@ function tick() {
if (props.mode === 'relative' || props.mode === 'detail') {
tick();
-
onUnmounted(() => {
window.clearTimeout(tickId);
});
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 2a92780306..c1efd9a06b 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -6,7 +6,7 @@
<template v-if="!self">
<span :class="$style.schema">{{ schema }}//</span>
<span :class="$style.hostname">{{ hostname }}</span>
- <span v-if="port != ''" :class="$style.port">:{{ port }}</span>
+ <span v-if="port != ''">:{{ port }}</span>
</template>
<template v-if="pathname === '/' && self">
<span :class="$style.self">{{ hostname }}</span>
diff --git a/packages/frontend/src/components/global/MkUserName.vue b/packages/frontend/src/components/global/MkUserName.vue
index 4186a4a4fb..c9e85c5460 100644
--- a/packages/frontend/src/components/global/MkUserName.vue
+++ b/packages/frontend/src/components/global/MkUserName.vue
@@ -1,5 +1,5 @@
<template>
-<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emoji-urls="user.emojis"/>
+<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emojiUrls="user.emojis"/>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts
index 1fd293ba10..2708b759aa 100644
--- a/packages/frontend/src/components/global/i18n.ts
+++ b/packages/frontend/src/components/global/i18n.ts
@@ -1,42 +1,24 @@
-import { h, defineComponent } from 'vue';
+import { h } from 'vue';
-export default defineComponent({
- props: {
- src: {
- type: String,
- required: true,
- },
- tag: {
- type: String,
- required: false,
- default: 'span',
- },
- textTag: {
- type: String,
- required: false,
- default: null,
- },
- },
- render() {
- let str = this.src;
- const parsed = [] as (string | { arg: string; })[];
- while (true) {
- const nextBracketOpen = str.indexOf('{');
- const nextBracketClose = str.indexOf('}');
+export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
+ let str = props.src;
+ const parsed = [] as (string | { arg: string; })[];
+ while (true) {
+ const nextBracketOpen = str.indexOf('{');
+ const nextBracketClose = str.indexOf('}');
- if (nextBracketOpen === -1) {
- parsed.push(str);
- break;
- } else {
- if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
- parsed.push({
- arg: str.substring(nextBracketOpen + 1, nextBracketClose),
- });
- }
-
- str = str.substr(nextBracketClose + 1);
+ if (nextBracketOpen === -1) {
+ parsed.push(str);
+ break;
+ } else {
+ if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
+ parsed.push({
+ arg: str.substring(nextBracketOpen + 1, nextBracketClose),
+ });
}
- return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]()));
- },
-});
+ str = str.substr(nextBracketClose + 1);
+ }
+
+ return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
+}
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index 4ef8111da9..ee2a2bc7bd 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -1,6 +1,6 @@
import { App } from 'vue';
-import Mfm from './global/MkMisskeyFlavoredMarkdown.vue';
+import Mfm from './global/MkMisskeyFlavoredMarkdown.ts';
import MkA from './global/MkA.vue';
import MkAcct from './global/MkAcct.vue';
import MkAvatar from './global/MkAvatar.vue';
diff --git a/packages/frontend/src/components/mfm.ts b/packages/frontend/src/components/mfm.ts
deleted file mode 100644
index c3c07b5834..0000000000
--- a/packages/frontend/src/components/mfm.ts
+++ /dev/null
@@ -1,390 +0,0 @@
-import { VNode, defineComponent, h } from 'vue';
-import * as mfm from 'mfm-js';
-import MkUrl from '@/components/global/MkUrl.vue';
-import MkLink from '@/components/MkLink.vue';
-import MkMention from '@/components/MkMention.vue';
-import MkEmoji from '@/components/global/MkEmoji.vue';
-import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
-import MkCode from '@/components/MkCode.vue';
-import MkGoogle from '@/components/MkGoogle.vue';
-import MkSparkle from '@/components/MkSparkle.vue';
-import MkA from '@/components/global/MkA.vue';
-import { host } from '@/config';
-import { defaultStore } from '@/store';
-
-const QUOTE_STYLE = `
-display: block;
-margin: 8px;
-padding: 6px 0 6px 12px;
-color: var(--fg);
-border-left: solid 3px var(--fg);
-opacity: 0.7;
-`.split('\n').join(' ');
-
-export default defineComponent({
- props: {
- text: {
- type: String,
- required: true,
- },
- plain: {
- type: Boolean,
- default: false,
- },
- nowrap: {
- type: Boolean,
- default: false,
- },
- author: {
- type: Object,
- default: null,
- },
- i: {
- type: Object,
- default: null,
- },
- isNote: {
- type: Boolean,
- default: true,
- },
- emojiUrls: {
- type: Object,
- default: null,
- },
- rootScale: {
- type: Number,
- default: 1,
- }
- },
-
- render() {
- if (this.text == null || this.text === '') return;
-
- const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text);
-
- const validTime = (t: string | null | undefined) => {
- if (t == null) return null;
- return t.match(/^[0-9.]+s$/) ? t : null;
- };
-
- const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
-
- /**
- * Gen Vue Elements from MFM AST
- * @param ast MFM AST
- * @param scale How times large the text is
- */
- const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
- switch (token.type) {
- case 'text': {
- const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
-
- if (!this.plain) {
- const res: (VNode | string)[] = [];
- for (const t of text.split('\n')) {
- res.push(h('br'));
- res.push(t);
- }
- res.shift();
- return res;
- } else {
- return [text.replace(/\n/g, ' ')];
- }
- }
-
- case 'bold': {
- return [h('b', genEl(token.children, scale))];
- }
-
- case 'strike': {
- return [h('del', genEl(token.children, scale))];
- }
-
- case 'italic': {
- return h('i', {
- style: 'font-style: oblique;',
- }, genEl(token.children, scale));
- }
-
- case 'fn': {
- // 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)' :
- token.props.args.v ? 'scaleY(-1)' :
- 'scaleX(-1)';
- style = `transform: ${transform};`;
- break;
- }
- case 'x2': {
- return h('span', {
- class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
- }, genEl(token.children, scale * 2));
- }
- case 'x3': {
- return h('span', {
- class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
- }, genEl(token.children, scale * 3));
- }
- case 'x4': {
- return h('span', {
- class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
- }, genEl(token.children, scale * 4));
- }
- case 'font': {
- const family =
- token.props.args.serif ? 'serif' :
- token.props.args.monospace ? 'monospace' :
- token.props.args.cursive ? 'cursive' :
- token.props.args.fantasy ? 'fantasy' :
- token.props.args.emoji ? 'emoji' :
- token.props.args.math ? 'math' :
- null;
- if (family) style = `font-family: ${family};`;
- break;
- }
- case 'blur': {
- return h('span', {
- class: '_mfm_blur_',
- }, genEl(token.children, scale));
- }
- case 'rainbow': {
- const speed = validTime(token.props.args.speed) ?? '1s';
- style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
- break;
- }
- case 'sparkle': {
- if (!useAnim) {
- return genEl(token.children, scale);
- }
- return h(MkSparkle, {}, genEl(token.children, scale));
- }
- case 'rotate': {
- const degrees = parseFloat(token.props.args.deg ?? '90');
- style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
- break;
- }
- case 'position': {
- if (!defaultStore.state.advancedMfm) break;
- const x = parseFloat(token.props.args.x ?? '0');
- const y = parseFloat(token.props.args.y ?? '0');
- style = `transform: translateX(${x}em) translateY(${y}em);`;
- break;
- }
- case 'scale': {
- if (!defaultStore.state.advancedMfm) {
- style = '';
- break;
- }
- const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
- const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
- style = `transform: scale(${x}, ${y});`;
- scale = scale * Math.max(x, y);
- break;
- }
- case 'fg': {
- let color = token.props.args.color;
- if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
- style = `color: #${color};`;
- break;
- }
- case 'bg': {
- let color = token.props.args.color;
- if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
- style = `background-color: #${color};`;
- break;
- }
- }
- if (style == null) {
- return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
- } else {
- return h('span', {
- style: 'display: inline-block; ' + style,
- }, genEl(token.children, scale));
- }
- }
-
- case 'small': {
- return [h('small', {
- style: 'opacity: 0.7;',
- }, genEl(token.children, scale))];
- }
-
- case 'center': {
- return [h('div', {
- style: 'text-align:center;',
- }, genEl(token.children, scale))];
- }
-
- case 'url': {
- return [h(MkUrl, {
- key: Math.random(),
- url: token.props.url,
- rel: 'nofollow noopener',
- })];
- }
-
- case 'link': {
- return [h(MkLink, {
- key: Math.random(),
- url: token.props.url,
- rel: 'nofollow noopener',
- }, genEl(token.children, scale))];
- }
-
- case 'mention': {
- return [h(MkMention, {
- key: Math.random(),
- host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
- username: token.props.username,
- })];
- }
-
- case 'hashtag': {
- return [h(MkA, {
- key: Math.random(),
- to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
- style: 'color:var(--hashtag);',
- }, `#${token.props.hashtag}`)];
- }
-
- case 'blockCode': {
- return [h(MkCode, {
- key: Math.random(),
- code: token.props.code,
- lang: token.props.lang,
- })];
- }
-
- case 'inlineCode': {
- return [h(MkCode, {
- key: Math.random(),
- code: token.props.code,
- inline: true,
- })];
- }
-
- case 'quote': {
- if (!this.nowrap) {
- return [h('div', {
- style: QUOTE_STYLE,
- }, genEl(token.children, scale))];
- } else {
- return [h('span', {
- style: QUOTE_STYLE,
- }, genEl(token.children, scale))];
- }
- }
-
- case 'emojiCode': {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (this.author?.host == null) {
- return [h(MkCustomEmoji, {
- key: Math.random(),
- name: token.props.name,
- normal: this.plain,
- host: null,
- useOriginalSize: scale >= 2.5,
- })];
- } else {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (this.emojiUrls && (this.emojiUrls[token.props.name] == null)) {
- return [h('span', `:${token.props.name}:`)];
- } else {
- return [h(MkCustomEmoji, {
- key: Math.random(),
- name: token.props.name,
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- url: this.emojiUrls ? this.emojiUrls[token.props.name] : null,
- normal: this.plain,
- host: this.author.host,
- useOriginalSize: scale >= 2.5,
- })];
- }
- }
- }
-
- case 'unicodeEmoji': {
- return [h(MkEmoji, {
- key: Math.random(),
- emoji: token.props.emoji,
- })];
- }
-
- case 'mathInline': {
- return [h('code', token.props.formula)];
- }
-
- case 'mathBlock': {
- return [h('code', token.props.formula)];
- }
-
- case 'search': {
- return [h(MkGoogle, {
- key: Math.random(),
- q: token.props.query,
- })];
- }
-
- case 'plain': {
- return [h('span', genEl(token.children, scale))];
- }
-
- default: {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- console.error('unrecognized ast type:', (token as any).type);
-
- return [];
- }
- }
- }).flat(Infinity) as (VNode | string)[];
-
- // Parse ast to DOM
- return h('span', genEl(ast, this.rootScale));
- },
-});
diff --git a/packages/frontend/src/components/page/block.type.ts b/packages/frontend/src/components/page/block.type.ts
new file mode 100644
index 0000000000..71249a8aff
--- /dev/null
+++ b/packages/frontend/src/components/page/block.type.ts
@@ -0,0 +1,29 @@
+export type BlockBase = {
+ id: string;
+ type: string;
+};
+
+export type TextBlock = BlockBase & {
+ type: 'text';
+ text: string;
+};
+
+export type SectionBlock = BlockBase & {
+ type: 'section';
+ title: string;
+ children: Block[];
+};
+
+export type ImageBlock = BlockBase & {
+ type: 'image';
+ fileId: string | null;
+};
+
+export type NoteBlock = BlockBase & {
+ type: 'note';
+ detailed: boolean;
+ note: string | null;
+};
+
+export type Block =
+ TextBlock | SectionBlock | ImageBlock | NoteBlock;
diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue
index f3e7764604..2bf3d12daa 100644
--- a/packages/frontend/src/components/page/page.block.vue
+++ b/packages/frontend/src/components/page/page.block.vue
@@ -1,44 +1,29 @@
<template>
-<component :is="'x-' + block.type" :key="block.id" :block="block" :hpml="hpml" :h="h"/>
+<component :is="getComponent(block.type)" :key="block.id" :page="page" :block="block" :h="h"/>
</template>
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
import XText from './page.text.vue';
import XSection from './page.section.vue';
import XImage from './page.image.vue';
-import XButton from './page.button.vue';
-import XNumberInput from './page.number-input.vue';
-import XTextInput from './page.text-input.vue';
-import XTextareaInput from './page.textarea-input.vue';
-import XSwitch from './page.switch.vue';
-import XIf from './page.if.vue';
-import XTextarea from './page.textarea.vue';
-import XPost from './page.post.vue';
-import XCounter from './page.counter.vue';
-import XRadioButton from './page.radio-button.vue';
-import XCanvas from './page.canvas.vue';
import XNote from './page.note.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { Block } from '@/scripts/hpml/block';
+import { Block } from './block.type';
-export default defineComponent({
- components: {
- XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote,
- },
- props: {
- block: {
- type: Object as PropType<Block>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- h: {
- type: Number,
- required: true,
- },
- },
-});
+function getComponent(type: string) {
+ switch (type) {
+ case 'text': return XText;
+ case 'section': return XSection;
+ case 'image': return XImage;
+ case 'note': return XNote;
+ default: return null;
+ }
+}
+
+defineProps<{
+ block: Block,
+ h: number,
+ page: Misskey.entities.Page,
+}>();
</script>
diff --git a/packages/frontend/src/components/page/page.button.vue b/packages/frontend/src/components/page/page.button.vue
deleted file mode 100644
index 83931021d8..0000000000
--- a/packages/frontend/src/components/page/page.button.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<template>
-<div>
- <MkButton class="kudkigyw" :primary="block.primary" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType, unref } from 'vue';
-import MkButton from '../MkButton.vue';
-import * as os from '@/os';
-import { ButtonBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-
-export default defineComponent({
- components: {
- MkButton,
- },
- props: {
- block: {
- type: Object as PropType<ButtonBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- methods: {
- click() {
- if (this.block.action === 'dialog') {
- this.hpml.eval();
- os.alert({
- text: this.hpml.interpolate(this.block.content),
- });
- } else if (this.block.action === 'resetRandom') {
- this.hpml.updateRandomSeed(Math.random());
- this.hpml.eval();
- } else if (this.block.action === 'pushEvent') {
- os.api('page-push', {
- pageId: this.hpml.page.id,
- event: this.block.event,
- ...(this.block.var ? {
- var: unref(this.hpml.vars)[this.block.var],
- } : {}),
- });
-
- os.alert({
- type: 'success',
- text: this.hpml.interpolate(this.block.message),
- });
- } else if (this.block.action === 'callAiScript') {
- this.hpml.callAiScript(this.block.fn);
- }
- },
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
- display: inline-block;
- min-width: 200px;
- max-width: 450px;
- margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.canvas.vue b/packages/frontend/src/components/page/page.canvas.vue
deleted file mode 100644
index 82ff36ec36..0000000000
--- a/packages/frontend/src/components/page/page.canvas.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<div class="ysrxegms">
- <canvas ref="canvas" :width="block.width" :height="block.height"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
-import { CanvasBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-
-export default defineComponent({
- props: {
- block: {
- type: Object as PropType<CanvasBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- setup(props, ctx) {
- const canvas: Ref<any> = ref(null);
-
- onMounted(() => {
- props.hpml.registerCanvas(props.block.name, canvas.value);
- });
-
- return {
- canvas,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.ysrxegms {
- display: inline-block;
- vertical-align: bottom;
- overflow: auto;
- max-width: 100%;
-
- > canvas {
- display: block;
- }
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.counter.vue b/packages/frontend/src/components/page/page.counter.vue
deleted file mode 100644
index 63fde6a120..0000000000
--- a/packages/frontend/src/components/page/page.counter.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-<div>
- <MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkButton from '../MkButton.vue';
-import { CounterVarBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-
-export default defineComponent({
- components: {
- MkButton,
- },
- props: {
- block: {
- type: Object as PropType<CounterVarBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- setup(props, ctx) {
- const value = computed(() => {
- return props.hpml.vars.value[props.block.name];
- });
-
- function click() {
- props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
- props.hpml.eval();
- }
-
- return {
- click,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.llumlmnx {
- display: inline-block;
- min-width: 300px;
- max-width: 450px;
- margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.if.vue b/packages/frontend/src/components/page/page.if.vue
deleted file mode 100644
index 372a15f0c6..0000000000
--- a/packages/frontend/src/components/page/page.if.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<div v-show="hpml.vars.value[block.var]">
- <XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h"/>
-</div>
-</template>
-
-<script lang="ts">
-import { IfBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { defineComponent, defineAsyncComponent, PropType } from 'vue';
-
-export default defineComponent({
- components: {
- XBlock: defineAsyncComponent(() => import('./page.block.vue')),
- },
- props: {
- block: {
- type: Object as PropType<IfBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- h: {
- type: Number,
- required: true,
- },
- },
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index 6ea81d257f..2edcfb8b1a 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -5,15 +5,15 @@
</template>
<script lang="ts" setup>
-import { PropType } from 'vue';
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
+import { ImageBlock } from './block.type';
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
-import { ImageBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
const props = defineProps<{
- block: PropType<ImageBlock>,
- hpml: PropType<Hpml>,
+ block: ImageBlock,
+ page: Misskey.entities.Page,
}>();
-const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
+const image = props.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index 8c65dabf08..7133a7f5a1 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -1,47 +1,29 @@
<template>
-<div class="voxdxuby">
+<div style="margin: 1em 0;">
<MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note"/>
<MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note"/>
</div>
</template>
-<script lang="ts">
-import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
+<script lang="ts" setup>
+import { onMounted, Ref, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { NoteBlock } from './block.type';
import MkNote from '@/components/MkNote.vue';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import * as os from '@/os';
-import { NoteBlock } from '@/scripts/hpml/block';
-export default defineComponent({
- components: {
- MkNote,
- MkNoteDetailed,
- },
- props: {
- block: {
- type: Object as PropType<NoteBlock>,
- required: true,
- },
- },
- setup(props, ctx) {
- const note: Ref<Record<string, any> | null> = ref(null);
+const props = defineProps<{
+ block: NoteBlock,
+ page: Misskey.entities.Page,
+}>();
- onMounted(() => {
- os.api('notes/show', { noteId: props.block.note })
- .then(result => {
- note.value = result;
- });
- });
+const note: Ref<Misskey.entities.Note | null> = ref(null);
- return {
- note,
- };
- },
+onMounted(() => {
+ os.api('notes/show', { noteId: props.block.note })
+ .then(result => {
+ note.value = result;
+ });
});
</script>
-
-<style lang="scss" scoped>
-.voxdxuby {
- margin: 1em 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.number-input.vue b/packages/frontend/src/components/page/page.number-input.vue
deleted file mode 100644
index 72c1b6deb0..0000000000
--- a/packages/frontend/src/components/page/page.number-input.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div>
- <MkInput class="kudkigyw" :model-value="value" type="number" @update:model-value="updateValue($event)">
- <template #label>{{ hpml.interpolate(block.text) }}</template>
- </MkInput>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../MkInput.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { NumberInputVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
- components: {
- MkInput,
- },
- props: {
- block: {
- type: Object as PropType<NumberInputVarBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- setup(props, ctx) {
- const value = computed(() => {
- return props.hpml.vars.value[props.block.name];
- });
-
- function updateValue(newValue) {
- props.hpml.updatePageVar(props.block.name, newValue);
- props.hpml.eval();
- }
-
- return {
- value,
- updateValue,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
- display: inline-block;
- min-width: 300px;
- max-width: 450px;
- margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.post.vue b/packages/frontend/src/components/page/page.post.vue
deleted file mode 100644
index 55da610cb6..0000000000
--- a/packages/frontend/src/components/page/page.post.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-<template>
-<div class="ngbfujlo">
- <MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
- <MkButton class="button" primary :disabled="posting || posted" @click="post()">
- <i v-if="posted" class="ti ti-check"></i>
- <i v-else class="ti ti-send"></i>
- </MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../MkTextarea.vue';
-import MkButton from '../MkButton.vue';
-import { apiUrl } from '@/config';
-import * as os from '@/os';
-import { PostBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { defaultStore } from '@/store';
-import { $i } from '@/account';
-
-export default defineComponent({
- components: {
- MkTextarea,
- MkButton,
- },
- props: {
- block: {
- type: Object as PropType<PostBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- data() {
- return {
- text: this.hpml.interpolate(this.block.text),
- posted: false,
- posting: false,
- };
- },
- watch: {
- 'hpml.vars': {
- handler() {
- this.text = this.hpml.interpolate(this.block.text);
- },
- deep: true,
- },
- },
- methods: {
- upload() {
- const promise = new Promise((ok) => {
- const canvas = this.hpml.canvases[this.block.canvasId];
- canvas.toBlob(blob => {
- const formData = new FormData();
- formData.append('file', blob);
- formData.append('i', $i.token);
- if (defaultStore.state.uploadFolder) {
- formData.append('folderId', defaultStore.state.uploadFolder);
- }
-
- window.fetch(apiUrl + '/drive/files/create', {
- method: 'POST',
- body: formData,
- })
- .then(response => response.json())
- .then(f => {
- ok(f);
- });
- });
- });
- os.promiseDialog(promise);
- return promise;
- },
- async post() {
- this.posting = true;
- const file = this.block.attachCanvasImage ? await this.upload() : null;
- os.apiWithDialog('notes/create', {
- text: this.text === '' ? null : this.text,
- fileIds: file ? [file.id] : undefined,
- }).then(() => {
- this.posted = true;
- });
- },
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.ngbfujlo {
- position: relative;
- padding: 32px;
- border-radius: 6px;
- box-shadow: 0 2px 8px var(--shadow);
- z-index: 1;
-
- > .button {
- margin-top: 32px;
- }
-
- @media (max-width: 600px) {
- padding: 16px;
-
- > .button {
- margin-top: 16px;
- }
- }
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.radio-button.vue b/packages/frontend/src/components/page/page.radio-button.vue
deleted file mode 100644
index ce8f252e44..0000000000
--- a/packages/frontend/src/components/page/page.radio-button.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<div>
- <div>{{ hpml.interpolate(block.title) }}</div>
- <MkRadio v-for="item in block.values" :key="item" :modelValue="value" :value="item" @update:model-value="updateValue($event)">{{ item }}</MkRadio>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkRadio from '../MkRadio.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { RadioButtonVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
- components: {
- MkRadio,
- },
- props: {
- block: {
- type: Object as PropType<RadioButtonVarBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- setup(props, ctx) {
- const value = computed(() => {
- return props.hpml.vars.value[props.block.name];
- });
-
- function updateValue(newValue: string) {
- props.hpml.updatePageVar(props.block.name, newValue);
- props.hpml.eval();
- }
-
- return {
- value,
- updateValue,
- };
- },
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue
index 50181b3905..83a16ae0a5 100644
--- a/packages/frontend/src/components/page/page.section.vue
+++ b/packages/frontend/src/components/page/page.section.vue
@@ -1,59 +1,49 @@
<template>
-<section class="sdgxphyu">
- <component :is="'h' + h">{{ block.title }}</component>
+<section>
+ <component
+ :is="'h' + h"
+ :class="{
+ 'h2': h === 2,
+ 'h3': h === 3,
+ 'h4': h === 4,
+ }"
+ >
+ {{ block.title }}
+ </component>
- <div class="children">
- <XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h + 1"/>
+ <div class="_gaps">
+ <XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/>
</div>
</section>
</template>
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, PropType } from 'vue';
-import { SectionBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
+import { SectionBlock } from './block.type';
-export default defineComponent({
- components: {
- XBlock: defineAsyncComponent(() => import('./page.block.vue')),
- },
- props: {
- block: {
- type: Object as PropType<SectionBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- h: {
- required: true,
- },
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.sdgxphyu {
- margin: 1.5em 0;
+const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
- > h2 {
- font-size: 1.35em;
- margin: 0 0 0.5em 0;
- }
+defineProps<{
+ block: SectionBlock,
+ h: number,
+ page: Misskey.entities.Page,
+}>();
+</script>
- > h3 {
- font-size: 1em;
- margin: 0 0 0.5em 0;
- }
+<style lang="scss" module>
+.h2 {
+ font-size: 1.35em;
+ margin: 0 0 0.5em 0;
+}
- > h4 {
- font-size: 1em;
- margin: 0 0 0.5em 0;
- }
+.h3 {
+ font-size: 1em;
+ margin: 0 0 0.5em 0;
+}
- > .children {
- //padding 16px
- }
+.h4 {
+ font-size: 1em;
+ margin: 0 0 0.5em 0;
}
</style>
diff --git a/packages/frontend/src/components/page/page.switch.vue b/packages/frontend/src/components/page/page.switch.vue
deleted file mode 100644
index b5f3464512..0000000000
--- a/packages/frontend/src/components/page/page.switch.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div class="hkcxmtwj">
- <MkSwitch :model-value="value" @update:model-value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkSwitch from '../MkSwitch.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { SwitchVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
- components: {
- MkSwitch,
- },
- props: {
- block: {
- type: Object as PropType<SwitchVarBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- setup(props, ctx) {
- const value = computed(() => {
- return props.hpml.vars.value[props.block.name];
- });
-
- function updateValue(newValue: boolean) {
- props.hpml.updatePageVar(props.block.name, newValue);
- props.hpml.eval();
- }
-
- return {
- value,
- updateValue,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.hkcxmtwj {
- display: inline-block;
- margin: 16px auto;
-
- & + .hkcxmtwj {
- margin-left: 16px;
- }
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.text-input.vue b/packages/frontend/src/components/page/page.text-input.vue
deleted file mode 100644
index d020a99de8..0000000000
--- a/packages/frontend/src/components/page/page.text-input.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div>
- <MkInput class="kudkigyw" :model-value="value" type="text" @update:model-value="updateValue($event)">
- <template #label>{{ hpml.interpolate(block.text) }}</template>
- </MkInput>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../MkInput.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { TextInputVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
- components: {
- MkInput,
- },
- props: {
- block: {
- type: Object as PropType<TextInputVarBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- setup(props, ctx) {
- const value = computed(() => {
- return props.hpml.vars.value[props.block.name];
- });
-
- function updateValue(newValue) {
- props.hpml.updatePageVar(props.block.name, newValue);
- props.hpml.eval();
- }
-
- return {
- value,
- updateValue,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
- display: inline-block;
- min-width: 300px;
- max-width: 450px;
- margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index e0e4959efa..48ce4b0e1e 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -1,70 +1,24 @@
<template>
-<div class="mrdgzndn">
- <Mfm :key="text" :text="text" :is-note="false" :i="$i"/>
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" class="url"/>
+<div class="_gaps">
+ <Mfm :text="block.text" :isNote="false" :i="$i"/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
</div>
</template>
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, PropType } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
import * as mfm from 'mfm-js';
-import { TextBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
+import * as Misskey from 'misskey-js';
+import { TextBlock } from './block.type';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account';
-export default defineComponent({
- components: {
- MkUrlPreview: defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')),
- },
- props: {
- block: {
- type: Object as PropType<TextBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- data() {
- return {
- text: this.hpml.interpolate(this.block.text),
- $i,
- };
- },
- computed: {
- urls(): string[] {
- if (this.text) {
- return extractUrlFromMfm(mfm.parse(this.text));
- } else {
- return [];
- }
- },
- },
- watch: {
- 'hpml.vars': {
- handler() {
- this.text = this.hpml.interpolate(this.block.text);
- },
- deep: true,
- },
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.mrdgzndn {
- &:not(:first-child) {
- margin-top: 0.5em;
- }
+const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
- &:not(:last-child) {
- margin-bottom: 0.5em;
- }
+const props = defineProps<{
+ block: TextBlock,
+ page: Misskey.entities.Page,
+}>();
- > .url {
- margin: 0.5em 0;
- }
-}
-</style>
+const urls = props.block.text ? extractUrlFromMfm(mfm.parse(props.block.text)) : [];
+</script>
diff --git a/packages/frontend/src/components/page/page.textarea-input.vue b/packages/frontend/src/components/page/page.textarea-input.vue
deleted file mode 100644
index db3a96dd1b..0000000000
--- a/packages/frontend/src/components/page/page.textarea-input.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-<div>
- <MkTextarea :model-value="value" @update:model-value="updateValue($event)">
- <template #label>{{ hpml.interpolate(block.text) }}</template>
- </MkTextarea>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkTextarea from '../MkTextarea.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { TextInputVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
- components: {
- MkTextarea,
- },
- props: {
- block: {
- type: Object as PropType<TextInputVarBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- setup(props, ctx) {
- const value = computed(() => {
- return props.hpml.vars.value[props.block.name];
- });
-
- function updateValue(newValue) {
- props.hpml.updatePageVar(props.block.name, newValue);
- props.hpml.eval();
- }
-
- return {
- value,
- updateValue,
- };
- },
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.textarea.vue b/packages/frontend/src/components/page/page.textarea.vue
deleted file mode 100644
index 9b82412e8a..0000000000
--- a/packages/frontend/src/components/page/page.textarea.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<MkTextarea :model-value="text" readonly></MkTextarea>
-</template>
-
-<script lang="ts">
-import { TextBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../MkTextarea.vue';
-
-export default defineComponent({
- components: {
- MkTextarea,
- },
- props: {
- block: {
- type: Object as PropType<TextBlock>,
- required: true,
- },
- hpml: {
- type: Object as PropType<Hpml>,
- required: true,
- },
- },
- data() {
- return {
- text: this.hpml.interpolate(this.block.text),
- };
- },
- watch: {
- 'hpml.vars': {
- handler() {
- this.text = this.hpml.interpolate(this.block.text);
- },
- deep: true,
- },
- },
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue
index 5f1f62581e..c2c2693224 100644
--- a/packages/frontend/src/components/page/page.vue
+++ b/packages/frontend/src/components/page/page.vue
@@ -1,56 +1,25 @@
<template>
-<div v-if="hpml" class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }">
- <XBlock v-for="child in page.content" :key="child.id" :block="child" :hpml="hpml" :h="2"/>
+<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }">
+ <XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/>
</div>
</template>
-<script lang="ts">
-import { defineComponent, onMounted, nextTick, PropType } from 'vue';
+<script lang="ts" setup>
+import { onMounted, nextTick } from 'vue';
+import * as Misskey from 'misskey-js';
import XBlock from './page.block.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { url } from '@/config';
-import { $i } from '@/account';
-export default defineComponent({
- components: {
- XBlock,
- },
- props: {
- page: {
- type: Object as PropType<Record<string, any>>,
- required: true,
- },
- },
- setup(props, ctx) {
- const hpml = new Hpml(props.page, {
- randomSeed: Math.random(),
- visitor: $i,
- url: url,
- });
-
- onMounted(() => {
- nextTick(() => {
- hpml.eval();
- });
- });
-
- return {
- hpml,
- };
- },
-});
+defineProps<{
+ page: Misskey.entities.Page,
+}>();
</script>
-<style lang="scss" scoped>
-.iroscrza {
- &.serif {
- > div {
- font-family: serif;
- }
- }
+<style lang="scss" module>
+.serif {
+ font-family: serif;
+}
- &.center {
- text-align: center;
- }
+.center {
+ text-align: center;
}
</style>