mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-17 07:55:27 +02:00
Merge branch 'develop' into pag-back
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkAnnouncementDialog from './MkAnnouncementDialog.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkAnnouncementDialog,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkAnnouncementDialog v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
announcement: {
|
||||
id: '1',
|
||||
title: 'Title',
|
||||
text: 'Text',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: null,
|
||||
icon: 'info',
|
||||
imageUrl: null,
|
||||
display: 'dialog',
|
||||
needConfirmationToRead: false,
|
||||
forYou: true,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkAnnouncementDialog>;
|
||||
104
packages/frontend/src/components/MkAnnouncementDialog.vue
Normal file
104
packages/frontend/src/components/MkAnnouncementDialog.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :zPriority="'middle'" @closed="$emit('closed')" @click="onBgClick">
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<div :class="$style.header">
|
||||
<span :class="$style.icon">
|
||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span :class="$style.title">{{ announcement.title }}</span>
|
||||
</div>
|
||||
<div :class="$style.text"><Mfm :text="announcement.text"/></div>
|
||||
<MkButton primary full @click="ok">{{ i18n.ts.ok }}</MkButton>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i, updateAccount } from '@/account';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
announcement: misskey.entities.Announcement;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
async function ok() {
|
||||
if (props.announcement.needConfirmationToRead) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.t('_announcement.readConfirmText', { title: props.announcement.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
modal.value.close();
|
||||
os.api('i/read-announcement', { announcementId: props.announcement.id });
|
||||
updateAccount({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
||||
});
|
||||
}
|
||||
|
||||
function onBgClick() {
|
||||
rootEl.value.animate([{
|
||||
offset: 0,
|
||||
transform: 'scale(1)',
|
||||
}, {
|
||||
offset: 0.5,
|
||||
transform: 'scale(1.1)',
|
||||
}, {
|
||||
offset: 1,
|
||||
transform: 'scale(1)',
|
||||
}], {
|
||||
duration: 100,
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="banner" :style="bannerStyle">
|
||||
<div class="fade"></div>
|
||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||
<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<i class="ti ti-users ti-fw"></i>
|
||||
@@ -102,6 +103,19 @@ const bannerStyle = computed(() => {
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> .sensitiveIndicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: var(--warn);
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 4px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
> article {
|
||||
|
||||
@@ -156,7 +156,7 @@ watch(q, () => {
|
||||
const newQ = q.value.replace(/:/g, '').toLowerCase();
|
||||
|
||||
const searchCustom = () => {
|
||||
const max = 8;
|
||||
const max = 100;
|
||||
const emojis = customEmojis.value;
|
||||
const matches = new Set<Misskey.entities.CustomEmoji>();
|
||||
|
||||
@@ -219,7 +219,7 @@ watch(q, () => {
|
||||
};
|
||||
|
||||
const searchUnicode = () => {
|
||||
const max = 8;
|
||||
const max = 100;
|
||||
const emojis = emojilist;
|
||||
const matches = new Set<UnicodeEmojiDef>();
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
>
|
||||
<source
|
||||
:src="video.url"
|
||||
:type="video.type"
|
||||
>
|
||||
</video>
|
||||
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
|
||||
|
||||
@@ -35,14 +35,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</button>
|
||||
<span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
|
||||
</span>
|
||||
<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
|
||||
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
|
||||
<span :class="$style.switchText">{{ item.text }}</span>
|
||||
</button>
|
||||
<div v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<span>{{ item.text }}</span>
|
||||
<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||
@@ -55,19 +56,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="childMenu">
|
||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
|
||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned" @close="close(false)"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
<script lang="ts">
|
||||
import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
|
||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
|
||||
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -81,6 +87,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'close', actioned?: boolean): void;
|
||||
(ev: 'hide'): void;
|
||||
}>();
|
||||
|
||||
let itemsEl = $shallowRef<HTMLDivElement>();
|
||||
@@ -97,6 +104,8 @@ let keymap = $computed(() => ({
|
||||
|
||||
let childShowingItem = $ref<MenuItem | null>();
|
||||
|
||||
let preferClick = isTouchUsing || props.asDrawer;
|
||||
|
||||
watch(() => props.items, () => {
|
||||
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
|
||||
|
||||
@@ -116,7 +125,7 @@ watch(() => props.items, () => {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
let childMenu = ref<MenuItem[] | null>();
|
||||
const childMenu = ref<MenuItem[] | null>();
|
||||
let childTarget = $shallowRef<HTMLElement | null>();
|
||||
|
||||
function closeChild() {
|
||||
@@ -129,11 +138,11 @@ function childActioned() {
|
||||
close(true);
|
||||
}
|
||||
|
||||
function onGlobalMousedown(event: MouseEvent) {
|
||||
const onGlobalMousedown = (event: MouseEvent) => {
|
||||
if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
|
||||
if (child && child.checkHit(event)) return;
|
||||
closeChild();
|
||||
}
|
||||
};
|
||||
|
||||
let childCloseTimer: null | number = null;
|
||||
function onItemMouseEnter(item) {
|
||||
@@ -145,31 +154,30 @@ function onItemMouseLeave(item) {
|
||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||
}
|
||||
|
||||
let childrenCache = new WeakMap();
|
||||
async function showChildren(item: MenuItem, ev: MouseEvent) {
|
||||
const children = ref([]);
|
||||
if (childrenCache.has(item)) {
|
||||
children.value = childrenCache.get(item);
|
||||
} else {
|
||||
if (typeof item.children === 'function') {
|
||||
children.value = [{
|
||||
type: 'pending',
|
||||
}];
|
||||
item.children().then(x => {
|
||||
children.value = x;
|
||||
childrenCache.set(item, x);
|
||||
});
|
||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||
const children = await (async () => {
|
||||
if (childrenCache.has(item)) {
|
||||
return childrenCache.get(item)!;
|
||||
} else {
|
||||
children.value = item.children;
|
||||
if (typeof item.children === 'function') {
|
||||
return Promise.resolve(item.children());
|
||||
} else {
|
||||
return item.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
childrenCache.set(item, children);
|
||||
|
||||
if (props.asDrawer) {
|
||||
os.popupMenu(children, ev.currentTarget ?? ev.target);
|
||||
close();
|
||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||
emit('close');
|
||||
});
|
||||
emit('hide');
|
||||
} else {
|
||||
childTarget = ev.currentTarget ?? ev.target;
|
||||
childMenu = children;
|
||||
// これでもリアクティビティは保たれる
|
||||
childMenu.value = children;
|
||||
childShowingItem = item;
|
||||
}
|
||||
}
|
||||
@@ -191,10 +199,15 @@ function focusDown() {
|
||||
focusNext(document.activeElement);
|
||||
}
|
||||
|
||||
function switchItem(item: MenuSwitch & { ref: any }) {
|
||||
if (item.disabled) return;
|
||||
item.ref = !item.ref;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.viaKeyboard) {
|
||||
nextTick(() => {
|
||||
focusNext(itemsEl.children[0], true, false);
|
||||
if (itemsEl) focusNext(itemsEl.children[0], true, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -342,6 +355,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
&.parent {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
@@ -357,6 +371,37 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switchDisabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.switchButton {
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.switchText {
|
||||
margin-left: 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.switchInput {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@@ -408,14 +408,16 @@ function onContextmenu(ev: MouseEvent): void {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
|
||||
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
|
||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
|
||||
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
|
||||
os.popupMenu(menu, menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus);
|
||||
}).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
async function clip() {
|
||||
|
||||
@@ -172,7 +172,6 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
}>();
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
@@ -385,14 +384,16 @@ function onContextmenu(ev: MouseEvent): void {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
|
||||
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
|
||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
|
||||
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
|
||||
os.popupMenu(menu, menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus);
|
||||
}).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
async function clip() {
|
||||
|
||||
@@ -37,7 +37,6 @@ import { userPage } from '@/filters/user';
|
||||
|
||||
defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import { $i } from '@/account';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
}>();
|
||||
|
||||
const showContent = $ref(false);
|
||||
|
||||
@@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<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 ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import MkModal from './MkModal.vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
@@ -29,6 +29,46 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||
const manualShowing = ref(true);
|
||||
const hiding = ref(false);
|
||||
|
||||
function click() {
|
||||
close();
|
||||
}
|
||||
|
||||
function onModalClose() {
|
||||
emit('closing');
|
||||
}
|
||||
|
||||
function onMenuClose() {
|
||||
close();
|
||||
if (hiding.value) {
|
||||
// hidingであればclosedを発火
|
||||
emit('closed');
|
||||
}
|
||||
}
|
||||
|
||||
function onModalClosed() {
|
||||
if (!hiding.value) {
|
||||
// hidingでなければclosedを発火
|
||||
emit('closed');
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
manualShowing.value = false;
|
||||
hiding.value = true;
|
||||
|
||||
// closeは呼ぶ必要がある
|
||||
modal?.close();
|
||||
}
|
||||
|
||||
function close() {
|
||||
manualShowing.value = false;
|
||||
|
||||
// closeは呼ぶ必要がある
|
||||
modal?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
88
packages/frontend/src/components/MkSwitch.button.vue
Normal file
88
packages/frontend/src/components/MkSwitch.button.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff"
|
||||
:class="{
|
||||
[$style.button]: true,
|
||||
[$style.buttonChecked]: checked,
|
||||
[$style.buttonDisabled]: props.disabled
|
||||
}"
|
||||
data-cy-switch-toggle
|
||||
@click.prevent.stop="toggle"
|
||||
>
|
||||
<div :class="{ [$style.knob]: true, [$style.knobChecked]: checked }"></div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, Ref } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
checked: boolean | Ref<boolean>;
|
||||
disabled?: boolean;
|
||||
}>(), {
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'toggle'): void;
|
||||
}>();
|
||||
|
||||
const checked = toRefs(props).checked;
|
||||
const toggle = () => {
|
||||
emit('toggle');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
width: 32px;
|
||||
height: 23px;
|
||||
outline: none;
|
||||
background: var(--switchOffBg);
|
||||
background-clip: content-box;
|
||||
border: solid 1px var(--switchOffBg);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: inherit;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.buttonChecked {
|
||||
background-color: var(--switchOnBg) !important;
|
||||
border-color: var(--switchOnBg) !important;
|
||||
}
|
||||
|
||||
.buttonDisabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.knob {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:not(.knobChecked) {
|
||||
left: 3px;
|
||||
background: var(--switchOffFg);
|
||||
}
|
||||
}
|
||||
|
||||
.knobChecked {
|
||||
left: 12px;
|
||||
background: var(--switchOnFg);
|
||||
}
|
||||
</style>
|
||||
@@ -12,12 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:class="$style.input"
|
||||
@keydown.enter="toggle"
|
||||
>
|
||||
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
|
||||
<div :class="$style.knob"></div>
|
||||
</span>
|
||||
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||
<span :class="$style.body">
|
||||
<!-- TODO: 無名slotの方は廃止 -->
|
||||
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
|
||||
<span :class="$style.label">
|
||||
<span @click="toggle">
|
||||
<slot name="label"></slot><slot></slot>
|
||||
</span>
|
||||
<span v-if="helpText" v-tooltip:dialog="helpText" class="_button _help" :class="$style.help"><i class="ti ti-help-circle"></i></span>
|
||||
</span>
|
||||
<p :class="$style.caption"><slot name="caption"></slot></p>
|
||||
</span>
|
||||
</div>
|
||||
@@ -25,26 +28,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, Ref } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import XButton from '@/components/MkSwitch.button.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean | Ref<boolean>;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: boolean): void;
|
||||
}>();
|
||||
|
||||
let button = $shallowRef<HTMLElement>();
|
||||
const checked = toRefs(props).modelValue;
|
||||
const toggle = () => {
|
||||
if (props.disabled) return;
|
||||
emit('update:modelValue', !checked.value);
|
||||
|
||||
if (!checked.value) {
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -66,17 +65,8 @@ const toggle = () => {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
> .button {
|
||||
background-color: var(--switchOnBg) !important;
|
||||
border-color: var(--switchOnBg) !important;
|
||||
|
||||
> .knob {
|
||||
left: 12px;
|
||||
background: var(--switchOnFg);
|
||||
}
|
||||
}
|
||||
}
|
||||
//&.checked {
|
||||
//}
|
||||
}
|
||||
|
||||
.input {
|
||||
@@ -86,36 +76,6 @@ const toggle = () => {
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
width: 32px;
|
||||
height: 23px;
|
||||
outline: none;
|
||||
background: var(--switchOffBg);
|
||||
background-clip: content-box;
|
||||
border: solid 1px var(--switchOffBg);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: inherit;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.knob {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: var(--switchOffFg);
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-left: 12px;
|
||||
margin-top: 2px;
|
||||
@@ -140,4 +100,10 @@ const toggle = () => {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.help {
|
||||
margin-left: 0.5em;
|
||||
font-size: 85%;
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
<template v-else-if="tweetId && tweetExpanded">
|
||||
<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}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
||||
<iframe
|
||||
ref="tweet"
|
||||
allow="fullscreen;web-share"
|
||||
sandbox="allow-popups allow-scripts allow-same-origin"
|
||||
scrolling="no"
|
||||
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
|
||||
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
|
||||
></iframe>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
<MkButton :small="true" inline @click="tweetExpanded = false">
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
@close="dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
|
||||
<template v-else #header>New announcement</template>
|
||||
|
||||
<div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="text">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkRadios v-model="icon">
|
||||
<template #label>{{ i18n.ts.icon }}</template>
|
||||
<option value="info"><i class="ti ti-info-circle"></i></option>
|
||||
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
|
||||
<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
|
||||
<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
|
||||
</MkRadios>
|
||||
<MkRadios v-model="display">
|
||||
<template #label>{{ i18n.ts.display }}</template>
|
||||
<option value="normal">{{ i18n.ts.normal }}</option>
|
||||
<option value="banner">{{ i18n.ts.banner }}</option>
|
||||
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
||||
</MkRadios>
|
||||
<MkSwitch v-model="needConfirmationToRead">
|
||||
{{ i18n.ts._announcement.needConfirmationToRead }}
|
||||
<template #caption>{{ i18n.ts._announcement.needConfirmationToReadDescription }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div :class="$style.footer">
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.announcement ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.User,
|
||||
announcement?: any,
|
||||
}>();
|
||||
|
||||
let dialog = $ref(null);
|
||||
let title: string = $ref(props.announcement ? props.announcement.title : '');
|
||||
let text: string = $ref(props.announcement ? props.announcement.text : '');
|
||||
let icon: string = $ref(props.announcement ? props.announcement.icon : 'info');
|
||||
let display: string = $ref(props.announcement ? props.announcement.display : 'dialog');
|
||||
let needConfirmationToRead = $ref(props.announcement ? props.announcement.needConfirmationToRead : false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
async function done() {
|
||||
const params = {
|
||||
title: title,
|
||||
text: text,
|
||||
icon: icon,
|
||||
imageUrl: null,
|
||||
display: display,
|
||||
needConfirmationToRead: needConfirmationToRead,
|
||||
userId: props.user.id,
|
||||
};
|
||||
|
||||
if (props.announcement) {
|
||||
await os.apiWithDialog('admin/announcements/update', {
|
||||
id: props.announcement.id,
|
||||
...params,
|
||||
});
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
id: props.announcement.id,
|
||||
...params,
|
||||
},
|
||||
});
|
||||
|
||||
dialog.close();
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/announcements/create', params);
|
||||
|
||||
emit('done', {
|
||||
created: created,
|
||||
});
|
||||
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: title }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.api('admin/announcements/delete', {
|
||||
id: props.announcement.id,
|
||||
}).then(() => {
|
||||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
@@ -86,7 +86,8 @@ let top = $ref(0);
|
||||
let left = $ref(0);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
|
||||
const { menu, cleanup } = getUserMenu(user);
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user