Merge tag '13.9.1' into io
This commit is contained in:
BIN
packages/frontend/assets/sounds/syuilo/n-aec-4va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-aec-4va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-aec-4vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-aec-4vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-aec-8va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-aec-8va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-aec-8vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-aec-8vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-aec.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-aec.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-cea-4va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-cea-4va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-cea-4vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-cea-4vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-cea-8va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-cea-8va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-cea-8vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-cea-8vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-cea.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-cea.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-ea-4va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-ea-4va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-ea-4vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-ea-4vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-ea-8va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-ea-8va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-ea-8vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-ea-8vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-ea-harmony.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-ea-harmony.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-ea.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-ea.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-eca-4va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-eca-4va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-eca-4vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-eca-4vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-eca-8va.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-eca-8va.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-eca-8vb.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-eca-8vb.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/sounds/syuilo/n-eca.mp3
Normal file
BIN
packages/frontend/assets/sounds/syuilo/n-eca.mp3
Normal file
Binary file not shown.
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="container">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" crossorigin="anonymous" @load="onImageLoad">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,7 +49,7 @@ const props = defineProps<{
|
||||
aspectRatio: number;
|
||||
}>();
|
||||
|
||||
const imgUrl = getProxiedImageUrl(props.file.url);
|
||||
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
|
||||
let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
let imgEl = $shallowRef<HTMLImageElement>();
|
||||
let cropper: Cropper | null = null;
|
||||
|
@@ -43,8 +43,8 @@
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
defaultOpen: boolean;
|
||||
maxHeight: number | null;
|
||||
defaultOpen?: boolean;
|
||||
maxHeight?: number | null;
|
||||
}>(), {
|
||||
defaultOpen: false,
|
||||
maxHeight: null,
|
||||
|
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, useCssModule } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
import PhotoSwipe from 'photoswipe';
|
||||
@@ -29,8 +29,11 @@ const props = defineProps<{
|
||||
raw?: boolean;
|
||||
}>();
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
const gallery = ref(null);
|
||||
const pswpZIndex = os.claimZIndex('middle');
|
||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
||||
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -54,17 +57,18 @@ onMounted(() => {
|
||||
return item;
|
||||
}),
|
||||
gallery: gallery.value,
|
||||
mainClass: $style.pswp,
|
||||
children: '.image',
|
||||
thumbSelector: '.image',
|
||||
loop: false,
|
||||
padding: window.innerWidth > 500 ? {
|
||||
top: 32,
|
||||
bottom: 32,
|
||||
bottom: 90,
|
||||
left: 32,
|
||||
right: 32,
|
||||
} : {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
bottom: 78,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
@@ -82,6 +86,7 @@ onMounted(() => {
|
||||
|
||||
const id = element.dataset.id;
|
||||
const file = props.mediaList.find(media => media.id === id);
|
||||
if (!file) return;
|
||||
|
||||
itemData.src = file.url;
|
||||
itemData.w = Number(file.properties.width);
|
||||
@@ -198,16 +203,14 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||
overflow: hidden; // clipにするとバグる
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pswp {
|
||||
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
|
||||
--pswp-bg: var(--modalBg) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.pswp {
|
||||
// なぜか機能しない
|
||||
//z-index: v-bind(pswpZIndex);
|
||||
z-index: 2000000;
|
||||
--pswp-bg: var(--modalBg);
|
||||
}
|
||||
|
||||
.pswp__bg {
|
||||
background: var(--modalBg);
|
||||
backdrop-filter: var(--modalBgFilter);
|
||||
@@ -219,7 +222,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||
align-items: center;
|
||||
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
|
@@ -36,7 +36,7 @@
|
||||
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="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-caret-right ti-fw"></i></span>
|
||||
<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||
</button>
|
||||
<button v-else :tabindex="i" class="_button" :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>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
:class="$style.root"
|
||||
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
||||
:tabindex="!isDeleted ? '-1' : undefined"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
@@ -32,6 +32,7 @@
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
|
||||
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget">
|
||||
@@ -76,14 +77,14 @@
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer :note="appearNote" :max-number="16">
|
||||
<template #more>
|
||||
<button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
|
||||
{{ i18n.ts.more }}
|
||||
</button>
|
||||
</template>
|
||||
</MkReactionsViewer>
|
||||
<footer :class="$style.footer">
|
||||
<MkReactionsViewer :note="appearNote" :max-number="16">
|
||||
<template #more>
|
||||
<button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
|
||||
{{ i18n.ts.more }}
|
||||
</button>
|
||||
</template>
|
||||
</MkReactionsViewer>
|
||||
<button :class="$style.footerButton" class="_button" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
|
||||
@@ -156,6 +157,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -255,9 +257,19 @@ function renote(viaKeyboard = false) {
|
||||
text: i18n.ts.inChannelRenote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.apiWithDialog('notes/create', {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
channelId: appearNote.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
});
|
||||
},
|
||||
}, {
|
||||
@@ -276,8 +288,18 @@ function renote(viaKeyboard = false) {
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.apiWithDialog('notes/create', {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
});
|
||||
},
|
||||
}, {
|
||||
@@ -443,6 +465,34 @@ function showReactions(): void {
|
||||
&:hover > .article > .main > .footer > .footerButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.showActionsOnlyHover {
|
||||
.footer {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
padding: 0 4px;
|
||||
margin-bottom: 0 !important;
|
||||
background: var(--popup);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 32px var(--shadow);
|
||||
}
|
||||
|
||||
.footerButton {
|
||||
font-size: 90%;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.showActionsOnlyHover:hover {
|
||||
.footer {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
@@ -541,14 +591,15 @@ function showReactions(): void {
|
||||
}
|
||||
|
||||
.article {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 28px 32px 18px;
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
display: block !important;
|
||||
margin: 0 14px 8px 0;
|
||||
margin: 0 14px 0 0;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
position: sticky !important;
|
||||
@@ -571,9 +622,9 @@ function showReactions(): void {
|
||||
|
||||
.showLess {
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
margin-top: 14px;
|
||||
position: sticky;
|
||||
bottom: 1em;
|
||||
bottom: calc(var(--stickyBottom, 0px) + 14px);
|
||||
}
|
||||
|
||||
.showLessLabel {
|
||||
@@ -653,6 +704,10 @@ function showReactions(): void {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: -14px;
|
||||
}
|
||||
|
||||
.footerButton {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
@@ -683,7 +738,7 @@ function showReactions(): void {
|
||||
}
|
||||
|
||||
.article {
|
||||
padding: 24px 26px 14px;
|
||||
padding: 24px 26px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -702,7 +757,11 @@ function showReactions(): void {
|
||||
}
|
||||
|
||||
.article {
|
||||
padding: 20px 22px 12px;
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,13 +780,13 @@ function showReactions(): void {
|
||||
}
|
||||
|
||||
.article {
|
||||
padding: 14px 16px 9px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
.avatar {
|
||||
margin: 0 10px 8px 0;
|
||||
margin: 0 10px 0 0;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
top: calc(14px + var(--stickyTop, 0px));
|
||||
@@ -735,17 +794,21 @@ function showReactions(): void {
|
||||
}
|
||||
|
||||
@container (max-width: 400px) {
|
||||
.footerButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
.root:not(.showActionsOnlyHover) {
|
||||
.footerButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 350px) {
|
||||
.footerButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
.root:not(.showActionsOnlyHover) {
|
||||
.footerButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -756,9 +819,11 @@ function showReactions(): void {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.footerButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
.root:not(.showActionsOnlyHover) {
|
||||
.footerButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -161,6 +161,7 @@ import { deepClone } from '@/scripts/clone';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -250,9 +251,19 @@ function renote(viaKeyboard = false) {
|
||||
text: i18n.ts.inChannelRenote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.apiWithDialog('notes/create', {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
channelId: appearNote.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
});
|
||||
},
|
||||
}, {
|
||||
@@ -271,8 +282,18 @@ function renote(viaKeyboard = false) {
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.apiWithDialog('notes/create', {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
});
|
||||
},
|
||||
}, {
|
||||
|
@@ -18,6 +18,7 @@
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
|
||||
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
@@ -437,8 +437,8 @@ function clear() {
|
||||
}
|
||||
|
||||
function onKeydown(ev: KeyboardEvent) {
|
||||
if ((ev.which === 10 || ev.which === 13) && (ev.ctrlKey || ev.metaKey) && canPost) post();
|
||||
if (ev.which === 27) emit('esc');
|
||||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post();
|
||||
if (ev.key === 'Escape') emit('esc');
|
||||
}
|
||||
|
||||
function onCompositionUpdate(ev: CompositionEvent) {
|
||||
@@ -489,9 +489,9 @@ function onDragover(ev) {
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
case 'copy':
|
||||
case 'copyLink':
|
||||
case 'copyMove':
|
||||
case 'copy':
|
||||
case 'copyLink':
|
||||
case 'copyMove':
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
break;
|
||||
case 'linkMove':
|
||||
|
@@ -34,7 +34,7 @@ export default defineComponent({
|
||||
> button {
|
||||
flex: 1;
|
||||
padding: 10px 8px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: 999px;
|
||||
|
||||
&:disabled {
|
||||
opacity: 1 !important;
|
||||
|
@@ -53,7 +53,7 @@ onMounted(() => {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
top: 50px;
|
||||
margin: 0 auto;
|
||||
margin-top: 16px;
|
||||
min-width: 300px;
|
||||
|
222
packages/frontend/src/components/MkUserPopup.vue
Normal file
222
packages/frontend/src/components/MkUserPopup.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<Transition
|
||||
:enter-active-class="$store.state.animation ? $style.transition_popup_enterActive : ''"
|
||||
:leave-active-class="$store.state.animation ? $style.transition_popup_leaveActive : ''"
|
||||
:enter-from-class="$store.state.animation ? $style.transition_popup_enterFrom : ''"
|
||||
:leave-to-class="$store.state.animation ? $style.transition_popup_leaveTo : ''"
|
||||
appear @after-leave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
|
||||
<div v-if="user != null">
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
|
||||
<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ $ts.followsYou }}</span>
|
||||
</div>
|
||||
<svg viewBox="0 0 128 128" :class="$style.avatarBack">
|
||||
<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
|
||||
<path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/>
|
||||
</g>
|
||||
</svg>
|
||||
<MkAvatar :class="$style.avatar" :user="user" indicator/>
|
||||
<div :class="$style.title">
|
||||
<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<div :class="$style.username"><MkAcct :user="user"/></div>
|
||||
</div>
|
||||
<div :class="$style.description">
|
||||
<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
|
||||
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
|
||||
</div>
|
||||
<div :class="$style.status">
|
||||
<div :class="$style.statusItem">
|
||||
<div :class="$style.statusItemLabel">{{ $ts.notes }}</div>
|
||||
<div>{{ number(user.notesCount) }}</div>
|
||||
</div>
|
||||
<div :class="$style.statusItem">
|
||||
<div :class="$style.statusItemLabel">{{ $ts.following }}</div>
|
||||
<div>{{ number(user.followingCount) }}</div>
|
||||
</div>
|
||||
<div :class="$style.statusItem">
|
||||
<div :class="$style.statusItemLabel">{{ $ts.followers }}</div>
|
||||
<div>{{ number(user.followersCount) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { getUserMenu } from '@/scripts/get-user-menu';
|
||||
import number from '@/filters/number';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
q: string;
|
||||
source: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'mouseover'): void;
|
||||
(ev: 'mouseleave'): void;
|
||||
}>();
|
||||
|
||||
const zIndex = os.claimZIndex('middle');
|
||||
let user = $ref<misskey.entities.UserDetailed | null>(null);
|
||||
let top = $ref(0);
|
||||
let left = $ref(0);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof props.q === 'object') {
|
||||
user = props.q;
|
||||
} else {
|
||||
const query = props.q.startsWith('@') ?
|
||||
Acct.parse(props.q.substr(1)) :
|
||||
{ userId: props.q };
|
||||
|
||||
os.api('users/show', query).then(res => {
|
||||
if (!props.showing) return;
|
||||
user = res;
|
||||
});
|
||||
}
|
||||
|
||||
const rect = props.source.getBoundingClientRect();
|
||||
const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
|
||||
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
|
||||
|
||||
top = y;
|
||||
left = x;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_popup_enterActive,
|
||||
.transition_popup_leaveActive {
|
||||
transition: opacity 0.15s, transform 0.15s !important;
|
||||
}
|
||||
.transition_popup_enterFrom,
|
||||
.transition_popup_leaveTo {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.root {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
overflow: clip;
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
.banner {
|
||||
height: 78px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.followed {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
padding: 4px 8px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
font-size: 0.7em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.avatarBack {
|
||||
width: 100px;
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
z-index: 2;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
display: block;
|
||||
padding: 8px 26px 16px 26px;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 16px 26px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-top: solid 1px var(--divider);
|
||||
border-bottom: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 16px 26px 16px 26px;
|
||||
}
|
||||
|
||||
.statusItem {
|
||||
display: inline-block;
|
||||
width: 33%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statusItemLabel {
|
||||
font-size: 0.7em;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 44px;
|
||||
padding: 6px;
|
||||
background: var(--panel);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.follow {
|
||||
position: absolute !important;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
</style>
|
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<Transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="emit('closed')">
|
||||
<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
|
||||
<div v-if="user != null" class="info">
|
||||
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
|
||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<div class="description">
|
||||
<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="menu _button" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" class="koudoku-button" :user="user" mini/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { getUserMenu } from '@/scripts/get-user-menu';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
q: string;
|
||||
source: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'mouseover'): void;
|
||||
(ev: 'mouseleave'): void;
|
||||
}>();
|
||||
|
||||
const zIndex = os.claimZIndex('middle');
|
||||
let user = $ref<misskey.entities.UserDetailed | null>(null);
|
||||
let top = $ref(0);
|
||||
let left = $ref(0);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof props.q === 'object') {
|
||||
user = props.q;
|
||||
} else {
|
||||
const query = props.q.startsWith('@') ?
|
||||
Acct.parse(props.q.substr(1)) :
|
||||
{ userId: props.q };
|
||||
|
||||
os.api('users/show', query).then(res => {
|
||||
if (!props.showing) return;
|
||||
user = res;
|
||||
});
|
||||
}
|
||||
|
||||
const rect = props.source.getBoundingClientRect();
|
||||
const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
|
||||
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
|
||||
|
||||
top = y;
|
||||
left = x;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-enter-active, .popup-leave-active {
|
||||
transition: opacity 0.15s, transform 0.15s !important;
|
||||
}
|
||||
.popup-enter-from, .popup-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.fxxzrfni {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
overflow: hidden;
|
||||
transform-origin: center top;
|
||||
|
||||
> .info {
|
||||
> .banner {
|
||||
height: 84px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
> .followed {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
padding: 4px 8px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
font-size: 0.7em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 62px;
|
||||
left: 13px;
|
||||
z-index: 2;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border: solid 3px var(--face);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
display: block;
|
||||
padding: 8px 0 8px 82px;
|
||||
|
||||
> .name {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
> .username {
|
||||
display: block;
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
font-size: 0.8em;
|
||||
color: var(--fg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 0 16px;
|
||||
font-size: 0.8em;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> .status {
|
||||
padding: 8px 16px;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
width: 33%;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.7em;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> span {
|
||||
font-size: 1em;
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .menu {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 44px;
|
||||
padding: 6px;
|
||||
background: var(--panel);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
> .koudoku-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -23,7 +23,7 @@
|
||||
<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<div v-container :class="$style.content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -465,7 +465,7 @@ defineExpose({
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
//border-bottom: solid 1px var(--divider);
|
||||
font-size: 95%;
|
||||
font-size: 90%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
@@ -96,7 +96,7 @@ function onTabClick(): void {
|
||||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = metadata?.bg ?? 'var(--bg)';
|
||||
const rawBg = 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
|
@@ -6,20 +6,19 @@
|
||||
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div ref="footerEl">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// なんか動かない
|
||||
//const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
|
||||
const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
|
||||
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
|
||||
|
||||
const rootEl = $shallowRef<HTMLElement>();
|
||||
const headerEl = $shallowRef<HTMLElement>();
|
||||
const footerEl = $shallowRef<HTMLElement>();
|
||||
const bodyEl = $shallowRef<HTMLElement>();
|
||||
|
||||
let headerHeight = $ref<string | undefined>();
|
||||
@@ -27,9 +26,23 @@ let childStickyTop = $ref(0);
|
||||
const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
|
||||
provide(CURRENT_STICKY_TOP, $$(childStickyTop));
|
||||
|
||||
let footerHeight = $ref<string | undefined>();
|
||||
let childStickyBottom = $ref(0);
|
||||
const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0));
|
||||
provide(CURRENT_STICKY_BOTTOM, $$(childStickyBottom));
|
||||
|
||||
const calc = () => {
|
||||
childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
|
||||
headerHeight = headerEl.offsetHeight.toString();
|
||||
// コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
|
||||
if (headerEl != null) {
|
||||
childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
|
||||
headerHeight = headerEl.offsetHeight.toString();
|
||||
}
|
||||
|
||||
// コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
|
||||
if (footerEl != null) {
|
||||
childStickyBottom = parentStickyBottom.value + footerEl.offsetHeight;
|
||||
footerHeight = footerEl.offsetHeight.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
@@ -41,7 +54,7 @@ const observer = new ResizeObserver(() => {
|
||||
onMounted(() => {
|
||||
calc();
|
||||
|
||||
watch(parentStickyTop, calc);
|
||||
watch([parentStickyTop, parentStickyBottom], calc);
|
||||
|
||||
watch($$(childStickyTop), () => {
|
||||
bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
|
||||
@@ -49,11 +62,22 @@ onMounted(() => {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
watch($$(childStickyBottom), () => {
|
||||
bodyEl.style.setProperty('--stickyBottom', `${childStickyBottom}px`);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
headerEl.style.position = 'sticky';
|
||||
headerEl.style.top = 'var(--stickyTop, 0)';
|
||||
headerEl.style.zIndex = '1000';
|
||||
|
||||
footerEl.style.position = 'sticky';
|
||||
footerEl.style.bottom = 'var(--stickyBottom, 0)';
|
||||
footerEl.style.zIndex = '1000';
|
||||
|
||||
observer.observe(headerEl);
|
||||
observer.observe(footerEl);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
@@ -46,3 +46,28 @@ https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
||||
|
||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const;
|
||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||
|
||||
export const ROLE_POLICIES = [
|
||||
'gtlAvailable',
|
||||
'ltlAvailable',
|
||||
'canPublicNote',
|
||||
'canInvite',
|
||||
'canManageCustomEmojis',
|
||||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'pinLimit',
|
||||
'antennaLimit',
|
||||
'wordMuteLimit',
|
||||
'webhookLimit',
|
||||
'clipLimit',
|
||||
'noteEachClipsLimit',
|
||||
'userListLimit',
|
||||
'userEachUserListsLimit',
|
||||
'rateLimitFactor',
|
||||
] as const;
|
||||
|
||||
// なんか動かない
|
||||
//export const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
|
||||
//export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM');
|
||||
export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
|
||||
export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
|
||||
|
@@ -3,9 +3,10 @@ import * as Misskey from 'misskey-js';
|
||||
import { api, apiGet } from './os';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
import { stream } from '@/stream';
|
||||
import { get, set } from '@/scripts/idb-proxy';
|
||||
|
||||
const storageCache = miLocalStorage.getItem('emojis');
|
||||
export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
|
||||
const storageCache = await get('emojis');
|
||||
export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(Array.isArray(storageCache) ? storageCache : []);
|
||||
export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
|
||||
const categories = new Set<string>();
|
||||
for (const emoji of customEmojis.value) {
|
||||
@@ -18,31 +19,39 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
|
||||
|
||||
stream.on('emojiAdded', emojiData => {
|
||||
customEmojis.value = [emojiData.emoji, ...customEmojis.value];
|
||||
set('emojis', customEmojis.value);
|
||||
});
|
||||
|
||||
stream.on('emojiUpdated', emojiData => {
|
||||
customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.CustomEmoji ?? item);
|
||||
set('emojis', customEmojis.value);
|
||||
});
|
||||
|
||||
stream.on('emojiDeleted', emojiData => {
|
||||
customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name));
|
||||
set('emojis', customEmojis.value);
|
||||
});
|
||||
|
||||
export async function fetchCustomEmojis(force = false) {
|
||||
const now = Date.now();
|
||||
const needsMigration = miLocalStorage.getItem('emojis') != null;
|
||||
|
||||
let res;
|
||||
if (force) {
|
||||
if (force || needsMigration) {
|
||||
res = await api('emojis', {});
|
||||
} else {
|
||||
const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt');
|
||||
if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return;
|
||||
const lastFetchedAt = await get('lastEmojisFetchedAt');
|
||||
if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return;
|
||||
res = await apiGet('emojis', {});
|
||||
}
|
||||
|
||||
customEmojis.value = res.emojis;
|
||||
miLocalStorage.setItem('emojis', JSON.stringify(res.emojis));
|
||||
miLocalStorage.setItem('lastEmojisFetchedAt', now.toString());
|
||||
set('emojis', res.emojis);
|
||||
set('lastEmojisFetchedAt', now);
|
||||
if (needsMigration) {
|
||||
miLocalStorage.removeItem('emojis');
|
||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||
}
|
||||
}
|
||||
|
||||
let cachedTags;
|
||||
|
21
packages/frontend/src/directives/container.ts
Normal file
21
packages/frontend/src/directives/container.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Directive } from 'vue';
|
||||
|
||||
const map = new WeakMap<HTMLElement, ResizeObserver>();
|
||||
|
||||
export default {
|
||||
mounted(el: HTMLElement, binding, vn) {
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
el.style.setProperty('--containerHeight', el.offsetHeight + 'px');
|
||||
});
|
||||
ro.observe(el);
|
||||
map.set(el, ro);
|
||||
},
|
||||
|
||||
unmounted(el, binding, vn) {
|
||||
const ro = map.get(el);
|
||||
if (ro) {
|
||||
ro.disconnect();
|
||||
map.delete(el);
|
||||
}
|
||||
},
|
||||
} as Directive;
|
@@ -11,6 +11,7 @@ import clickAnime from './click-anime';
|
||||
import panel from './panel';
|
||||
import adaptiveBorder from './adaptive-border';
|
||||
import adaptiveBg from './adaptive-bg';
|
||||
import container from './container';
|
||||
|
||||
export default function(app: App) {
|
||||
app.directive('userPreview', userPreview);
|
||||
@@ -25,4 +26,5 @@ export default function(app: App) {
|
||||
app.directive('panel', panel);
|
||||
app.directive('adaptive-border', adaptiveBorder);
|
||||
app.directive('adaptive-bg', adaptiveBg);
|
||||
app.directive('container', container);
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export class UserPreview {
|
||||
|
||||
const showing = ref(true);
|
||||
|
||||
popup(defineAsyncComponent(() => import('@/components/MkUserPreview.vue')), {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), {
|
||||
showing,
|
||||
q: this.user,
|
||||
source: this.el,
|
||||
|
@@ -2,8 +2,6 @@ type Keys =
|
||||
'v' |
|
||||
'lastVersion' |
|
||||
'instance' |
|
||||
'emojis' | // TODO: indexed db
|
||||
'lastEmojisFetchedAt' |
|
||||
'account' |
|
||||
'accounts' |
|
||||
'latestDonationInfoShownAt' |
|
||||
@@ -28,7 +26,9 @@ type Keys =
|
||||
`miux:${string}` |
|
||||
`ui:folder:${string}` |
|
||||
`themes:${string}` |
|
||||
`aiscript:${string}`;
|
||||
`aiscript:${string}` |
|
||||
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
|
||||
'emojis' // DEPRECATED, stored in indexeddb (13.9.0~);
|
||||
|
||||
export const miLocalStorage = {
|
||||
getItem: (key: Keys) => window.localStorage.getItem(key),
|
||||
|
@@ -362,7 +362,7 @@ export function select<C = any>(props: {
|
||||
});
|
||||
}
|
||||
|
||||
export function success() {
|
||||
export function success(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const showing = ref(true);
|
||||
window.setTimeout(() => {
|
||||
@@ -377,7 +377,7 @@ export function success() {
|
||||
});
|
||||
}
|
||||
|
||||
export function waiting() {
|
||||
export function waiting(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const showing = ref(true);
|
||||
popup(MkWaitingDialog, {
|
||||
@@ -528,7 +528,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
onClosing?: () => void;
|
||||
}) {
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let dispose;
|
||||
popup(MkPopupMenu, {
|
||||
@@ -551,7 +551,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
||||
});
|
||||
}
|
||||
|
||||
export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) {
|
||||
export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent): Promise<void> {
|
||||
ev.preventDefault();
|
||||
return new Promise((resolve, reject) => {
|
||||
let dispose;
|
||||
@@ -569,7 +569,7 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent)
|
||||
});
|
||||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}) {
|
||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
||||
|
@@ -84,10 +84,6 @@
|
||||
</div>
|
||||
<p>{{ i18n.ts._aboutMisskey.morePatrons }}</p>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>Credits</template>
|
||||
<p>Misskeyで使われる画像の一部は、許可を得て「あの子がこっちを見てるメーカー」で作成したものが含まれます。</p>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
@@ -121,6 +117,9 @@ const patronsWithIcon = [{
|
||||
}, {
|
||||
name: 'ひとぅ',
|
||||
icon: 'https://misskey-hub.net/patrons/8cc0d0a0a6d84c88bca1aedabf6ed5ab.jpg',
|
||||
}, {
|
||||
name: 'ぱーこ',
|
||||
icon: 'https://misskey-hub.net/patrons/79c6602ffade489e8df2fcf2c2bc5d9d.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><XHeader :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
@@ -45,6 +45,16 @@
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="16">
|
||||
<div class="_buttons">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton rounded @click="testEmail"><i class="ti ti-send"></i> {{ i18n.ts.testEmail }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
@@ -61,6 +71,7 @@ import * as os from '@/os';
|
||||
import { fetchInstance, instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
let enableEmail: boolean = $ref(false);
|
||||
let email: any = $ref(null);
|
||||
@@ -109,17 +120,6 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
text: i18n.ts.testEmail,
|
||||
handler: testEmail,
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
@@ -127,3 +127,10 @@ definePageMetadata({
|
||||
icon: 'ti ti-mail',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><XHeader :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
@@ -65,6 +65,13 @@
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="16">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
@@ -79,6 +86,7 @@ import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
let useObjectStorage: boolean = $ref(false);
|
||||
let objectStorageBaseUrl: string | null = $ref(null);
|
||||
@@ -131,13 +139,6 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
@@ -145,3 +146,10 @@ definePageMetadata({
|
||||
icon: 'ti ti-cloud',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,22 +1,31 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="600">
|
||||
<XEditor :role="role" @created="created" @updated="updated"/>
|
||||
<template #header><XHeader :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="600" :margin-min="16" :margin-max="32">
|
||||
<XEditor v-if="data" v-model="data"/>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :content-max="600" :margin-min="16" :margin-max="16">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import XHeader from './_header_.vue';
|
||||
import XEditor from './roles.editor.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { useRouter } from '@/router';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -25,23 +34,45 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
let role = $ref(null);
|
||||
let data = $ref(null);
|
||||
|
||||
if (props.id) {
|
||||
role = await os.api('admin/roles/show', {
|
||||
roleId: props.id,
|
||||
});
|
||||
|
||||
data = role;
|
||||
} else {
|
||||
data = {
|
||||
name: 'New Role',
|
||||
description: '',
|
||||
rolePermission: 'normal',
|
||||
color: null,
|
||||
iconUrl: null,
|
||||
target: 'manual',
|
||||
condFormula: { id: uuid(), type: 'isRemote' },
|
||||
isPublic: false,
|
||||
asBadge: false,
|
||||
canEditMembersByModerator: false,
|
||||
policies: {},
|
||||
};
|
||||
}
|
||||
|
||||
function created(r) {
|
||||
router.push('/admin/roles/' + r.id);
|
||||
async function save() {
|
||||
if (role) {
|
||||
os.apiWithDialog('admin/roles/update', {
|
||||
roleId: role.id,
|
||||
...data,
|
||||
});
|
||||
router.push('/admin/roles/' + role.id);
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/roles/create', {
|
||||
...data,
|
||||
});
|
||||
router.push('/admin/roles/' + created.id);
|
||||
}
|
||||
}
|
||||
|
||||
function updated() {
|
||||
router.push('/admin/roles/' + role.id);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => role ? {
|
||||
@@ -54,5 +85,8 @@ definePageMetadata(computed(() => role ? {
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="name" :readonly="readonly">
|
||||
<MkInput v-model="role.name" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="description" :readonly="readonly">
|
||||
<MkTextarea v-model="role.description" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkInput v-model="color">
|
||||
<MkInput v-model="role.color">
|
||||
<template #label>{{ i18n.ts.color }}</template>
|
||||
<template #caption>#RRGGBB</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="iconUrl">
|
||||
<MkInput v-model="role.iconUrl">
|
||||
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
@@ -25,31 +25,31 @@
|
||||
<option value="administrator">{{ i18n.ts.administrator }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkSelect v-model="target" :readonly="readonly">
|
||||
<MkSelect v-model="role.target" :readonly="readonly">
|
||||
<template #label><i class="ti ti-users"></i> {{ i18n.ts._role.assignTarget }}</template>
|
||||
<template #caption><div v-html="i18n.ts._role.descriptionOfAssignTarget.replaceAll('\n', '<br>')"></div></template>
|
||||
<option value="manual">{{ i18n.ts._role.manual }}</option>
|
||||
<option value="conditional">{{ i18n.ts._role.conditional }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkFolder v-if="target === 'conditional'" default-open>
|
||||
<MkFolder v-if="role.target === 'conditional'" default-open>
|
||||
<template #label>{{ i18n.ts._role.condition }}</template>
|
||||
<div class="_gaps">
|
||||
<RolesEditorFormula v-model="condFormula"/>
|
||||
<RolesEditorFormula v-model="role.condFormula"/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
|
||||
<MkSwitch v-model="role.canEditMembersByModerator" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="isPublic" :readonly="readonly">
|
||||
<MkSwitch v-model="role.isPublic" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.isPublic }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="asBadge" :readonly="readonly">
|
||||
<MkSwitch v-model="role.asBadge" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.asBadge }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
|
||||
</MkSwitch>
|
||||
@@ -64,19 +64,19 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])">
|
||||
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.rateLimitFactor.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ `${Math.floor(policies.rateLimitFactor.value * 100)}%` }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.rateLimitFactor)"></i></span>
|
||||
<span v-if="role.policies.rateLimitFactor.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ `${Math.floor(role.policies.rateLimitFactor.value * 100)}%` }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.rateLimitFactor)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.rateLimitFactor.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.rateLimitFactor.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange :model-value="policies.rateLimitFactor.value * 100" :min="0" :max="400" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => policies.rateLimitFactor.value = (v / 100)">
|
||||
<MkRange :model-value="role.policies.rateLimitFactor.value * 100" :min="0" :max="400" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => role.policies.rateLimitFactor.value = (v / 100)">
|
||||
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
|
||||
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
|
||||
</MkRange>
|
||||
<MkRange v-model="policies.rateLimitFactor.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.rateLimitFactor.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -85,18 +85,18 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])">
|
||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.gtlAvailable.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.gtlAvailable.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.gtlAvailable)"></i></span>
|
||||
<span v-if="role.policies.gtlAvailable.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.gtlAvailable.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.gtlAvailable)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.gtlAvailable.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.gtlAvailable.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="policies.gtlAvailable.value" :disabled="policies.gtlAvailable.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.gtlAvailable.value" :disabled="role.policies.gtlAvailable.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="policies.gtlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.gtlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -105,18 +105,18 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])">
|
||||
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.ltlAvailable.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.ltlAvailable.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.ltlAvailable)"></i></span>
|
||||
<span v-if="role.policies.ltlAvailable.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.ltlAvailable.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.ltlAvailable)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.ltlAvailable.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.ltlAvailable.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="policies.ltlAvailable.value" :disabled="policies.ltlAvailable.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.ltlAvailable.value" :disabled="role.policies.ltlAvailable.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="policies.ltlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.ltlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -125,18 +125,18 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])">
|
||||
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.canPublicNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.canPublicNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.canPublicNote)"></i></span>
|
||||
<span v-if="role.policies.canPublicNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canPublicNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canPublicNote)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.canPublicNote.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canPublicNote.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="policies.canPublicNote.value" :disabled="policies.canPublicNote.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canPublicNote.value" :disabled="role.policies.canPublicNote.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="policies.canPublicNote.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.canPublicNote.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -145,18 +145,18 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.canInvite.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.canInvite.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.canInvite)"></i></span>
|
||||
<span v-if="role.policies.canInvite.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canInvite.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canInvite)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.canInvite.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canInvite.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="policies.canInvite.value" :disabled="policies.canInvite.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canInvite.value" :disabled="role.policies.canInvite.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="policies.canInvite.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.canInvite.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -165,18 +165,18 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.canManageCustomEmojis.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.canManageCustomEmojis.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.canManageCustomEmojis)"></i></span>
|
||||
<span v-if="role.policies.canManageCustomEmojis.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canManageCustomEmojis.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canManageCustomEmojis)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.canManageCustomEmojis.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canManageCustomEmojis.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="policies.canManageCustomEmojis.value" :disabled="policies.canManageCustomEmojis.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canManageCustomEmojis.value" :disabled="role.policies.canManageCustomEmojis.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="policies.canManageCustomEmojis.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.canManageCustomEmojis.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -185,18 +185,18 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.driveCapacityMb.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.driveCapacityMb.value + 'MB' }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.driveCapacityMb)"></i></span>
|
||||
<span v-if="role.policies.driveCapacityMb.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.driveCapacityMb.value + 'MB' }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.driveCapacityMb)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.driveCapacityMb.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.driveCapacityMb.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.driveCapacityMb.value" :disabled="policies.driveCapacityMb.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.driveCapacityMb.value" :disabled="role.policies.driveCapacityMb.useDefault" type="number" :readonly="readonly">
|
||||
<template #suffix>MB</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.driveCapacityMb.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.driveCapacityMb.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -205,17 +205,17 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.pinLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.pinLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.pinLimit)"></i></span>
|
||||
<span v-if="role.policies.pinLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.pinLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.pinLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.pinLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.pinLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.pinLimit.value" :disabled="policies.pinLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.pinLimit.value" :disabled="role.policies.pinLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.pinLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.pinLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -224,17 +224,17 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.antennaLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.antennaLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.antennaLimit)"></i></span>
|
||||
<span v-if="role.policies.antennaLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.antennaLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.antennaLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.antennaLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.antennaLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.antennaLimit.value" :disabled="policies.antennaLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.antennaLimit.value" :disabled="role.policies.antennaLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.antennaLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.antennaLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -243,18 +243,18 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.wordMuteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.wordMuteLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.wordMuteLimit)"></i></span>
|
||||
<span v-if="role.policies.wordMuteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.wordMuteLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.wordMuteLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.wordMuteLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.wordMuteLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.wordMuteLimit.value" :disabled="policies.wordMuteLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.wordMuteLimit.value" :disabled="role.policies.wordMuteLimit.useDefault" type="number" :readonly="readonly">
|
||||
<template #suffix>chars</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.wordMuteLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.wordMuteLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -263,17 +263,17 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.webhookLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.webhookLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.webhookLimit)"></i></span>
|
||||
<span v-if="role.policies.webhookLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.webhookLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.webhookLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.webhookLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.webhookLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.webhookLimit.value" :disabled="policies.webhookLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.webhookLimit.value" :disabled="role.policies.webhookLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.webhookLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.webhookLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -282,17 +282,17 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.clipLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.clipLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.clipLimit)"></i></span>
|
||||
<span v-if="role.policies.clipLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.clipLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.clipLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.clipLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.clipLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.clipLimit.value" :disabled="policies.clipLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.clipLimit.value" :disabled="role.policies.clipLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.clipLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.clipLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -301,17 +301,17 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.noteEachClipsLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.noteEachClipsLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.noteEachClipsLimit)"></i></span>
|
||||
<span v-if="role.policies.noteEachClipsLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.noteEachClipsLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.noteEachClipsLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.noteEachClipsLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.noteEachClipsLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.noteEachClipsLimit.value" :disabled="policies.noteEachClipsLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.noteEachClipsLimit.value" :disabled="role.policies.noteEachClipsLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.noteEachClipsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.noteEachClipsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -320,17 +320,17 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.userListLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.userListLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.userListLimit)"></i></span>
|
||||
<span v-if="role.policies.userListLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.userListLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.userListLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.userListLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.userListLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.userListLimit.value" :disabled="policies.userListLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.userListLimit.value" :disabled="role.policies.userListLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.userListLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.userListLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -339,17 +339,17 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.userEachUserListsLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.userEachUserListsLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.userEachUserListsLimit)"></i></span>
|
||||
<span v-if="role.policies.userEachUserListsLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.userEachUserListsLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.userEachUserListsLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.userEachUserListsLimit.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.userEachUserListsLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="policies.userEachUserListsLimit.value" :disabled="policies.userEachUserListsLimit.useDefault" type="number" :readonly="readonly">
|
||||
<MkInput v-model="role.policies.userEachUserListsLimit.value" :disabled="role.policies.userEachUserListsLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="policies.userEachUserListsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.userEachUserListsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
@@ -358,105 +358,74 @@
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="policies.canHideAds.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ policies.canHideAds.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.canHideAds)"></i></span>
|
||||
<span v-if="role.policies.canHideAds.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canHideAds.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canHideAds)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="policies.canHideAds.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canHideAds.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="policies.canHideAds.value" :disabled="policies.canHideAds.useDefault" :readonly="readonly">
|
||||
<MkSwitch v-model="role.policies.canHideAds.value" :disabled="role.policies.canHideAds.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="policies.canHideAds.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<MkRange v-model="role.policies.canHideAds.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSlot>
|
||||
|
||||
<div v-if="!readonly" class="_buttons">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ role ? i18n.ts.save : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { watch } from 'vue';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import RolesEditorFormula from './RolesEditorFormula.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { ROLE_POLICIES } from '@/const';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
const ROLE_POLICIES = [
|
||||
'gtlAvailable',
|
||||
'ltlAvailable',
|
||||
'canPublicNote',
|
||||
'canInvite',
|
||||
'canManageCustomEmojis',
|
||||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'pinLimit',
|
||||
'antennaLimit',
|
||||
'wordMuteLimit',
|
||||
'webhookLimit',
|
||||
'clipLimit',
|
||||
'noteEachClipsLimit',
|
||||
'userListLimit',
|
||||
'userEachUserListsLimit',
|
||||
'rateLimitFactor',
|
||||
] as const;
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'created', payload: any): void;
|
||||
(ev: 'updated'): void;
|
||||
(ev: 'update:modelValue', v: any): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
role?: any;
|
||||
modelValue: any;
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const role = props.role;
|
||||
let q = $ref('');
|
||||
let role = $ref(deepClone(props.modelValue));
|
||||
|
||||
let name = $ref(role?.name ?? 'New Role');
|
||||
let description = $ref(role?.description ?? '');
|
||||
let rolePermission = $ref(role?.isAdministrator ? 'administrator' : role?.isModerator ? 'moderator' : 'normal');
|
||||
let color = $ref(role?.color ?? null);
|
||||
let iconUrl = $ref(role?.iconUrl ?? null);
|
||||
let target = $ref(role?.target ?? 'manual');
|
||||
let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
|
||||
let isPublic = $ref(role?.isPublic ?? false);
|
||||
let asBadge = $ref(role?.asBadge ?? false);
|
||||
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
|
||||
|
||||
const policies = reactive<Record<typeof ROLE_POLICIES[number], { useDefault: boolean; priority: number; value: any; }>>({});
|
||||
// fill missing policy
|
||||
for (const ROLE_POLICY of ROLE_POLICIES) {
|
||||
const _policies = role?.policies ?? {};
|
||||
policies[ROLE_POLICY] = {
|
||||
useDefault: _policies[ROLE_POLICY]?.useDefault ?? true,
|
||||
priority: _policies[ROLE_POLICY]?.priority ?? 0,
|
||||
value: _policies[ROLE_POLICY]?.value ?? instance.policies[ROLE_POLICY],
|
||||
};
|
||||
if (role.policies[ROLE_POLICY] == null) {
|
||||
role.policies[ROLE_POLICY] = {
|
||||
useDefault: true,
|
||||
priority: 0,
|
||||
value: instance.policies[ROLE_POLICY],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (_DEV_) {
|
||||
watch($$(condFormula), () => {
|
||||
console.log(JSON.parse(JSON.stringify(condFormula)));
|
||||
}, { deep: true });
|
||||
}
|
||||
let rolePermission = $computed({
|
||||
get: () => role.isAdministrator ? 'administrator' : role.isModerator ? 'moderator' : 'normal',
|
||||
set: (val) => {
|
||||
role.isAdministrator = val === 'administrator';
|
||||
role.isModerator = val === 'moderator';
|
||||
},
|
||||
});
|
||||
|
||||
let q = $ref('');
|
||||
|
||||
function getPriorityIcon(option) {
|
||||
if (option.priority === 2) return 'ti ti-arrows-up';
|
||||
@@ -469,43 +438,26 @@ function matchQuery(keywords: string[]): boolean {
|
||||
return keywords.some(keyword => keyword.toLowerCase().includes(q.toLowerCase()));
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (props.readonly) return;
|
||||
if (role) {
|
||||
os.apiWithDialog('admin/roles/update', {
|
||||
roleId: role.id,
|
||||
name,
|
||||
description,
|
||||
color: color === '' ? null : color,
|
||||
iconUrl: iconUrl === '' ? null : iconUrl,
|
||||
target,
|
||||
condFormula,
|
||||
isAdministrator: rolePermission === 'administrator',
|
||||
isModerator: rolePermission === 'moderator',
|
||||
isPublic,
|
||||
asBadge,
|
||||
canEditMembersByModerator,
|
||||
policies,
|
||||
});
|
||||
emit('updated');
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/roles/create', {
|
||||
name,
|
||||
description,
|
||||
color: color === '' ? null : color,
|
||||
iconUrl: iconUrl === '' ? null : iconUrl,
|
||||
target,
|
||||
condFormula,
|
||||
isAdministrator: rolePermission === 'administrator',
|
||||
isModerator: rolePermission === 'moderator',
|
||||
isPublic,
|
||||
asBadge,
|
||||
canEditMembersByModerator,
|
||||
policies,
|
||||
});
|
||||
emit('created', created);
|
||||
}
|
||||
}
|
||||
const save = throttle(100, () => {
|
||||
const data = {
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
color: role.color === '' ? null : role.color,
|
||||
iconUrl: role.iconUrl === '' ? null : role.iconUrl,
|
||||
target: role.target,
|
||||
condFormula: role.condFormula,
|
||||
isAdministrator: role.isAdministrator,
|
||||
isModerator: role.isModerator,
|
||||
isPublic: role.isPublic,
|
||||
asBadge: role.asBadge,
|
||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||
policies: role.policies,
|
||||
};
|
||||
|
||||
emit('update:modelValue', data);
|
||||
});
|
||||
|
||||
watch($$(role), save, { deep: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@@ -11,7 +11,7 @@
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
<template #label>{{ i18n.ts.info }}</template>
|
||||
<XEditor :role="role" readonly/>
|
||||
<XEditor v-model="role" readonly/>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="role.target === 'manual'" default-open>
|
||||
<template #icon><i class="ti ti-users"></i></template>
|
||||
@@ -30,11 +30,19 @@
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.user.id" :class="$style.userItem">
|
||||
<MkA :class="$style.user" :to="`/user-info/${item.user.id}`">
|
||||
<MkUserCardMini :user="item.user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
|
||||
<div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedItems.includes(item.id) }]">
|
||||
<div :class="$style.userItemMain">
|
||||
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.user.id}`">
|
||||
<MkUserCardMini :user="item.user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
|
||||
<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub">
|
||||
<div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div>
|
||||
<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,6 +84,8 @@ const usersPagination = {
|
||||
})),
|
||||
};
|
||||
|
||||
let expandedItems = $ref([]);
|
||||
|
||||
const role = reactive(await os.api('admin/roles/show', {
|
||||
roleId: props.id,
|
||||
}));
|
||||
@@ -98,13 +108,37 @@ async function del() {
|
||||
router.push('/admin/roles');
|
||||
}
|
||||
|
||||
function assign() {
|
||||
os.selectUser({
|
||||
async function assign() {
|
||||
const user = await os.selectUser({
|
||||
includeSelf: true,
|
||||
}).then(async (user) => {
|
||||
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id });
|
||||
role.users.push(user);
|
||||
});
|
||||
|
||||
const { canceled: canceled2, result: period } = await os.select({
|
||||
title: i18n.ts.period,
|
||||
items: [{
|
||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||
}, {
|
||||
value: 'oneHour', text: i18n.ts.oneHour,
|
||||
}, {
|
||||
value: 'oneDay', text: i18n.ts.oneDay,
|
||||
}, {
|
||||
value: 'oneWeek', text: i18n.ts.oneWeek,
|
||||
}, {
|
||||
value: 'oneMonth', text: i18n.ts.oneMonth,
|
||||
}],
|
||||
default: 'indefinitely',
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const expiresAt = period === 'indefinitely' ? null
|
||||
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
|
||||
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
|
||||
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
|
||||
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
|
||||
: null;
|
||||
|
||||
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt });
|
||||
//role.users.push(user);
|
||||
}
|
||||
|
||||
async function unassign(user, ev) {
|
||||
@@ -114,11 +148,19 @@ async function unassign(user, ev) {
|
||||
danger: true,
|
||||
action: async () => {
|
||||
await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id });
|
||||
role.users = role.users.filter(u => u.id !== user.id);
|
||||
//role.users = role.users.filter(u => u.id !== user.id);
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function toggleItem(item) {
|
||||
if (expandedItems.includes(item.id)) {
|
||||
expandedItems = expandedItems.filter(x => x !== item.id);
|
||||
} else {
|
||||
expandedItems.push(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
@@ -130,19 +172,41 @@ definePageMetadata(computed(() => ({
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.userItem {
|
||||
.userItemMain {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.user {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.userItemSub {
|
||||
padding: 6px 12px;
|
||||
font-size: 85%;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
|
||||
.userItemMainBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.userToggle,
|
||||
.unassign {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
display: block;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.userItem.userItemOpend {
|
||||
.chevron {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><XHeader :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
@@ -133,6 +133,13 @@
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="16">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -150,6 +157,7 @@ import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
let name: string | null = $ref(null);
|
||||
let description: string | null = $ref(null);
|
||||
@@ -223,13 +231,6 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
@@ -237,3 +238,10 @@ definePageMetadata({
|
||||
icon: 'ti ti-settings',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
@@ -3,48 +3,48 @@
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="lknzcolw">
|
||||
<div class="users">
|
||||
<div class="inputs">
|
||||
<MkSelect v-model="sort" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
<option value="-createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="state" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="available">{{ i18n.ts.normal }}</option>
|
||||
<option value="admin">{{ i18n.ts.administrator }}</option>
|
||||
<option value="moderator">{{ i18n.ts.moderator }}</option>
|
||||
<option value="suspended">{{ i18n.ts.suspend }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="origin" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.instance }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:model-value="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:model-value="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div class="_gaps">
|
||||
<div :class="$style.inputs">
|
||||
<MkSelect v-model="sort" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
<option value="-createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="state" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="available">{{ i18n.ts.normal }}</option>
|
||||
<option value="admin">{{ i18n.ts.administrator }}</option>
|
||||
<option value="moderator">{{ i18n.ts.moderator }}</option>
|
||||
<option value="suspended">{{ i18n.ts.suspend }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="origin" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.instance }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div :class="$style.inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:model-value="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:model-value="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
|
||||
<div :class="$style.users">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/user-info/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
@@ -138,33 +138,20 @@ definePageMetadata(computed(() => ({
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lknzcolw {
|
||||
> .users {
|
||||
<style lang="scss" module>
|
||||
.inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
> .inputs {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
.users {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
grid-gap: 12px;
|
||||
|
||||
> * {
|
||||
margin-right: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .users {
|
||||
margin-top: var(--margin);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
grid-gap: 12px;
|
||||
|
||||
> .user:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
> .user:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -27,11 +27,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<MkFolder class="_margin">
|
||||
<MkFolder :default-open="false" :max-height="280" class="_margin">
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
||||
|
||||
<MkTextarea :model-value="flash.script" readonly tall class="_monospace" spellcheck="false"></MkTextarea>
|
||||
<MkCode :code="flash.script" :inline="false" class="_monospace"/>
|
||||
</MkFolder>
|
||||
<div :class="$style.footer">
|
||||
<Mfm :text="`By @${flash.user.username}`"/>
|
||||
@@ -62,7 +62,7 @@ import MkAsUi from '@/components/MkAsUi.vue';
|
||||
import { AsUiComponent, AsUiRoot, registerAsUiLib } from '@/scripts/aiscript/ui';
|
||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
id: string;
|
||||
|
@@ -1,39 +1,30 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="mk-list-page">
|
||||
<Transition :name="$store.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<div v-if="list" class="">
|
||||
<div class="">
|
||||
<MkButton inline @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
<MkButton inline @click="renameList()">{{ i18n.ts.rename }}</MkButton>
|
||||
<MkButton inline @click="deleteList()">{{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
<MkSpacer :content-max="700" :class="$style.main">
|
||||
<div v-if="list" class="members _margin">
|
||||
<div class="">{{ i18n.ts.members }}</div>
|
||||
<div class="_gaps_s">
|
||||
<div v-for="user in users" :key="user.id" :class="$style.userItem">
|
||||
<MkA :class="$style.userItemBody" :to="`${userPage(user)}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition :name="$store.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<div v-if="list" class="members _margin">
|
||||
<div class="">{{ i18n.ts.members }}</div>
|
||||
<div class="">
|
||||
<div class="users">
|
||||
<div v-for="user in users" :key="user.id" class="user _panel">
|
||||
<MkAvatar :user="user" class="avatar" indicator link preview/>
|
||||
<div class="body">
|
||||
<MkUserName :user="user" class="name"/>
|
||||
<MkAcct :user="user" class="acct"/>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button class="_button" @click="removeUser(user)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="16">
|
||||
<div class="_buttons">
|
||||
<MkButton inline rounded primary @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
<MkButton inline rounded @click="renameList()">{{ i18n.ts.rename }}</MkButton>
|
||||
<MkButton inline rounded danger @click="deleteList()">{{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
@@ -44,6 +35,8 @@ import * as os from '@/os';
|
||||
import { mainRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { userPage } from '@/filters/user';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
listId: string;
|
||||
@@ -76,13 +69,20 @@ function addUser() {
|
||||
});
|
||||
}
|
||||
|
||||
function removeUser(user) {
|
||||
os.api('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
users = users.filter(x => x.id !== user.id);
|
||||
});
|
||||
async function removeUser(user, ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.remove,
|
||||
icon: 'ti ti-x',
|
||||
danger: true,
|
||||
action: async () => {
|
||||
os.api('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
users = users.filter(x => x.id !== user.id);
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function renameList() {
|
||||
@@ -126,37 +126,34 @@ definePageMetadata(computed(() => list ? {
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-list-page {
|
||||
> .members {
|
||||
> ._content {
|
||||
> .users {
|
||||
> .user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
<style lang="scss" module>
|
||||
.main {
|
||||
min-height: calc(var(--containerHeight) - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.userItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
.userItemBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
> .name {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .acct {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
</style>
|
||||
|
@@ -45,6 +45,7 @@
|
||||
|
||||
<div class="_gaps_m">
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
|
||||
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
|
||||
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
||||
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
|
||||
@@ -140,6 +141,7 @@ async function reloadAsk() {
|
||||
|
||||
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
|
||||
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
||||
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
||||
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||
|
@@ -59,6 +59,8 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||
'tl',
|
||||
'overridedDeviceKind',
|
||||
'serverDisconnectedBehavior',
|
||||
'collapseRenotes',
|
||||
'showNoteActionsOnlyHover',
|
||||
'nsfw',
|
||||
'animation',
|
||||
'animatedMfm',
|
||||
@@ -420,7 +422,6 @@ onUnmounted(() => {
|
||||
definePageMetadata(computed(() => ({
|
||||
title: ts.preferencesBackups,
|
||||
icon: 'ti ti-device-floppy',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
@@ -47,6 +47,5 @@ const headerTabs = $computed(() => []);
|
||||
definePageMetadata({
|
||||
title: i18n.ts.statusbar,
|
||||
icon: 'ti ti-list',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@@ -337,7 +337,31 @@ async function assignRole() {
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id });
|
||||
const { canceled: canceled2, result: period } = await os.select({
|
||||
title: i18n.ts.period,
|
||||
items: [{
|
||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||
}, {
|
||||
value: 'oneHour', text: i18n.ts.oneHour,
|
||||
}, {
|
||||
value: 'oneDay', text: i18n.ts.oneDay,
|
||||
}, {
|
||||
value: 'oneWeek', text: i18n.ts.oneWeek,
|
||||
}, {
|
||||
value: 'oneMonth', text: i18n.ts.oneMonth,
|
||||
}],
|
||||
default: 'indefinitely',
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const expiresAt = period === 'indefinitely' ? null
|
||||
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
|
||||
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
|
||||
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
|
||||
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
|
||||
: null;
|
||||
|
||||
await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id, expiresAt });
|
||||
refreshUser();
|
||||
}
|
||||
|
||||
|
@@ -26,6 +26,9 @@
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="desc" v-html="meta.description || i18n.ts.headlineMisskey"></div>
|
||||
</div>
|
||||
<div v-if="instance.disableRegistration" class="warn">
|
||||
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||
</div>
|
||||
<div class="action _gaps_s">
|
||||
<MkButton full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||
<MkButton full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||
@@ -62,6 +65,7 @@ import XSignupDialog from '@/components/MkSignupDialog.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instanceName } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
@@ -249,6 +253,10 @@ function exploreOtherServers() {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
> .warn {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
|
||||
> .action {
|
||||
padding: 32px;
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
|
||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
||||
import { inputText } from '@/os';
|
||||
import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store';
|
||||
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store';
|
||||
|
||||
const parser = new Parser();
|
||||
const pluginContexts = new Map<string, Interpreter>();
|
||||
|
||||
export function install(plugin) {
|
||||
export function install(plugin: Plugin): void {
|
||||
// 後方互換性のため
|
||||
if (plugin.src == null) return;
|
||||
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
|
||||
@@ -15,7 +15,7 @@ export function install(plugin) {
|
||||
plugin: plugin,
|
||||
storageKey: 'plugins:' + plugin.id,
|
||||
}), {
|
||||
in: (q) => {
|
||||
in: (q): Promise<string> => {
|
||||
return new Promise(ok => {
|
||||
inputText({
|
||||
title: q,
|
||||
@@ -28,10 +28,10 @@ export function install(plugin) {
|
||||
});
|
||||
});
|
||||
},
|
||||
out: (value) => {
|
||||
out: (value): void => {
|
||||
console.log(value);
|
||||
},
|
||||
log: (type, params) => {
|
||||
log: (): void => {
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,9 +40,9 @@ export function install(plugin) {
|
||||
aiscript.exec(parser.parse(plugin.src));
|
||||
}
|
||||
|
||||
function createPluginEnv(opts) {
|
||||
const config = new Map();
|
||||
for (const [k, v] of Object.entries(opts.plugin.config || {})) {
|
||||
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
|
||||
const config = new Map<string, values.Value>();
|
||||
for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
|
||||
config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default));
|
||||
}
|
||||
|
||||
@@ -50,22 +50,28 @@ function createPluginEnv(opts) {
|
||||
...createAiScriptEnv({ ...opts, token: opts.plugin.token }),
|
||||
//#region Deprecated
|
||||
'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
|
||||
utils.assertString(title);
|
||||
registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
|
||||
utils.assertString(title);
|
||||
registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
|
||||
utils.assertString(title);
|
||||
registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
//#endregion
|
||||
'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
|
||||
utils.assertString(title);
|
||||
registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
|
||||
utils.assertString(title);
|
||||
registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
|
||||
utils.assertString(title);
|
||||
registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
|
||||
@@ -75,54 +81,78 @@ function createPluginEnv(opts) {
|
||||
registerNotePostInterruptor({ pluginId: opts.plugin.id, handler });
|
||||
}),
|
||||
'Plugin:open_url': values.FN_NATIVE(([url]) => {
|
||||
utils.assertString(url);
|
||||
window.open(url.value, '_blank');
|
||||
}),
|
||||
'Plugin:config': values.OBJ(config),
|
||||
};
|
||||
}
|
||||
|
||||
function initPlugin({ plugin, aiscript }) {
|
||||
function initPlugin({ plugin, aiscript }): void {
|
||||
pluginContexts.set(plugin.id, aiscript);
|
||||
}
|
||||
|
||||
function registerPostFormAction({ pluginId, title, handler }) {
|
||||
function registerPostFormAction({ pluginId, title, handler }): void {
|
||||
postFormActions.push({
|
||||
title, handler: (form, update) => {
|
||||
pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
|
||||
update(key.value, value.value);
|
||||
const pluginContext = pluginContexts.get(pluginId);
|
||||
if (!pluginContext) {
|
||||
return;
|
||||
}
|
||||
pluginContext.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
update(utils.valToJs(key), utils.valToJs(value));
|
||||
})]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerUserAction({ pluginId, title, handler }) {
|
||||
function registerUserAction({ pluginId, title, handler }): void {
|
||||
userActions.push({
|
||||
title, handler: (user) => {
|
||||
pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]);
|
||||
const pluginContext = pluginContexts.get(pluginId);
|
||||
if (!pluginContext) {
|
||||
return;
|
||||
}
|
||||
pluginContext.execFn(handler, [utils.jsToVal(user)]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerNoteAction({ pluginId, title, handler }) {
|
||||
function registerNoteAction({ pluginId, title, handler }): void {
|
||||
noteActions.push({
|
||||
title, handler: (note) => {
|
||||
pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
|
||||
const pluginContext = pluginContexts.get(pluginId);
|
||||
if (!pluginContext) {
|
||||
return;
|
||||
}
|
||||
pluginContext.execFn(handler, [utils.jsToVal(note)]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerNoteViewInterruptor({ pluginId, handler }) {
|
||||
function registerNoteViewInterruptor({ pluginId, handler }): void {
|
||||
noteViewInterruptors.push({
|
||||
handler: async (note) => {
|
||||
return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]));
|
||||
const pluginContext = pluginContexts.get(pluginId);
|
||||
if (!pluginContext) {
|
||||
return;
|
||||
}
|
||||
return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)]));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerNotePostInterruptor({ pluginId, handler }) {
|
||||
function registerNotePostInterruptor({ pluginId, handler }): void {
|
||||
notePostInterruptors.push({
|
||||
handler: async (note) => {
|
||||
return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]));
|
||||
const pluginContext = pluginContexts.get(pluginId);
|
||||
if (!pluginContext) {
|
||||
return;
|
||||
}
|
||||
return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)]));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -143,8 +143,32 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||
|
||||
return roles.filter(r => r.target === 'manual').map(r => ({
|
||||
text: r.name,
|
||||
action: () => {
|
||||
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id });
|
||||
action: async () => {
|
||||
const { canceled, result: period } = await os.select({
|
||||
title: i18n.ts.period,
|
||||
items: [{
|
||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||
}, {
|
||||
value: 'oneHour', text: i18n.ts.oneHour,
|
||||
}, {
|
||||
value: 'oneDay', text: i18n.ts.oneDay,
|
||||
}, {
|
||||
value: 'oneWeek', text: i18n.ts.oneWeek,
|
||||
}, {
|
||||
value: 'oneMonth', text: i18n.ts.oneMonth,
|
||||
}],
|
||||
default: 'indefinitely',
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const expiresAt = period === 'indefinitely' ? null
|
||||
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
|
||||
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
|
||||
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
|
||||
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
|
||||
: null;
|
||||
|
||||
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt });
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
@@ -53,10 +53,10 @@ const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, c
|
||||
return result;
|
||||
});
|
||||
|
||||
const ignoreElemens = ['input', 'textarea'];
|
||||
const ignoreElements = ['input', 'textarea'];
|
||||
|
||||
function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean {
|
||||
const key = ev.code.toLowerCase();
|
||||
const key = ev.key.toLowerCase();
|
||||
return patterns.some(pattern => pattern.which.includes(key) &&
|
||||
pattern.ctrl === ev.ctrlKey &&
|
||||
pattern.shift === ev.shiftKey &&
|
||||
@@ -70,7 +70,7 @@ export const makeHotkey = (keymap: Keymap) => {
|
||||
|
||||
return (ev: KeyboardEvent) => {
|
||||
if (document.activeElement) {
|
||||
if (ignoreElemens.some(el => document.activeElement!.matches(el))) return;
|
||||
if (ignoreElements.some(el => document.activeElement!.matches(el))) return;
|
||||
if (document.activeElement.attributes['contenteditable']) return;
|
||||
}
|
||||
|
||||
|
@@ -16,18 +16,3 @@ export const aliases = {
|
||||
'right': 'ArrowRight',
|
||||
'plus': ['NumpadAdd', 'Semicolon'],
|
||||
};
|
||||
|
||||
/*!
|
||||
* Programmatically add the following
|
||||
*/
|
||||
|
||||
// lower case chars
|
||||
for (let i = 97; i < 123; i++) {
|
||||
const char = String.fromCharCode(i);
|
||||
aliases[char] = `Key${char.toUpperCase()}`;
|
||||
}
|
||||
|
||||
// numbers
|
||||
for (let i = 0; i < 10; i++) {
|
||||
aliases[i] = [`Numpad${i}`, `Digit${i}`];
|
||||
}
|
||||
|
@@ -1,20 +1,20 @@
|
||||
import { query, appendQuery } from '@/scripts/url';
|
||||
import { query } from '@/scripts/url';
|
||||
import { url } from '@/config';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string {
|
||||
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/')) {
|
||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
||||
return appendQuery(imageUrl, query({
|
||||
fallback: '1',
|
||||
...(type ? { [type]: '1' } : {}),
|
||||
}));
|
||||
export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigin: boolean = false): string {
|
||||
const localProxy = `${url}/proxy`;
|
||||
|
||||
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
|
||||
// もう既にproxyっぽそうだったらurlを取り出す
|
||||
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
|
||||
}
|
||||
|
||||
return `${instance.mediaProxy}/image.webp?${query({
|
||||
return `${mustOrigin ? localProxy : instance.mediaProxy}/image.webp?${query({
|
||||
url: imageUrl,
|
||||
fallback: '1',
|
||||
...(type ? { [type]: '1' } : {}),
|
||||
...(mustOrigin ? { origin: '1' } : {}),
|
||||
})}`;
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,6 @@ export type PageMetadata = {
|
||||
icon?: string | null;
|
||||
avatar?: misskey.entities.User | null;
|
||||
userName?: misskey.entities.User | null;
|
||||
bg?: string;
|
||||
};
|
||||
|
||||
export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void {
|
||||
|
@@ -4,6 +4,27 @@ const cache = new Map<string, HTMLAudioElement>();
|
||||
|
||||
export const soundsTypes = [
|
||||
null,
|
||||
'syuilo/n-aec',
|
||||
'syuilo/n-aec-4va',
|
||||
'syuilo/n-aec-4vb',
|
||||
'syuilo/n-aec-8va',
|
||||
'syuilo/n-aec-8vb',
|
||||
'syuilo/n-cea',
|
||||
'syuilo/n-cea-4va',
|
||||
'syuilo/n-cea-4vb',
|
||||
'syuilo/n-cea-8va',
|
||||
'syuilo/n-cea-8vb',
|
||||
'syuilo/n-eca',
|
||||
'syuilo/n-eca-4va',
|
||||
'syuilo/n-eca-4vb',
|
||||
'syuilo/n-eca-8va',
|
||||
'syuilo/n-eca-8vb',
|
||||
'syuilo/n-ea',
|
||||
'syuilo/n-ea-4va',
|
||||
'syuilo/n-ea-4vb',
|
||||
'syuilo/n-ea-8va',
|
||||
'syuilo/n-ea-8vb',
|
||||
'syuilo/n-ea-harmony',
|
||||
'syuilo/up',
|
||||
'syuilo/down',
|
||||
'syuilo/pope1',
|
||||
|
@@ -273,6 +273,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: 5,
|
||||
},
|
||||
showNoteActionsOnlyHover: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
aiChanMode: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
@@ -283,12 +287,15 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
|
||||
const PREFIX = 'miux:' as const;
|
||||
|
||||
type Plugin = {
|
||||
export type Plugin = {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
config?: Record<string, { default: any }>;
|
||||
configData: Record<string, any>;
|
||||
token: string;
|
||||
src: string | null;
|
||||
version: string;
|
||||
ast: any[];
|
||||
};
|
||||
|
||||
@@ -312,14 +319,14 @@ export class ColdDeviceStorage {
|
||||
syncDeviceDarkMode: true,
|
||||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
sound_masterVolume: 0.3,
|
||||
sound_note: { type: 'syuilo/down', volume: 1 },
|
||||
sound_noteMy: { type: 'syuilo/up', volume: 1 },
|
||||
sound_notification: { type: 'syuilo/pope2', volume: 1 },
|
||||
sound_chat: { type: 'syuilo/pope1', volume: 1 },
|
||||
sound_chatBg: { type: 'syuilo/waon', volume: 1 },
|
||||
sound_antenna: { type: 'syuilo/triple', volume: 1 },
|
||||
sound_channel: { type: 'syuilo/square-pico', volume: 1 },
|
||||
sound_masterVolume: 0.5,
|
||||
sound_note: { type: 'syuilo/n-aec', volume: 0.5 },
|
||||
sound_noteMy: { type: 'syuilo/n-cea', volume: 0.5 },
|
||||
sound_notification: { type: 'syuilo/n-ea', volume: 0.5 },
|
||||
sound_chat: { type: 'syuilo/pope1', volume: 0.5 },
|
||||
sound_chatBg: { type: 'syuilo/waon', volume: 0.5 },
|
||||
sound_antenna: { type: 'syuilo/triple', volume: 0.5 },
|
||||
sound_channel: { type: 'syuilo/square-pico', volume: 0.5 },
|
||||
};
|
||||
|
||||
public static watchers: Watcher[] = [];
|
||||
|
@@ -22,7 +22,7 @@
|
||||
<span :class="$style.title"><slot name="header"></slot></span>
|
||||
<button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button>
|
||||
</header>
|
||||
<div v-show="active" ref="body" :class="$style.body">
|
||||
<div v-show="active" ref="body" v-container :class="$style.body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</section>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div :class="[$style.root, { [$style.withWallpaper]: wallpaper }]">
|
||||
<XSidebar v-if="!isMobile" :class="$style.sidebar"/>
|
||||
|
||||
<MkStickyContainer :class="$style.contents">
|
||||
<MkStickyContainer v-container :class="$style.contents">
|
||||
<template #header><XStatusBars :class="$style.statusbars"/></template>
|
||||
<main style="min-width: 0;" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
|
||||
<div :class="$style.content" style="container-type: inline-size;">
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
|
||||
|
||||
<div v-if="isMobile" :class="$style.nav">
|
||||
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
|
||||
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"><i :class="$style.navButtonIcon" class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
|
||||
@@ -84,7 +84,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef } from 'vue';
|
||||
import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, inject, Ref } from 'vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import { instanceName } from '@/config';
|
||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||
@@ -98,6 +98,7 @@ import { mainRouter } from '@/router';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { CURRENT_STICKY_BOTTOM } from '@/const';
|
||||
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
||||
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
|
||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||
@@ -115,6 +116,7 @@ window.addEventListener('resize', () => {
|
||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||
const widgetsEl = $shallowRef<HTMLElement>();
|
||||
const widgetsShowing = $ref(false);
|
||||
const navFooter = $shallowRef<HTMLElement>();
|
||||
|
||||
provide('router', mainRouter);
|
||||
provideMetadataReceiver((info) => {
|
||||
@@ -207,6 +209,21 @@ function top() {
|
||||
}
|
||||
|
||||
const wallpaper = miLocalStorage.getItem('wallpaper') != null;
|
||||
|
||||
let navFooterHeight = $ref(0);
|
||||
provide<Ref<number>>(CURRENT_STICKY_BOTTOM, $$(navFooterHeight));
|
||||
|
||||
watch($$(navFooter), () => {
|
||||
if (navFooter) {
|
||||
navFooterHeight = navFooter.offsetHeight;
|
||||
document.body.style.setProperty('--stickyBottom', `${navFooterHeight}px`);
|
||||
} else {
|
||||
navFooterHeight = 0;
|
||||
document.body.style.setProperty('--stickyBottom', '0px');
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -342,8 +359,8 @@ $widgets-hide-threshold: 1090px;
|
||||
grid-gap: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
-webkit-backdrop-filter: var(--blur, blur(32px));
|
||||
backdrop-filter: var(--blur, blur(32px));
|
||||
-webkit-backdrop-filter: var(--blur, blur(24px));
|
||||
backdrop-filter: var(--blur, blur(24px));
|
||||
background-color: var(--header);
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
Reference in New Issue
Block a user