Merge tag '13.9.1' into io

This commit is contained in:
Cookie Ramen
2023-03-05 18:30:02 +09:00
139 changed files with 2591 additions and 1675 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
});
},
}, {

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ export default defineComponent({
> button {
flex: 1;
padding: 10px 8px;
border-radius: var(--radius);
border-radius: 999px;
&:disabled {
opacity: 1 !important;

View File

@@ -53,7 +53,7 @@ onMounted(() => {
position: fixed;
left: 0;
right: 0;
top: 0;
top: 50px;
margin: 0 auto;
margin-top: 16px;
min-width: 300px;

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

View File

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

View File

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

View File

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

View File

@@ -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(() => {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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インスタンスが使いまわされ、

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.statusbar,
icon: 'ti ti-list',
bg: 'var(--bg)',
});
</script>

View File

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

View File

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

View File

@@ -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)]));
},
});
}

View File

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

View File

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

View File

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

View File

@@ -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' } : {}),
})}`;
}

View File

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

View File

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

View File

@@ -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[] = [];

View File

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

View File

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