diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-08-07 00:39:21 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-08-07 00:39:21 +0900 |
| commit | 3b1669fb6b131e0809b3a4a93270b2b9e4a74247 (patch) | |
| tree | d9c0397e52f722e53fb99be6c6cae7023f5f054a /packages/client/src/components | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.118.0 (diff) | |
| download | misskey-3b1669fb6b131e0809b3a4a93270b2b9e4a74247.tar.gz misskey-3b1669fb6b131e0809b3a4a93270b2b9e4a74247.tar.bz2 misskey-3b1669fb6b131e0809b3a4a93270b2b9e4a74247.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src/components')
46 files changed, 667 insertions, 489 deletions
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue index 30c27e6235..9ae773bfb6 100644 --- a/packages/client/src/components/MkNoteSub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -6,7 +6,7 @@ <XNoteHeader class="header" :note="note" :mini="true"/> <div class="body"> <p v-if="note.cw != null" class="cw"> - <Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" /> + <Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> <XCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent" class="content"> @@ -19,7 +19,7 @@ <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/> </template> <div v-else class="more"> - <MkA class="text _link" :to="notePage(note)">{{ $ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA> + <MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA> </div> </div> </template> @@ -27,11 +27,12 @@ <script lang="ts" setup> import { } from 'vue'; import * as misskey from 'misskey-js'; -import { notePage } from '@/filters/note'; import XNoteHeader from './note-header.vue'; import MkNoteSubNoteContent from './sub-note-content.vue'; import XCwButton from './cw-button.vue'; +import { notePage } from '@/filters/note'; import * as os from '@/os'; +import { i18n } from '@/i18n'; const props = withDefaults(defineProps<{ note: misskey.entities.Note; @@ -49,7 +50,7 @@ let replies: misskey.entities.Note[] = $ref([]); if (props.detail) { os.api('notes/children', { noteId: props.note.id, - limit: 5 + limit: 5, }).then(res => { replies = res; }); diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue index 2b89eef85a..8c25df1107 100644 --- a/packages/client/src/components/abuse-report.vue +++ b/packages/client/src/components/abuse-report.vue @@ -9,7 +9,7 @@ </div> </MkA> <MkKeyValue class="_formBlock"> - <template #key>{{ $ts.registeredDate }}</template> + <template #key>{{ i18n.ts.registeredDate }}</template> <template #value>{{ new Date(report.targetUser.createdAt).toLocaleString() }} (<MkTime :time="report.targetUser.createdAt"/>)</template> </MkKeyValue> </div> @@ -18,18 +18,18 @@ <Mfm :text="report.comment"/> </div> <hr/> - <div>{{ $ts.reporter }}: <MkAcct :user="report.reporter"/></div> + <div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div> <div v-if="report.assignee"> - {{ $ts.moderator }}: + {{ i18n.ts.moderator }}: <MkAcct :user="report.assignee"/> </div> <div><MkTime :time="report.createdAt"/></div> <div class="action"> <MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved"> - {{ $ts.forwardReport }} - <template #caption>{{ $ts.forwardReportIsAnonymous }}</template> + {{ i18n.ts.forwardReport }} + <template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template> </MkSwitch> - <MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton> + <MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton> </div> </div> </div> @@ -41,6 +41,7 @@ import MkSwitch from '@/components/form/switch.vue'; import MkKeyValue from '@/components/key-value.vue'; import { acct, userPage } from '@/filters/user'; import * as os from '@/os'; +import { i18n } from '@/i18n'; const props = defineProps<{ report: any; diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue index 18dd1e3f41..b138bfcb46 100644 --- a/packages/client/src/components/analog-clock.vue +++ b/packages/client/src/components/analog-clock.vue @@ -1,12 +1,30 @@ <template> <svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none"> - <circle v-for="(angle, i) in graduations" - :key="i" - :cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" - :cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" - :r="i % 5 == 0 ? 0.125 : 0.05" - :fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor" - /> + <template v-if="props.graduations === 'dots'"> + <circle + v-for="(angle, i) in graduationsMajor" + :cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" + :cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" + :r="0.125" + :fill="(props.twentyfour ? h : h % 12) === i ? nowColor : majorGraduationColor" + :opacity="!props.fadeGraduations || (props.twentyfour ? h : h % 12) === i ? 1 : Math.max(0, 1 - (angleDiff(hAngle, angle) / Math.PI) - numbersOpacityFactor)" + /> + </template> + <template v-else-if="props.graduations === 'numbers'"> + <text + v-for="(angle, i) in texts" + :x="5 + (Math.sin(angle) * (5 - textsPadding))" + :y="5 - (Math.cos(angle) * (5 - textsPadding))" + text-anchor="middle" + dominant-baseline="middle" + :font-size="(props.twentyfour ? h : h % 12) === i ? 1 : 0.7" + :font-weight="(props.twentyfour ? h : h % 12) === i ? 'bold' : 'normal'" + :fill="(props.twentyfour ? h : h % 12) === i ? nowColor : 'currentColor'" + :opacity="!props.fadeGraduations || (props.twentyfour ? h : h % 12) === i ? 1 : Math.max(0, 1 - (angleDiff(hAngle, angle) / Math.PI) - numbersOpacityFactor)" + > + {{ i === 0 ? (props.twentyfour ? '24' : '12') : i }} + </text> + </template> <line :x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))" @@ -41,63 +59,116 @@ </template> <script lang="ts" setup> -import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; +import { ref, computed, onMounted, onBeforeUnmount, shallowRef } from 'vue'; import tinycolor from 'tinycolor2'; +import { globalEvents } from '@/events.js'; + +// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles +const angleDiff = (a: number, b: number) => { + const x = Math.abs(a - b); + return Math.abs((x + Math.PI) % (Math.PI * 2) - Math.PI); +}; + +const graduationsPadding = 0.5; +const textsPadding = 0.6; +const handsPadding = 1; +const handsTailLength = 0.7; +const hHandLengthRatio = 0.75; +const mHandLengthRatio = 1; +const sHandLengthRatio = 1; +const numbersOpacityFactor = 0.35; -withDefaults(defineProps<{ - thickness: number; +const props = withDefaults(defineProps<{ + thickness?: number; + offset?: number; + twentyfour?: boolean; + graduations?: 'none' | 'dots' | 'numbers'; + fadeGraduations?: boolean; }>(), { + numbers: false, thickness: 0.1, + offset: 0 - new Date().getTimezoneOffset(), + twentyfour: false, + graduations: 'dots', + fadeGraduations: true, }); -const now = ref(new Date()); -const enabled = ref(true); -const graduationsPadding = ref(0.5); -const handsPadding = ref(1); -const handsTailLength = ref(0.7); -const hHandLengthRatio = ref(0.75); -const mHandLengthRatio = ref(1); -const sHandLengthRatio = ref(1); -const computedStyle = getComputedStyle(document.documentElement); - -const dark = computed(() => tinycolor(computedStyle.getPropertyValue('--bg')).isDark()); -const majorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'); -const minorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'); -const sHandColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'); -const mHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--fg')).toHexString()); -const hHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--accent')).toHexString()); -const s = computed(() => now.value.getSeconds()); -const m = computed(() => now.value.getMinutes()); -const h = computed(() => now.value.getHours()); -const hAngle = computed(() => Math.PI * (h.value % 12 + (m.value + s.value / 60) / 60) / 6); -const mAngle = computed(() => Math.PI * (m.value + s.value / 60) / 30); -const sAngle = computed(() => Math.PI * s.value / 30); -const graduations = computed(() => { +const graduationsMajor = computed(() => { const angles: number[] = []; - for (let i = 0; i < 60; i++) { - const angle = Math.PI * i / 30; + const times = props.twentyfour ? 24 : 12; + for (let i = 0; i < times; i++) { + const angle = Math.PI * i / (times / 2); angles.push(angle); } - return angles; }); +const texts = computed(() => { + const angles: number[] = []; + const times = props.twentyfour ? 24 : 12; + for (let i = 0; i < times; i++) { + const angle = Math.PI * i / (times / 2); + angles.push(angle); + } + return angles; +}); + +let enabled = true; +let majorGraduationColor = $ref<string>(); +//let minorGraduationColor = $ref<string>(); +let sHandColor = $ref<string>(); +let mHandColor = $ref<string>(); +let hHandColor = $ref<string>(); +let nowColor = $ref<string>(); +let h = $ref<number>(0); +let m = $ref<number>(0); +let s = $ref<number>(0); +let hAngle = $ref<number>(0); +let mAngle = $ref<number>(0); +let sAngle = $ref<number>(0); function tick() { - now.value = new Date(); + const now = new Date(); + now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); + s = now.getSeconds(); + m = now.getMinutes(); + h = now.getHours(); + hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); + mAngle = Math.PI * (m + s / 60) / 30; + sAngle = Math.PI * s / 30; +} + +tick(); + +function calcColors() { + const computedStyle = getComputedStyle(document.documentElement); + const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark(); + const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + majorGraduationColor = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; + //minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + sHandColor = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; + mHandColor = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString(); + hHandColor = accent; + nowColor = accent; } +calcColors(); + onMounted(() => { const update = () => { - if (enabled.value) { + if (enabled) { tick(); window.setTimeout(update, 1000); } }; update(); + + globalEvents.on('themeChanged', calcColors); }); onBeforeUnmount(() => { - enabled.value = false; + enabled = false; + + globalEvents.off('themeChanged', calcColors); }); </script> diff --git a/packages/client/src/components/cropper-dialog.vue b/packages/client/src/components/cropper-dialog.vue index a8bde6ea05..c320b21d72 100644 --- a/packages/client/src/components/cropper-dialog.vue +++ b/packages/client/src/components/cropper-dialog.vue @@ -9,7 +9,7 @@ @ok="ok()" @closed="$emit('closed')" > - <template #header>{{ $ts.cropImage }}</template> + <template #header>{{ i18n.ts.cropImage }}</template> <template #default="{ width, height }"> <div class="mk-cropper-dialog" :style="`--vw: ${width}px; --vh: ${height}px;`"> <Transition name="fade"> @@ -36,6 +36,7 @@ import { $i } from '@/account'; import { defaultStore } from '@/store'; import { apiUrl, url } from '@/config'; import { query } from '@/scripts/url'; +import { i18n } from '@/i18n'; const emit = defineEmits<{ (ev: 'ok', cropped: misskey.entities.DriveFile): void; diff --git a/packages/client/src/components/digital-clock.vue b/packages/client/src/components/digital-clock.vue new file mode 100644 index 0000000000..9ed8d63d19 --- /dev/null +++ b/packages/client/src/components/digital-clock.vue @@ -0,0 +1,77 @@ +<template> +<span class="zjobosdg"> + <span v-text="hh"></span> + <span class="colon" :class="{ showColon }">:</span> + <span v-text="mm"></span> + <span v-if="showS" class="colon" :class="{ showColon }">:</span> + <span v-if="showS" v-text="ss"></span> + <span v-if="showMs" class="colon" :class="{ showColon }">:</span> + <span v-if="showMs" v-text="ms"></span> +</span> +</template> + +<script lang="ts" setup> +import { onUnmounted, ref, watch } from 'vue'; + +const props = withDefaults(defineProps<{ + showS?: boolean; + showMs?: boolean; + offset?: number; +}>(), { + showS: true, + showMs: false, + offset: 0 - new Date().getTimezoneOffset(), +}); + +let intervalId; +const hh = ref(''); +const mm = ref(''); +const ss = ref(''); +const ms = ref(''); +const showColon = ref(false); +let prevSec: number | null = null; + +watch(showColon, (v) => { + if (v) { + window.setTimeout(() => { + showColon.value = false; + }, 30); + } +}); + +const tick = () => { + const now = new Date(); + now.setMinutes(now.getMinutes() + (new Date().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'); + ms.value = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0'); + if (now.getSeconds() !== prevSec) showColon.value = true; + prevSec = now.getSeconds(); +}; + +tick(); + +watch(() => props.showMs, () => { + if (intervalId) window.clearInterval(intervalId); + intervalId = window.setInterval(tick, props.showMs ? 10 : 1000); +}, { immediate: true }); + +onUnmounted(() => { + window.clearInterval(intervalId); +}); +</script> + +<style lang="scss" scoped> +.zjobosdg { + > .colon { + opacity: 0; + transition: opacity 1s ease; + + &.showColon { + opacity: 1; + transition: opacity 0s; + } + } +} +</style> diff --git a/packages/client/src/components/form/checkbox.vue b/packages/client/src/components/form/checkbox.vue index fadb770aee..fb5c82bb48 100644 --- a/packages/client/src/components/form/checkbox.vue +++ b/packages/client/src/components/form/checkbox.vue @@ -9,7 +9,7 @@ :disabled="disabled" @keydown.enter="toggle" > - <span ref="button" v-adaptive-border v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle"> + <span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> <i class="check fas fa-check"></i> </span> <span class="label"> @@ -24,6 +24,7 @@ import { toRefs, Ref } from 'vue'; import * as os from '@/os'; import Ripple from '@/components/ripple.vue'; +import { i18n } from '@/i18n'; const props = defineProps<{ modelValue: boolean | Ref<boolean>; diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue index 2a03d6a5d4..1c9fee8c77 100644 --- a/packages/client/src/components/form/input.vue +++ b/packages/client/src/components/form/input.vue @@ -29,7 +29,7 @@ </div> <div class="caption"><slot name="caption"></slot></div> - <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-check"></i> {{ $ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-check"></i> {{ i18n.ts.save }}</MkButton> </div> </template> @@ -38,6 +38,7 @@ import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from ' import { debounce } from 'throttle-debounce'; import MkButton from '@/components/ui/button.vue'; import { useInterval } from '@/scripts/use-interval'; +import { i18n } from '@/i18n'; const props = defineProps<{ modelValue: string | number; diff --git a/packages/client/src/components/form/link.vue b/packages/client/src/components/form/link.vue index b74e9bd684..34b641ffb6 100644 --- a/packages/client/src/components/form/link.vue +++ b/packages/client/src/components/form/link.vue @@ -19,33 +19,16 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - props: { - to: { - type: String, - required: true - }, - active: { - type: Boolean, - required: false - }, - external: { - type: Boolean, - required: false - }, - behavior: { - type: String, - required: false, - }, - inline: { - type: Boolean, - required: false - }, - }, -}); +const props = defineProps<{ + to: string; + active?: boolean; + external?: boolean; + behavior?: null | 'window' | 'browser' | 'modalWindow'; + inline?: boolean; +}>(); </script> <style lang="scss" scoped> @@ -61,7 +44,7 @@ export default defineComponent({ align-items: center; width: 100%; box-sizing: border-box; - padding: 12px 14px 12px 14px; + padding: 10px 14px; background: var(--buttonBg); border-radius: 6px; font-size: 0.9em; diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue index b4d39507e3..b36f7e9fdc 100644 --- a/packages/client/src/components/form/radio.vue +++ b/packages/client/src/components/form/radio.vue @@ -18,34 +18,25 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - props: { - modelValue: { - required: false, - }, - value: { - required: false, - }, - disabled: { - type: Boolean, - default: false, - }, - }, - computed: { - checked(): boolean { - return this.modelValue === this.value; - }, - }, - methods: { - toggle() { - if (this.disabled) return; - this.$emit('update:modelValue', this.value); - }, - }, -}); +const props = defineProps<{ + modelValue: any; + value: any; + disabled: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'update:modelValue', value: any): void; +}>(); + +let checked = $computed(() => props.modelValue === props.value); + +function toggle(): void { + if (props.disabled) return; + emit('update:modelValue', props.value); +} </script> <style lang="scss" scoped> @@ -54,13 +45,13 @@ export default defineComponent({ display: inline-block; text-align: left; cursor: pointer; - padding: 9px 12px; + padding: 8px 10px; min-width: 60px; background-color: var(--panel); background-clip: padding-box !important; border: solid 1px var(--panel); border-radius: 6px; - transition: all 0.3s; + transition: all 0.2s; > * { user-select: none; diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue index 78282dfdc1..70db2dbae3 100644 --- a/packages/client/src/components/form/select.vue +++ b/packages/client/src/components/form/select.vue @@ -22,7 +22,7 @@ </div> <div class="caption"><slot name="caption"></slot></div> - <MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> </div> </template> @@ -31,6 +31,7 @@ import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode, import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import { useInterval } from '@/scripts/use-interval'; +import { i18n } from '@/i18n'; const props = defineProps<{ modelValue: string; @@ -144,6 +145,8 @@ const onClick = (ev: MouseEvent) => { } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある const fragment = vnode; scanOptions(fragment.children); + } else if (vnode.props == null) { // v-if で条件が false のときにこうなる + // nop? } else { const option = vnode; pushOption(option); diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue index fead163552..1ed00ae655 100644 --- a/packages/client/src/components/form/switch.vue +++ b/packages/client/src/components/form/switch.vue @@ -9,7 +9,7 @@ :disabled="disabled" @keydown.enter="toggle" > - <span ref="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle"> + <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> <div class="knob"></div> </span> <span class="label"> @@ -23,6 +23,7 @@ <script lang="ts" setup> import { toRefs, Ref } from 'vue'; import * as os from '@/os'; +import { i18n } from '@/i18n'; const props = defineProps<{ modelValue: boolean | Ref<boolean>; diff --git a/packages/client/src/components/form/textarea.vue b/packages/client/src/components/form/textarea.vue index c9ba9b97a2..73633399de 100644 --- a/packages/client/src/components/form/textarea.vue +++ b/packages/client/src/components/form/textarea.vue @@ -2,7 +2,8 @@ <div class="adhpbeos"> <div class="label" @click="focus"><slot name="label"></slot></div> <div class="input" :class="{ disabled, focused, tall, pre }"> - <textarea ref="inputEl" + <textarea + ref="inputEl" v-model="v" v-adaptive-border :class="{ code, _monospace: code }" @@ -21,14 +22,15 @@ </div> <div class="caption"><slot name="caption"></slot></div> - <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> </div> </template> <script lang="ts"> import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; -import MkButton from '@/components/ui/button.vue'; import { debounce } from 'throttle-debounce'; +import MkButton from '@/components/ui/button.vue'; +import { i18n } from '@/i18n'; export default defineComponent({ components: { @@ -37,66 +39,66 @@ export default defineComponent({ props: { modelValue: { - required: true + required: true, }, type: { type: String, - required: false + required: false, }, required: { type: Boolean, - required: false + required: false, }, readonly: { type: Boolean, - required: false + required: false, }, disabled: { type: Boolean, - required: false + required: false, }, pattern: { type: String, - required: false + required: false, }, placeholder: { type: String, - required: false + required: false, }, autofocus: { type: Boolean, required: false, - default: false + default: false, }, autocomplete: { - required: false + required: false, }, spellcheck: { - required: false + required: false, }, code: { type: Boolean, - required: false + required: false, }, tall: { type: Boolean, required: false, - default: false + default: false, }, pre: { type: Boolean, required: false, - default: false + default: false, }, debounce: { type: Boolean, required: false, - default: false + default: false, }, manualSave: { type: Boolean, required: false, - default: false + default: false, }, }, @@ -166,6 +168,7 @@ export default defineComponent({ onInput, onKeydown, updated, + i18n, }; }, }); diff --git a/packages/client/src/components/global/error.vue b/packages/client/src/components/global/error.vue index 98b96fb414..4e2ba07d30 100644 --- a/packages/client/src/components/global/error.vue +++ b/packages/client/src/components/global/error.vue @@ -2,14 +2,15 @@ <transition :name="$store.state.animation ? 'zoom' : ''" appear> <div class="mjndxjcg"> <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> - <p><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</p> - <MkButton class="button" @click="() => $emit('retry')">{{ $ts.retry }}</MkButton> + <p><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.somethingHappened }}</p> + <MkButton class="button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton> </div> </transition> </template> <script lang="ts" setup> import MkButton from '@/components/ui/button.vue'; +import { i18n } from '@/i18n'; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue index fca2371f0d..1d841e050c 100644 --- a/packages/client/src/components/global/router-view.vue +++ b/packages/client/src/components/global/router-view.vue @@ -1,12 +1,18 @@ <template> <KeepAlive :max="defaultStore.state.numberOfPageCache"> - <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> + <Suspense> + <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> + + <template #fallback> + Loading... + </template> + </Suspense> </KeepAlive> </template> <script lang="ts" setup> -import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue'; -import { Router } from '@/nirax'; +import { inject, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, watch } from 'vue'; +import { Resolved, Router } from '@/nirax'; import { defaultStore } from '@/store'; const props = defineProps<{ @@ -19,19 +25,37 @@ if (router == null) { throw new Error('no router provided'); } -let currentPageComponent = $shallowRef(router.getCurrentComponent()); -let currentPageProps = $ref(router.getCurrentProps()); -let key = $ref(router.getCurrentKey()); +const currentDepth = inject('routerCurrentDepth', 0); +provide('routerCurrentDepth', currentDepth + 1); + +function resolveNested(current: Resolved, d = 0): Resolved | null { + if (d === currentDepth) { + return current; + } else { + if (current.child) { + return resolveNested(current.child, d + 1); + } else { + return null; + } + } +} + +const current = resolveNested(router.current)!; +let currentPageComponent = $shallowRef(current.route.component); +let currentPageProps = $ref(current.props); +let key = $ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); -function onChange({ route, props: newProps, key: newKey }) { - currentPageComponent = route.component; - currentPageProps = newProps; - key = newKey; +function onChange({ resolved, key: newKey }) { + const current = resolveNested(resolved); + if (current == null) return; + currentPageComponent = current.route.component; + currentPageProps = current.props; + key = current.route.path + JSON.stringify(Object.fromEntries(current.props)); } router.addListener('change', onChange); -onUnmounted(() => { +onBeforeUnmount(() => { router.removeListener('change', onChange); }); </script> diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue index 801490225b..f72b153f56 100644 --- a/packages/client/src/components/global/time.vue +++ b/packages/client/src/components/global/time.vue @@ -20,7 +20,7 @@ const props = withDefaults(defineProps<{ const _time = typeof props.time === 'string' ? new Date(props.time) : props.time; const absolute = _time.toLocaleString(); -let now = $ref(new Date()); +let now = $shallowRef(new Date()); const relative = $computed(() => { const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/; return ( diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue index 1a811c2d87..65465dd9a2 100644 --- a/packages/client/src/components/instance-stats.vue +++ b/packages/client/src/components/instance-stats.vue @@ -4,29 +4,29 @@ <div class="body"> <div class="selects" style="display: flex;"> <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> - <optgroup :label="$ts.federation"> - <option value="federation">{{ $ts._charts.federation }}</option> - <option value="ap-request">{{ $ts._charts.apRequest }}</option> + <optgroup :label="i18n.ts.federation"> + <option value="federation">{{ i18n.ts._charts.federation }}</option> + <option value="ap-request">{{ i18n.ts._charts.apRequest }}</option> </optgroup> - <optgroup :label="$ts.users"> - <option value="users">{{ $ts._charts.usersIncDec }}</option> - <option value="users-total">{{ $ts._charts.usersTotal }}</option> - <option value="active-users">{{ $ts._charts.activeUsers }}</option> + <optgroup :label="i18n.ts.users"> + <option value="users">{{ i18n.ts._charts.usersIncDec }}</option> + <option value="users-total">{{ i18n.ts._charts.usersTotal }}</option> + <option value="active-users">{{ i18n.ts._charts.activeUsers }}</option> </optgroup> - <optgroup :label="$ts.notes"> - <option value="notes">{{ $ts._charts.notesIncDec }}</option> - <option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option> - <option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option> - <option value="notes-total">{{ $ts._charts.notesTotal }}</option> + <optgroup :label="i18n.ts.notes"> + <option value="notes">{{ i18n.ts._charts.notesIncDec }}</option> + <option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option> + <option value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option> + <option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option> </optgroup> - <optgroup :label="$ts.drive"> - <option value="drive-files">{{ $ts._charts.filesIncDec }}</option> - <option value="drive">{{ $ts._charts.storageUsageIncDec }}</option> + <optgroup :label="i18n.ts.drive"> + <option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option> + <option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option> </optgroup> </MkSelect> <MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;"> - <option value="hour">{{ $ts.perHour }}</option> - <option value="day">{{ $ts.perDay }}</option> + <option value="hour">{{ i18n.ts.perHour }}</option> + <option value="day">{{ i18n.ts.perDay }}</option> </MkSelect> </div> <div class="chart"> @@ -71,6 +71,7 @@ import MkSelect from '@/components/form/select.vue'; import MkChart from '@/components/chart.vue'; import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import * as os from '@/os'; +import { i18n } from '@/i18n'; Chart.register( ArcElement, diff --git a/packages/client/src/components/key-value.vue b/packages/client/src/components/key-value.vue index 3d665e159d..586f7a3f9d 100644 --- a/packages/client/src/components/key-value.vue +++ b/packages/client/src/components/key-value.vue @@ -5,7 +5,7 @@ </div> <div class="value"> <slot name="value"></slot> - <button v-if="copy" v-tooltip="$ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="far fa-copy"></i></button> + <button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="far fa-copy"></i></button> </div> </div> </template> @@ -14,6 +14,7 @@ import { } from 'vue'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import * as os from '@/os'; +import { i18n } from '@/i18n'; const props = withDefaults(defineProps<{ copy?: string | null; diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index 85bffca4a5..1e0625b6c9 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -14,7 +14,7 @@ <div v-if="isRenote" class="renote"> <MkAvatar class="avatar" :user="note.user"/> <i class="fas fa-retweet"></i> - <I18n :src="$ts.renotedBy" tag="span"> + <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"/> @@ -54,7 +54,7 @@ </p> <div v-show="appearNote.cw == null || showContent" class="content"> <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span> + <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="fas fa-reply"></i></MkA> <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <a v-if="appearNote.renote != null" class="rp">RN:</a> @@ -103,7 +103,7 @@ <MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> </div> <div v-else class="_panel muted" @click="muted = false"> - <I18n :src="$ts.userSaysSomething" tag="small"> + <I18n :src="i18n.ts.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> <MkUserName :user="appearNote.user"/> diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 3c9d361702..0279f014c6 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -41,7 +41,7 @@ <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <XCwButton v-model="showContent" :note="appearNote"/> </p> - <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }"> + <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }"> <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="fas fa-reply"></i></MkA> @@ -61,9 +61,12 @@ <XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> - <button v-if="collapsed" class="fade _button" @click="collapsed = false"> + <button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false"> <span>{{ i18n.ts.showMore }}</span> </button> + <button v-else-if="isLong && !collapsed" class="showLess _button" @click="collapsed = true"> + <span>{{ i18n.ts.showLess }}</span> + </button> </div> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA> </div> @@ -162,10 +165,11 @@ const reactButton = ref<HTMLElement>(); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); -const collapsed = ref(appearNote.cw == null && appearNote.text != null && ( +const isLong = (appearNote.cw == null && appearNote.text != null && ( (appearNote.text.split('\n').length > 9) || (appearNote.text.length > 500) )); +const collapsed = ref(appearNote.cw == null && isLong); const isDeleted = ref(false); const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); const translation = ref(null); @@ -442,6 +446,24 @@ function readPromo() { } > .content { + &.isLong { + > .showLess { + width: 100%; + margin-top: 1em; + position: sticky; + bottom: 1em; + + > span { + display: inline-block; + background: var(--popup); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); + } + } + } + &.collapsed { position: relative; max-height: 9em; diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue index 41bec5a579..e351a76eb5 100644 --- a/packages/client/src/components/notes.vue +++ b/packages/client/src/components/notes.vue @@ -3,7 +3,7 @@ <template #empty> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noNotes }}</div> + <div>{{ i18n.ts.noNotes }}</div> </div> </template> @@ -21,8 +21,8 @@ import { ref } from 'vue'; import XNote from '@/components/note.vue'; import XList from '@/components/date-separated-list.vue'; -import MkPagination from '@/components/ui/pagination.vue'; -import { Paging } from '@/components/ui/pagination.vue'; +import MkPagination, { Paging } from '@/components/ui/pagination.vue'; +import { i18n } from '@/i18n'; const props = defineProps<{ pagination: Paging; diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index 10cbe20902..9589970a44 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -61,10 +61,10 @@ <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> <i class="fas fa-quote-right"></i> </MkA> - <span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> - <span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $ts.followRequestAccepted }}</span> - <span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $ts.reject }}</button></div></span> - <span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $ts.reject }}</button></div></span> + <span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> + <span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> + <span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> + <span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span> <span v-if="notification.type === 'app'" class="text"> <Mfm :text="notification.body" :nowrap="!full"/> </span> diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue index eb19ad488c..baac2fdca2 100644 --- a/packages/client/src/components/notifications.vue +++ b/packages/client/src/components/notifications.vue @@ -3,7 +3,7 @@ <template #empty> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noNotifications }}</div> + <div>{{ i18n.ts.noNotifications }}</div> </div> </template> @@ -26,6 +26,7 @@ import XNote from '@/components/note.vue'; import * as os from '@/os'; import { stream } from '@/stream'; import { $i } from '@/account'; +import { i18n } from '@/i18n'; const props = defineProps<{ includeTypes?: typeof notificationTypes[number][]; diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue index 98140b95c0..43d75b0cf9 100644 --- a/packages/client/src/components/page-window.vue +++ b/packages/client/src/components/page-window.vue @@ -114,7 +114,7 @@ function menu(ev) { function back() { history.pop(); - router.change(history[history.length - 1].path, history[history.length - 1].key); + router.replace(history[history.length - 1].path, history[history.length - 1].key); } function close() { diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue index a068aca79e..6c1a4cc89f 100644 --- a/packages/client/src/components/poll-editor.vue +++ b/packages/client/src/components/poll-editor.vue @@ -1,7 +1,7 @@ <template> <div class="zmdxowus"> <p v-if="choices.length < 2" class="caution"> - <i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }} + <i class="fas fa-exclamation-triangle"></i>{{ i18n.ts._poll.noOnlyOneChoice }} </p> <ul> <li v-for="(choice, i) in choices" :key="i"> @@ -12,34 +12,34 @@ </button> </li> </ul> - <MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton> - <MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton> - <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch> + <MkButton v-if="choices.length < 10" class="add" @click="add">{{ i18n.ts.add }}</MkButton> + <MkButton v-else class="add" disabled>{{ i18n.ts._poll.noMore }}</MkButton> + <MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch> <section> <div> <MkSelect v-model="expiration" small> - <template #label>{{ $ts._poll.expiration }}</template> - <option value="infinite">{{ $ts._poll.infinite }}</option> - <option value="at">{{ $ts._poll.at }}</option> - <option value="after">{{ $ts._poll.after }}</option> + <template #label>{{ i18n.ts._poll.expiration }}</template> + <option value="infinite">{{ i18n.ts._poll.infinite }}</option> + <option value="at">{{ i18n.ts._poll.at }}</option> + <option value="after">{{ i18n.ts._poll.after }}</option> </MkSelect> <section v-if="expiration === 'at'"> <MkInput v-model="atDate" small type="date" class="input"> - <template #label>{{ $ts._poll.deadlineDate }}</template> + <template #label>{{ i18n.ts._poll.deadlineDate }}</template> </MkInput> <MkInput v-model="atTime" small type="time" class="input"> - <template #label>{{ $ts._poll.deadlineTime }}</template> + <template #label>{{ i18n.ts._poll.deadlineTime }}</template> </MkInput> </section> <section v-else-if="expiration === 'after'"> <MkInput v-model="after" small type="number" class="input"> - <template #label>{{ $ts._poll.duration }}</template> + <template #label>{{ i18n.ts._poll.duration }}</template> </MkInput> <MkSelect v-model="unit" small> - <option value="second">{{ $ts._time.second }}</option> - <option value="minute">{{ $ts._time.minute }}</option> - <option value="hour">{{ $ts._time.hour }}</option> - <option value="day">{{ $ts._time.day }}</option> + <option value="second">{{ i18n.ts._time.second }}</option> + <option value="minute">{{ i18n.ts._time.minute }}</option> + <option value="hour">{{ i18n.ts._time.hour }}</option> + <option value="day">{{ i18n.ts._time.day }}</option> </MkSelect> </section> </div> @@ -55,6 +55,7 @@ import MkSwitch from './form/switch.vue'; import MkButton from './ui/button.vue'; import { formatDateTimeString } from '@/scripts/format-time-string'; import { addTime } from '@/scripts/time'; +import { i18n } from '@/i18n'; const props = defineProps<{ modelValue: { diff --git a/packages/client/src/components/poll.vue b/packages/client/src/components/poll.vue index 35f87325d8..d90af1cfee 100644 --- a/packages/client/src/components/poll.vue +++ b/packages/client/src/components/poll.vue @@ -13,97 +13,77 @@ <p v-if="!readOnly"> <span>{{ $t('_poll.totalVotes', { n: total }) }}</span> <span> · </span> - <a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a> - <span v-if="isVoted">{{ $ts._poll.voted }}</span> - <span v-else-if="closed">{{ $ts._poll.closed }}</span> + <a v-if="!closed && !isVoted" @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> </p> </div> </template> -<script lang="ts"> -import { computed, defineComponent, onUnmounted, ref, toRef } from 'vue'; +<script lang="ts" setup> +import { computed, onUnmounted, ref, toRef } from 'vue'; +import * as misskey from 'misskey-js'; import { sum } from '@/scripts/array'; import { pleaseLogin } from '@/scripts/please-login'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { useInterval } from '@/scripts/use-interval'; -export default defineComponent({ - props: { - note: { - type: Object, - required: true, - }, - readOnly: { - type: Boolean, - required: false, - default: false, - }, - }, +const props = defineProps<{ + note: misskey.entities.Note; + readOnly?: boolean; +}>(); - setup(props) { - const remaining = ref(-1); +const remaining = ref(-1); - const total = computed(() => sum(props.note.poll.choices.map(x => x.votes))); - const closed = computed(() => remaining.value === 0); - const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted)); - const timer = computed(() => i18n.t( - remaining.value >= 86400 ? '_poll.remainingDays' : - remaining.value >= 3600 ? '_poll.remainingHours' : - remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', { - s: Math.floor(remaining.value % 60), - m: Math.floor(remaining.value / 60) % 60, - h: Math.floor(remaining.value / 3600) % 24, - d: Math.floor(remaining.value / 86400), - })); +const total = computed(() => sum(props.note.poll.choices.map(x => x.votes))); +const closed = computed(() => remaining.value === 0); +const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted)); +const timer = computed(() => i18n.t( + remaining.value >= 86400 ? '_poll.remainingDays' : + remaining.value >= 3600 ? '_poll.remainingHours' : + remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', { + s: Math.floor(remaining.value % 60), + m: Math.floor(remaining.value / 60) % 60, + h: Math.floor(remaining.value / 3600) % 24, + d: Math.floor(remaining.value / 86400), + })); - const showResult = ref(props.readOnly || isVoted.value); +const showResult = ref(props.readOnly || isVoted.value); - // 期限付きアンケート - if (props.note.poll.expiresAt) { - const tick = () => { - remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000); - if (remaining.value === 0) { - showResult.value = true; - } - }; - - useInterval(tick, 3000, { - immediate: true, - afterMounted: false, - }); +// 期限付きアンケート +if (props.note.poll.expiresAt) { + const tick = () => { + remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000); + if (remaining.value === 0) { + showResult.value = true; } + }; - const vote = async (id) => { - pleaseLogin(); + useInterval(tick, 3000, { + immediate: true, + afterMounted: false, + }); +} - if (props.readOnly || closed.value || isVoted.value) return; +const vote = async (id) => { + pleaseLogin(); - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }), - }); - if (canceled) return; + if (props.readOnly || closed.value || isVoted.value) return; - await os.api('notes/polls/vote', { - noteId: props.note.id, - choice: id, - }); - if (!showResult.value) showResult.value = !props.note.poll.multiple; - }; + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }), + }); + if (canceled) return; - return { - remaining, - showResult, - total, - isVoted, - closed, - timer, - vote, - }; - }, -}); + await os.api('notes/polls/vote', { + noteId: props.note.id, + choice: id, + }); + if (!showResult.value) showResult.value = !props.note.poll.multiple; +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 77fcd79c13..6dfb2edcb8 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -479,7 +479,22 @@ function onDragover(ev) { if (isFile || isDriveFile) { ev.preventDefault(); draghover = true; - ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move'; + switch (ev.dataTransfer.effectAllowed) { + case 'all': + case 'uninitialized': + case 'copy': + case 'copyLink': + case 'copyMove': + ev.dataTransfer.dropEffect = 'copy'; + break; + case 'linkMove': + case 'move': + ev.dataTransfer.dropEffect = 'move'; + break; + default: + ev.dataTransfer.dropEffect = 'none'; + break; + } } } diff --git a/packages/client/src/components/remote-caution.vue b/packages/client/src/components/remote-caution.vue index 130a0249b6..e9461197ca 100644 --- a/packages/client/src/components/remote-caution.vue +++ b/packages/client/src/components/remote-caution.vue @@ -1,8 +1,10 @@ <template> -<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ $ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div> +<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div> </template> <script lang="ts" setup> +import { i18n } from '@/i18n'; + defineProps<{ href: string; }>(); diff --git a/packages/client/src/components/renote-button.vue b/packages/client/src/components/renote-button.vue index 3bcbe665bf..d267f30403 100644 --- a/packages/client/src/components/renote-button.vue +++ b/packages/client/src/components/renote-button.vue @@ -1,5 +1,6 @@ <template> -<button v-if="canRenote" +<button + v-if="canRenote" ref="buttonRef" class="eddddedb _button canRenote" @click="renote()" @@ -12,8 +13,9 @@ </button> </template> -<script lang="ts"> -import { computed, defineComponent, ref } from 'vue'; +<script lang="ts" setup> +import { computed, ref } from 'vue'; +import * as misskey from 'misskey-js'; import XDetails from '@/components/users-tooltip.vue'; import { pleaseLogin } from '@/scripts/please-login'; import * as os from '@/os'; @@ -21,71 +23,55 @@ import { $i } from '@/account'; import { useTooltip } from '@/scripts/use-tooltip'; import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - count: { - type: Number, - required: true, - }, - note: { - type: Object, - required: true, - }, - }, +const props = defineProps<{ + note: misskey.entities.Note; + count: number; +}>(); - setup(props) { - const buttonRef = ref<HTMLElement>(); +const buttonRef = ref<HTMLElement>(); - const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); +const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); - useTooltip(buttonRef, async (showing) => { - const renotes = await os.api('notes/renotes', { - noteId: props.note.id, - limit: 11 - }); +useTooltip(buttonRef, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: props.note.id, + limit: 11, + }); - const users = renotes.map(x => x.user); + const users = renotes.map(x => x.user); - if (users.length < 1) return; + if (users.length < 1) return; - os.popup(XDetails, { - showing, - users, - count: props.count, - targetElement: buttonRef.value - }, {}, 'closed'); - }); + os.popup(XDetails, { + showing, + users, + count: props.count, + targetElement: buttonRef.value, + }, {}, 'closed'); +}); - const renote = (viaKeyboard = false) => { - pleaseLogin(); - os.popupMenu([{ - text: i18n.ts.renote, - icon: 'fas fa-retweet', - action: () => { - os.api('notes/create', { - renoteId: props.note.id - }); - } - }, { - text: i18n.ts.quote, - icon: 'fas fa-quote-right', - action: () => { - os.post({ - renote: props.note, - }); - } - }], buttonRef.value, { - viaKeyboard +const renote = (viaKeyboard = false) => { + pleaseLogin(); + os.popupMenu([{ + text: i18n.ts.renote, + icon: 'fas fa-retweet', + action: () => { + os.api('notes/create', { + renoteId: props.note.id, }); - }; - - return { - buttonRef, - canRenote, - renote, - }; - }, -}); + }, + }, { + text: i18n.ts.quote, + icon: 'fas fa-quote-right', + action: () => { + os.post({ + renote: props.note, + }); + }, + }], buttonRef.value, { + viaKeyboard, + }); +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/signin-dialog.vue b/packages/client/src/components/signin-dialog.vue index 848b11fada..ec68668a7f 100644 --- a/packages/client/src/components/signin-dialog.vue +++ b/packages/client/src/components/signin-dialog.vue @@ -1,11 +1,12 @@ <template> -<XModalWindow ref="dialog" +<XModalWindow + ref="dialog" :width="370" :height="400" @close="onClose" @closed="emit('closed')" > - <template #header>{{ $ts.login }}</template> + <template #header>{{ i18n.ts.login }}</template> <MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/> </XModalWindow> @@ -13,15 +14,16 @@ <script lang="ts" setup> import { } from 'vue'; -import XModalWindow from '@/components/ui/modal-window.vue'; import MkSignin from './signin.vue'; +import XModalWindow from '@/components/ui/modal-window.vue'; +import { i18n } from '@/i18n'; const props = withDefaults(defineProps<{ autoSet?: boolean; message?: string, }>(), { autoSet: false, - message: '' + message: '', }); const emit = defineEmits<{ diff --git a/packages/client/src/components/signup-dialog.vue b/packages/client/src/components/signup-dialog.vue index 6dad9257a4..c5f933f6b3 100644 --- a/packages/client/src/components/signup-dialog.vue +++ b/packages/client/src/components/signup-dialog.vue @@ -1,11 +1,12 @@ <template> -<XModalWindow ref="dialog" +<XModalWindow + ref="dialog" :width="366" :height="500" @close="dialog.close()" @closed="$emit('closed')" > - <template #header>{{ $ts.signup }}</template> + <template #header>{{ i18n.ts.signup }}</template> <div class="_monolithic_"> <div class="_section"> @@ -17,8 +18,9 @@ <script lang="ts" setup> import { } from 'vue'; -import XModalWindow from '@/components/ui/modal-window.vue'; import XSignup from './signup.vue'; +import XModalWindow from '@/components/ui/modal-window.vue'; +import { i18n } from '@/i18n'; const props = withDefaults(defineProps<{ autoSet?: boolean; diff --git a/packages/client/src/components/signup.vue b/packages/client/src/components/signup.vue index c35d65d5de..f8e39985bc 100644 --- a/packages/client/src/components/signup.vue +++ b/packages/client/src/components/signup.vue @@ -1,65 +1,65 @@ <template> <form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit"> <MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required> - <template #label>{{ $ts.invitationCode }}</template> + <template #label>{{ i18n.ts.invitationCode }}</template> <template #prefix><i class="fas fa-key"></i></template> </MkInput> <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername"> - <template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template> + <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template> <template #prefix>@</template> <template #suffix>@{{ host }}</template> <template #caption> - <span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span> - <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span> - <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span> - <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span> - <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span> - <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span> - <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span> + <span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ i18n.ts.checking }}</span> + <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> + <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.tooShort }}</span> + <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.tooLong }}</span> </template> </MkInput> <MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> - <template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template> + <template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template> <template #prefix><i class="fas fa-envelope"></i></template> <template #caption> - <span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span> - <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span> - <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span> - <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span> - <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span> - <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span> - <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span> - <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span> - <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span> + <span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ i18n.ts.checking }}</span> + <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> + <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> + <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> + <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> + <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> + <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.error }}</span> </template> </MkInput> <MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword"> - <template #label>{{ $ts.password }}</template> + <template #label>{{ i18n.ts.password }}</template> <template #prefix><i class="fas fa-lock"></i></template> <template #caption> - <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span> - <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span> - <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span> + <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.weakPassword }}</span> + <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ i18n.ts.normalPassword }}</span> + <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ i18n.ts.strongPassword }}</span> </template> </MkInput> <MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> - <template #label>{{ $ts.password }} ({{ $ts.retype }})</template> + <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> <template #prefix><i class="fas fa-lock"></i></template> <template #caption> - <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span> - <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span> + <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ i18n.ts.passwordMatched }}</span> + <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> </template> </MkInput> <MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou"> - <I18n :src="$ts.agreeTo"> + <I18n :src="i18n.ts.agreeTo"> <template #0> - <a :href="instance.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a> + <a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a> </template> </I18n> </MkSwitch> <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> - <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton> + <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton> </form> </template> diff --git a/packages/client/src/components/sparkle.vue b/packages/client/src/components/sparkle.vue index b52dbe31c4..cdeaf9c417 100644 --- a/packages/client/src/components/sparkle.vue +++ b/packages/client/src/components/sparkle.vue @@ -63,63 +63,51 @@ </span> </template> -<script lang="ts"> -import { defineComponent, onMounted, onUnmounted, ref } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> +import { onMounted, onUnmounted, ref } from 'vue'; -export default defineComponent({ - setup() { - const particles = ref([]); - const el = ref<HTMLElement>(); - const width = ref(0); - const height = ref(0); - const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202']; - let stop = false; - let ro: ResizeObserver | undefined; +const particles = ref([]); +const el = ref<HTMLElement>(); +const width = ref(0); +const height = ref(0); +const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202']; +let stop = false; +let ro: ResizeObserver | undefined; - onMounted(() => { - ro = new ResizeObserver((entries, observer) => { - width.value = el.value?.offsetWidth + 64; - height.value = el.value?.offsetHeight + 64; - }); - ro.observe(el.value); - const add = () => { - if (stop) return; - const x = (Math.random() * (width.value - 64)); - const y = (Math.random() * (height.value - 64)); - const sizeFactor = Math.random(); - const particle = { - id: Math.random().toString(), - x, - y, - size: 0.2 + ((sizeFactor / 10) * 3), - dur: 1000 + (sizeFactor * 1000), - color: colors[Math.floor(Math.random() * colors.length)], - }; - particles.value.push(particle); - window.setTimeout(() => { - particles.value = particles.value.filter(x => x.id !== particle.id); - }, particle.dur - 100); +onMounted(() => { + ro = new ResizeObserver((entries, observer) => { + width.value = el.value?.offsetWidth + 64; + height.value = el.value?.offsetHeight + 64; + }); + ro.observe(el.value); + const add = () => { + if (stop) return; + const x = (Math.random() * (width.value - 64)); + const y = (Math.random() * (height.value - 64)); + const sizeFactor = Math.random(); + const particle = { + id: Math.random().toString(), + x, + y, + size: 0.2 + ((sizeFactor / 10) * 3), + dur: 1000 + (sizeFactor * 1000), + color: colors[Math.floor(Math.random() * colors.length)], + }; + particles.value.push(particle); + window.setTimeout(() => { + particles.value = particles.value.filter(x => x.id !== particle.id); + }, particle.dur - 100); - window.setTimeout(() => { - add(); - }, 500 + (Math.random() * 500)); - }; + window.setTimeout(() => { add(); - }); - - onUnmounted(() => { - if (ro) ro.disconnect(); - stop = true; - }); + }, 500 + (Math.random() * 500)); + }; + add(); +}); - return { - el, - width, - height, - particles, - }; - }, +onUnmounted(() => { + if (ro) ro.disconnect(); + stop = true; }); </script> diff --git a/packages/client/src/components/sub-note-content.vue b/packages/client/src/components/sub-note-content.vue index d6a37d07be..25ab883f40 100644 --- a/packages/client/src/components/sub-note-content.vue +++ b/packages/client/src/components/sub-note-content.vue @@ -1,8 +1,8 @@ <template> <div class="wrmlmaau" :class="{ collapsed }"> <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span> - <span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span> + <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="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA> <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> @@ -12,20 +12,21 @@ <XMediaList :media-list="note.files"/> </details> <details v-if="note.poll"> - <summary>{{ $ts.poll }}</summary> + <summary>{{ i18n.ts.poll }}</summary> <XPoll :note="note"/> </details> <button v-if="collapsed" class="fade _button" @click="collapsed = false"> - <span>{{ $ts.showMore }}</span> + <span>{{ i18n.ts.showMore }}</span> </button> </div> </template> <script lang="ts" setup> import { } from 'vue'; +import * as misskey from 'misskey-js'; import XPoll from './poll.vue'; import XMediaList from './media-list.vue'; -import * as misskey from 'misskey-js'; +import { i18n } from '@/i18n'; const props = defineProps<{ note: misskey.entities.Note; diff --git a/packages/client/src/components/tab.vue b/packages/client/src/components/tab.vue index c629727358..669e9e2e11 100644 --- a/packages/client/src/components/tab.vue +++ b/packages/client/src/components/tab.vue @@ -18,13 +18,13 @@ export default defineComponent({ disabled: this.modelValue === option.props.value, onClick: () => { this.$emit('update:modelValue', option.props.value); - } + }, }, option.children), [ - [resolveDirective('click-anime')] + [resolveDirective('click-anime')], ]))), [ - [resolveDirective('size'), { max: [500] }] + [resolveDirective('size'), { max: [500] }], ]); - } + }, }); </script> diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue index bbebff497f..2dfd26edb0 100644 --- a/packages/client/src/components/tag-cloud.vue +++ b/packages/client/src/components/tag-cloud.vue @@ -25,23 +25,25 @@ let tagsEl = $ref<HTMLElement | null>(null); let width = $ref(300); watch($$(available), () => { - window.TagCanvas.Start(idForCanvas, idForTags, { - textColour: '#ffffff', - outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(), - outlineRadius: 10, - initial: [-0.030, -0.010], - frontSelect: true, - imageRadius: 8, - //dragControl: true, - dragThreshold: 3, - wheelZoom: false, - reverse: true, - depth: 0.5, - maxSpeed: 0.2, - minSpeed: 0.003, - stretchX: 0.8, - stretchY: 0.8, - }); + try { + window.TagCanvas.Start(idForCanvas, idForTags, { + textColour: '#ffffff', + outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(), + outlineRadius: 10, + initial: [-0.030, -0.010], + frontSelect: true, + imageRadius: 8, + //dragControl: true, + dragThreshold: 3, + wheelZoom: false, + reverse: true, + depth: 0.5, + maxSpeed: 0.2, + minSpeed: 0.003, + stretchX: 0.8, + stretchY: 0.8, + }); + } catch (err) {} }); onMounted(() => { @@ -58,7 +60,7 @@ onMounted(() => { }); onBeforeUnmount(() => { - window.TagCanvas.Delete(idForCanvas); + if (window.TagCanvas) window.TagCanvas.Delete(idForCanvas); }); defineExpose({ diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue index d3a4b5ea92..350629bf08 100644 --- a/packages/client/src/components/ui/button.vue +++ b/packages/client/src/components/ui/button.vue @@ -141,7 +141,7 @@ export default defineComponent({ display: block; min-width: 100px; width: max-content; - padding: 8px 14px; + padding: 8px 16px; text-align: center; font-weight: normal; font-size: 1em; diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue index 6d1a2cc770..60b68954d6 100644 --- a/packages/client/src/components/ui/menu.vue +++ b/packages/client/src/components/ui/menu.vue @@ -46,7 +46,7 @@ </button> </template> <span v-if="items2.length === 0" class="none item"> - <span>{{ $ts.none }}</span> + <span>{{ i18n.ts.none }}</span> </span> </div> <div v-if="childMenu" class="child"> @@ -61,6 +61,8 @@ import { focusPrev, focusNext } from '@/scripts/focus'; import FormSwitch from '@/components/form/switch.vue'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu'; import * as os from '@/os'; +import { i18n } from '@/i18n'; + const XChild = defineAsyncComponent(() => import('./menu.child.vue')); const props = defineProps<{ @@ -335,6 +337,9 @@ onBeforeUnmount(() => { &.asDrawer { padding: 12px 0 calc(env(safe-area-inset-bottom, 0px) + 12px) 0; width: 100%; + border-radius: 24px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; > .item { font-size: 1em; diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue index a03c2b3a1d..7650c5b33a 100644 --- a/packages/client/src/components/ui/pagination.vue +++ b/packages/client/src/components/ui/pagination.vue @@ -8,7 +8,7 @@ <slot name="empty"> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.nothing }}</div> + <div>{{ i18n.ts.nothing }}</div> </div> </slot> </div> @@ -16,14 +16,14 @@ <div v-else ref="rootEl"> <div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap"> <MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead"> - {{ $ts.loadMore }} + {{ i18n.ts.loadMore }} </MkButton> <MkLoading v-else class="loading"/> </div> <slot :items="items"></slot> <div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap"> <MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> - {{ $ts.loadMore }} + {{ i18n.ts.loadMore }} </MkButton> <MkLoading v-else class="loading"/> </div> @@ -37,6 +37,7 @@ import * as misskey from 'misskey-js'; import * as os from '@/os'; import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; import MkButton from '@/components/ui/button.vue'; +import { i18n } from '@/i18n'; const SECOND_FETCH_LIMIT = 30; @@ -196,21 +197,23 @@ const prepend = (item: Item): void => { if (props.pagination.reversed) { if (rootEl.value) { const container = getScrollContainer(rootEl.value); - if (container == null) return; // TODO? - - const pos = getScrollPosition(rootEl.value); - const viewHeight = container.clientHeight; - const height = container.scrollHeight; - const isBottom = (pos + viewHeight > height - 32); - if (isBottom) { - // オーバーフローしたら古いアイテムは捨てる - if (items.value.length >= props.displayLimit) { - // このやり方だとVue 3.2以降アニメーションが動かなくなる - //items.value = items.value.slice(-props.displayLimit); - while (items.value.length >= props.displayLimit) { - items.value.shift(); + if (container == null) { + // TODO? + } else { + const pos = getScrollPosition(rootEl.value); + const viewHeight = container.clientHeight; + const height = container.scrollHeight; + const isBottom = (pos + viewHeight > height - 32); + if (isBottom) { + // オーバーフローしたら古いアイテムは捨てる + if (items.value.length >= props.displayLimit) { + // このやり方だとVue 3.2以降アニメーションが動かなくなる + //items.value = items.value.slice(-props.displayLimit); + while (items.value.length >= props.displayLimit) { + items.value.shift(); + } + more.value = true; } - more.value = true; } } } diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue index 460cf7d597..758d4d47b6 100644 --- a/packages/client/src/components/ui/window.vue +++ b/packages/client/src/components/ui/window.vue @@ -170,6 +170,7 @@ function onHeaderMousedown(evt: MouseEvent) { beforeClickedAt = Date.now(); const main = rootEl; + if (main == null) return; if (!contains(main, document.activeElement)) main.focus(); diff --git a/packages/client/src/components/updated.vue b/packages/client/src/components/updated.vue index 375ac0dbbb..1c1e5f4aed 100644 --- a/packages/client/src/components/updated.vue +++ b/packages/client/src/components/updated.vue @@ -1,10 +1,10 @@ <template> <MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')"> <div class="ewlycnyt"> - <div class="title"><MkSparkle>{{ $ts.misskeyUpdated }}</MkSparkle></div> + <div class="title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div> <div class="version">✨{{ version }}🚀</div> - <MkButton full @click="whatIsNew">{{ $ts.whatIsNew }}</MkButton> - <MkButton class="gotIt" primary full @click="$refs.modal.close()">{{ $ts.gotIt }}</MkButton> + <MkButton full @click="whatIsNew">{{ i18n.ts.whatIsNew }}</MkButton> + <MkButton class="gotIt" primary full @click="$refs.modal.close()">{{ i18n.ts.gotIt }}</MkButton> </div> </MkModal> </template> @@ -15,8 +15,9 @@ import MkModal from '@/components/ui/modal.vue'; import MkButton from '@/components/ui/button.vue'; import MkSparkle from '@/components/sparkle.vue'; import { version } from '@/config'; +import { i18n } from '@/i18n'; -const modal = ref(); +const modal = ref<InstanceType<typeof MkModal>>(); const whatIsNew = () => { modal.value.close(); diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue index e15d28a382..df4b0e53b8 100644 --- a/packages/client/src/components/url-preview.vue +++ b/packages/client/src/components/url-preview.vue @@ -1,6 +1,6 @@ <template> <div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> - <button class="disablePlayer" :title="$ts.disablePlayer" @click="playerEnabled = false"><i class="fas fa-times"></i></button> + <button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="fas fa-times"></i></button> <iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/> </div> <div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter"> @@ -10,7 +10,7 @@ <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> <div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`"> - <button v-if="!playerEnabled && player.url" class="_button" :title="$ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button> + <button v-if="!playerEnabled && player.url" class="_button" :title="i18n.ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button> </div> <article> <header> @@ -26,7 +26,7 @@ </transition> <div v-if="tweetId" class="expandTweet"> <a @click="tweetExpanded = true"> - <i class="fab fa-twitter"></i> {{ $ts.expandTweet }} + <i class="fab fa-twitter"></i> {{ i18n.ts.expandTweet }} </a> </div> </div> @@ -35,6 +35,7 @@ <script lang="ts" setup> import { onMounted, onUnmounted } from 'vue'; import { url as local, lang } from '@/config'; +import { i18n } from '@/i18n'; const props = withDefaults(defineProps<{ url: string; diff --git a/packages/client/src/components/user-info.vue b/packages/client/src/components/user-info.vue index 6a25d412fc..1cd275a6df 100644 --- a/packages/client/src/components/user-info.vue +++ b/packages/client/src/components/user-info.vue @@ -10,17 +10,17 @@ <div v-if="user.description" class="mfm"> <Mfm :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/> </div> - <span v-else style="opacity: 0.7;">{{ $ts.noAccountDescription }}</span> + <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> </div> <div class="status"> <div> - <p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span> + <p>{{ i18n.ts.notes }}</p><span>{{ user.notesCount }}</span> </div> <div> - <p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span> + <p>{{ i18n.ts.following }}</p><span>{{ user.followingCount }}</span> </div> <div> - <p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span> + <p>{{ i18n.ts.followers }}</p><span>{{ user.followersCount }}</span> </div> </div> <MkFollowButton v-if="$i && user.id != $i.id" class="koudoku-button" :user="user" mini/> @@ -31,6 +31,7 @@ import * as misskey from 'misskey-js'; import MkFollowButton from './follow-button.vue'; import { userPage } from '@/filters/user'; +import { i18n } from '@/i18n'; defineProps<{ user: misskey.entities.UserDetailed; diff --git a/packages/client/src/components/user-list.vue b/packages/client/src/components/user-list.vue index 3e273721c7..fe30d371fe 100644 --- a/packages/client/src/components/user-list.vue +++ b/packages/client/src/components/user-list.vue @@ -3,7 +3,7 @@ <template #empty> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noUsers }}</div> + <div>{{ i18n.ts.noUsers }}</div> </div> </template> @@ -18,9 +18,9 @@ <script lang="ts" setup> import { ref } from 'vue'; import MkUserInfo from '@/components/user-info.vue'; -import MkPagination from '@/components/ui/pagination.vue'; -import { Paging } from '@/components/ui/pagination.vue'; +import MkPagination, { Paging } from '@/components/ui/pagination.vue'; import { userPage } from '@/filters/user'; +import { i18n } from '@/i18n'; const props = defineProps<{ pagination: Paging; diff --git a/packages/client/src/components/user-select-dialog.vue b/packages/client/src/components/user-select-dialog.vue index 972d353486..4d8e427a72 100644 --- a/packages/client/src/components/user-select-dialog.vue +++ b/packages/client/src/components/user-select-dialog.vue @@ -1,5 +1,6 @@ <template> -<XModalWindow ref="dialogEl" +<XModalWindow + ref="dialogEl" :with-ok-button="true" :ok-button-disabled="selected == null" @click="cancel()" @@ -7,16 +8,16 @@ @ok="ok()" @closed="$emit('closed')" > - <template #header>{{ $ts.selectUser }}</template> + <template #header>{{ i18n.ts.selectUser }}</template> <div class="tbhwbxda"> <div class="form"> <FormSplit :min-width="170"> <MkInput v-model="username" :autofocus="true" @update:modelValue="search"> - <template #label>{{ $ts.username }}</template> + <template #label>{{ i18n.ts.username }}</template> <template #prefix>@</template> </MkInput> <MkInput v-model="host" @update:modelValue="search"> - <template #label>{{ $ts.host }}</template> + <template #label>{{ i18n.ts.host }}</template> <template #prefix>@</template> </MkInput> </FormSplit> @@ -32,7 +33,7 @@ </div> </div> <div v-else class="empty"> - <span>{{ $ts.noUsers }}</span> + <span>{{ i18n.ts.noUsers }}</span> </div> </div> <div v-if="username == '' && host == ''" class="recent"> @@ -58,6 +59,7 @@ import FormSplit from '@/components/form/split.vue'; import XModalWindow from '@/components/ui/modal-window.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; +import { i18n } from '@/i18n'; const emit = defineEmits<{ (ev: 'ok', selected: misskey.entities.UserDetailed): void; @@ -81,7 +83,7 @@ const search = () => { username: username, host: host, limit: 10, - detail: false + detail: false, }).then(_users => { users = _users; }); diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue index 7fe55858cc..f4830cd2c6 100644 --- a/packages/client/src/components/visibility-picker.vue +++ b/packages/client/src/components/visibility-picker.vue @@ -4,37 +4,37 @@ <button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')"> <div><i class="fas fa-globe"></i></div> <div> - <span>{{ $ts._visibility.public }}</span> - <span>{{ $ts._visibility.publicDescription }}</span> + <span>{{ i18n.ts._visibility.public }}</span> + <span>{{ i18n.ts._visibility.publicDescription }}</span> </div> </button> <button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')"> <div><i class="fas fa-home"></i></div> <div> - <span>{{ $ts._visibility.home }}</span> - <span>{{ $ts._visibility.homeDescription }}</span> + <span>{{ i18n.ts._visibility.home }}</span> + <span>{{ i18n.ts._visibility.homeDescription }}</span> </div> </button> <button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')"> <div><i class="fas fa-unlock"></i></div> <div> - <span>{{ $ts._visibility.followers }}</span> - <span>{{ $ts._visibility.followersDescription }}</span> + <span>{{ i18n.ts._visibility.followers }}</span> + <span>{{ i18n.ts._visibility.followersDescription }}</span> </div> </button> <button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')"> <div><i class="fas fa-envelope"></i></div> <div> - <span>{{ $ts._visibility.specified }}</span> - <span>{{ $ts._visibility.specifiedDescription }}</span> + <span>{{ i18n.ts._visibility.specified }}</span> + <span>{{ i18n.ts._visibility.specifiedDescription }}</span> </div> </button> <div class="divider"></div> <button key="localOnly" class="_button localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly"> <div><i class="fas fa-biohazard"></i></div> <div> - <span>{{ $ts._visibility.localOnly }}</span> - <span>{{ $ts._visibility.localOnlyDescription }}</span> + <span>{{ i18n.ts._visibility.localOnly }}</span> + <span>{{ i18n.ts._visibility.localOnlyDescription }}</span> </div> <div><i :class="localOnly ? 'fas fa-toggle-on' : 'fas fa-toggle-off'"></i></div> </button> @@ -46,6 +46,7 @@ import { nextTick, watch } from 'vue'; import * as misskey from 'misskey-js'; import MkModal from '@/components/ui/modal.vue'; +import { i18n } from '@/i18n'; const modal = $ref<InstanceType<typeof MkModal>>(); diff --git a/packages/client/src/components/widgets.vue b/packages/client/src/components/widgets.vue index 0a9769e197..54d4c57af3 100644 --- a/packages/client/src/components/widgets.vue +++ b/packages/client/src/components/widgets.vue @@ -3,11 +3,11 @@ <template v-if="edit"> <header> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" class="mk-widget-select"> - <template #label>{{ $ts.selectWidget }}</template> + <template #label>{{ i18n.ts.selectWidget }}</template> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option> </MkSelect> - <MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> - <MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton> + <MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> </header> <XDraggable v-model="widgets_" |