Merge remote-tracking branch 'mattyateaFork/develop' into develop
# Conflicts: # CHANGELOG.md # README.md # locales/index.d.ts # locales/ja-JP.yml # package.json # packages/backend/src/core/activitypub/models/ApNoteService.ts # packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts # packages/backend/src/server/api/endpoints/get-avatar-decorations.ts # packages/backend/test/unit/entities/UserEntityService.ts # packages/frontend/src/components/MkFollowButton.vue # packages/frontend/src/components/MkTimeline.vue # packages/frontend/src/pages/about.vue # packages/frontend/src/pages/emoji-edit-dialog.vue # packages/frontend/src/ui/universal.vue
This commit is contained in:
commit
71382a6f85
297 changed files with 60420 additions and 4574 deletions
|
|
@ -20,6 +20,7 @@
|
|||
"@discordapp/twemoji": "15.0.3",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@meersagor/wavesurfer-vue": "^0.1.0",
|
||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
|
|
@ -73,7 +74,8 @@
|
|||
"v-code-diff": "1.11.0",
|
||||
"vite": "5.2.11",
|
||||
"vue": "3.4.26",
|
||||
"vuedraggable": "next"
|
||||
"vuedraggable": "next",
|
||||
"wavesurfer.js": "^7.7.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export async function mainBoot() {
|
|||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
ui === 'twilike' ? defineAsyncComponent(() => import('@/ui/twilike.vue')) :
|
||||
defineAsyncComponent(() => import('@/ui/universal.vue')),
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -12,3 +12,6 @@ export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/role
|
|||
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
|
||||
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
|
||||
export const favoritedChannelsCache = new Cache<Misskey.entities.Channel[]>(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 }));
|
||||
export const userFavoriteListsCache = new Cache(1000 * 60 * 30, () => misskeyApi('users/lists/list-favorite'));
|
||||
export const userChannelsCache = new Cache<Misskey.entities.UserChannel[]>(1000 * 60 * 30, () => misskeyApi('channels/owned'));
|
||||
export const userChannelFollowingsCache = new Cache<Misskey.entities.UserChannelFollowing[]>(1000 * 60 * 30, () => misskeyApi('channels/followed'));
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="bcekxzvu _margin _panel">
|
||||
<div class="target">
|
||||
<MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
|
||||
<MkAvatar class="avatar" :user="report.targetUser" indicator/>
|
||||
<div class="names">
|
||||
<MkUserName class="name" :user="report.targetUser"/>
|
||||
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.target">
|
||||
<MkA v-user-preview="report.targetUserId" :class="$style.info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
|
||||
<MkAvatar :class="$style.avatar" :user="report.targetUser" indicator/>
|
||||
<div :class="$style.name">
|
||||
<MkUserName :class="$style.names" :user="report.targetUser"/>
|
||||
<MkAcct :class="$style.names" :user="report.targetUser" style="display: block;"/>
|
||||
</div>
|
||||
</MkA>
|
||||
<MkKeyValue>
|
||||
|
|
@ -18,9 +18,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<div :class="$style.detail">
|
||||
<div>
|
||||
<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
|
||||
<MkFolder v-if="report.notes.length !== 0" :class="$style.notes">
|
||||
<template #label>{{ i18n.ts.reportedNote }}</template>
|
||||
<div v-for="note in report.notes" :class="$style.notes">
|
||||
<MkNoteSimple v-if="note !== 'deleted'" :note="note"/>
|
||||
<div v-else> note is deleted </div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
<hr/>
|
||||
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
|
||||
|
|
@ -42,15 +49,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
const props = defineProps<{
|
||||
report: any;
|
||||
report: {
|
||||
id: string;
|
||||
createdAt:string;
|
||||
targetUserId:Misskey.entities.User['id'];
|
||||
targetUser:Misskey.entities.User & { createdAt:string; };
|
||||
reporter:Misskey.entities.User;
|
||||
assignee:Misskey.entities.User['id'];
|
||||
comment:string;
|
||||
notes:Misskey.entities.Note['id'][];
|
||||
forwarded:boolean;
|
||||
resolved:boolean;
|
||||
};
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -69,47 +89,57 @@ function resolve() {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bcekxzvu {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
|
||||
> .target {
|
||||
width: 35%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
padding: 24px;
|
||||
border-right: solid 1px var(--divider);
|
||||
|
||||
> .info {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
--c: rgb(255 196 0 / 15%);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
||||
background-size: 16px 16px;
|
||||
|
||||
> .avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
> .names {
|
||||
margin-left: 0.3em;
|
||||
padding: 0 8px;
|
||||
flex: 1;
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .detail {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
}
|
||||
margin: var(--margin) 0;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.notes {
|
||||
margin: var(--margin) 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.target {
|
||||
width: 35%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
padding: 24px;
|
||||
border-right: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
--c: rgb(255 196 0 / 15%);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.names {
|
||||
margin-left: 0.3em;
|
||||
padding: 0 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.detail {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')">
|
||||
<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" style="overflow-x: clip;" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
|
||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||
|
|
@ -13,19 +13,45 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<div class="">
|
||||
<MkTextarea v-model="comment">
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
>
|
||||
<template v-if="page === 0">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<MkPagination v-slot="{items}" :key="user.id" :pagination="Pagination" :disableAutoLoad="true">
|
||||
<div v-for="item in items" :key="item.id" :class="$style.note">
|
||||
<MkSwitch :modelValue="abuseNotesId.includes(item.id)" @update:modelValue="pushAbuseReportNote($event,item.id)"></MkSwitch>
|
||||
<MkAvatar :user="item.user" preview/>
|
||||
<MkNoteSimple :note="item"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<template v-else-if="page === 1">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<MkTextarea v-model="comment">
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
|
||||
</MkTextarea>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
</Transition>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
|
|
@ -37,23 +63,45 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.UserDetailed;
|
||||
initialComment?: string;
|
||||
initialNoteId?: Misskey.entities.Note['id'];
|
||||
}>();
|
||||
|
||||
const Pagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
userId: props.user.id,
|
||||
},
|
||||
};
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const abuseNotesId = ref(props.initialNoteId ? [props.initialNoteId] : []);
|
||||
const page = ref(0);
|
||||
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const comment = ref(props.initialComment ?? '');
|
||||
|
||||
function pushAbuseReportNote(ev, id) {
|
||||
if (ev) {
|
||||
abuseNotesId.value.push(id);
|
||||
} else {
|
||||
abuseNotesId.value = abuseNotesId.value.filter(noteId => noteId !== id);
|
||||
}
|
||||
}
|
||||
|
||||
function send() {
|
||||
os.apiWithDialog('users/report-abuse', {
|
||||
userId: props.user.id,
|
||||
comment: comment.value,
|
||||
noteIds: abuseNotesId.value,
|
||||
}, undefined).then(res => {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
|
|
@ -69,4 +117,22 @@ function send() {
|
|||
.root {
|
||||
--root-margin: 16px;
|
||||
}
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
|
||||
}
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
.note{
|
||||
display: flex;
|
||||
margin: var(--margin) 0;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ const emojiDb = computed(() => {
|
|||
const customEmojiDB: EmojiDef[] = [];
|
||||
|
||||
for (const x of customEmojis.value) {
|
||||
if (x.draft) {
|
||||
continue;
|
||||
}
|
||||
|
||||
customEmojiDB.push({
|
||||
name: x.name,
|
||||
emoji: `:${x.name}:`,
|
||||
|
|
|
|||
164
packages/frontend/src/components/MkAvatarDecoEditDialog.vue
Normal file
164
packages/frontend/src/components/MkAvatarDecoEditDialog.vue
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
@close="dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template v-if="avatarDecoration" #header>:{{ avatarDecoration.name }}</template>
|
||||
<template v-else #header>New create</template>
|
||||
|
||||
<div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<div class="_gaps_m">
|
||||
<XDecoration
|
||||
v-if="avatarDecoration"
|
||||
:key="avatarDecoration.id"
|
||||
:decoration="avatarDecoration"
|
||||
/>
|
||||
<MkInput v-model="name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category">
|
||||
<template #label>{{ i18n.ts.category }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.footerButtons">
|
||||
<MkButton danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="save"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import XDecoration from '@/pages/settings/avatar-decoration.decoration.vue';
|
||||
const props = defineProps<{
|
||||
avatarDecoration?: {
|
||||
id: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
category: string;
|
||||
};
|
||||
}>();
|
||||
let name = ref(props.avatarDecoration?.name ?? '');
|
||||
let category = ref(props.avatarDecoration?.category ?? '');
|
||||
let description = ref(props.avatarDecoration?.description ?? '');
|
||||
let url = ref(props.avatarDecoration?.url ?? '');
|
||||
const emit = defineEmits<{
|
||||
(ev: 'del'): void
|
||||
}>();
|
||||
|
||||
let dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
|
||||
|
||||
function del() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: props.avatarDecoration?.name }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
misskeyApi('admin/avatar-decorations/delete', { id: props.avatarDecoration?.id }).then(() => {
|
||||
|
||||
});
|
||||
});
|
||||
emit('del');
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (props.avatarDecoration == null) {
|
||||
await os.apiWithDialog('admin/avatar-decorations/create', {
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
url: url.value,
|
||||
category: category.value,
|
||||
});
|
||||
} else {
|
||||
await os.apiWithDialog('admin/avatar-decorations/update', {
|
||||
id: props.avatarDecoration.id ?? '',
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
url: url.value,
|
||||
category: category.value,
|
||||
});
|
||||
}
|
||||
emit('del');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.imgs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.img {
|
||||
display: block;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.role {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roleUnassign {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
|
||||
.footerButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,7 +6,23 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<button
|
||||
v-if="!link"
|
||||
ref="el" class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||
:class="[
|
||||
$style.root,
|
||||
{
|
||||
[$style.inline]: inline,
|
||||
[$style.primary]: primary,
|
||||
[$style.gradate]: gradate,
|
||||
[$style.danger]: danger,
|
||||
[$style.rounded]: rounded,
|
||||
[$style.full]: full,
|
||||
[$style.small]: small,
|
||||
[$style.large]: large,
|
||||
[$style.transparent]: transparent,
|
||||
[$style.asLike]: asLike,
|
||||
[$style.gamingDark]: gaming === 'dark',
|
||||
[$style.gamingLight]: gaming === 'light',
|
||||
}
|
||||
]"
|
||||
:type="type"
|
||||
:name="name"
|
||||
:value="value"
|
||||
|
|
@ -21,7 +37,23 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</button>
|
||||
<MkA
|
||||
v-else class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||
:class="[
|
||||
$style.root,
|
||||
{
|
||||
[$style.inline]: inline,
|
||||
[$style.primary]: primary,
|
||||
[$style.gradate]: gradate,
|
||||
[$style.danger]: danger,
|
||||
[$style.rounded]: rounded,
|
||||
[$style.full]: full,
|
||||
[$style.small]: small,
|
||||
[$style.large]: large,
|
||||
[$style.transparent]: transparent,
|
||||
[$style.asLike]: asLike,
|
||||
[$style.gamingDark]: gaming === 'dark',
|
||||
[$style.gamingLight]: gaming === 'light',
|
||||
}
|
||||
]"
|
||||
:to="to ?? '#'"
|
||||
:behavior="linkBehavior"
|
||||
@mousedown="onMousedown"
|
||||
|
|
@ -34,32 +66,66 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, shallowRef } from 'vue';
|
||||
import { nextTick, onMounted, shallowRef, computed, ref, watch } from 'vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
primary?: boolean;
|
||||
gradate?: boolean;
|
||||
rounded?: boolean;
|
||||
inline?: boolean;
|
||||
link?: boolean;
|
||||
to?: string;
|
||||
linkBehavior?: null | 'window' | 'browser';
|
||||
autofocus?: boolean;
|
||||
wait?: boolean;
|
||||
danger?: boolean;
|
||||
full?: boolean;
|
||||
small?: boolean;
|
||||
large?: boolean;
|
||||
transparent?: boolean;
|
||||
asLike?: boolean;
|
||||
name?: string;
|
||||
value?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
primary?: boolean;
|
||||
gradate?: boolean;
|
||||
rounded?: boolean;
|
||||
inline?: boolean;
|
||||
link?: boolean;
|
||||
to?: string;
|
||||
linkBehavior?: null | 'window' | 'browser';autofocus?: boolean;
|
||||
wait?: boolean;
|
||||
danger?: boolean;
|
||||
full?: boolean;
|
||||
small?: boolean;
|
||||
large?: boolean;
|
||||
transparent?: boolean;
|
||||
gamingdark?: boolean;
|
||||
gaminglight?: boolean;
|
||||
asLike?: boolean;
|
||||
name?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode'));
|
||||
// gamingをrefで初期化する
|
||||
let gaming = ref(''); // 0-off , 1-dark , 2-light
|
||||
// gaming.valueに新しい値を代入する
|
||||
|
||||
if (darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
|
||||
watch(darkMode, () => {
|
||||
if (darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(gamingMode, () => {
|
||||
if (darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(ev: 'click', payload: MouseEvent): void;
|
||||
(ev: 'click', payload: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
const el = shallowRef<HTMLElement | null>(null);
|
||||
|
|
@ -168,7 +234,62 @@ function onMousedown(evt: MouseEvent): void {
|
|||
font-weight: bold;
|
||||
color: var(--fgOnAccent) !important;
|
||||
background: var(--accent);
|
||||
&.gamingLight {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
color: white !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
color: white !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
color: white !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ;
|
||||
}
|
||||
&:hover{
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&.gamingDark {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
color: black;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% ;
|
||||
color: black;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ;
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% !important;
|
||||
color: black;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
}
|
||||
}
|
||||
&:not(:disabled):hover {
|
||||
background: var(--X8);
|
||||
}
|
||||
|
|
@ -225,6 +346,59 @@ function onMousedown(evt: MouseEvent): void {
|
|||
&:not(:disabled):active {
|
||||
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||
}
|
||||
&.gamingLight {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
color: white !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
color: white !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
color: white !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ;
|
||||
}
|
||||
}
|
||||
|
||||
&.gamingDark {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
color: black;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% ;
|
||||
color: black;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ;
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% !important;
|
||||
color: black;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
|
@ -292,4 +466,34 @@ function onMousedown(evt: MouseEvent): void {
|
|||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
@keyframes AnimationLight {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,132 +3,293 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
-->
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full }]"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="isFollowing">
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full },[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]]"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="isFollowing">
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{
|
||||
i18n.ts.unfollow
|
||||
}}</span><i class="ti ti-minus"></i>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{
|
||||
i18n.ts.follow
|
||||
}}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{
|
||||
i18n.ts.processing
|
||||
}}</span>
|
||||
<MkLoading :em="true"/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import {i18n} from '@/i18n.js';
|
||||
import {defaultStore} from "@/store.js";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
channel: Misskey.entities.Channel;
|
||||
full?: boolean;
|
||||
channel: Misskey.entities.Channel;
|
||||
full?: boolean;
|
||||
}>(), {
|
||||
full: false,
|
||||
full: false,
|
||||
});
|
||||
|
||||
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const isFollowing = ref(props.channel.isFollowing);
|
||||
const wait = ref(false);
|
||||
|
||||
async function onClick() {
|
||||
wait.value = true;
|
||||
wait.value = true;
|
||||
|
||||
try {
|
||||
if (isFollowing.value) {
|
||||
await misskeyApi('channels/unfollow', {
|
||||
channelId: props.channel.id,
|
||||
});
|
||||
isFollowing.value = false;
|
||||
} else {
|
||||
await misskeyApi('channels/follow', {
|
||||
channelId: props.channel.id,
|
||||
});
|
||||
isFollowing.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
wait.value = false;
|
||||
}
|
||||
try {
|
||||
if (isFollowing.value) {
|
||||
await misskeyApi('channels/unfollow', {
|
||||
channelId: props.channel.id,
|
||||
});
|
||||
isFollowing.value = false;
|
||||
} else {
|
||||
await misskeyApi('channels/follow', {
|
||||
channelId: props.channel.id,
|
||||
});
|
||||
isFollowing.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
wait.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
background: transparent;
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
height: 31px;
|
||||
font-size: 16px;
|
||||
border-radius: 32px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
height: 31px;
|
||||
font-size: 16px;
|
||||
border-radius: 32px;
|
||||
background: #fff;
|
||||
|
||||
&.full {
|
||||
padding: 0 8px 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.gamingDark {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&:not(.full) {
|
||||
width: 31px;
|
||||
}
|
||||
&.gamingLight {
|
||||
color: #fff;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//background: mix($primary, #fff, 20);
|
||||
}
|
||||
&.full {
|
||||
padding: 0 8px 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
//background: mix($primary, #fff, 40);
|
||||
}
|
||||
&:not(.full) {
|
||||
width: 31px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
border-color: var(--accentLighten);
|
||||
}
|
||||
&:hover {
|
||||
//background: mix($primary, #fff, 20);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accentDarken);
|
||||
border-color: var(--accentDarken);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
//background: mix($primary, #fff, 40);
|
||||
}
|
||||
|
||||
&.wait {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
border-color: var(--accentLighten);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accentDarken);
|
||||
border-color: var(--accentDarken);
|
||||
}
|
||||
|
||||
&.gamingDark:hover {
|
||||
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&.gamingDark:active {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
&.gamingLight:hover {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
&.gamingLight:active {
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
&.gamingDark {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
|
||||
}
|
||||
|
||||
&.wait {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-right: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<div>
|
||||
<div v-if="game.ready" :class="$style.game">
|
||||
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
|
||||
<div :class="$style.count" class="" data-testid="count"><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
|
||||
<div :class="$style.count" class="" data-testid="count">
|
||||
<img
|
||||
:class="[$style.icon,{[$style.dark]:darkMode}]" alt="Cosaque daihuku"
|
||||
src="https://files.prismisskey.space/misskey/630c737c-e96f-4c10-94a4-73e138278576.webp"
|
||||
/>
|
||||
{{ number(cookies) }}
|
||||
</div>
|
||||
<button v-click-anime class="_button" @click="onClick">
|
||||
<img src="/client-assets/cookie.png" :class="$style.img">
|
||||
<img
|
||||
src="https://files.prismisskey.space/misskey/630c737c-e96f-4c10-94a4-73e138278576.webp"
|
||||
:class="$style.img"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
@ -25,7 +34,9 @@ import { useInterval } from '@/scripts/use-interval.js';
|
|||
import * as game from '@/scripts/clicker-game.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
const saveData = game.saveData;
|
||||
const cookies = computed(() => saveData.value?.cookies);
|
||||
const cps = ref(0);
|
||||
|
|
@ -91,4 +102,15 @@ onUnmounted(() => {
|
|||
.img {
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
$color-scheme: var(--color-scheme);
|
||||
|
||||
.icon {
|
||||
width: 1.3em;
|
||||
vertical-align: -24%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
filter: invert(1);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
262
packages/frontend/src/components/MkCustomEmojiEditLocal.vue
Normal file
262
packages/frontend/src/components/MkCustomEmojiEditLocal.vue
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
<template>
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||
<template #label>Select mode</template>
|
||||
</MkSwitch>
|
||||
<div v-if="selectMode" class="_buttons">
|
||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||
<MkButton inline @click="setLisenceBulk">Set Lisence</MkButton>
|
||||
<MkButton inline @click="isSensitiveBulk">Set isSensitive</MkButton>
|
||||
<MkButton inline @click="setlocalOnlyBulk">Set localOnly</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination" :displayLimit="100">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div :class="$style.root">
|
||||
<div v-for="emoji in items" :key="emoji.id">
|
||||
<button v-if="emoji.request" class="_panel _button" :class="[{ [$style.selected]: selectedEmojis.includes(emoji.id) },$style.emoji,$style.emojirequest]" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else class="_panel _button" :class="[{ [$style.selected]: selectedEmojis.includes(emoji.id) },$style.emoji]" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" :class="$style.img" :alt="emoji.name"/>
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.name" class="_monospace">{{ emoji.name }}</div>
|
||||
<div :class="$style.info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const query = ref(null);
|
||||
const selectMode = ref(false);
|
||||
const selectedEmojis = ref<string[]>([]);
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'admin/emoji/list' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
query: (query.value && query.value !== '') ? query.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
|
||||
if (selectedEmojis.value.length > 0) {
|
||||
selectedEmojis.value = [];
|
||||
} else {
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
|
||||
}
|
||||
};
|
||||
const setisSensitiveBulk = async () => {
|
||||
const { canceled, result } = await os.switch1({
|
||||
title: 'isSensitive',
|
||||
type: "mksw"
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-issensitive-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
isSensitive: result
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
const setlocalOnlyBulk = async () => {
|
||||
const { canceled, result } = await os.switch1({
|
||||
title: 'localOnly',
|
||||
type: "mksw"
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-localonly-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
localOnly: result
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
|
||||
const toggleSelect = (emoji) => {
|
||||
console.log(selectedEmojis.value)
|
||||
if (selectedEmojis.value.includes(emoji.id)) {
|
||||
selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
|
||||
} else {
|
||||
selectedEmojis.value.push(emoji.id);
|
||||
}
|
||||
};
|
||||
|
||||
const edit = (emoji) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), {
|
||||
emoji: emoji,
|
||||
isRequest: false,
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
emojisPaginationComponent.value.reload();
|
||||
} else if (result.deleted) {
|
||||
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
};
|
||||
|
||||
const setCategoryBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Category',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-category-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
category: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const setLisenceBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'License',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-license-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const isSensitiveBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'License',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-issensitive-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const addTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const removeTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const setTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const delBulk = async () => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: var(--margin);
|
||||
}
|
||||
.emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
text-align: left;
|
||||
border: solid 1px var(--panel);
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.selected {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
.body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info {
|
||||
opacity: 0.5;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.emojirequest {
|
||||
--c: rgb(255 196 0 / 15%);;
|
||||
background-image: linear-gradient(45deg,var(--c) 16.67%,transparent 16.67%,transparent 50%,var(--c) 50%,var(--c) 66.67%,transparent 66.67%,transparent 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
</style>
|
||||
106
packages/frontend/src/components/MkCustomEmojiEditRemote.vue
Normal file
106
packages/frontend/src/components/MkCustomEmojiEditRemote.vue
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div :class="$style.root">
|
||||
<div v-for="emoji in items" :key="emoji.id" :class="$style.emoji" class="_panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" :class="$style.img" :alt="emoji.name"/>
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.name" class="_monospace">{{ emoji.name }}</div>
|
||||
<div :class="$style.info">{{ emoji.host }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const queryRemote = ref(null);
|
||||
const host = ref(null);
|
||||
|
||||
const remotePagination = {
|
||||
endpoint: 'admin/emoji/list-remote' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
|
||||
host: (host.value && host.value !== '') ? host.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
const im = (emoji) => {
|
||||
os.apiWithDialog('admin/emoji/copy', {
|
||||
emojiId: emoji.id,
|
||||
});
|
||||
};
|
||||
|
||||
const remoteMenu = (emoji, ev: MouseEvent) => {
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: ':' + emoji.name + ':',
|
||||
}, {
|
||||
text: i18n.ts.import,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => { im(emoji); },
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info {
|
||||
opacity: 0.5;
|
||||
font-size: 90%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
199
packages/frontend/src/components/MkCustomEmojiEditRequest.vue
Normal file
199
packages/frontend/src/components/MkCustomEmojiEditRequest.vue
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<template>
|
||||
<MkPagination ref="emojisRequestPaginationComponent" :pagination="paginationRequest">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<template v-for="emoji in items" :key="emoji.id">
|
||||
<div :class="$style.emoji" class="_panel">
|
||||
<div :class="$style.img">
|
||||
<div :class="$style.imgLight"><img :src="emoji.url" :alt="emoji.name"/></div>
|
||||
<div :class="$style.imgDark"><img :src="emoji.url" :alt="emoji.name"/></div>
|
||||
</div>
|
||||
<div :class="$style.info">
|
||||
<div :class="$style.name">{{ i18n.ts.name }}: {{ emoji.name }}</div>
|
||||
<div :class="$style.category">{{ i18n.ts.category }}:{{ emoji.category }}</div>
|
||||
<div :class="$style.aliases">{{ i18n.ts.tags }}:{{ emoji.aliases.join(' ') }}</div>
|
||||
<div :class="$style.license">{{ i18n.ts.license }}:{{ emoji.license }}</div>
|
||||
</div>
|
||||
<div :class="$style.editbutton">
|
||||
<MkButton primary :class="$style.edit" @click="editRequest(emoji)">
|
||||
{{ i18n.ts.edit }}
|
||||
</MkButton>
|
||||
<MkButton :class="$style.request" @click="unrequested(emoji)">
|
||||
{{ i18n.ts.approval }}
|
||||
</MkButton>
|
||||
<MkButton danger :class="$style.delete" @click="deleteRequest(emoji)">
|
||||
{{ i18n.ts.delete }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import {misskeyApi} from "@/scripts/misskey-api.js";
|
||||
|
||||
const emojisRequestPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const query = ref(null);
|
||||
|
||||
const paginationRequest = {
|
||||
endpoint: 'admin/emoji/list-request' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
query: (query.value && query.value !== '') ? query.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
function editRequest(emoji) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), {
|
||||
emoji: emoji,
|
||||
isRequest: true,
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisRequestPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
} else if (result.deleted) {
|
||||
emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
async function unrequested(emoji) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('requestApprovalAreYouSure', { x: emoji.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await misskeyApi('admin/emoji/update-request', {
|
||||
id: emoji.id,
|
||||
fileId: emoji.fileId,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
aliases: emoji.aliases,
|
||||
license: emoji.license,
|
||||
isSensitive: emoji.isSensitive,
|
||||
localOnly: emoji.localOnly,
|
||||
isRequest: false,
|
||||
});
|
||||
|
||||
emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
}
|
||||
|
||||
async function deleteRequest(emoji) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: emoji.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('admin/emoji/delete', {
|
||||
id: emoji.id,
|
||||
}).then(() => {
|
||||
emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.emoji {
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
text-align: left;
|
||||
border: solid 1px var(--panel);
|
||||
margin: 10px;
|
||||
}
|
||||
.img {
|
||||
display: grid;
|
||||
grid-row: 1;
|
||||
grid-column: 1/ span 2;
|
||||
grid-template-columns: 50% 50%;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
}
|
||||
.imgLight {
|
||||
display: grid;
|
||||
grid-column: 1;
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
img {
|
||||
max-height: 64px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.imgDark {
|
||||
display: grid;
|
||||
grid-column: 2;
|
||||
background-color: #000;
|
||||
margin-bottom: 12px;
|
||||
img {
|
||||
max-height: 64px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.info {
|
||||
display: grid;
|
||||
grid-row: 2;
|
||||
grid-template-rows: 30px 30px 30px;
|
||||
}
|
||||
.name {
|
||||
grid-row: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category {
|
||||
grid-row: 2;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aliases {
|
||||
grid-row: 3;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.license {
|
||||
grid-row: 4;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.editbutton {
|
||||
display: grid;
|
||||
grid-template-rows: 42px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.edit {
|
||||
grid-row: 1;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.request {
|
||||
grid-row: 2;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.delete {
|
||||
grid-row: 3;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -43,65 +43,73 @@ export default defineComponent({
|
|||
setup(props, { slots, expose }) {
|
||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
||||
|
||||
const dateTextCache = new Map<string, string>();
|
||||
|
||||
function getDateText(time: string) {
|
||||
if (dateTextCache.has(time)) {
|
||||
return dateTextCache.get(time)!;
|
||||
}
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
return i18n.tsx.monthAndDay({
|
||||
const text = i18n.tsx.monthAndDay({
|
||||
month: month.toString(),
|
||||
day: date.toString(),
|
||||
});
|
||||
dateTextCache.set(time, text);
|
||||
return text;
|
||||
}
|
||||
|
||||
if (props.items.length === 0) return;
|
||||
|
||||
const renderChildrenImpl = () => props.items.map((item, i) => {
|
||||
if (!slots || !slots.default) return;
|
||||
const renderChildrenImpl = () => {
|
||||
const slotContent = slots.default ? slots.default : () => [];
|
||||
return props.items.map((item, i) => {
|
||||
const el = slotContent({
|
||||
item: item,
|
||||
})[0];
|
||||
if (el.key == null && item.id) el.key = item.id;
|
||||
|
||||
const el = slots.default({
|
||||
item: item,
|
||||
})[0];
|
||||
if (el.key == null && item.id) el.key = item.id;
|
||||
|
||||
if (
|
||||
i !== props.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: $style['separator'],
|
||||
key: item.id + ':separator',
|
||||
}, h('p', {
|
||||
class: $style['date'],
|
||||
}, [
|
||||
h('span', {
|
||||
class: $style['date-1'],
|
||||
if (
|
||||
i !== props.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: $style['separator'],
|
||||
key: item.id + ':separator',
|
||||
}, h('p', {
|
||||
class: $style['date'],
|
||||
}, [
|
||||
h('i', {
|
||||
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
||||
}),
|
||||
getDateText(item.createdAt),
|
||||
]),
|
||||
h('span', {
|
||||
class: $style['date-2'],
|
||||
}, [
|
||||
getDateText(props.items[i + 1].createdAt),
|
||||
h('i', {
|
||||
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
||||
}),
|
||||
]),
|
||||
]));
|
||||
h('span', {
|
||||
class: $style['date-1'],
|
||||
}, [
|
||||
h('i', {
|
||||
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
||||
}),
|
||||
getDateText(item.createdAt),
|
||||
]),
|
||||
h('span', {
|
||||
class: $style['date-2'],
|
||||
}, [
|
||||
getDateText(props.items[i + 1].createdAt),
|
||||
h('i', {
|
||||
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
||||
}),
|
||||
]),
|
||||
]));
|
||||
|
||||
return [el, separator];
|
||||
} else {
|
||||
if (props.ad && item._shouldInsertAd_) {
|
||||
return [h(MkAd, {
|
||||
key: item.id + ':ad',
|
||||
prefer: ['horizontal', 'horizontal-big'],
|
||||
}), el];
|
||||
return [el, separator];
|
||||
} else {
|
||||
return el;
|
||||
if (props.ad && item._shouldInsertAd_) {
|
||||
return [h(MkAd, {
|
||||
key: item.id + ':ad',
|
||||
prefer: ['horizontal', 'horizontal-big'],
|
||||
}), el];
|
||||
} else {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const renderChildren = () => {
|
||||
const children = renderChildrenImpl();
|
||||
|
|
@ -120,14 +128,12 @@ export default defineComponent({
|
|||
|
||||
function onBeforeLeave(element: Element) {
|
||||
const el = element as HTMLElement;
|
||||
el.style.top = `${el.offsetTop}px`;
|
||||
el.style.left = `${el.offsetLeft}px`;
|
||||
el.classList.add('before-leave');
|
||||
}
|
||||
|
||||
function onLeaveCancelled(element: Element) {
|
||||
const el = element as HTMLElement;
|
||||
el.style.top = '';
|
||||
el.style.left = '';
|
||||
el.classList.remove('before-leave');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
|
|
@ -157,21 +163,21 @@ export default defineComponent({
|
|||
container-type: inline-size;
|
||||
|
||||
&:global {
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
&.deny-move-transition > .list-move {
|
||||
transition: none !important;
|
||||
}
|
||||
&.deny-move-transition > .list-move {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
> .list-enter-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
> .list-enter-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.date-separated-list-nogap) > *:not(:last-child) {
|
||||
|
|
@ -194,20 +200,20 @@ export default defineComponent({
|
|||
|
||||
.direction-up {
|
||||
&:global {
|
||||
> .list-enter-from,
|
||||
> .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(64px);
|
||||
}
|
||||
> .list-enter-from,
|
||||
> .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(64px);
|
||||
}
|
||||
}
|
||||
}
|
||||
.direction-down {
|
||||
&:global {
|
||||
> .list-enter-from,
|
||||
> .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-64px);
|
||||
}
|
||||
> .list-enter-from,
|
||||
> .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-64px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,5 +252,8 @@ export default defineComponent({
|
|||
.date-2-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.before-leave {
|
||||
position: absolute !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<i v-else-if="type === 'info'" :class="$style.iconInner" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i>
|
||||
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
|
||||
<div v-if="type === 'mksw'" :class="$style.text"><MkSwitch :helpText="text" v-model="mkresult"/></div>
|
||||
</div>
|
||||
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
|
||||
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
|
||||
|
|
@ -56,9 +57,10 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSwitch from "@/components/MkSwitch.vue";
|
||||
|
||||
type Input = {
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local' | 'mksw';
|
||||
placeholder?: string | null;
|
||||
autocomplete?: string;
|
||||
default: string | number | null;
|
||||
|
|
@ -77,11 +79,12 @@ type Select = {
|
|||
type Result = string | number | true | null;
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
|
||||
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting' | 'mksw';
|
||||
title?: string;
|
||||
text?: string;
|
||||
input?: Input;
|
||||
select?: Select;
|
||||
mksw?: boolean;
|
||||
icon?: string;
|
||||
actions?: {
|
||||
text: string;
|
||||
|
|
@ -110,7 +113,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
|
|||
|
||||
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
||||
const selectedValue = ref(props.select?.default ?? null);
|
||||
|
||||
const mkresult= ref(false)
|
||||
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
||||
if (props.input) {
|
||||
if (props.input.minLength) {
|
||||
|
|
@ -142,6 +145,7 @@ async function ok() {
|
|||
const result =
|
||||
props.input ? inputValue.value :
|
||||
props.select ? selectedValue.value :
|
||||
mkresult ? mkresult.value :
|
||||
true;
|
||||
done(false, result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<template>
|
||||
<div
|
||||
:class="[$style.root, { [$style.isSelected]: isSelected }]"
|
||||
:class="[$style.root, { [$style.isSelected]: isSelected || isSelectedFile }]"
|
||||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
|
|
@ -37,14 +37,15 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
|
||||
import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ const props = withDefaults(defineProps<{
|
|||
folder: Misskey.entities.DriveFolder | null;
|
||||
isSelected?: boolean;
|
||||
selectMode?: boolean;
|
||||
SelectFiles?: string[];
|
||||
}>(), {
|
||||
isSelected: false,
|
||||
selectMode: false,
|
||||
|
|
@ -67,13 +69,25 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const isDragging = ref(false);
|
||||
|
||||
const isSelectedFile = ref(false);
|
||||
const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
|
||||
|
||||
watch(props.SelectFiles, () => {
|
||||
const index = props.SelectFiles.findIndex(item => item.id === props.file.id);
|
||||
isSelectedFile.value = index !== -1;
|
||||
});
|
||||
|
||||
function onClick(ev: MouseEvent) {
|
||||
|
||||
if (props.selectMode) {
|
||||
emit('chosen', props.file);
|
||||
} else {
|
||||
} else if (!ev.shiftKey && !isTouchUsing && !isSelectedFile.value) {
|
||||
os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
} else if (!ev.shiftKey && isSelectedFile.value && props.SelectFiles.length === 0) {
|
||||
os.popupMenu(getDriveMultiFileMenu(props.SelectFiles), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
} else if (isTouchUsing && !isSelectedFile.value && props.SelectFiles.length === 0) {
|
||||
os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
}else {
|
||||
if (deviceKind === 'desktop') {
|
||||
router.push(`/my/drive/file/${props.file.id}`);
|
||||
} else {
|
||||
|
|
@ -83,7 +97,14 @@ function onClick(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(getDriveFileMenu(props.file, props.folder), ev);
|
||||
|
||||
if (!isTouchUsing) {
|
||||
if (!ev.shiftKey && !isSelectedFile.value) {
|
||||
os.contextMenu(getDriveFileMenu(props.file, props.folder), ev);
|
||||
} else if (isSelectedFile.value) {
|
||||
os.contextMenu(getDriveMultiFileMenu(props.SelectFiles), ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDragstart(ev: DragEvent) {
|
||||
|
|
@ -92,7 +113,7 @@ function onDragstart(ev: DragEvent) {
|
|||
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
|
||||
}
|
||||
isDragging.value = true;
|
||||
|
||||
(isDragging.value)
|
||||
emit('dragstart');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const props = withDefaults(defineProps<{
|
|||
folder: Misskey.entities.DriveFolder;
|
||||
isSelected?: boolean;
|
||||
selectMode?: boolean;
|
||||
selectedFiles?: string[];
|
||||
}>(), {
|
||||
isSelected: false,
|
||||
selectMode: false,
|
||||
|
|
@ -144,10 +145,19 @@ function onDrop(ev: DragEvent) {
|
|||
if (driveFile != null && driveFile !== '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
emit('removeFile', file.id);
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: props.folder.id,
|
||||
});
|
||||
if (props.selectedFiles.length > 0) {
|
||||
props.selectedFiles.forEach((e) => {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: e.id,
|
||||
folderId: props.folder.id,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: props.folder.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
@ -222,26 +232,108 @@ function rename() {
|
|||
}
|
||||
|
||||
function deleteFolder() {
|
||||
misskeyApi('drive/folders/delete', {
|
||||
misskeyApi('drive/folders/show', {
|
||||
folderId: props.folder.id,
|
||||
}).then(() => {
|
||||
if (defaultStore.state.uploadFolder === props.folder.id) {
|
||||
defaultStore.set('uploadFolder', null);
|
||||
}).then(async (r) => {
|
||||
if (r.foldersCount > 0) {
|
||||
await os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToDelete,
|
||||
text: 'フォルダ内にフォルダが存在するため、削除できません。 \n フォルダ内のフォルダを削除してから試してみてください。',
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
switch (err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToDelete,
|
||||
text: i18n.ts.hasChildFilesOrFolders,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.unableToDelete,
|
||||
});
|
||||
|
||||
if (r.filesCount > 0) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('driveFolderDeleteConfirm', { name: props.folder.name }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
let allResults = [];
|
||||
let Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31 });
|
||||
allResults = allResults.concat(Result);
|
||||
while (Result.length >= 31) {
|
||||
const untilId = Result[Result.length - 1].id;
|
||||
Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31, untilId });
|
||||
allResults = allResults.concat(Result); // pushをconcatに変更
|
||||
}
|
||||
allResults.forEach((r, i) => {
|
||||
misskeyApi('drive/files/delete', { fileId: r.id });
|
||||
});
|
||||
|
||||
misskeyApi('drive/folders/show', {
|
||||
folderId: props.folder.id,
|
||||
}).then(async (r) => {
|
||||
if (r.filesCount > 0) {
|
||||
let allResults = [];
|
||||
let Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31 });
|
||||
allResults = allResults.concat(Result);
|
||||
while (Result.length >= 31) {
|
||||
const untilId = Result[Result.length - 1].id;
|
||||
Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31, untilId });
|
||||
allResults = allResults.concat(Result);
|
||||
}
|
||||
allResults.forEach((r, i) => {
|
||||
misskeyApi('drive/files/delete', { fileId: r.id });
|
||||
});
|
||||
|
||||
misskeyApi('drive/folders/delete', {
|
||||
folderId: props.folder.id,
|
||||
}).then(() => {
|
||||
if (defaultStore.state.uploadFolder === props.folder.id) {
|
||||
defaultStore.set('uploadFolder', null);
|
||||
}
|
||||
}).catch(err => {
|
||||
switch (err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToDelete,
|
||||
text: i18n.ts.hasChildFilesOrFolders,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.unableToDelete,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
misskeyApi('drive/folders/delete', {
|
||||
folderId: props.folder.id,
|
||||
});
|
||||
} else {
|
||||
misskeyApi('drive/folders/delete', {
|
||||
folderId: props.folder.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await misskeyApi('drive/folders/delete', {
|
||||
folderId: props.folder.id,
|
||||
}).then(() => {
|
||||
if (defaultStore.state.uploadFolder === props.folder.id) {
|
||||
defaultStore.set('uploadFolder', null);
|
||||
}
|
||||
}).catch(err => {
|
||||
switch (err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToDelete,
|
||||
text: i18n.ts.hasChildFilesOrFolders,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.unableToDelete,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { i18n } from '@/i18n.js';
|
|||
const props = defineProps<{
|
||||
folder?: Misskey.entities.DriveFolder;
|
||||
parentFolder: Misskey.entities.DriveFolder | null;
|
||||
selectedFiles: string[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -111,10 +112,19 @@ function onDrop(ev: DragEvent) {
|
|||
if (driveFile != null && driveFile !== '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
emit('removeFile', file.id);
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: props.folder ? props.folder.id : null,
|
||||
});
|
||||
if (props.selectedFiles.length > 0) {
|
||||
props.selectedFiles.forEach((e) => {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: e.id,
|
||||
folderId: props.folder ? props.folder.id : null,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: props.folder ? props.folder.id : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<XNavFolder
|
||||
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
|
||||
:parentFolder="folder"
|
||||
:selectedFiles="selectedFiles"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
|
|
@ -20,16 +21,30 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
:folder="f"
|
||||
:parentFolder="folder"
|
||||
:class="[$style.navPathItem]"
|
||||
:selectedFiles="selectedFiles"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i
|
||||
class="ti ti-chevron-right"
|
||||
></i></span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
|
||||
</div>
|
||||
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
<button v-if="!multiple" class="_button" :class="$style.navMenu" @click="filesSelect">複数選択モード</button>
|
||||
<span v-if="multiple && selectedFiles.length > 0" style="padding-right: 12px; margin-top: auto; margin-bottom: auto;opacity: 0.5;">
|
||||
({{ number(selectedFiles.length) }})
|
||||
</span>
|
||||
<button v-if="multiple" class="_button" :class="$style.navMenu" @click="filesSelect">複数選択モード解除</button>
|
||||
<button v-if="multiple && selectedFiles.length === 0" style="padding-right: 12px;" class="_button" @click="filesAllSelect">
|
||||
全選択
|
||||
</button>
|
||||
<button v-if="multiple && selectedFiles.length !== 0" style="padding-right: 12px;" class="_button" @click="filesAllSelect">
|
||||
全選択解除
|
||||
</button>
|
||||
<button class="_button" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</nav>
|
||||
<div
|
||||
ref="main"
|
||||
|
|
@ -50,6 +65,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
:folder="f"
|
||||
:selectMode="select === 'folder'"
|
||||
:isSelected="selectedFolders.some(x => x.id === f.id)"
|
||||
:selectedFiles="selectedFiles"
|
||||
@chosen="chooseFolder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
|
|
@ -71,7 +87,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
:file="file"
|
||||
:folder="folder"
|
||||
:selectMode="select === 'file'"
|
||||
:SelectFiles="selectedFiles"
|
||||
:isSelected="selectedFiles.some(x => x.id === file.id)"
|
||||
@click.shift.left.exact="filesSelect"
|
||||
@chosen="chooseFile"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
|
|
@ -82,14 +100,21 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</div>
|
||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
|
||||
<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
|
||||
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
|
||||
<div v-if="!draghover && folder == null">
|
||||
<strong>{{
|
||||
i18n.ts.emptyDrive
|
||||
}}</strong><br/>{{ i18n.ts['empty-drive-description'] }}
|
||||
</div>
|
||||
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
<div v-if="draghover" :class="$style.dropzone"></div>
|
||||
<input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
||||
<input
|
||||
ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1"
|
||||
@change="onChangeFileInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -108,23 +133,24 @@ import { defaultStore } from '@/store.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { uploadFile, uploads } from '@/scripts/upload.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder;
|
||||
type?: string;
|
||||
multiple?: boolean;
|
||||
select?: 'file' | 'folder' | null;
|
||||
initialFolder?: Misskey.entities.DriveFolder;
|
||||
type?: string;
|
||||
multiple?: boolean;
|
||||
select?: 'file' | 'folder' | null;
|
||||
}>(), {
|
||||
multiple: false,
|
||||
select: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
|
||||
(ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'move-root'): void;
|
||||
(ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
|
||||
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
||||
(ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
|
||||
(ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'move-root'): void;
|
||||
(ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
|
||||
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
||||
}>();
|
||||
|
||||
const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>();
|
||||
|
|
@ -141,6 +167,8 @@ const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
|||
const uploadings = uploads;
|
||||
const connection = useStream().useChannel('drive');
|
||||
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
const multiple = ref(props.multiple || false);
|
||||
const select = ref(props.select || null);
|
||||
|
||||
// ドロップされようとしているか
|
||||
const draghover = ref(false);
|
||||
|
|
@ -391,7 +419,7 @@ function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null
|
|||
|
||||
function chooseFile(file: Misskey.entities.DriveFile) {
|
||||
const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id);
|
||||
if (props.multiple) {
|
||||
if (multiple.value) {
|
||||
if (isAlreadySelected) {
|
||||
selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
|
||||
} else {
|
||||
|
|
@ -410,7 +438,7 @@ function chooseFile(file: Misskey.entities.DriveFile) {
|
|||
|
||||
function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
|
||||
const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id);
|
||||
if (props.multiple) {
|
||||
if (multiple.value) {
|
||||
if (isAlreadySelected) {
|
||||
selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id);
|
||||
} else {
|
||||
|
|
@ -497,6 +525,7 @@ function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
|
|||
function removeFile(file: Misskey.entities.DriveFile | string) {
|
||||
const fileId = typeof file === 'object' ? file.id : file;
|
||||
files.value = files.value.filter(f => f.id !== fileId);
|
||||
selectedFiles.value = selectedFiles.value.filter(f => f.id !== fileId);
|
||||
}
|
||||
|
||||
function appendFile(file: Misskey.entities.DriveFile) {
|
||||
|
|
@ -623,33 +652,129 @@ function getMenu() {
|
|||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'ti ti-upload',
|
||||
action: () => { selectLocalFile(); },
|
||||
action: () => {
|
||||
selectLocalFile();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.fromUrl,
|
||||
icon: 'ti ti-link',
|
||||
action: () => { urlUpload(); },
|
||||
action: () => {
|
||||
urlUpload();
|
||||
},
|
||||
}, { type: 'divider' }, {
|
||||
text: folder.value ? folder.value.name : i18n.ts.drive,
|
||||
type: 'label',
|
||||
}, folder.value ? {
|
||||
text: i18n.ts.renameFolder,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => { if (folder.value) renameFolder(folder.value); },
|
||||
action: () => {
|
||||
if (folder.value)renameFolder(folder.value);
|
||||
},
|
||||
} : undefined, folder.value ? {
|
||||
text: i18n.ts.deleteFolder,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
|
||||
action: () => {
|
||||
deleteFolder(folder.value as Misskey.entities.DriveFolder);
|
||||
},
|
||||
} : undefined, {
|
||||
text: i18n.ts.createFolder,
|
||||
icon: 'ti ti-folder-plus',
|
||||
action: () => { createFolder(); },
|
||||
action: () => {
|
||||
createFolder();
|
||||
},
|
||||
}];
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
async function isSensitive(Files, isSensitive: boolean) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t(isSensitive ? 'driveFilesSensitiveonConfirm' : 'driveFilesSensitiveoffConfirm', { name: Files.length }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
Files.forEach((file) => {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
isSensitive,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function fileDelete(Files) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('driveFilesDeleteConfirm', { name: Files.length }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
Files.forEach((file) => {
|
||||
misskeyApi('drive/files/delete', {
|
||||
fileId: file.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getFilesMenu(Files) {
|
||||
return [{
|
||||
text: i18n.ts.createNoteFromTheFile,
|
||||
icon: 'ti ti-pencil',
|
||||
action: () => {
|
||||
if (Files.length >= 16) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: '16ファイル以上添付しようとしています',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
os.post({
|
||||
initialFiles: [...Files],
|
||||
});
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.unmarkAsSensitive,
|
||||
icon: 'ti ti-eye',
|
||||
action: () => {
|
||||
isSensitive(Files, false);
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.markAsSensitive,
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
action: () => {
|
||||
isSensitive(Files, true);
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => {
|
||||
fileDelete(Files);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
function filesSelect() {
|
||||
multiple.value = !multiple.value;
|
||||
select.value = (select.value === null) ? 'file' : null;
|
||||
selectedFiles.value = [];
|
||||
}
|
||||
|
||||
function filesAllSelect() {
|
||||
if (selectedFiles.value.length === 0) {
|
||||
selectedFiles.value = files.value;
|
||||
} else {
|
||||
selectedFiles.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
if (selectedFiles.value.length === 0) {
|
||||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
} else {
|
||||
os.popupMenu(getFilesMenu(selectedFiles.value), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
|
|
@ -693,114 +818,119 @@ onBeforeUnmount(() => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 1px 0 var(--divider);
|
||||
user-select: none;
|
||||
display: flex;
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
background-color: var(--bg);
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 1px 0 var(--divider);
|
||||
user-select: none;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.navPath {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
line-height: 42px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
line-height: 42px;
|
||||
white-space: nowrap;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.navPathItem {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
line-height: 42px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
line-height: 42px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.navCurrent {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
&.navCurrent {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.navSeparator {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
&.navSeparator {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.navMenu {
|
||||
margin-left: auto;
|
||||
padding: 0 12px;
|
||||
margin-left: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--margin);
|
||||
user-select: none;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--margin);
|
||||
user-select: none;
|
||||
|
||||
&.fetching {
|
||||
cursor: wait !important;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
&.fetching {
|
||||
cursor: wait !important;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.uploading {
|
||||
height: calc(100% - 38px - 100px);
|
||||
}
|
||||
&.uploading {
|
||||
height: calc(100% - 38px - 100px);
|
||||
}
|
||||
}
|
||||
|
||||
.folders,
|
||||
.files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.folder,
|
||||
.file {
|
||||
flex-grow: 1;
|
||||
width: 128px;
|
||||
margin: 4px;
|
||||
box-sizing: border-box;
|
||||
flex-grow: 1;
|
||||
width: 128px;
|
||||
margin: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.padding {
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
width: 128px + 8px;
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
width: 128px + 8px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 38px;
|
||||
width: 100%;
|
||||
height: calc(100% - 38px);
|
||||
border: dashed 2px var(--focus);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 38px;
|
||||
width: 100%;
|
||||
height: calc(100% - 38px);
|
||||
border: dashed 2px var(--focus);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
309
packages/frontend/src/components/MkEmojiEditDialog.vue
Normal file
309
packages/frontend/src/components/MkEmojiEditDialog.vue
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initialWidth="600"
|
||||
:initialHeight="600"
|
||||
:canResize="true"
|
||||
@close="windowEl.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
||||
<template v-else-if="isRequest && !emoji" #header>{{ i18n.ts.requestCustomEmojis }}</template>
|
||||
<template v-else #header>New emoji</template>
|
||||
|
||||
<div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" style="display: flex; flex-direction: row">
|
||||
<div>
|
||||
<div v-if="imgUrl != null" :class="$style.imgs">
|
||||
<div style="background: #000;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #222;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #ddd;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #fff;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
<template #caption>{{ i18n.ts.emojiNameValidation }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" :datalist="customEmojiCategories">
|
||||
<template #label>{{ i18n.ts.category }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="aliases" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.tags }}</template>
|
||||
<template #caption>
|
||||
{{ i18n.ts.theKeywordWhenSearchingForCustomEmoji }}<br/>
|
||||
{{ i18n.ts.setMultipleBySeparatingWithSpace }}
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="license" :mfmAutocomplete="true">
|
||||
<template #label>{{ i18n.ts.license }}</template>
|
||||
</MkInput>
|
||||
<MkFolder v-if="!isRequest">
|
||||
<template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template>
|
||||
<template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
|
||||
<MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo>
|
||||
<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkSwitch v-model="isSensitive">{{ i18n.ts.isSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="isNotifyIsHome">
|
||||
{{ i18n.ts.isNotifyIsHome }}
|
||||
</MkSwitch>
|
||||
</div>
|
||||
<div v-if="imgUrl" style="width: 30%">
|
||||
<MkInput v-model="text">
|
||||
<template #label>テスト文章</template>
|
||||
</MkInput><br/>
|
||||
<MkNoteSimple :emojireq="true" :note="{isHidden:false,replyId:null,renoteId:null,files:[],user: $i,text:text,cw:null, emojis: {[name]: imgUrl}}"/>
|
||||
<p v-if="speed ">基準より眩しい可能性があります。</p>
|
||||
<p v-if="!speed">問題は見つかりませんでした。</p>
|
||||
<p>※上記の物は問題がないことを保証するものではありません。</p>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.footerButtons">
|
||||
<MkButton v-if="!isRequest" danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
<MkButton v-if="validation" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
<MkButton v-else rounded style="margin: 0 auto;"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { customEmojiCategories } from '@/custom-emojis.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { selectFile, selectFiles } from '@/scripts/select-file.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
const props = defineProps<{
|
||||
emoji?: any,
|
||||
isRequest: boolean,
|
||||
}>();
|
||||
const text = ref<string>('テスト文章');
|
||||
const speed = ref<boolean>(false);
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||
const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
|
||||
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
|
||||
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
|
||||
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||
const rolesThatCanBeUsedThisEmojiAsReaction = ref<Misskey.entities.Role[]>([]);
|
||||
const file = ref<Misskey.entities.DriveFile>();
|
||||
let isRequest = ref(props.isRequest ?? false);
|
||||
watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
||||
}, { immediate: true });
|
||||
const isNotifyIsHome = ref(props.emoji ? props.emoji.isNotifyIsHome : false);
|
||||
const imgUrl = computed(() => file.value ? file.value.url : props.emoji && !isRequest.value ? `/emoji/${props.emoji.name}.webp` : props.emoji && props.emoji.url ? props.emoji.url : null);
|
||||
const validation = computed(() => {
|
||||
return name.value.match(/^[a-zA-Z0-9_]+$/) && imgUrl.value != null;
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const colorChanges = ref<number | null>(null);
|
||||
|
||||
watch(colorChanges, (value) => {
|
||||
console.log(value);
|
||||
});
|
||||
|
||||
async function changeImage(ev) {
|
||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||
name.value = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
async function addRole() {
|
||||
const roles = await misskeyApi('admin/roles/list');
|
||||
const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id);
|
||||
|
||||
const { canceled, result: role } = await os.select({
|
||||
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
|
||||
});
|
||||
if (canceled || role == null) return;
|
||||
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||
}
|
||||
|
||||
async function removeRole(role, ev) {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
|
||||
}
|
||||
|
||||
async function done() {
|
||||
const params = {
|
||||
name: name.value,
|
||||
category: category.value === '' ? null : category.value,
|
||||
aliases: aliases.value.replace(' ', ' ').split(' ').filter(x => x !== ''),
|
||||
license: license.value === '' ? null : license.value,
|
||||
Request: isRequest.value,
|
||||
isSensitive: isSensitive.value,
|
||||
localOnly: localOnly.value,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id),
|
||||
isNotifyIsHome: isNotifyIsHome.value,
|
||||
};
|
||||
|
||||
if (file.value) {
|
||||
params.fileId = file.value.id;
|
||||
}
|
||||
if (props.emoji) {
|
||||
if (isRequest.value) {
|
||||
await os.apiWithDialog('admin/emoji/update-request', {
|
||||
id: props.emoji.id,
|
||||
...params,
|
||||
});
|
||||
} else {
|
||||
await os.apiWithDialog('admin/emoji/update', {
|
||||
id: props.emoji.id,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
id: props.emoji.id,
|
||||
...params,
|
||||
},
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
} else {
|
||||
const created = isRequest.value
|
||||
? await os.apiWithDialog('admin/emoji/add-request', params)
|
||||
: await os.apiWithDialog('admin/emoji/add', params);
|
||||
|
||||
emit('done', {
|
||||
created: created,
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.removeAreYouSure({ x: name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('admin/emoji/delete', {
|
||||
id: props.emoji.id,
|
||||
}).then(() => {
|
||||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
windowEl.value.close();
|
||||
});
|
||||
}
|
||||
|
||||
watch(imgUrl, async (value) => {
|
||||
speed.value = await misskeyApi('emoji/speedtest', {
|
||||
url: value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.imgs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.img {
|
||||
display: block;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.preview {
|
||||
display: block;
|
||||
height: 16px;
|
||||
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.role {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roleUnassign {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
|
||||
.footerButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,60 +3,58 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
-->
|
||||
|
||||
<template>
|
||||
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
|
||||
<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
|
||||
<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||
<header class="_acrylic" @click="shown = !shown">
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
|
||||
</header>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
:disabled="disabledEmojis?.value.includes(emoji)"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true" :fallbackToImage="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
|
||||
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||
<header class="_acrylic" @click="shown = !shown">
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
|
||||
</header>
|
||||
<div v-if="shown" style="padding-left: 9px;">
|
||||
<MkEmojiPickerSection
|
||||
v-for="child in customEmojiTree"
|
||||
:key="`custom:${child.value}`"
|
||||
:initialShown="initialShown"
|
||||
:emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="nestedChosen"
|
||||
>
|
||||
{{ child.value || i18n.ts.other }}
|
||||
</MkEmojiPickerSection>
|
||||
</div>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
:disabled="disabledEmojis?.value.includes(emoji)"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
|
||||
<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
|
||||
<section v-if="!hasChildSection" style="border-radius: 6px;">
|
||||
<header class="_acrylic" @click="shown = !shown" >
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> ({{ emojis.length }})
|
||||
</header>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
:disabled="disabledEmojis?.value.includes(emoji)"@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true":fallbackToImage="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
|
||||
<section v-else style="border-radius: 6px;">
|
||||
<header class="_acrylic" @click="shown = !shown">
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (フォルダー)
|
||||
</header>
|
||||
<div v-if="shown" style="padding-left: 9px; ">
|
||||
<MkEmojiPickerSection
|
||||
v-for="child in customEmojiTree"
|
||||
:key="`custom:${child.value}`"
|
||||
:initialShown="initialShown"
|
||||
:emojis="computed(() => customEmojis.filter(e => e.category === child.category || e.category === child.category+'/'+child.category).map(e => `:${e.name}:`))"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="nestedChosen"
|
||||
>
|
||||
{{ child.value || i18n.ts.other }}
|
||||
</MkEmojiPickerSection>
|
||||
</div>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item":disabled="disabledEmojis?.value.includes(emoji)"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -67,19 +65,18 @@ import { customEmojis } from '@/custom-emojis.js';
|
|||
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
category?: string[];
|
||||
emojis: string[] | Ref<string[]>;
|
||||
disabledEmojis?: Ref<string[]>;
|
||||
initialShown?: boolean;
|
||||
hasChildSection?: boolean;
|
||||
customEmojiTree?: CustomEmojiFolderTree[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'chosen', v: string, event: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
const emojis = computed(() => Array.isArray(props.emojis) ? props.emojis : props.emojis.value);
|
||||
|
||||
const shown = ref(!!props.initialShown);
|
||||
|
||||
/** @see MkEmojiPicker.vue */
|
||||
|
|
@ -89,7 +86,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
|||
elm.title = getEmojiName(emoji);
|
||||
}
|
||||
|
||||
function nestedChosen(emoji: any, ev: MouseEvent) {
|
||||
function nestedChosen(emoji: any, ev?: MouseEvent) {
|
||||
emit('chosen', emoji, ev);
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,12 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</section>
|
||||
|
||||
<div v-if="tab === 'index'" class="group index">
|
||||
<section v-if="showPinned && (pinned && pinned.length > 0)">
|
||||
<section v-if="showPinned">
|
||||
<div style="display: flex; ">
|
||||
<div v-for="a in profileMax" :key="a" :title="defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`]" class="sllfktkhgl" :class="{ active: activeIndex === a || isDefaultProfile === a }" @click="pinnedProfileSelect(a)">
|
||||
{{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<button
|
||||
v-for="emoji in pinnedEmojisDef"
|
||||
|
|
@ -121,12 +126,15 @@ import { deviceKind } from '@/scripts/device-kind.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||
const $i = signinRequired();
|
||||
const props = withDefaults(defineProps<{
|
||||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[];
|
||||
pinnedEmojis?: string[];
|
||||
maxHeight?: number;
|
||||
asDrawer?: boolean;
|
||||
asWindow?: boolean;
|
||||
|
|
@ -139,7 +147,7 @@ const props = withDefaults(defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'chosen', v: string): void;
|
||||
}>();
|
||||
|
||||
const profileMax = $i.policies.emojiPickerProfileLimit;
|
||||
const searchEl = shallowRef<HTMLInputElement>();
|
||||
const emojisEl = shallowRef<HTMLDivElement>();
|
||||
|
||||
|
|
@ -154,7 +162,7 @@ const recentlyUsedEmojisDef = computed(() => {
|
|||
return recentlyUsedEmojis.value.map(getDef);
|
||||
});
|
||||
const pinnedEmojisDef = computed(() => {
|
||||
return pinned.value?.map(getDef);
|
||||
return pinnedEmojis.value?.map(getDef);
|
||||
});
|
||||
|
||||
const pinned = computed(() => props.pinnedEmojis);
|
||||
|
|
@ -165,25 +173,29 @@ const q = ref<string>('');
|
|||
const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]);
|
||||
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
||||
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
||||
|
||||
const pinnedEmojis = ref(pinned.value);
|
||||
const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] };
|
||||
|
||||
function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
|
||||
const parts = input.split('/').map(p => p.trim());
|
||||
let currentNode: CustomEmojiFolderTree = root;
|
||||
|
||||
for (const part of parts) {
|
||||
let existingNode = currentNode.children.find((node) => node.value === part);
|
||||
|
||||
if (!existingNode) {
|
||||
const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
|
||||
currentNode.children.push(newNode);
|
||||
existingNode = newNode;
|
||||
}
|
||||
|
||||
currentNode = existingNode;
|
||||
const parts = input.split('/').map(p => p.trim()); // スラッシュで区切って配列にしてる
|
||||
let currentNode: CustomEmojiFolderTree = root; // currentNode は root
|
||||
let includesPart = customEmojis.value.some(emoji => emoji.category !== null && emoji.category.includes(parts[0] + '/'));
|
||||
if (parts.length === 1 && parts[0] !== '' && includesPart) { // parts が 1 つで空じゃなかったら
|
||||
parts.push(parts[0]); // parts に parts[0] を追加 (test category だったら test/test category になる)
|
||||
}
|
||||
|
||||
for (const part of parts) { // parts を順番に見ていく
|
||||
let existingNode = currentNode.children.find((node) => node.value === part); // currentNode の children から part と同じ value を持つ node を探す
|
||||
|
||||
if (!existingNode) { // なかったら
|
||||
const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; // 新しい node を作る
|
||||
|
||||
currentNode.children.push(newNode); // currentNode の children に newNode を追加
|
||||
existingNode = newNode; // existingNode に newNode を代入
|
||||
}
|
||||
|
||||
currentNode = existingNode; // currentNode に existingNode を代入
|
||||
}
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
|
|
@ -245,9 +257,7 @@ watch(q, () => {
|
|||
if (matches.size >= max) break;
|
||||
}
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (matches.size >= max) return matches; for (const emoji of emojis) {
|
||||
if (emoji.name.startsWith(newQ)) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
|
|
@ -357,7 +367,7 @@ function canReact(emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string
|
|||
}
|
||||
|
||||
function filterCategory(emoji: Misskey.entities.EmojiSimple, category: string): boolean {
|
||||
return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category;
|
||||
return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category && !customEmojis.value.some(e => e.category !== null && e.category.includes(emoji.category + '/')) || emoji.category === category + '/' + category && !emoji.category;
|
||||
}
|
||||
|
||||
function focus() {
|
||||
|
|
@ -435,6 +445,14 @@ function onEnter(ev: KeyboardEvent) {
|
|||
done();
|
||||
}
|
||||
|
||||
const activeIndex = ref(defaultStore.state.pickerProfileDefault);
|
||||
pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]);
|
||||
|
||||
function pinnedProfileSelect(index:number) {
|
||||
pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${index > 1 ? index - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${index > 1 ? index - 1 : ''}`]);
|
||||
activeIndex.value = index;
|
||||
}
|
||||
|
||||
function done(query?: string): boolean | void {
|
||||
if (query == null) query = q.value;
|
||||
if (query == null || typeof query !== 'string') return;
|
||||
|
|
@ -660,8 +678,8 @@ defineExpose({
|
|||
|
||||
> header {
|
||||
/*position: sticky;
|
||||
top: 0;
|
||||
left: 0;*/
|
||||
top: 0;
|
||||
left: 0;*/
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
z-index: 2;
|
||||
|
|
@ -675,7 +693,8 @@ defineExpose({
|
|||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
line-height: 28px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
z-index: 1;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
|
|
@ -745,4 +764,24 @@ defineExpose({
|
|||
}
|
||||
}
|
||||
}
|
||||
.sllfktkhgl{
|
||||
display: inline-block;
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 1.5px 0 var(--divider);
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
transform: translateY(1.5px);
|
||||
}
|
||||
&.active {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ const props = withDefaults(defineProps<{
|
|||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const bg = ref<string>();
|
||||
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
|
||||
|
||||
watch(showBody, () => {
|
||||
if (props.persistKey) {
|
||||
miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f');
|
||||
|
|
|
|||
|
|
@ -1,56 +1,79 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License-Identifier: AGPL-3.0-only
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
|
||||
</template>
|
||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
|
||||
<!-- つまりリモートフォローの場合。 -->
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
<template v-else-if="isFollowing">
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && user.isLocked">
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && !user.isLocked">
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large },{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'
|
||||
,}]"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light',}]">{{
|
||||
i18n.ts.followRequestPending
|
||||
}}</span><i class="ti ti-hourglass-empty"></i>
|
||||
</template>
|
||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
|
||||
<!-- つまりリモートフォローの場合。 -->
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }] ">{{
|
||||
i18n.ts.processing
|
||||
}}</span>
|
||||
<MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
<template v-else-if="isFollowing">
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }] ">{{
|
||||
i18n.ts.unfollow
|
||||
}}</span><i class="ti ti-minus"></i>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && user.isLocked">
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]">{{
|
||||
i18n.ts.followRequest
|
||||
}}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && !user.isLocked">
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]">{{
|
||||
i18n.ts.follow
|
||||
}}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full"
|
||||
:class="[$style.text,{[$style.gamingDark]: gamingType === 'dark' ,[$style.gamingLight]: gamingType === 'light'} ]">{{
|
||||
i18n.ts.processing
|
||||
}}</span>
|
||||
<MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import {useStream} from '@/stream.js';
|
||||
import {i18n} from '@/i18n.js';
|
||||
import {claimAchievement} from '@/scripts/achievements.js';
|
||||
import {$i} from '@/account.js';
|
||||
import {defaultStore} from '@/store.js';
|
||||
|
||||
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
}>(), {
|
||||
full: false,
|
||||
large: false,
|
||||
full: false,
|
||||
large: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -63,36 +86,36 @@ const wait = ref(false);
|
|||
const connection = useStream().useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
})
|
||||
.then(onFollowChange);
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
})
|
||||
.then(onFollowChange);
|
||||
}
|
||||
|
||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||
if (user.id === props.user.id) {
|
||||
isFollowing.value = user.isFollowing;
|
||||
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
|
||||
}
|
||||
if (user.id === props.user.id) {
|
||||
isFollowing.value = user.isFollowing;
|
||||
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
|
||||
}
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
wait.value = true;
|
||||
wait.value = true;
|
||||
|
||||
try {
|
||||
if (isFollowing.value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
|
||||
});
|
||||
try {
|
||||
if (isFollowing.value) {
|
||||
const {canceled} = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.unfollowConfirm({name: props.user.name || props.user.username}),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
if (canceled) return;
|
||||
|
||||
await misskeyApi('following/delete', {
|
||||
userId: props.user.id,
|
||||
});
|
||||
} else {
|
||||
if (defaultStore.state.alwaysConfirmFollow) {
|
||||
await misskeyApi('following/delete', {
|
||||
userId: props.user.id,
|
||||
});
|
||||
} else {
|
||||
if (defaultStore.state.alwaysConfirmFollow) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
|
||||
|
|
@ -105,14 +128,14 @@ async function onClick() {
|
|||
}
|
||||
|
||||
if (hasPendingFollowRequestFromYou.value) {
|
||||
await misskeyApi('following/requests/cancel', {
|
||||
userId: props.user.id,
|
||||
});
|
||||
hasPendingFollowRequestFromYou.value = false;
|
||||
} else {
|
||||
await misskeyApi('following/create', {
|
||||
userId: props.user.id,
|
||||
withReplies: defaultStore.state.defaultWithReplies,
|
||||
await misskeyApi('following/requests/cancel', {
|
||||
userId: props.user.id,
|
||||
});
|
||||
hasPendingFollowRequestFromYou.value = false;
|
||||
} else {
|
||||
await misskeyApi('following/create', {
|
||||
userId: props.user.id,
|
||||
withReplies: defaultStore.state.defaultWithReplies,
|
||||
});
|
||||
emit('update:user', {
|
||||
...props.user,
|
||||
|
|
@ -120,113 +143,298 @@ async function onClick() {
|
|||
});
|
||||
hasPendingFollowRequestFromYou.value = true;
|
||||
|
||||
if ($i == null) return;
|
||||
if ($i == null) return;
|
||||
|
||||
claimAchievement('following1');
|
||||
|
||||
if ($i.followingCount >= 10) {
|
||||
claimAchievement('following10');
|
||||
}
|
||||
if ($i.followingCount >= 50) {
|
||||
claimAchievement('following50');
|
||||
}
|
||||
if ($i.followingCount >= 100) {
|
||||
claimAchievement('following100');
|
||||
}
|
||||
if ($i.followingCount >= 300) {
|
||||
claimAchievement('following300');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
wait.value = false;
|
||||
}
|
||||
if ($i.followingCount >= 10) {
|
||||
claimAchievement('following10');
|
||||
}
|
||||
if ($i.followingCount >= 50) {
|
||||
claimAchievement('following50');
|
||||
}
|
||||
if ($i.followingCount >= 100) {
|
||||
claimAchievement('following100');
|
||||
}
|
||||
if ($i.followingCount >= 300) {
|
||||
claimAchievement('following300');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
wait.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connection.on('follow', onFollowChange);
|
||||
connection.on('unfollow', onFollowChange);
|
||||
connection.on('follow', onFollowChange);
|
||||
connection.on('unfollow', onFollowChange);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
connection.dispose();
|
||||
connection.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--fgOnWhite);
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
height: 31px;
|
||||
font-size: 16px;
|
||||
border-radius: 32px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--fgOnWhite);
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
height: 31px;
|
||||
font-size: 16px;
|
||||
border-radius: 32px;
|
||||
background: #fff;
|
||||
|
||||
&.full {
|
||||
padding: 0 8px 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 16px;
|
||||
height: 38px;
|
||||
padding: 0 12px 0 16px;
|
||||
}
|
||||
&.gamingDark {
|
||||
color: black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
border: solid 1px black;
|
||||
}
|
||||
|
||||
&:not(.full) {
|
||||
width: 31px;
|
||||
}
|
||||
&.gamingLight {
|
||||
color: white !important;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
&.full {
|
||||
padding: 0 8px 0 12px;
|
||||
font-size: 14px;
|
||||
&.gamingDark {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
|
||||
&:hover {
|
||||
//background: mix($primary, #fff, 20);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
//background: mix($primary, #fff, 40);
|
||||
}
|
||||
&.gamingLight {
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
&.large {
|
||||
font-size: 16px;
|
||||
height: 38px;
|
||||
padding: 0 12px 0 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
border-color: var(--accentLighten);
|
||||
}
|
||||
&:not(.full) {
|
||||
width: 31px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accentDarken);
|
||||
border-color: var(--accentDarken);
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&.wait {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
&:hover {
|
||||
//background: mix($primary, #fff, 20);
|
||||
}
|
||||
|
||||
&:active {
|
||||
//background: mix($primary, #fff, 40);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
border-color: var(--accentLighten);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accentDarken);
|
||||
border-color: var(--accentDarken);
|
||||
}
|
||||
|
||||
&.gamingDark:hover {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingDark:active {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingLight:hover {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingLight:active {
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingDark {
|
||||
-webkit-text-fill-color: unset !important;
|
||||
color: black;
|
||||
border: solid 1px white;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
-webkit-text-fill-color: unset !important;
|
||||
color: white;
|
||||
border: solid 1px white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.wait {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-right: 6px;
|
||||
margin-right: 6px;
|
||||
|
||||
&.gamingDark {
|
||||
color: black;
|
||||
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,16 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div :class="$style.root" :style="bg">
|
||||
<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
|
||||
<img v-if="faviconUrl && !defaultStore.state.enableUltimateDataSaverMode" :class="$style.icon" :src="faviconUrl"/>
|
||||
<div :class="$style.name">{{ instance.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { instanceName } from '@/config.js';
|
||||
import { instance as Instance } from '@/instance.js';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||
import { instanceName } from '@/config';
|
||||
import { instance as Instance } from '@/instance';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
|
||||
import {defaultStore} from "@/store";
|
||||
|
||||
const props = defineProps<{
|
||||
instance?: {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||
<div class="main">
|
||||
<template v-for="item in items" :key="item.text">
|
||||
<button v-if="item.action" v-click-anime class="_button item" @click="$event => { item.action($event); close(); }">
|
||||
<button v-if="item.action" v-click-anime class="_button item" :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }" @click="$event => { item.action($event); close(); }">
|
||||
<i class="icon" :class="item.icon"></i>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
|
||||
<span v-if="item.indicate && item.indicateValue && indicatorCounterToggle" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
|
||||
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||
</button>
|
||||
<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
|
||||
<MkA v-else v-click-anime :to="item.to" class="item" :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }" @click.passive="close()">
|
||||
<i class="icon" :class="item.icon"></i>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
|
||||
<span v-if="item.indicate && item.indicateValue && indicatorCounterToggle" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
|
||||
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||
</MkA>
|
||||
</template>
|
||||
|
|
@ -26,12 +26,16 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref , computed , watch} from 'vue';
|
||||
import { shallowRef } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import { navbarItemDef } from '@/navbar.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
|
||||
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src?: HTMLElement;
|
||||
anchor?: { x: string; y: string; };
|
||||
|
|
@ -100,6 +104,20 @@ function close() {
|
|||
vertical-align: bottom;
|
||||
height: 100px;
|
||||
border-radius: 10px;
|
||||
&.gamingDark:hover{
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
color: black;
|
||||
}
|
||||
&.gamingLight:hover{
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
color: white;
|
||||
}
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
|
@ -148,4 +166,69 @@ function close() {
|
|||
}
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<template>
|
||||
<div
|
||||
|
||||
ref="playerEl"
|
||||
v-hotkey="keymap"
|
||||
tabindex="0"
|
||||
|
|
@ -28,9 +29,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
preload="metadata"
|
||||
controls
|
||||
:class="$style.nativeAudio"
|
||||
:src="audio.url"
|
||||
@keydown.prevent
|
||||
>
|
||||
<source :src="audio.url">
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
|
|
@ -38,8 +39,8 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<audio
|
||||
ref="audioEl"
|
||||
preload="metadata"
|
||||
:src="audio.url"
|
||||
>
|
||||
<source :src="audio.url">
|
||||
</audio>
|
||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||
<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
|
||||
|
|
@ -63,10 +64,24 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
:class="$style.volumeSeekbar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<WaveSurferPlayer
|
||||
v-if="!defaultStore.state.dataSaver.media && audioEl"
|
||||
:class="$style.seekbarRoot"
|
||||
:options="{ media: audioEl,
|
||||
height: 32,
|
||||
waveColor: 'gray',
|
||||
progressColor: accent,
|
||||
barGap: 3,
|
||||
barWidth: 3,
|
||||
barRadius: 5,
|
||||
duration: 80,
|
||||
}"
|
||||
></WaveSurferPlayer>
|
||||
<MkMediaRange
|
||||
v-if="defaultStore.state.dataSaver.media && !hide"
|
||||
v-model="rangePercent"
|
||||
:class="$style.seekbarRoot"
|
||||
:buffer="bufferedDataRatio"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -75,6 +90,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<script lang="ts" setup>
|
||||
import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { WaveSurferPlayer } from '@meersagor/wavesurfer-vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import type WaveSurfer from 'wavesurfer.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -83,7 +101,6 @@ import bytes from '@/filters/bytes.js';
|
|||
import { hms } from '@/filters/hms.js';
|
||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
|
||||
const props = defineProps<{
|
||||
audio: Misskey.entities.DriveFile;
|
||||
}>();
|
||||
|
|
@ -130,6 +147,7 @@ const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.data
|
|||
|
||||
// Menu
|
||||
const menuShowing = ref(false);
|
||||
const accent = ref();
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
let menu: MenuItem[] = [];
|
||||
|
|
@ -225,10 +243,7 @@ const volume = ref(.25);
|
|||
const speed = ref(1);
|
||||
const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える
|
||||
const bufferedEnd = ref(0);
|
||||
const bufferedDataRatio = computed(() => {
|
||||
if (!audioEl.value) return 0;
|
||||
return bufferedEnd.value / audioEl.value.duration;
|
||||
});
|
||||
let audioContext = new AudioContext();
|
||||
|
||||
// MediaControl Events
|
||||
function togglePlayPause() {
|
||||
|
|
@ -259,7 +274,8 @@ let stopAudioElWatch: () => void;
|
|||
function init() {
|
||||
if (onceInit) return;
|
||||
onceInit = true;
|
||||
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
accent.value = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
|
||||
stopAudioElWatch = watch(audioEl, () => {
|
||||
if (audioEl.value) {
|
||||
isReady.value = true;
|
||||
|
|
@ -324,7 +340,7 @@ watch(loop, (to) => {
|
|||
if (audioEl.value) audioEl.value.loop = to;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
init();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior">
|
||||
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe && gamingType === '' , [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior">
|
||||
<img :class="$style.icon" :src="avatarUrl" alt="">
|
||||
<span>
|
||||
<span>@{{ username }}</span>
|
||||
|
|
@ -14,7 +14,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { toUnicode } from 'punycode';
|
||||
import { computed } from 'vue';
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { host as localHost } from '@/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
|
@ -22,6 +22,8 @@ import { defaultStore } from '@/store.js';
|
|||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||
|
||||
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
host: string;
|
||||
|
|
@ -38,12 +40,13 @@ const isMe = $i && (
|
|||
|
||||
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
|
||||
bg.setAlpha(0.1);
|
||||
const bgCss = bg.toRgbString();
|
||||
|
||||
const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
|
||||
: `/avatar/@${props.username}@${props.host}`,
|
||||
);
|
||||
const bgCss = (gamingType.value === '') ? bg.toRgbString() : "";
|
||||
//const bgCss = `background:${bg.toRgbString()}; ${result}` ;
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
@ -53,8 +56,26 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
|
|||
border-radius: 999px;
|
||||
color: var(--mention);
|
||||
|
||||
&.gamingLight{
|
||||
color: white;
|
||||
opacity: 0.9;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
&.gamingDark{
|
||||
opacity: 0.9;
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%;
|
||||
-webkit-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&.isMe {
|
||||
color: var(--mentionMe);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,4 +91,69 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
|
|||
.host {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<div
|
||||
ref="itemsEl" v-hotkey="keymap"
|
||||
class="_popup _shadow"
|
||||
:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]"
|
||||
:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer },{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]"
|
||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||
@contextmenu.self="e => e.preventDefault()"
|
||||
>
|
||||
<template v-for="(item, i) in (items2 ?? [])">
|
||||
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
|
||||
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
|
||||
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]">
|
||||
<span style="opacity: 0.7;">{{ item.text }}</span>
|
||||
</span>
|
||||
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
|
||||
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]">
|
||||
<span><MkEllipsis/></span>
|
||||
</span>
|
||||
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="[$style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||
<div :class="$style.item_content">
|
||||
|
|
@ -27,22 +27,22 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</MkA>
|
||||
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="[$style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active },{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||
<div v-if="item.indicate" :class="$style.item_content">
|
||||
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } , { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" model-value/>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span>
|
||||
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||
|
|
@ -63,15 +63,15 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
|
||||
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item } , { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(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>
|
||||
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :disabled="getValue(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, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
|
|
@ -92,11 +92,13 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<script lang="ts">
|
||||
import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
import {defaultStore} from '@/store.js'
|
||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
|
||||
</script>
|
||||
|
|
@ -357,10 +359,32 @@ onBeforeUnmount(() => {
|
|||
&:not(:disabled):hover {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
}
|
||||
&.gamingDark{
|
||||
color:black !important;
|
||||
}
|
||||
&.gamingLight{
|
||||
color:white !important;
|
||||
}
|
||||
&.gamingDark:before{
|
||||
color:black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
|
||||
}
|
||||
&.gamingLight:before{
|
||||
color:white !important;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
|
||||
}
|
||||
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
|
@ -385,12 +409,32 @@ onBeforeUnmount(() => {
|
|||
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--fgOnAccent) !important;
|
||||
color: var(--fgOnAccent);
|
||||
opacity: 1;
|
||||
|
||||
&.gamingDark{
|
||||
color:black !important;
|
||||
}
|
||||
&.gamingLight{
|
||||
color:white !important;
|
||||
}
|
||||
&:before {
|
||||
background: var(--accent) !important;
|
||||
background: var(--accent);
|
||||
}
|
||||
&.gamingDark:before{
|
||||
color:black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&.gamingLight:before{
|
||||
color:white !important;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.radioActive {
|
||||
|
|
@ -432,9 +476,32 @@ onBeforeUnmount(() => {
|
|||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
}
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
}
|
||||
&.gamingDark{
|
||||
color:black !important;
|
||||
}
|
||||
&.gamingLight{
|
||||
color:white !important;
|
||||
}
|
||||
&.gamingDark:before{
|
||||
color:black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&.gamingLight:before{
|
||||
color:white !important;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -502,10 +569,11 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
|
||||
.indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 18px;
|
||||
color: var(--indicator);
|
||||
font-size: 12px;
|
||||
font-size: 8px;
|
||||
animation: global-blink 1s infinite;
|
||||
}
|
||||
|
||||
|
|
@ -541,4 +609,69 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
v-show="!isDeleted"
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
||||
:class="[$style.root,
|
||||
{ [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover } ,
|
||||
{[$style.home] : defaultStore.state.showVisibilityColor && note.visibility === 'home'
|
||||
,[$style.followers] : defaultStore.state.showVisibilityColor && note.visibility === 'followers'
|
||||
,[$style.specified] : defaultStore.state.showVisibilityColor && note.visibility === 'specified'
|
||||
},{[$style.localonly] : defaultStore.state.showVisibilityColor && note.localOnly }
|
||||
]"
|
||||
|
||||
:tabindex="!isDeleted ? '-1' : undefined"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
|
|
@ -31,7 +38,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||
<span
|
||||
v-if="note.visibility !== 'public'" style="margin-left: 0.5em;"
|
||||
:title="i18n.ts._visibility[note.visibility]"
|
||||
>
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
|
|
@ -118,8 +128,8 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReactions?.length >= 4 " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReactions?.length >= 3 || appearNote.myReaction && appearNote.user.host" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
|
|
@ -134,7 +144,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<div v-else-if="muted && !hideMutedNotes" :class="$style.muted" @click="muted = false">
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
|
|
@ -199,7 +209,7 @@ import { shouldCollapsed } from '@/scripts/collapsed.js';
|
|||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note & {myReactions: string[]};
|
||||
pinned?: boolean;
|
||||
mock?: boolean;
|
||||
withHardMute?: boolean;
|
||||
|
|
@ -217,7 +227,6 @@ const emit = defineEmits<{
|
|||
const inTimeline = inject<boolean>('inTimeline', false);
|
||||
const inChannel = inject('inChannel', null);
|
||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
const note = ref(deepClone(props.note));
|
||||
|
||||
// plugin
|
||||
|
|
@ -433,25 +442,9 @@ function react(viaKeyboard = false): void {
|
|||
}
|
||||
}
|
||||
|
||||
function undoReact(targetNote: Misskey.entities.Note): void {
|
||||
const oldReaction = targetNote.myReaction;
|
||||
if (!oldReaction) return;
|
||||
|
||||
if (props.mock) {
|
||||
emit('removeReaction', oldReaction);
|
||||
return;
|
||||
}
|
||||
|
||||
misskeyApi('notes/reactions/delete', {
|
||||
noteId: targetNote.id,
|
||||
});
|
||||
}
|
||||
|
||||
function toggleReact() {
|
||||
if (appearNote.value.myReaction == null) {
|
||||
if (appearNote.value.myReactions?.length < 4 || appearNote.value.myReaction && appearNote.value.user.host || !appearNote.value.myReactions ) {
|
||||
react();
|
||||
} else {
|
||||
undoReact(appearNote.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +470,14 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||
const { menu, cleanup } = getNoteMenu({
|
||||
note: note.value,
|
||||
translating,
|
||||
translation,
|
||||
|
||||
isDeleted,
|
||||
currentClip: currentClip?.value,
|
||||
});
|
||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||
}
|
||||
}
|
||||
|
|
@ -487,7 +487,14 @@ function showMenu(viaKeyboard = false): void {
|
|||
return;
|
||||
}
|
||||
|
||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||
const { menu, cleanup } = getNoteMenu({
|
||||
note: note.value,
|
||||
translating,
|
||||
translation,
|
||||
|
||||
isDeleted,
|
||||
currentClip: currentClip?.value,
|
||||
});
|
||||
os.popupMenu(menu, menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus).finally(cleanup);
|
||||
|
|
@ -498,7 +505,11 @@ async function clip() {
|
|||
return;
|
||||
}
|
||||
|
||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
os.popupMenu(await getNoteClipMenu({
|
||||
note: note.value,
|
||||
isDeleted,
|
||||
currentClip: currentClip?.value,
|
||||
}), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
function showRenoteMenu(viaKeyboard = false): void {
|
||||
|
|
@ -557,13 +568,6 @@ function focusAfter() {
|
|||
focusNext(rootEl.value ?? null);
|
||||
}
|
||||
|
||||
function readPromo() {
|
||||
misskeyApi('promo/read', {
|
||||
noteId: appearNote.value.id,
|
||||
});
|
||||
isDeleted.value = true;
|
||||
}
|
||||
|
||||
function emitUpdReaction(emoji: string, delta: number) {
|
||||
if (delta < 0) {
|
||||
emit('removeReaction', emoji);
|
||||
|
|
@ -581,6 +585,20 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
overflow: clip;
|
||||
contain: content;
|
||||
|
||||
&.home {
|
||||
background-color: rgba(var(--homeColor), 0.20) !important;
|
||||
}
|
||||
|
||||
&.followers {
|
||||
background-color: rgba(var(--followerColor), 0.20) !important;
|
||||
}
|
||||
|
||||
&.specified {
|
||||
background-color: rgba(var(--specifiedColor), 0.20) !important;
|
||||
}
|
||||
&.localonly {
|
||||
background-color: rgba(var(--localOnlyColor), 0.20) !important;
|
||||
}
|
||||
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
||||
// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
|
||||
// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
|
||||
|
|
@ -1024,4 +1042,8 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
opacity: .8;
|
||||
font-size: 95%;
|
||||
}
|
||||
|
||||
.root:has(.ti-home) {
|
||||
background-color: rgba(255, 255, 100, 0.10) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
v-if="appearNote.text"
|
||||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'respect'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
|
|
@ -87,7 +86,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else-if="translation">
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||
|
|
@ -103,6 +102,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</div>
|
||||
<footer>
|
||||
<div :class="$style.noteFooterInfo">
|
||||
<div v-if="appearNote.updatedAt">
|
||||
{{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
<MkA :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
|
|
@ -126,8 +128,8 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReactions?.length >= 4 " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReactions?.length >= 4 || appearNote.myReaction && appearNote.user.host " class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
|
|
@ -141,9 +143,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</footer>
|
||||
</article>
|
||||
<div :class="$style.tabs">
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' },{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' && tab === 'replies'}]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes'},{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' && tab === 'renotes'}]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions'},{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' && tab === 'reactions'}]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'history' },{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'}]" @click="tab = 'history'"><i class="ti ti-pencil"></i> {{ i18n.ts.edited }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="tab === 'replies'">
|
||||
|
|
@ -180,25 +183,46 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'history'" :class="$style.tab_history">
|
||||
<div style="display: grid;">
|
||||
<div v-for="(text, index) in appearNote.noteEditHistory" :key="text" :class="$style.historyRoot">
|
||||
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
|
||||
<div :class="$style.historyMain">
|
||||
<div :class="$style.historyHeader">
|
||||
<MkUserName :user="appearNote.user" :nowrap="true"/>
|
||||
<MkTime :class="$style.updatedAt" :time="appearNote.updatedAtHistory![index]"/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<Mfm :text="text.trim()" :author="appearNote.user" :i="$i"/>
|
||||
</div>
|
||||
<CodeDiff
|
||||
:oldString="appearNote.noteEditHistory[index - 1] || ''"
|
||||
:newString="text"
|
||||
:trim="true"
|
||||
:hideHeader="true"
|
||||
diffStyle="char"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.noteEditHistory == null" class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
|
||||
import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
|
|
@ -231,7 +255,12 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { infoImageUrl, instance, isEnabledUrlPreview } from '@/instance.js';
|
||||
import MkPostForm from '@/components/MkPostFormSimple.vue';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
|
||||
const MOBILE_THRESHOLD = 500;
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -244,6 +273,37 @@ const inChannel = inject('inChannel', null);
|
|||
|
||||
const note = ref(deepClone(props.note));
|
||||
|
||||
let gaming = ref('');
|
||||
|
||||
const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode'));
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
if (darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
|
||||
watch(darkMode, () => {
|
||||
if (darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(gamingMode, () => {
|
||||
if (darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
});
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
|
|
@ -441,10 +501,8 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
|||
}
|
||||
|
||||
function toggleReact() {
|
||||
if (appearNote.value.myReaction == null) {
|
||||
if (appearNote.value.myReactions?.length < 4 || appearNote.value.myReaction && appearNote.value.user.host || !appearNote.value.myReactions ) {
|
||||
react();
|
||||
} else {
|
||||
undoReact(appearNote.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -748,6 +806,9 @@ function loadConversation() {
|
|||
|
||||
.tabActive {
|
||||
border-bottom: solid 2px var(--accent);
|
||||
&.gamingLight{
|
||||
border-bottom: solid 2px black;
|
||||
}
|
||||
}
|
||||
|
||||
.tab_renotes {
|
||||
|
|
@ -758,6 +819,9 @@ function loadConversation() {
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
.tab_history {
|
||||
padding: 16px;
|
||||
}
|
||||
.reactionTabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
|
@ -773,6 +837,12 @@ function loadConversation() {
|
|||
|
||||
.reactionTabActive {
|
||||
border-color: var(--accent);
|
||||
&.gamingLight{
|
||||
border-bottom: solid 2px black;
|
||||
}
|
||||
&.gamingDark{
|
||||
border-bottom: solid 2px black;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
|
|
@ -813,12 +883,44 @@ function loadConversation() {
|
|||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.noteFooterButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.historyRoot {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
overflow: clip;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.noteFooterButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
.historyMain {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.historyHeader {
|
||||
display: flex;
|
||||
margin-bottom: 2px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.historyNote {
|
||||
padding-top: 10px;
|
||||
min-height: 75px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.updatedAt {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.muted {
|
||||
|
|
@ -826,4 +928,37 @@ function loadConversation() {
|
|||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<template>
|
||||
<header :class="$style.root">
|
||||
<div v-if="mock" :class="$style.name">
|
||||
<MkUserName :user="note.user"/>
|
||||
</div>
|
||||
<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
|
|
@ -16,12 +13,14 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
</div>
|
||||
<div :class="$style.info">
|
||||
<div v-if="mock">
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</div>
|
||||
<span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span>
|
||||
<div v-if="mock">
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</div>
|
||||
<MkTime v-else-if="note.isSchedule" mode="absolute" :time="note.createdAt" colored/>
|
||||
<MkA v-else :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</MkA>
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</MkA>
|
||||
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
|
|
@ -34,17 +33,16 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
import {inject} from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
|
||||
defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
const mock = inject<boolean>('mock', false);
|
||||
defineProps<{
|
||||
note: Misskey.entities.Note & {isSchedule? : boolean};
|
||||
scheduled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,21 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div v-show="!isDeleted" :class="$style.root" :tabindex="!isDeleted ? '-1' : undefined">
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
<Mfm v-if="note.cw != ''" :emojireq="emojireq" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
<MkSubNoteContent :class="$style.text" :note="note"/>
|
||||
<MkSubNoteContent :emojireq="emojireq" :class="$style.text" :note="note"/>
|
||||
</div>
|
||||
<div v-if="note.isSchedule" style="margin-top: 10px;">
|
||||
<MkButton :class="$style.button" inline @click="editScheduleNote()"><i class="ti ti-pencil"></i> {{ i18n.ts.deleteAndEdit }}</MkButton>
|
||||
<MkButton :class="$style.button" inline danger @click="deleteScheduleNote()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,14 +27,62 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '../i18n.js';
|
||||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
const isDeleted = ref(false);
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note & {
|
||||
id: string | null;
|
||||
isSchedule?: boolean;
|
||||
scheduledNoteId?: string;
|
||||
};
|
||||
emojireq:boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'editScheduleNote'): void;
|
||||
}>();
|
||||
|
||||
async function deleteScheduleNote() {
|
||||
if (!props.note.isSchedule || !props.note.scheduledNoteId) return;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._schedulePost.deleteAreYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('notes/schedule/delete', { scheduledNoteId: props.note.scheduledNoteId })
|
||||
.then(() => {
|
||||
isDeleted.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
async function editScheduleNote() {
|
||||
if (!props.note.isSchedule || !props.note.scheduledNoteId) return;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._schedulePost.deleteAndEditConfirm,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
await misskeyApi('notes/schedule/delete', { scheduledNoteId: props.note.scheduledNoteId })
|
||||
.then(() => {
|
||||
isDeleted.value = true;
|
||||
});
|
||||
|
||||
await os.post({ initialNote: props.note, renote: props.note.renote, reply: props.note.reply, channel: props.note.channel });
|
||||
|
||||
emit('editScheduleNote');
|
||||
}
|
||||
|
||||
const showContent = ref(false);
|
||||
</script>
|
||||
|
||||
|
|
@ -40,8 +92,12 @@ const showContent = ref(false);
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
}
|
||||
.button{
|
||||
margin-right: var(--margin);
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -27,15 +27,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="note.userId" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-else />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
:ad="true"
|
||||
:class="$style.notes"
|
||||
>
|
||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
<MkNote v-if="props.withCw && !note.cw || !props.withCw" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</MkDateSeparatedList>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -42,8 +42,8 @@ const props = defineProps<{
|
|||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
disableAutoLoad?: boolean;
|
||||
withCw?: boolean;
|
||||
}>();
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
defineExpose({
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.root">
|
||||
<div :class="$style.head">
|
||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'loginbonus'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||
|
|
@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$style.t_quote]: notification.type === 'quote',
|
||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||
[$style.t_achievementEarned]: notification.type === 'loginbonus',
|
||||
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||
}]"
|
||||
>
|
||||
|
|
@ -37,6 +38,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||
<i v-else-if="notification.type === 'loginbonus'" class="ti ti-medal"></i>
|
||||
|
||||
<template v-else-if="notification.type === 'roleAssigned'">
|
||||
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||
<i v-else class="ti ti-badges"></i>
|
||||
|
|
@ -56,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
||||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<span v-else-if="notification.type === 'loginbonus'">{{ i18n.ts._notification.loginbonus }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
|
|
@ -94,10 +98,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkA>
|
||||
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
|
||||
{{ notification.role.name }}
|
||||
</div> <div v-else-if="notification.type === 'loginbonus'" :class="$style.text">
|
||||
{{ notification.loginbonus }}プリズム入手しました!
|
||||
</div>
|
||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
||||
</MkA>
|
||||
|
||||
<template v-else-if="notification.type === 'follow'">
|
||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -94,13 +94,6 @@ onUnmounted(() => {
|
|||
if (connection) connection.dispose();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
366
packages/frontend/src/components/MkNotifyButton.vue
Normal file
366
packages/frontend/src/components/MkNotifyButton.vue
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
<template>
|
||||
<button
|
||||
v-if="isFollowing"
|
||||
class="_button" :class="[$style.root,{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'
|
||||
,}]"
|
||||
@click="onClick"
|
||||
>
|
||||
<span v-if="props.user.notify === 'none'" :class="[{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }] "><i class="ti ti-bell"></i></span>
|
||||
<span v-else-if="props.user.notify === 'normal'" :class="[{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]"><i class="ti ti-bell-off"></i></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
let gaming = ref('');
|
||||
|
||||
const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode'));
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
if (darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
|
||||
watch(darkMode, () => {
|
||||
if (darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(gamingMode, () => {
|
||||
if (darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'dark';
|
||||
} else if (!darkMode.value && gamingMode.value == true) {
|
||||
gaming.value = 'light';
|
||||
} else {
|
||||
gaming.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
}>(), {
|
||||
full: false,
|
||||
large: false,
|
||||
});
|
||||
|
||||
let isFollowing = ref(props.user.isFollowing);
|
||||
let notify = ref(props.user.notify);
|
||||
const connection = useStream().useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
}).then(onFollowChange);
|
||||
}
|
||||
|
||||
if (props.user.notify == null) {
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
}).then(onNotifyChange);
|
||||
}
|
||||
|
||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||
if (user.id === props.user.id) {
|
||||
isFollowing.value = user.isFollowing;
|
||||
}
|
||||
}
|
||||
|
||||
function onNotifyChange(user: Misskey.entities.UserDetailed) {
|
||||
if (user.id === props.user.id) {
|
||||
notify.value = user.notify;
|
||||
console.log(props.user.notify);
|
||||
}
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
try {
|
||||
await os.apiWithDialog('following/update', {
|
||||
userId: props.user.id,
|
||||
notify: props.user.notify === 'normal' ? 'none' : 'normal',
|
||||
}).then(() => {
|
||||
props.user.notify = props.user.notify === 'normal' ? 'none' : 'normal';
|
||||
});
|
||||
} finally {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connection.on('follow', onFollowChange);
|
||||
connection.on('unfollow', onFollowChange);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
connection.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--fgOnWhite);
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
height: 31px;
|
||||
font-size: 16px;
|
||||
border-radius: 32px;
|
||||
background: #fff;
|
||||
vertical-align: bottom;
|
||||
|
||||
&.gamingDark {
|
||||
color: black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
border: solid 1px black;
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
color: white !important;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.full {
|
||||
padding: 0 8px 0 12px;
|
||||
font-size: 14px;
|
||||
&.gamingDark {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 16px;
|
||||
height: 38px;
|
||||
padding: 0 12px 0 16px;
|
||||
}
|
||||
|
||||
&:not(.full) {
|
||||
width: 31px;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//background: mix($primary, #fff, 20);
|
||||
}
|
||||
|
||||
&:active {
|
||||
//background: mix($primary, #fff, 40);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
border-color: var(--accentLighten);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accentDarken);
|
||||
border-color: var(--accentDarken);
|
||||
}
|
||||
|
||||
&.gamingDark:hover {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingDark:active {
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingLight:hover {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingLight:active {
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
border: solid 1px white;
|
||||
}
|
||||
|
||||
&.gamingDark {
|
||||
-webkit-text-fill-color: unset !important;
|
||||
color: black;
|
||||
border: solid 1px white;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
-webkit-text-fill-color: unset !important;
|
||||
color: white;
|
||||
border: solid 1px white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.gamingDark {
|
||||
color: black;
|
||||
|
||||
}
|
||||
|
||||
.gamingLight {
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -212,7 +212,6 @@ async function init(): Promise<void> {
|
|||
const item = res[i];
|
||||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
if (res.length === 0 || props.pagination.noPaging) {
|
||||
concatItems(res);
|
||||
more.value = false;
|
||||
|
|
@ -221,7 +220,6 @@ async function init(): Promise<void> {
|
|||
concatItems(res);
|
||||
more.value = true;
|
||||
}
|
||||
|
||||
offset.value = res.length;
|
||||
error.value = false;
|
||||
fetching.value = false;
|
||||
|
|
@ -235,69 +233,43 @@ const reload = (): Promise<void> => {
|
|||
return init();
|
||||
};
|
||||
|
||||
const fetchMore = async (): Promise<void> => {
|
||||
async function fetchMore(): Promise<void> {
|
||||
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
||||
|
||||
moreFetching.value = true;
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
||||
...params,
|
||||
limit: SECOND_FETCH_LIMIT,
|
||||
...(props.pagination.offsetMode ? {
|
||||
offset: offset.value,
|
||||
} : {
|
||||
untilId: Array.from(items.value.keys()).at(-1),
|
||||
}),
|
||||
}).then(res => {
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i];
|
||||
if (i === 10) item._shouldInsertAd_ = true;
|
||||
}
|
||||
try {
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
const response = await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
||||
...params,
|
||||
limit: SECOND_FETCH_LIMIT,
|
||||
...(props.pagination.offsetMode ? { offset: offset.value } : { untilId: Array.from(items.value.keys()).pop() }),
|
||||
});
|
||||
|
||||
const reverseConcat = _res => {
|
||||
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
|
||||
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
|
||||
const isReversed = props.pagination.reversed;
|
||||
if (isReversed) {
|
||||
const oldHeight = scrollableElement.value?.scrollHeight || 0;
|
||||
const oldScroll = scrollableElement.value?.scrollTop || 0;
|
||||
|
||||
items.value = concatMapWithArray(items.value, _res);
|
||||
items.value = concatMapWithArray(items.value, response);
|
||||
|
||||
return nextTick(() => {
|
||||
if (scrollableElement.value) {
|
||||
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
} else {
|
||||
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
});
|
||||
};
|
||||
|
||||
if (res.length === 0) {
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = false;
|
||||
moreFetching.value = false;
|
||||
await nextTick();
|
||||
if (scrollableElement.value) {
|
||||
scroll(scrollableElement.value, {
|
||||
top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight),
|
||||
behavior: 'instant',
|
||||
});
|
||||
} else {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = false;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
} else {
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = true;
|
||||
moreFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = true;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
items.value = concatMapWithArray(items.value, response);
|
||||
}
|
||||
offset.value += res.length;
|
||||
}, err => {
|
||||
|
||||
more.value = response.length > 0;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
moreFetching.value = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fetchMoreAhead = async (): Promise<void> => {
|
||||
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<MkButton v-if="choices.length < 10" class="add" @click="add">{{ i18n.ts.add }}</MkButton>
|
||||
<MkButton v-else class="add" disabled>{{ i18n.ts._poll.noMore }}</MkButton>
|
||||
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
||||
<section>
|
||||
<section style="margin-bottom: 8px; border-top: solid 1.5px var(--divider);">
|
||||
<div>
|
||||
<MkSelect v-model="expiration" small>
|
||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||
|
|
@ -146,8 +146,10 @@ watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.zmdxowus {
|
||||
padding: 8px 16px;
|
||||
|
||||
margin: 4px 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
border: solid 1.5px var(--divider);
|
||||
> .caution {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 0.8em;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -31,6 +31,7 @@ const props = withDefaults(defineProps<{
|
|||
instant?: boolean;
|
||||
fixed?: boolean;
|
||||
autofocus?: boolean;
|
||||
updateMode?: boolean;
|
||||
}>(), {
|
||||
initialLocalOnly: undefined,
|
||||
});
|
||||
|
|
@ -49,6 +50,7 @@ function onPosted() {
|
|||
}
|
||||
|
||||
function onModalClosed() {
|
||||
form.value?.closed();
|
||||
emit('closed');
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
155
packages/frontend/src/components/MkPostFormDrafts.vue
Normal file
155
packages/frontend/src/components/MkPostFormDrafts.vue
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="500"
|
||||
:height="600"
|
||||
@close="dialog?.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.drafts }}</template>
|
||||
|
||||
<div :class="$style.container">
|
||||
<div v-if="notes === null" :class="$style.center">{{ i18n.ts.loading }}</div>
|
||||
<div v-else-if="Object.keys(notes).length === 0" :class="$style.center">{{ i18n.ts.nothing }}</div>
|
||||
<div v-for="(note, key) of notes" v-else :key="key" class="_panel" :class="$style.wrapper" :aria-disabled="!noteFilter(note)">
|
||||
<div v-if="note" :class="$style.note" @click="() => select(note)">
|
||||
<div v-if="note.type === 'quote'" :class="$style.subtext"><i class="ti ti-quote"></i> {{ i18n.ts.quote }}</div>
|
||||
<div v-if="note.type === 'reply'" :class="$style.subtext"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.reply }}</div>
|
||||
<div v-if="note.type === 'channel'" :class="$style.subtext"><i class="ti ti-device-tv"></i> {{ i18n.ts.channel }}</div>
|
||||
<Mfm v-if="note.data.text" :text="note.data.text" :nyaize="'respect'"/>
|
||||
<div :class="[$style.subtext, $style.bottom]">
|
||||
<MkTime :time="note.updatedAt"/>
|
||||
<div v-if="note.data.files.length"><i class="ti ti-photo-plus" :class="$style.icon"></i>{{ note.data.files.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.trash" @click="() => remove(note)"><i class="ti ti-trash"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, ref, onMounted } from 'vue';
|
||||
import * as noteDrafts from '@/scripts/note-drafts.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
const props = defineProps<{
|
||||
channelId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'selected', res: noteDrafts.NoteDraft): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const notes = ref<Record<string, noteDrafts.NoteDraft | undefined> | null>(null);
|
||||
|
||||
function noteFilter(note: noteDrafts.NoteDraft | undefined) {
|
||||
if (!note) return false;
|
||||
|
||||
// チャンネルモードの場合はチャンネル内での下書きのみを表示
|
||||
if (props.channelId) return note.type === 'channel' && note.auxId === props.channelId;
|
||||
|
||||
// チャンネル外ならチャンネル内の下書きは表示しない
|
||||
if (note.type === 'channel') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function select(note: noteDrafts.NoteDraft) {
|
||||
if (!noteFilter(note)) return;
|
||||
emit('selected', note);
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function remove(note: noteDrafts.NoteDraft | undefined) {
|
||||
if (!note) return;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
await noteDrafts.remove(note.type, $i.id, note.uniqueId, note.auxId as string);
|
||||
notes.value = await noteDrafts.getAll($i.id);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
notes.value = await noteDrafts.getAll($i.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
overflow-x: clip;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
border-radius: 12px;
|
||||
background-color: var(--buttonBg);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not([aria-disabled="true"]) {
|
||||
background-color: var(--buttonHoverBg);
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
gap: 6px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trash {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
color: var(--error);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--error);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<template>
|
||||
<div
|
||||
v-adaptive-border
|
||||
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
||||
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked ,[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' } ]"
|
||||
:aria-checked="checked"
|
||||
:aria-disabled="disabled"
|
||||
@click="toggle"
|
||||
|
|
@ -15,7 +15,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
:disabled="disabled"
|
||||
:class="$style.input"
|
||||
>
|
||||
<span :class="$style.button">
|
||||
<span :class="[$style.button , {[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">
|
||||
<span></span>
|
||||
</span>
|
||||
<span :class="$style.label"><slot></slot></span>
|
||||
|
|
@ -23,7 +23,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { ref,computed,watch } from 'vue';
|
||||
import {defaultStore} from "@/store.js";
|
||||
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
|
|
@ -73,14 +76,48 @@ function toggle(): void {
|
|||
border-color: var(--accentedBg) !important;
|
||||
color: var(--accent);
|
||||
cursor: default !important;
|
||||
|
||||
&.gamingDark{
|
||||
color:black !important;
|
||||
border-color: black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite;
|
||||
}
|
||||
&.gamingLight{
|
||||
color:white;
|
||||
border-color: white !important;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important;
|
||||
}
|
||||
> .button {
|
||||
border-color: var(--accent);
|
||||
|
||||
&.gamingDark{
|
||||
border-color:black;
|
||||
color:black !important;
|
||||
}
|
||||
&.gamingLight{
|
||||
border-color: white;
|
||||
color:white;
|
||||
}
|
||||
&.gamingDark:after{
|
||||
background-color: black;
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
&.gamingLight:after{
|
||||
background-color:white !important;
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
&:after {
|
||||
background-color: var(--accent);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,4 +161,69 @@ function toggle(): void {
|
|||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<div v-adaptive-border class="body">
|
||||
<div ref="containerEl" class="container">
|
||||
<div class="track">
|
||||
<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
|
||||
<div :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light'}" class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div v-if="steps && showTicks" class="ticks">
|
||||
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
|
||||
<div ref="thumbEl" v-tooltip="textConverter(finalValue)" :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light'}" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
|
|
@ -23,6 +23,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch, shallowRef } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import {defaultStore} from "@/store.js";
|
||||
|
||||
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: number;
|
||||
|
|
@ -213,6 +217,20 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
|||
height: 100%;
|
||||
background: var(--accent);
|
||||
opacity: 0.5;
|
||||
&.gamingLight{
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
&.gamingDark{
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,9 +263,36 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
|||
cursor: grab;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
|
||||
&.gamingDark{
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
&.gamingLight{
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
&.gamingDark{
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
&.gamingLight{
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -269,4 +314,69 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
ref="buttonEl"
|
||||
v-ripple="canToggle"
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
||||
:class="[$style.root, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' ,[$style.reacted]: note.myReactions?.includes(reaction) , [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
||||
@click="toggleReaction()"
|
||||
@contextmenu.prevent.stop="menu"
|
||||
>
|
||||
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||
<span :class="$style.count">{{ count }}</span>
|
||||
<span :class="[$style.count,{ [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, shallowRef, watch } from 'vue';
|
||||
import { computed, inject, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
|
|
@ -35,11 +35,15 @@ import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.j
|
|||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import { getUnicodeEmoji } from '@/scripts/emojilist.js';
|
||||
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
count: number;
|
||||
isInitial: boolean;
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note & {
|
||||
myReactions: string[];
|
||||
}
|
||||
}>();
|
||||
|
||||
const mock = inject<boolean>('mock', false);
|
||||
|
|
@ -61,14 +65,14 @@ const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction
|
|||
async function toggleReaction() {
|
||||
if (!canToggle.value) return;
|
||||
|
||||
const oldReaction = props.note.myReaction;
|
||||
const oldReaction = props.note.myReactions?.includes(props.reaction) ? props.reaction : null;
|
||||
if (oldReaction) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
|
||||
props.note.myReactions.splice(props.note.myReactions.indexOf(oldReaction), 1);
|
||||
if (oldReaction !== props.reaction) {
|
||||
sound.playMisskeySfx('reaction');
|
||||
}
|
||||
|
|
@ -80,8 +84,9 @@ async function toggleReaction() {
|
|||
|
||||
misskeyApi('notes/reactions/delete', {
|
||||
noteId: props.note.id,
|
||||
reaction: oldReaction,
|
||||
}).then(() => {
|
||||
if (oldReaction !== props.reaction) {
|
||||
if (oldReaction !== props.reaction ) {
|
||||
misskeyApi('notes/reactions/create', {
|
||||
noteId: props.note.id,
|
||||
reaction: props.reaction,
|
||||
|
|
@ -211,8 +216,32 @@ if (!mock) {
|
|||
color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent) inset;
|
||||
|
||||
&.gamingDark{
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
box-shadow: 0 0 0px 1px white inset;
|
||||
}
|
||||
|
||||
&.gamingLight{
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
box-shadow: 0 0 0px 1px white inset;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
> .count {
|
||||
color: var(--accent);
|
||||
&.gamingLight{
|
||||
color: white;
|
||||
}
|
||||
&.gamingDark{
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
> .icon {
|
||||
|
|
@ -231,4 +260,69 @@ if (!mock) {
|
|||
line-height: 42px;
|
||||
margin: 0 0 0 4px;
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
|||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note & {
|
||||
myReactions: string[];
|
||||
}
|
||||
maxNumber?: number;
|
||||
}>(), {
|
||||
maxNumber: Infinity,
|
||||
|
|
@ -56,7 +58,6 @@ function onMockToggleReaction(emoji: string, count: number) {
|
|||
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
||||
let newReactions: [string, number][] = [];
|
||||
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
||||
|
||||
for (let i = 0; i < reactions.value.length; i++) {
|
||||
const reaction = reactions.value[i][0];
|
||||
if (reaction in newSource && newSource[reaction] !== 0) {
|
||||
|
|
@ -75,7 +76,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
|||
|
||||
newReactions = newReactions.slice(0, props.maxNumber);
|
||||
|
||||
if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
|
||||
if (props.note. myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
|
||||
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
|
||||
}
|
||||
|
||||
|
|
|
|||
39
packages/frontend/src/components/MkRemoteInfoUpdate.vue
Normal file
39
packages/frontend/src/components/MkRemoteInfoUpdate.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<a :class="$style.root" @click="UserInfoUpdate"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserInfoUpdate }}</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
const props = withDefaults(defineProps<{
|
||||
UserId: string;
|
||||
}>(), {
|
||||
UserId: null,
|
||||
});
|
||||
|
||||
function UserInfoUpdate() {
|
||||
misskeyApi('federation/update-remote-user', { userId: props.UserId });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
font-size: 0.8em;
|
||||
padding: 16px;
|
||||
background: var(--infoWarnBg);
|
||||
color: var(--infoWarnFg);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-left: 4px;
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
61
packages/frontend/src/components/MkScheduleEditor.vue
Normal file
61
packages/frontend/src/components/MkScheduleEditor.vue
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div style="padding: 8px 16px">
|
||||
<section class="_gaps_s">
|
||||
<MkInput v-model="atDate" small type="date" class="input">
|
||||
<template #label>{{ i18n.ts._schedulePost.postDate }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="atTime" small type="time" class="input">
|
||||
<template #label>{{ i18n.ts._schedulePost.postTime }}</template>
|
||||
<template #caption>{{ i18n.ts._schedulePost.localTime }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||
import { addTime } from '@/scripts/time.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: {
|
||||
scheduledAt: string;
|
||||
};
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: {
|
||||
scheduledAt: string;
|
||||
}): void;
|
||||
}>();
|
||||
|
||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||
const atTime = ref('00:00');
|
||||
if ( props.modelValue.scheduledAt) {
|
||||
const date = new Date(props.modelValue.scheduledAt);
|
||||
atDate.value = formatDateTimeString(date, 'yyyy-MM-dd');
|
||||
atTime.value = formatDateTimeString(date, 'HH:mm');
|
||||
}
|
||||
|
||||
function get() {
|
||||
const calcAt = () => {
|
||||
return new Date(`${atDate.value}T${atTime.value}`).toISOString();
|
||||
};
|
||||
|
||||
return {
|
||||
...(
|
||||
props.modelValue ? { scheduledAt: calcAt() } : ''
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
watch([atDate, atTime], () => emit('update:modelValue', get()), {
|
||||
immediate: true,
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialogEl"
|
||||
:withOkButton="false"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
>
|
||||
<template #header>{{ i18n.ts._schedulePost.list }}</template>
|
||||
<MkSpacer :marginMin="14" :marginMax="16">
|
||||
<MkPagination ref="paginationEl" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps">
|
||||
<MkNoteSimple v-for="item in items" :key="item.id" :scheduled="true" :note="item.note" @editScheduleNote="listUpdate"/>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'cancel'): void;
|
||||
}>();
|
||||
|
||||
const dialogEl = ref();
|
||||
const cancel = () => {
|
||||
emit('cancel');
|
||||
dialogEl.value.close();
|
||||
};
|
||||
const paginationEl = ref();
|
||||
const pagination: Paging = {
|
||||
endpoint: 'notes/schedule/list',
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
function listUpdate() {
|
||||
paginationEl.value.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
</style>
|
||||
|
|
@ -181,7 +181,7 @@ function show() {
|
|||
<style lang="scss" module>
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
padding: 8px 0;
|
||||
user-select: none;
|
||||
|
||||
&:empty {
|
||||
|
|
|
|||
|
|
@ -3,102 +3,141 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div :class="$style.banner">
|
||||
<i class="ti ti-user-edit"></i>
|
||||
</div>
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
<template #caption>
|
||||
<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
|
||||
<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
|
||||
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
|
||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
|
||||
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
|
||||
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
|
||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
|
||||
<template v-if="submitting">
|
||||
<MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
<template v-else>{{ i18n.ts.start }}</template>
|
||||
</MkButton>
|
||||
</form>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="[$style.banner ,{[$style.gamingDark]: gamingType ==='dark' , [$style.gamingLight]: gamingType ==='light'}]">
|
||||
<i class="ti ti-user-edit"></i>
|
||||
</div>
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false"
|
||||
autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||
<template #label>{{ i18n.ts.username }}
|
||||
<div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div>
|
||||
</template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
<template #caption>
|
||||
<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
|
||||
<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{
|
||||
i18n.ts.checking
|
||||
}}</span>
|
||||
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i
|
||||
class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
|
||||
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
|
||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email"
|
||||
:spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||
<template #label>{{ i18n.ts.emailAddress }}
|
||||
<div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i
|
||||
class="ti ti-help-circle"></i></div>
|
||||
</template>
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i
|
||||
class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
|
||||
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password
|
||||
@update:modelValue="onChangePassword">
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
|
||||
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i
|
||||
class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i
|
||||
class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required
|
||||
data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{
|
||||
i18n.ts.passwordMatched
|
||||
}}</span>
|
||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i
|
||||
class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha"
|
||||
provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha"
|
||||
provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha"
|
||||
provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit
|
||||
style="margin: 0 auto;">
|
||||
<template v-if="submitting">
|
||||
<MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
<template v-else>{{ i18n.ts.start }}</template>
|
||||
</MkButton>
|
||||
</form>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||
import MkCaptcha, {type Captcha} from '@/components/MkCaptcha.vue';
|
||||
import * as config from '@/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { login } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import {login} from '@/account.js';
|
||||
import {instance} from '@/instance.js';
|
||||
import {i18n} from '@/i18n.js';
|
||||
import {defaultStore} from "@/store.js";
|
||||
|
||||
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
autoSet?: boolean;
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
autoSet: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
|
||||
(ev: 'signupEmailPending'): void;
|
||||
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
|
||||
(ev: 'signupEmailPending'): void;
|
||||
}>();
|
||||
|
||||
const host = toUnicode(config.host);
|
||||
|
|
@ -125,182 +164,272 @@ const usernameAbortController = ref<null | AbortController>(null);
|
|||
const emailAbortController = ref<null | AbortController>(null);
|
||||
|
||||
const shouldDisableSubmitting = computed((): boolean => {
|
||||
return submitting.value ||
|
||||
instance.enableHcaptcha && !hCaptchaResponse.value ||
|
||||
instance.enableMcaptcha && !mCaptchaResponse.value ||
|
||||
instance.enableRecaptcha && !reCaptchaResponse.value ||
|
||||
instance.enableTurnstile && !turnstileResponse.value ||
|
||||
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
|
||||
usernameState.value !== 'ok' ||
|
||||
passwordRetypeState.value !== 'match';
|
||||
return submitting.value ||
|
||||
instance.enableHcaptcha && !hCaptchaResponse.value ||
|
||||
instance.enableMcaptcha && !mCaptchaResponse.value ||instance.enableRecaptcha && !reCaptchaResponse.value ||
|
||||
instance.enableTurnstile && !turnstileResponse.value ||
|
||||
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
|
||||
usernameState.value !== 'ok' ||
|
||||
passwordRetypeState.value !== 'match';
|
||||
});
|
||||
|
||||
function getPasswordStrength(source: string): number {
|
||||
let strength = 0;
|
||||
let power = 0.018;
|
||||
let strength = 0;
|
||||
let power = 0.018;
|
||||
|
||||
// 英数字
|
||||
if (/[a-zA-Z]/.test(source) && /[0-9]/.test(source)) {
|
||||
power += 0.020;
|
||||
}
|
||||
// 英数字
|
||||
if (/[a-zA-Z]/.test(source) && /[0-9]/.test(source)) {
|
||||
power += 0.020;
|
||||
}
|
||||
|
||||
// 大文字と小文字が混ざってたら
|
||||
if (/[a-z]/.test(source) && /[A-Z]/.test(source)) {
|
||||
power += 0.015;
|
||||
}
|
||||
// 大文字と小文字が混ざってたら
|
||||
if (/[a-z]/.test(source) && /[A-Z]/.test(source)) {
|
||||
power += 0.015;
|
||||
}
|
||||
|
||||
// 記号が混ざってたら
|
||||
if (/[!\x22\#$%&@'()*+,-./_]/.test(source)) {
|
||||
power += 0.02;
|
||||
}
|
||||
// 記号が混ざってたら
|
||||
if (/[!\x22\#$%&@'()*+,-./_]/.test(source)) {
|
||||
power += 0.02;
|
||||
}
|
||||
|
||||
strength = power * source.length;
|
||||
strength = power * source.length;
|
||||
|
||||
return Math.max(0, Math.min(1, strength));
|
||||
return Math.max(0, Math.min(1, strength));
|
||||
}
|
||||
|
||||
function onChangeUsername(): void {
|
||||
if (username.value === '') {
|
||||
usernameState.value = null;
|
||||
return;
|
||||
}
|
||||
if (username.value === '') {
|
||||
usernameState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const err =
|
||||
!username.value.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
|
||||
username.value.length < 1 ? 'min-range' :
|
||||
username.value.length > 20 ? 'max-range' :
|
||||
null;
|
||||
{
|
||||
const err =
|
||||
!username.value.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
|
||||
username.value.length < 1 ? 'min-range' :
|
||||
username.value.length > 20 ? 'max-range' :
|
||||
null;
|
||||
|
||||
if (err) {
|
||||
usernameState.value = err;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (err) {
|
||||
usernameState.value = err;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (usernameAbortController.value != null) {
|
||||
usernameAbortController.value.abort();
|
||||
}
|
||||
usernameState.value = 'wait';
|
||||
usernameAbortController.value = new AbortController();
|
||||
if (usernameAbortController.value != null) {
|
||||
usernameAbortController.value.abort();
|
||||
}
|
||||
usernameState.value = 'wait';
|
||||
usernameAbortController.value = new AbortController();
|
||||
|
||||
misskeyApi('username/available', {
|
||||
username: username.value,
|
||||
}, undefined, usernameAbortController.value.signal).then(result => {
|
||||
usernameState.value = result.available ? 'ok' : 'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
usernameState.value = 'error';
|
||||
}
|
||||
});
|
||||
misskeyApi('username/available', {
|
||||
username: username.value,
|
||||
}, undefined, usernameAbortController.value.signal).then(result => {
|
||||
usernameState.value = result.available ? 'ok' : 'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
usernameState.value = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeEmail(): void {
|
||||
if (email.value === '') {
|
||||
emailState.value = null;
|
||||
return;
|
||||
}
|
||||
if (email.value === '') {
|
||||
emailState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailAbortController.value != null) {
|
||||
emailAbortController.value.abort();
|
||||
}
|
||||
emailState.value = 'wait';
|
||||
emailAbortController.value = new AbortController();
|
||||
if (emailAbortController.value != null) {
|
||||
emailAbortController.value.abort();
|
||||
}
|
||||
emailState.value = 'wait';
|
||||
emailAbortController.value = new AbortController();
|
||||
|
||||
misskeyApi('email-address/available', {
|
||||
emailAddress: email.value,
|
||||
}, undefined, emailAbortController.value.signal).then(result => {
|
||||
emailState.value = result.available ? 'ok' :
|
||||
result.reason === 'used' ? 'unavailable:used' :
|
||||
result.reason === 'format' ? 'unavailable:format' :
|
||||
result.reason === 'disposable' ? 'unavailable:disposable' :
|
||||
result.reason === 'banned' ? 'unavailable:banned' :
|
||||
misskeyApi('email-address/available', {
|
||||
emailAddress: email.value,
|
||||
}, undefined, emailAbortController.value.signal).then(result => {
|
||||
emailState.value = result.available ? 'ok' :
|
||||
result.reason === 'used' ? 'unavailable:used' :
|
||||
result.reason === 'format' ? 'unavailable:format' :
|
||||
result.reason === 'disposable' ? 'unavailable:disposable' :
|
||||
result.reason === 'banned' ? 'unavailable:banned' :
|
||||
result.reason === 'mx' ? 'unavailable:mx' :
|
||||
result.reason === 'smtp' ? 'unavailable:smtp' :
|
||||
'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
emailState.value = 'error';
|
||||
}
|
||||
});
|
||||
result.reason === 'smtp' ? 'unavailable:smtp' :
|
||||
'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
emailState.value = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onChangePassword(): void {
|
||||
if (password.value === '') {
|
||||
passwordStrength.value = '';
|
||||
return;
|
||||
}
|
||||
if (password.value === '') {
|
||||
passwordStrength.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const strength = getPasswordStrength(password.value);
|
||||
passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
const strength = getPasswordStrength(password.value);
|
||||
passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
}
|
||||
|
||||
function onChangePasswordRetype(): void {
|
||||
if (retypedPassword.value === '') {
|
||||
passwordRetypeState.value = null;
|
||||
return;
|
||||
}
|
||||
if (retypedPassword.value === '') {
|
||||
passwordRetypeState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match';
|
||||
passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match';
|
||||
}
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
await misskeyApi('signup', {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
emailAddress: email.value,
|
||||
invitationCode: invitationCode.value,
|
||||
'hcaptcha-response': hCaptchaResponse.value,
|
||||
'm-captcha-response': mCaptchaResponse.value,
|
||||
try {
|
||||
await misskeyApi('signup', {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
emailAddress: email.value,
|
||||
invitationCode: invitationCode.value,
|
||||
'hcaptcha-response': hCaptchaResponse.value,
|
||||
'm-captcha-response': mCaptchaResponse.value,
|
||||
'g-recaptcha-response': reCaptchaResponse.value,
|
||||
'turnstile-response': turnstileResponse.value,
|
||||
});
|
||||
if (instance.emailRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else {
|
||||
const res = await misskeyApi('signin', {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
emit('signup', res);
|
||||
'turnstile-response': turnstileResponse.value,
|
||||
});
|
||||
if (instance.emailRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.tsx._signup.emailSent({email: email.value}),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else {
|
||||
const res = await misskeyApi('signin', {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
emit('signup', res);
|
||||
|
||||
if (props.autoSet) {
|
||||
return login(res.i);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
submitting.value = false;
|
||||
hcaptcha.value?.reset?.();
|
||||
recaptcha.value?.reset?.();
|
||||
turnstile.value?.reset?.();
|
||||
if (props.autoSet) {
|
||||
return login(res.i);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
submitting.value = false;
|
||||
hcaptcha.value?.reset?.();
|
||||
recaptcha.value?.reset?.();
|
||||
turnstile.value?.reset?.();
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
|
||||
&.gamingDark {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
color: var(--navFg);
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
color: var(--navFg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.captcha {
|
||||
margin: 16px 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div :class="$style.banner">
|
||||
<div :class="[$style.banner ,{[$style.gamingDark]: gamingType ==='dark' , [$style.gamingLight]: gamingType ==='light'}]">
|
||||
<i class="ti ti-checklist"></i>
|
||||
</div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
|
|
@ -69,6 +69,8 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import {defaultStore} from "@/store.js";
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const availableServerRules = instance.serverRules.length > 0;
|
||||
const availableTos = instance.tosUrl != null && instance.tosUrl !== '';
|
||||
|
|
@ -146,11 +148,30 @@ async function updateAgreeNote(v: boolean) {
|
|||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
|
||||
&.gamingDark {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
color: var(--navFg);
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
color: var(--navFg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.rules {
|
||||
|
|
@ -188,4 +209,70 @@ async function updateAgreeNote(v: boolean) {
|
|||
.ruleText {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
|
||||
<Mfm v-if="note.text" :emojireq="emojireq" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files && note.files.length > 0">
|
||||
|
|
@ -39,6 +40,7 @@ import { shouldCollapsed } from '@/scripts/collapsed.js';
|
|||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
emojireq: boolean;
|
||||
}>();
|
||||
|
||||
const isLong = shouldCollapsed(props.note, []);
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<div class="items">
|
||||
<template v-for="(item, i) in group.items">
|
||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active, gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||
<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active , gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</button>
|
||||
<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active , gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</MkA>
|
||||
|
|
@ -28,7 +28,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import {ref , computed , watch } from 'vue';
|
||||
import {defaultStore} from "@/store.js";
|
||||
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
defineProps<{
|
||||
def: any[];
|
||||
|
|
@ -69,6 +72,21 @@ defineProps<{
|
|||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--accentedBg);
|
||||
&.gamingDark{
|
||||
color: black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
&.gamingLight{
|
||||
color: white;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
|
@ -153,4 +171,70 @@ defineProps<{
|
|||
}
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,87 +4,198 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<template>
|
||||
<span
|
||||
v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff"
|
||||
:class="{
|
||||
v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff"
|
||||
:class="{
|
||||
[$style.button]: true,
|
||||
[$style.gamingDark]: gamingType === 'dark' && checked,
|
||||
[$style.gamingLight]: gamingType === 'light' && checked,
|
||||
[$style.buttonChecked]: checked,
|
||||
[$style.buttonDisabled]: props.disabled
|
||||
[$style.buttonDisabled]: props.disabled,
|
||||
|
||||
}"
|
||||
data-cy-switch-toggle
|
||||
@click.prevent.stop="toggle"
|
||||
data-cy-switch-toggle
|
||||
@click.prevent.stop="toggle"
|
||||
>
|
||||
<div :class="{ [$style.knob]: true, [$style.knobChecked]: checked }"></div>
|
||||
<div
|
||||
:class="{ [$style.knob]: true, [$style.knobChecked]: checked, [$style.gamingDark]: gamingType === 'dark' && checked,[$style.gamingLight]: gamingType === 'light' && checked}"></div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, Ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import {toRefs, Ref, computed} from 'vue';
|
||||
import {i18n} from '@/i18n.js';
|
||||
import {defaultStore} from "@/store.js";
|
||||
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
checked: boolean | Ref<boolean>;
|
||||
disabled?: boolean | Ref<boolean>;
|
||||
checked: boolean | Ref<boolean>;
|
||||
disabled?: boolean | Ref<boolean>;
|
||||
}>(), {
|
||||
disabled: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'toggle'): void;
|
||||
(ev: 'toggle'): void;
|
||||
}>();
|
||||
|
||||
const checked = toRefs(props).checked;
|
||||
const toggle = () => {
|
||||
emit('toggle');
|
||||
emit('toggle');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.button {
|
||||
--height: 21px;
|
||||
--height: 21px;position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
width: calc(var(--height) * 1.6);
|
||||
height: calc(var(--height) + 2px); // 枠線
|
||||
outline: none;
|
||||
background: var(--switchOffBg);
|
||||
background-clip: content-box;
|
||||
border: solid 1px var(--switchOffBg);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: inherit;
|
||||
user-select: none;
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
width: calc(var(--height) * 1.6);
|
||||
height: calc(var(--height) + 2px); // 枠線
|
||||
outline: none;
|
||||
background: var(--switchOffBg);
|
||||
background-clip: content-box;
|
||||
border: solid 1px var(--switchOffBg);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: inherit;
|
||||
user-select: none;
|
||||
&.gamingLight {
|
||||
border-image: conic-gradient(#e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd) 1;
|
||||
border: solid 1px;
|
||||
}
|
||||
|
||||
&.gamingDark {
|
||||
border-image: conic-gradient(#c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4) 1;
|
||||
border: solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonChecked {
|
||||
background-color: var(--switchOnBg) !important;
|
||||
border-color: var(--switchOnBg) !important;
|
||||
background-color: var(--switchOnBg);
|
||||
border-color: var(--switchOnBg);
|
||||
}
|
||||
|
||||
.gamingLight {
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
|
||||
.gamingDark {
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
|
||||
.buttonDisabled {
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.knob {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
top: 3px;
|
||||
width: calc(var(--height) - 6px);
|
||||
height: calc(var(--height) - 6px);
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
position: absolute;
|
||||
box-sizing: border-box;top: 3px;
|
||||
width: calc(var(--height) - 6px);
|
||||
height: calc(var(--height) - 6px);
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:not(.knobChecked) {
|
||||
left: 3px;
|
||||
background: var(--switchOffFg);
|
||||
}
|
||||
&:not(.knobChecked) {
|
||||
left: 3px;
|
||||
background: var(--switchOffFg);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.knobChecked {
|
||||
left: calc(calc(100% - var(--height)) + 3px);
|
||||
background: var(--switchOnFg);
|
||||
left: calc(calc(100% - var(--height)) + 3px);
|
||||
background: var(--switchOnFg);
|
||||
|
||||
&.gamingDark {
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
&.gamingLight {
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.disabled]: disabled }]">
|
||||
<div :class="[$style.root, { [$style.disabled]: disabled }]">
|
||||
<input
|
||||
ref="input"
|
||||
type="checkbox"
|
||||
|
|
@ -12,7 +12,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
@keydown.enter="toggle"
|
||||
>
|
||||
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||
<span v-if="!noBody" :class="$style.body">
|
||||
<span v-if="!noBody" :class="$style.body,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}">
|
||||
<!-- TODO: 無名slotの方は廃止 -->
|
||||
<span :class="$style.label">
|
||||
<span @click="toggle">
|
||||
|
|
@ -26,8 +26,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, Ref } from 'vue';
|
||||
import {toRefs, Ref, ref, computed, watch} from 'vue';
|
||||
import XButton from '@/components/MkSwitch.button.vue';
|
||||
import {defaultStore} from "@/store.js";
|
||||
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean | Ref<boolean>;
|
||||
|
|
@ -66,6 +68,29 @@ const toggle = () => {
|
|||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.gamingDarkDisabled{
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: linear-gradient(270deg, #a84f4f, #a88c4f, #9aa24b, #6da85c, #53a8a6, #7597b5, #8679b5, #b579b5, #b56d96);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important;
|
||||
|
||||
}
|
||||
&.gamingLightDisabled{
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite;
|
||||
|
||||
}
|
||||
//&.checked {
|
||||
//}
|
||||
}
|
||||
|
||||
.input {
|
||||
|
|
@ -105,4 +130,6 @@ const toggle = () => {
|
|||
font-size: 85%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
|
||||
import {computed, defineComponent, h, resolveDirective, withDirectives , ref , watch} from 'vue';
|
||||
import {defaultStore} from "@/store.js";
|
||||
|
||||
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
|
@ -17,7 +20,7 @@ export default defineComponent({
|
|||
return () => h('div', {
|
||||
class: 'pxhvhrfw',
|
||||
}, options.map(option => withDirectives(h('button', {
|
||||
class: ['_button', { active: props.modelValue === option.props?.value }],
|
||||
class: ['_button', { active: props.modelValue === option.props?.value , gamingDark: gamingType.value == 'dark' && props.modelValue === option.props.value,gamingLight: gamingType.value == 'light' && props.modelValue === option.props.value } ],
|
||||
key: option.key as string,
|
||||
disabled: props.modelValue === option.props?.value,
|
||||
onClick: () => {
|
||||
|
|
@ -48,6 +51,24 @@ export default defineComponent({
|
|||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--accentedBg);
|
||||
|
||||
&.gamingDark{
|
||||
color: black !important;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
&.gamingLight{
|
||||
|
||||
color:white !important;
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active):hover {
|
||||
|
|
@ -74,4 +95,69 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License-Identifier: AGPL-3.0-only
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
|
|
@ -9,6 +10,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
ref="tlComponent"
|
||||
:pagination="paginationQuery"
|
||||
:noGap="!defaultStore.state.showGapBetweenNotesInTimeline"
|
||||
:withCw="props.withCw"
|
||||
@queue="emit('queue', $event)"
|
||||
@status="prComponent?.setDisabled($event)"
|
||||
/>
|
||||
|
|
@ -16,7 +18,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import { computed, watch, onUnmounted, provide, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
|
|
@ -28,7 +30,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { Paging } from '@/components/MkPagination.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
||||
src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role' | 'media';
|
||||
list?: string;
|
||||
antenna?: string;
|
||||
channel?: string;
|
||||
|
|
@ -37,10 +39,12 @@ const props = withDefaults(defineProps<{
|
|||
withRenotes?: boolean;
|
||||
withReplies?: boolean;
|
||||
onlyFiles?: boolean;
|
||||
withCw?: boolean;
|
||||
}>(), {
|
||||
withRenotes: true,
|
||||
withReplies: false,
|
||||
onlyFiles: false,
|
||||
withCw: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -51,24 +55,13 @@ const emit = defineEmits<{
|
|||
provide('inTimeline', true);
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
type TimelineQueryType = {
|
||||
antennaId?: string,
|
||||
withRenotes?: boolean,
|
||||
withReplies?: boolean,
|
||||
withFiles?: boolean,
|
||||
visibility?: string,
|
||||
listId?: string,
|
||||
channelId?: string,
|
||||
roleId?: string
|
||||
}
|
||||
|
||||
const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>();
|
||||
const tlComponent = shallowRef<InstanceType<typeof MkNotes>>();
|
||||
|
||||
let tlNotesCount = 0;
|
||||
|
||||
function prepend(note) {
|
||||
if (tlComponent.value == null) return;
|
||||
if (!tlComponent.value) return;
|
||||
|
||||
tlNotesCount++;
|
||||
|
||||
|
|
@ -92,11 +85,8 @@ let paginationQuery: Paging | null = null;
|
|||
const stream = useStream();
|
||||
|
||||
function connectChannel() {
|
||||
if (props.src === 'antenna') {
|
||||
if (props.antenna == null) return;
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
});
|
||||
if (props.src === 'antenna' && props.antenna) {
|
||||
connection = stream.useChannel('antenna', { antennaId: props.antenna });
|
||||
} else if (props.src === 'home') {
|
||||
connection = stream.useChannel('homeTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
|
|
@ -109,14 +99,15 @@ function connectChannel() {
|
|||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
} else if (props.src === 'social') {
|
||||
} else if (props.src === 'media') {
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
withFiles: true,
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
} else if (props.src === 'global') {
|
||||
connection = stream.useChannel('globalTimeline', {
|
||||
} else if (props.src === 'social' || props.src === 'global') {
|
||||
const channel = props.src === 'social' ? 'hybridTimeline' : 'globalTimeline';
|
||||
connection = stream.useChannel(channel, {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
|
|
@ -125,31 +116,25 @@ function connectChannel() {
|
|||
connection.on('mention', prepend);
|
||||
} else if (props.src === 'directs') {
|
||||
const onNote = note => {
|
||||
if (note.visibility === 'specified') {
|
||||
prepend(note);
|
||||
}
|
||||
if (note.visibility === 'specified') prepend(note);
|
||||
};
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', onNote);
|
||||
} else if (props.src === 'list') {
|
||||
if (props.list == null) return;
|
||||
} else if (props.src === 'list' && props.list) {
|
||||
connection = stream.useChannel('userList', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
} else if (props.src === 'channel') {
|
||||
if (props.channel == null) return;
|
||||
connection = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
});
|
||||
} else if (props.src === 'role') {
|
||||
if (props.role == null) return;
|
||||
connection = stream.useChannel('roleTimeline', {
|
||||
roleId: props.role,
|
||||
});
|
||||
} else if (props.src === 'channel' && props.channel) {
|
||||
connection = stream.useChannel('channel', { channelId: props.channel });
|
||||
} else if (props.src === 'role' && props.role) {
|
||||
connection = stream.useChannel('roleTimeline', { roleId: props.role });
|
||||
}
|
||||
|
||||
if (props.src !== 'directs' && props.src !== 'mentions') {
|
||||
connection?.on('note', prepend);
|
||||
}
|
||||
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
||||
}
|
||||
|
||||
function disconnectChannel() {
|
||||
|
|
@ -158,78 +143,45 @@ function disconnectChannel() {
|
|||
}
|
||||
|
||||
function updatePaginationQuery() {
|
||||
let endpoint: keyof Misskey.Endpoints | null;
|
||||
let query: TimelineQueryType | null;
|
||||
const endpoints = {
|
||||
antenna: 'antennas/notes',
|
||||
home: 'notes/timeline',
|
||||
local: 'notes/local-timeline',
|
||||
social: 'notes/hybrid-timeline',
|
||||
global: 'notes/global-timeline',
|
||||
media: 'notes/hybrid-timeline',
|
||||
mentions: 'notes/mentions',
|
||||
directs: 'notes/mentions',
|
||||
list: 'notes/user-list-timeline',
|
||||
channel: 'channels/timeline',
|
||||
role: 'roles/notes',
|
||||
};
|
||||
|
||||
if (props.src === 'antenna') {
|
||||
endpoint = 'antennas/notes';
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
} else if (props.src === 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'global') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'mentions') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = null;
|
||||
} else if (props.src === 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = {
|
||||
visibility: 'specified',
|
||||
};
|
||||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
} else if (props.src === 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
query = {
|
||||
channelId: props.channel,
|
||||
};
|
||||
} else if (props.src === 'role') {
|
||||
endpoint = 'roles/notes';
|
||||
query = {
|
||||
roleId: props.role,
|
||||
};
|
||||
} else {
|
||||
endpoint = null;
|
||||
query = null;
|
||||
}
|
||||
|
||||
if (endpoint && query) {
|
||||
const queries = {
|
||||
antenna: { antennaId: props.antenna },
|
||||
home: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined },
|
||||
local: { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined },
|
||||
social: { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined },
|
||||
global: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined },
|
||||
media: { withFiles: true, withRenotes: props.withRenotes, withReplies: false },
|
||||
mentions: null,
|
||||
directs: { visibility: 'specified' },
|
||||
list: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list },
|
||||
channel: { channelId: props.channel },
|
||||
role: { roleId: props.role },
|
||||
};
|
||||
if (props.src.startsWith('remoteLocalTimeline')) {
|
||||
paginationQuery = {
|
||||
endpoint: endpoint,
|
||||
endpoint: 'notes/any-local-timeline',
|
||||
limit: 10,
|
||||
params: query,
|
||||
params: {
|
||||
host: props.list,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
paginationQuery = null;
|
||||
const endpoint = endpoints[props.src];
|
||||
const query = queries[props.src];
|
||||
paginationQuery = endpoint && query ? { endpoint, limit: 10, params: query } : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -238,12 +190,9 @@ function refreshEndpointAndChannel() {
|
|||
disconnectChannel();
|
||||
connectChannel();
|
||||
}
|
||||
|
||||
updatePaginationQuery();
|
||||
}
|
||||
|
||||
// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
|
||||
// IDが切り替わったら切り替え先のTLを表示させたい
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
||||
|
||||
// 初回表示用
|
||||
|
|
@ -255,13 +204,11 @@ onUnmounted(() => {
|
|||
|
||||
function reloadTimeline() {
|
||||
return new Promise<void>((res) => {
|
||||
if (tlComponent.value == null) return;
|
||||
if (!tlComponent.value) return;
|
||||
|
||||
tlNotesCount = 0;
|
||||
|
||||
tlComponent.value.pagingComponent?.reload().then(() => {
|
||||
res();
|
||||
});
|
||||
tlComponent.value.pagingComponent?.reload().then(() => res());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
|
||||
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
|
||||
scrolling="no"
|
||||
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
|
||||
:allow="player.allow == null ? 'encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
|
||||
:class="$style.playerIframe"
|
||||
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
|
||||
:style="{ border: 0 }"
|
||||
:src="player.url"
|
||||
:style="{ border: 0, backgroundColor: 'transparent' }"
|
||||
allowtransparency="true"
|
||||
></iframe>
|
||||
<span v-else>invalid url</span>
|
||||
</div>
|
||||
|
|
@ -27,14 +28,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
<template v-else-if="tweetId && tweetExpanded">
|
||||
<div ref="twitter">
|
||||
<div ref="twitter" :class="$style.twitter">
|
||||
<iframe
|
||||
ref="tweet"
|
||||
allow="fullscreen;web-share"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
|
||||
scrolling="no"
|
||||
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
|
||||
data-transparent="true"
|
||||
|
||||
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0,borderRadius: '14px'}"
|
||||
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
|
||||
frameborder="0"
|
||||
allowtransparency="true"
|
||||
></iframe>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
|
|
@ -83,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
|
||||
import { defineAsyncComponent, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
|
||||
import type { summaly } from '@misskey-dev/summaly';
|
||||
import { url as local } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -135,6 +140,15 @@ onDeactivated(() => {
|
|||
playerEnabled.value = false;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.alwaysShowPlayer) {
|
||||
playerEnabled.value = true;
|
||||
}
|
||||
if (defaultStore.state.alwaysExpandTweet) {
|
||||
tweetExpanded.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const requestUrl = new URL(props.url);
|
||||
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
|
||||
|
||||
|
|
@ -206,7 +220,13 @@ onUnmounted(() => {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.twitter{
|
||||
width: 70%;
|
||||
|
||||
}
|
||||
.app{
|
||||
background: red;
|
||||
}
|
||||
.disablePlayer {
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkModalWindow
|
||||
ref="dialogEl"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="selected == null"
|
||||
:okButtonDisabled="(!selected && multipleSelected.length < 1)"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
|
|
@ -31,9 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<div v-if="username != '' || host != ''" :class="[$style.result, { [$style.hit]: users.length > 0 }]">
|
||||
<div v-if="users.length > 0" :class="$style.users">
|
||||
<div v-for="user in users" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()">
|
||||
<div v-for="user in users" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id || multipleSelected.includes(user)}]" @click="multiple ? (multipleSelected.includes(user) ? multipleSelected.splice(multipleSelected.indexOf(user), 1) : multipleSelected.push(user)) : selected = user" @dblclick="ok()">
|
||||
<MkAvatar :user="user" :class="$style.avatar" indicator/>
|
||||
<div :class="$style.userBody">
|
||||
<MkUserName :user="user" :class="$style.userName"/>
|
||||
|
|
@ -47,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-if="username == '' && host == ''" :class="$style.recent">
|
||||
<div :class="$style.users">
|
||||
<div v-for="user in recentUsers" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()">
|
||||
<div v-for="user in recentUsers" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id || multipleSelected.includes(user) }]" @click="multiple ? (multipleSelected.includes(user) ? multipleSelected.splice(multipleSelected.indexOf(user), 1) : multipleSelected.push(user)) : selected = user" @dblclick="ok()">
|
||||
<MkAvatar :user="user" :class="$style.avatar" indicator/>
|
||||
<div :class="$style.userBody">
|
||||
<MkUserName :user="user" :class="$style.userName"/>
|
||||
|
|
@ -71,7 +72,6 @@ import { defaultStore } from '@/store.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { host as currentHost, hostname } from '@/config.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
|
||||
(ev: 'cancel'): void;
|
||||
|
|
@ -80,6 +80,7 @@ const emit = defineEmits<{
|
|||
|
||||
const props = withDefaults(defineProps<{
|
||||
includeSelf?: boolean;
|
||||
multiple?: boolean;
|
||||
localOnly?: boolean;
|
||||
}>(), {
|
||||
includeSelf: false,
|
||||
|
|
@ -91,6 +92,7 @@ const host = ref('');
|
|||
const users = ref<Misskey.entities.UserLite[]>([]);
|
||||
const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
||||
const selected = ref<Misskey.entities.UserLite | null>(null);
|
||||
const multipleSelected = ref<Misskey.entities.UserDetailed[]>([]);
|
||||
const dialogEl = ref();
|
||||
|
||||
function search() {
|
||||
|
|
@ -114,17 +116,13 @@ function search() {
|
|||
});
|
||||
}
|
||||
|
||||
async function ok() {
|
||||
if (selected.value == null) return;
|
||||
|
||||
const user = await misskeyApi('users/show', {
|
||||
userId: selected.value.id,
|
||||
});
|
||||
emit('ok', user);
|
||||
|
||||
function ok() {
|
||||
if ((!selected.value && multipleSelected.value.length < 1)) return;
|
||||
emit('ok', selected.value ?? multipleSelected.value);
|
||||
dialogEl.value.close();
|
||||
|
||||
// 最近使ったユーザー更新
|
||||
if (multipleSelected.value.length < 0) return;
|
||||
let recents = defaultStore.state.recentlyUsedUsers;
|
||||
recents = recents.filter(x => x !== selected.value?.id);
|
||||
recents.unshift(selected.value.id);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<div style="overflow-x: clip;">
|
||||
<div :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
|
||||
<div :class="[$style.progressBarValue , {[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :style="{ width: `${(page / 5) * 100}%` }"></div>
|
||||
</div>
|
||||
<Transition
|
||||
mode="out-in"
|
||||
|
|
@ -127,7 +127,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue';
|
||||
import {computed, ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
|
||||
|
|
@ -140,6 +140,7 @@ import { host } from '@/config.js';
|
|||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
|
|
@ -222,6 +223,20 @@ async function later(later: boolean) {
|
|||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||
transition: all 0.5s cubic-bezier(0,.5,.5,1);
|
||||
&.gamingLight{
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
&.gamingDark{
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
}
|
||||
|
||||
.centerPage {
|
||||
|
|
@ -253,4 +268,69 @@ async function later(later: boolean) {
|
|||
-webkit-backdrop-filter: blur(15px);
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||
</div>
|
||||
<div class="_gaps_s" :class="$style.mainActions">
|
||||
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||
<MkButton :class="[$style.mainAction , $style.gamingDark]" full rounded data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
|
|
@ -165,6 +165,22 @@ function exploreOtherServers() {
|
|||
line-height: 28px;
|
||||
}
|
||||
|
||||
.gamingDark{
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
.gamingDark:hover{
|
||||
color: black;
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd) !important;
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
@ -200,4 +216,69 @@ function exploreOtherServers() {
|
|||
height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
} @keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
1075
packages/frontend/src/components/XNote.vue
Normal file
1075
packages/frontend/src/components/XNote.vue
Normal file
File diff suppressed because it is too large
Load diff
137
packages/frontend/src/components/XNoteHeader.vue
Normal file
137
packages/frontend/src/components/XNoteHeader.vue
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<header :class="$style.root">
|
||||
<div v-if="mock" :class="$style.name">
|
||||
<MkUserName :user="note.user"/>
|
||||
</div>
|
||||
<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
</div>
|
||||
<div v-if="mock">
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</div>
|
||||
<MkA v-else :class="$style.time" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</MkA>
|
||||
<div :class="$style.info">
|
||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<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-rocket-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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu.js';
|
||||
import * as os from '@/os.js';
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
function showMenu(viaKeyboard = false): void {
|
||||
if (mock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { menu, cleanup } = getNoteMenu({ note: props.note });
|
||||
os.popupMenu(menu, menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
const mock = inject<boolean>('mock', false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.footerButton {
|
||||
margin: -12px 0 0;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
.name {
|
||||
flex-shrink: 1;
|
||||
display: block;
|
||||
margin: 0 .5em 0 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.isBot {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
margin: 0 .5em 0 0;
|
||||
padding: 1px 6px;
|
||||
font-size: 80%;
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.username {
|
||||
flex-shrink: 9999999;
|
||||
margin: 0 .5em 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
|
||||
}
|
||||
|
||||
.badgeRoles {
|
||||
margin: 0 .5em 0 0;
|
||||
}
|
||||
|
||||
.badgeRole {
|
||||
height: 1.3em;
|
||||
vertical-align: -20%;
|
||||
|
||||
& + .badgeRole {
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
.time{
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
</style>
|
||||
1349
packages/frontend/src/components/XPostForm.vue
Normal file
1349
packages/frontend/src/components/XPostForm.vue
Normal file
File diff suppressed because it is too large
Load diff
62
packages/frontend/src/components/XPostFormDialog.vue
Normal file
62
packages/frontend/src/components/XPostFormDialog.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
|
||||
<XPostForm ref="form" :class="$style.form" :dialog="true" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import XPostForm from '@/components/XPostForm.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
reply?: Misskey.entities.Note;
|
||||
renote?: Misskey.entities.Note;
|
||||
channel?: any; // TODO
|
||||
mention?: Misskey.entities.User;
|
||||
specified?: Misskey.entities.UserDetailed;
|
||||
initialText?: string;
|
||||
initialCw?: string;
|
||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||
initialFiles?: Misskey.entities.DriveFile[];
|
||||
initialLocalOnly?: boolean;
|
||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||
initialNote?: Misskey.entities.Note;
|
||||
instant?: boolean;
|
||||
fixed?: boolean;
|
||||
autofocus?: boolean;
|
||||
}>(), {
|
||||
initialLocalOnly: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
const form = shallowRef<InstanceType<typeof MkPostForm>>();
|
||||
|
||||
function onPosted() {
|
||||
modal.value?.close({
|
||||
useSendAnimation: true,
|
||||
});
|
||||
}
|
||||
|
||||
function onModalClosed() {
|
||||
emit('closed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.form {
|
||||
max-height: 100%;
|
||||
margin: 0 auto auto auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,7 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<template>
|
||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="defaultStore.state.enableUltimateDataSaverMode ? null:user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
src="/client-assets/dummy.png"
|
||||
:title="alt"
|
||||
/>
|
||||
<span v-else-if="errored">:{{ customEmojiName }}:</span>
|
||||
<span v-else-if="errored || isDraft">:{{ customEmojiName }}:</span>
|
||||
<img
|
||||
v-else
|
||||
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
||||
|
|
@ -51,6 +51,7 @@ const react = inject<((name: string) => void) | null>('react', null);
|
|||
|
||||
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
|
||||
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
||||
const isDraft = computed(() => customEmojisMap.get(customEmojiName.value)?.draft ?? false);
|
||||
|
||||
const rawUrl = computed(() => {
|
||||
if (props.url) {
|
||||
|
|
@ -64,13 +65,20 @@ const rawUrl = computed(() => {
|
|||
|
||||
const url = computed(() => {
|
||||
if (rawUrl.value == null) return undefined;
|
||||
|
||||
const useOriginalSize = props.useOriginalSize;
|
||||
const enableDataSaverMode = defaultStore.state.enableUltimateDataSaverMode;
|
||||
let datasaver_result ;
|
||||
if (enableDataSaverMode) {
|
||||
datasaver_result = useOriginalSize ? undefined : 'datasaver';
|
||||
} else {
|
||||
datasaver_result = useOriginalSize ? undefined : 'emoji';
|
||||
}
|
||||
const proxied =
|
||||
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
|
||||
? rawUrl.value
|
||||
: getProxiedImageUrl(
|
||||
rawUrl.value,
|
||||
props.useOriginalSize ? undefined : 'emoji',
|
||||
datasaver_result,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
|
@ -123,7 +131,8 @@ function onClick(ev: MouseEvent) {
|
|||
height: 2em;
|
||||
vertical-align: middle;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
max-width: 100% !important;
|
||||
object-fit: contain !important;
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
|
|
|||
46
packages/frontend/src/components/global/MkEmojiKitchen.vue
Normal file
46
packages/frontend/src/components/global/MkEmojiKitchen.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<span v-if="errored">{{ alt }}</span>
|
||||
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, ref} from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
normal?: boolean;
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const rawUrl = computed(() => props.url);
|
||||
|
||||
const url = computed(() => rawUrl.value);
|
||||
|
||||
const alt = computed(() => props.name);
|
||||
let errored = ref(url.value == null);
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
height: 2em;
|
||||
vertical-align: middle;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.normal {
|
||||
height: 1.25em;
|
||||
vertical-align: -0.25em;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.noStyle {
|
||||
height: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -55,7 +55,7 @@ const props = withDefaults(defineProps<{
|
|||
--size: 38px;
|
||||
|
||||
&.colored {
|
||||
color: var(--accent);
|
||||
color: #5f5f5f;
|
||||
}
|
||||
|
||||
&.inline {
|
||||
|
|
@ -83,7 +83,9 @@ const props = withDefaults(defineProps<{
|
|||
height: var(--size);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.text{
|
||||
color: var(--fg);
|
||||
}
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
|||
|
|
@ -6,20 +6,24 @@
|
|||
import { VNode, h, SetupContext, provide } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { ID, Instance } from 'misskey-js/built/entities.js';
|
||||
import MkUrl from '@/components/global/MkUrl.vue';
|
||||
import MkTime from '@/components/global/MkTime.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkMention from '@/components/MkMention.vue';
|
||||
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||
import MkEmojiKitchen from '@/components/global/MkEmojiKitchen.vue';
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
import MkCodeInline from '@/components/MkCodeInline.vue';
|
||||
import MkGoogle from '@/components/MkGoogle.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import MkA, { MkABehavior } from '@/components/global/MkA.vue';
|
||||
import { host } from '@/config.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { host } from '@/config';
|
||||
import { defaultStore } from '@/store';
|
||||
import { mixEmoji } from '@/scripts/emojiKitchen/emojiMixer';
|
||||
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
|
||||
import { uhoize as doUhoize } from '@/scripts/uhoize.js';
|
||||
import { safeParseFloat } from '@/scripts/safe-parse.js';
|
||||
|
||||
const QUOTE_STYLE = `
|
||||
|
|
@ -35,11 +39,42 @@ type MfmProps = {
|
|||
text: string;
|
||||
plain?: boolean;
|
||||
nowrap?: boolean;
|
||||
author?: Misskey.entities.UserLite;
|
||||
emojireq?: boolean;
|
||||
author?: {
|
||||
id: ID;
|
||||
username: string;
|
||||
host: string | null;
|
||||
name: string | null;
|
||||
onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
|
||||
avatarUrl: string;
|
||||
avatarBlurhash: string;
|
||||
avatarDecorations: {
|
||||
id: ID;
|
||||
url: string;
|
||||
angle?: number;
|
||||
flipH?: boolean;
|
||||
}[];
|
||||
emojis: {
|
||||
name: string;
|
||||
url: string;
|
||||
}[];
|
||||
instance?: {
|
||||
name: Instance['name'];
|
||||
softwareName: Instance['softwareName'];
|
||||
softwareVersion: Instance['softwareVersion'];
|
||||
iconUrl: Instance['iconUrl'];
|
||||
faviconUrl: Instance['faviconUrl'];
|
||||
themeColor: Instance['themeColor'];
|
||||
};
|
||||
isGorilla?: boolean;
|
||||
isCat?: boolean;
|
||||
isBot?: boolean;};
|
||||
i?: Misskey.entities.UserLite | null;
|
||||
isNote?: boolean;
|
||||
emojiUrls?: Record<string, string>;
|
||||
rootScale?: number;
|
||||
nyaize?: boolean | 'respect';
|
||||
uhoize?: boolean | 'respect';
|
||||
parsedNodes?: mfm.MfmNode[] | null;
|
||||
enableEmojiMenu?: boolean;
|
||||
enableEmojiMenuReaction?: boolean;
|
||||
|
|
@ -56,14 +91,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
|
||||
const isNote = props.isNote ?? true;
|
||||
const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
|
||||
|
||||
const shouldUhoize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isGorilla : false : false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (props.text == null || props.text === '') return;
|
||||
|
||||
const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
|
||||
|
||||
const validTime = (t: string | boolean | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
if (t == null || typeof t === 'boolean') return null;
|
||||
if (typeof t === 'boolean') return null;
|
||||
return t.match(/^[0-9.]+s$/) ? t : null;
|
||||
};
|
||||
|
|
@ -80,15 +115,18 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
* @param ast MFM AST
|
||||
* @param scale How times large the text is
|
||||
* @param disableNyaize Whether nyaize is disabled or not
|
||||
* @param disableUhoize
|
||||
*/
|
||||
const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => {
|
||||
const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false, disableUhoize = false) => ast.map((token): VNode | string | (VNode | string)[] => {
|
||||
switch (token.type) {
|
||||
case 'text': {
|
||||
let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
if (!disableNyaize && shouldNyaize) {
|
||||
text = doNyaize(text);
|
||||
}
|
||||
|
||||
if (!disableUhoize && shouldUhoize) {
|
||||
text = doUhoize(text);
|
||||
}
|
||||
if (!props.plain) {
|
||||
const res: (VNode | string)[] = [];
|
||||
for (const t of text.split('\n')) {
|
||||
|
|
@ -206,19 +244,16 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
break;
|
||||
}
|
||||
case 'blur': {
|
||||
const radius = parseFloat(token.props.args.rad ?? '6');
|
||||
return h('span', {
|
||||
class: '_mfm_blur_',
|
||||
style: `--blur-px: ${radius}px;`,
|
||||
}, genEl(token.children, scale));
|
||||
}
|
||||
case 'rainbow': {
|
||||
if (!useAnim) {
|
||||
return h('span', {
|
||||
class: '_mfm_rainbow_fallback_',
|
||||
}, genEl(token.children, scale));
|
||||
}
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`;
|
||||
style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};` : '';
|
||||
break;
|
||||
}
|
||||
case 'sparkle': {
|
||||
|
|
@ -229,7 +264,23 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
}
|
||||
case 'rotate': {
|
||||
const degrees = safeParseFloat(token.props.args.deg) ?? 90;
|
||||
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
|
||||
let rotateText = `rotate(${degrees}deg)`;
|
||||
if (!token.props.args.deg && (token.props.args.x || token.props.args.y || token.props.args.z)) {
|
||||
rotateText = '';
|
||||
}
|
||||
if (token.props.args.x) {
|
||||
const degrees = parseFloat(token.props.args.x ?? '0');
|
||||
rotateText += ` rotateX(${degrees}deg)`;
|
||||
}
|
||||
if (token.props.args.y) {
|
||||
const degrees = parseFloat(token.props.args.y ?? '0');
|
||||
rotateText += ` rotateY(${degrees}deg)`;
|
||||
}
|
||||
if (token.props.args.z) {
|
||||
const degrees = parseFloat(token.props.args.z ?? '0');
|
||||
rotateText += ` rotateZ(${degrees}deg)`;
|
||||
}
|
||||
style = `transform: ${rotateText}; transform-origin: center center;`;
|
||||
break;
|
||||
}
|
||||
case 'position': {
|
||||
|
|
@ -293,6 +344,29 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
|
||||
}
|
||||
}
|
||||
case 'mix': {
|
||||
const ch = token.children;
|
||||
if (ch.length != 2 || ch.some(c => c.type !== 'unicodeEmoji')) {
|
||||
style = null;
|
||||
break;
|
||||
}
|
||||
|
||||
const emoji1 = ch[0].props.emoji;
|
||||
const emoji2 = ch[1].props.emoji;
|
||||
|
||||
const mixedEmojiUrl = mixEmoji(emoji1, emoji2);
|
||||
if (!mixedEmojiUrl) {
|
||||
style = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return h(MkEmojiKitchen, {
|
||||
key: Math.random(),
|
||||
name: emoji1 + emoji2,
|
||||
normal: props.plain,
|
||||
url: mixedEmojiUrl,
|
||||
});
|
||||
}
|
||||
case 'unixtime': {
|
||||
const child = token.children[0];
|
||||
const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
|
||||
|
|
@ -401,7 +475,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
|
||||
case 'emojiCode': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (props.author?.host == null) {
|
||||
console.log('emojiCode', props.emojireq);
|
||||
if (props.author?.host == null && !props.emojireq ) {
|
||||
return [h(MkCustomEmoji, {
|
||||
key: Math.random(),
|
||||
name: token.props.name,
|
||||
|
|
@ -422,7 +497,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
name: token.props.name,
|
||||
url: props.emojiUrls && props.emojiUrls[token.props.name],
|
||||
normal: props.plain,
|
||||
host: props.author.host,
|
||||
host: props.author.host ? props.author.host : null,
|
||||
useOriginalSize: scale >= 2.5,
|
||||
})];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
|
||||
<template>
|
||||
<div ref="el" :class="$style.tabs" @wheel="onTabWheel">
|
||||
<div :class="$style.tabsInner">
|
||||
<div :class="ui !== 'twilike' ? $style.tabsInner : $style.tabsInnerX">
|
||||
<button
|
||||
v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
|
||||
class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]"
|
||||
class="_button" :class="[ui !== 'twilike' ? $style.tab : $style.tabX, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]"
|
||||
@mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
|
||||
>
|
||||
<div :class="$style.tabInner">
|
||||
|
|
@ -29,7 +29,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
</div>
|
||||
<div
|
||||
ref="tabHighlightEl"
|
||||
:class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]"
|
||||
:class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value , [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -52,8 +52,11 @@ export type Tab = {
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue';
|
||||
import { onMounted, onUnmounted, watch, nextTick, shallowRef, ref, computed } from 'vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { ui } from '@/config.js';
|
||||
|
||||
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
|
|
@ -204,6 +207,15 @@ onUnmounted(() => {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabsInnerX {
|
||||
display: flex;
|
||||
height: var(--height);
|
||||
white-space: nowrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.tabX{
|
||||
flex: 1;
|
||||
}
|
||||
.tab {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
|
@ -228,10 +240,12 @@ onUnmounted(() => {
|
|||
.tabInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tabIcon + .tabTitle {
|
||||
padding-left: 4px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.tabTitle {
|
||||
|
|
@ -250,9 +264,88 @@ onUnmounted(() => {
|
|||
border-radius: 999px;
|
||||
transition: none;
|
||||
pointer-events: none;
|
||||
|
||||
&.gamingLight{
|
||||
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||
background-size: 1800% 1800% !important;
|
||||
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||
}
|
||||
&.gamingDark{
|
||||
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||
background-size: 1800% 1800%;
|
||||
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||
}
|
||||
&.animate {
|
||||
transition: width 0.15s ease, left 0.15s ease;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationLight {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@-moz-keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
@keyframes AnimationDark {
|
||||
0% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
|||
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
|
||||
<MkAvatar :class="$style.avatar" :user="$i"/>
|
||||
</div>
|
||||
<div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft"/>
|
||||
<div v-else-if="!thin_ && narrow && !hideTitle"/>
|
||||
|
||||
<template v-if="pageMetadata">
|
||||
<div v-if="!hideTitle" :class="$style.titleContainer" @click="top">
|
||||
<div v-if="!hideTitle && !hide" :class="$style.titleContainer" @click="top">
|
||||
<div v-if="pageMetadata.avatar" :class="$style.titleAvatarContainer">
|
||||
<MkAvatar :class="$style.titleAvatar" :user="pageMetadata.avatar" indicator/>
|
||||
</div>
|
||||
|
|
@ -55,6 +55,7 @@ const props = withDefaults(defineProps<{
|
|||
actions?: PageHeaderItem[] | null;
|
||||
thin?: boolean;
|
||||
displayMyAvatar?: boolean;
|
||||
hide?:boolean;
|
||||
}>(), {
|
||||
tabs: () => ([] as Tab[]),
|
||||
});
|
||||
|
|
@ -142,14 +143,14 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
.upper {
|
||||
--height: 50px;
|
||||
--height: 55px;
|
||||
display: flex;
|
||||
gap: var(--margin);
|
||||
height: var(--height);
|
||||
|
||||
.tabs:first-child {
|
||||
margin-left: auto;
|
||||
padding: 0 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.tabs {
|
||||
margin-right: auto;
|
||||
|
|
@ -166,14 +167,16 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
&.slim {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
gap: 0;
|
||||
|
||||
.buttonsRight {
|
||||
margin-left: auto;
|
||||
}
|
||||
.tabs:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
> .titleContainer {
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
packages/frontend/src/components/global/MkRuby.vue
Normal file
21
packages/frontend/src/components/global/MkRuby.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import MkEmoji from './MkEmoji.vue';
|
||||
import MkCustomEmoji from './MkCustomEmoji.vue';
|
||||
const props = defineProps<{
|
||||
base: string;
|
||||
text: string;
|
||||
basetype: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ruby>
|
||||
<MkEmoji v-if="basetype === 'unicodeEmoji' " class="emoji" :emoji="base" :normal="true" />
|
||||
<MkCustomEmoji v-else-if="basetype === 'emojiCode' " :name="base"/>
|
||||
<span style="white-space: pre-wrap;" v-else >{{base}}</span>
|
||||
<rt>{{text}}</rt>
|
||||
</ruby>
|
||||
</template>
|
||||
<style>
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { App } from 'vue';
|
||||
|
||||
import Mfm from './global/MkMisskeyFlavoredMarkdown.js';
|
||||
import Mfm from './global/MkMisskeyFlavoredMarkdown.ts';
|
||||
import MkA from './global/MkA.vue';
|
||||
import MkAcct from './global/MkAcct.vue';
|
||||
import MkAvatar from './global/MkAvatar.vue';
|
||||
|
|
@ -23,7 +23,6 @@ import MkError from './global/MkError.vue';
|
|||
import MkAd from './global/MkAd.vue';
|
||||
import MkPageHeader from './global/MkPageHeader.vue';
|
||||
import MkSpacer from './global/MkSpacer.vue';
|
||||
import MkFooterSpacer from './global/MkFooterSpacer.vue';
|
||||
import MkStickyContainer from './global/MkStickyContainer.vue';
|
||||
import MkLazy from './global/MkLazy.vue';
|
||||
|
||||
|
|
@ -52,7 +51,6 @@ export const components = {
|
|||
MkAd: MkAd,
|
||||
MkPageHeader: MkPageHeader,
|
||||
MkSpacer: MkSpacer,
|
||||
MkFooterSpacer: MkFooterSpacer,
|
||||
MkStickyContainer: MkStickyContainer,
|
||||
MkLazy: MkLazy,
|
||||
};
|
||||
|
|
@ -77,7 +75,6 @@ declare module '@vue/runtime-core' {
|
|||
MkAd: typeof MkAd;
|
||||
MkPageHeader: typeof MkPageHeader;
|
||||
MkSpacer: typeof MkSpacer;
|
||||
MkFooterSpacer: typeof MkFooterSpacer;
|
||||
MkStickyContainer: typeof MkStickyContainer;
|
||||
MkLazy: typeof MkLazy;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,12 +75,15 @@ export const ROLE_POLICIES = [
|
|||
'gtlAvailable',
|
||||
'ltlAvailable',
|
||||
'canPublicNote',
|
||||
'canEditNote',
|
||||
'canScheduleNote',
|
||||
'mentionLimit',
|
||||
'canInvite',
|
||||
'inviteLimit',
|
||||
'inviteLimitCycle',
|
||||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canRequestCustomEmojis',
|
||||
'canManageAvatarDecorations',
|
||||
'canSearchNotes',
|
||||
'canUseTranslator',
|
||||
|
|
@ -97,6 +100,9 @@ export const ROLE_POLICIES = [
|
|||
'userEachUserListsLimit',
|
||||
'rateLimitFactor',
|
||||
'avatarDecorationLimit',
|
||||
'emojiPickerProfileLimit',
|
||||
'listPinnedLimit',
|
||||
'localTimelineAnyLimit',
|
||||
] as const;
|
||||
|
||||
// なんか動かない
|
||||
|
|
@ -126,7 +132,7 @@ export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
|||
position: ['x=', 'y='],
|
||||
fg: ['color='],
|
||||
bg: ['color='],
|
||||
border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
|
||||
border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
|
||||
font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
|
||||
blur: [],
|
||||
rainbow: ['speed=', 'delay='],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { shallowRef, computed, markRaw, watch } from 'vue';
|
||||
import { shallowRef, computed, markRaw, triggerRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
|
|
@ -11,6 +11,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
|
|||
|
||||
const storageCache = await get('emojis');
|
||||
export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []);
|
||||
export const customEmojisNameMap = computed(() => new Map(customEmojis.value.map(item => [item.name, item])));
|
||||
export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
|
||||
const categories = new Set<string>();
|
||||
for (const emoji of customEmojis.value) {
|
||||
|
|
@ -34,16 +35,19 @@ const stream = useStream();
|
|||
|
||||
stream.on('emojiAdded', emojiData => {
|
||||
customEmojis.value = [emojiData.emoji, ...customEmojis.value];
|
||||
triggerRef(customEmojis);
|
||||
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.EmojiSimple ?? item);
|
||||
set('emojis', customEmojis.value);
|
||||
triggerRef(customEmojis);
|
||||
set('emojis', customEmojis.value);
|
||||
});
|
||||
|
||||
stream.on('emojiDeleted', emojiData => {
|
||||
customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name));
|
||||
triggerRef(customEmojis);
|
||||
set('emojis', customEmojis.value);
|
||||
});
|
||||
|
||||
|
|
@ -60,6 +64,7 @@ export async function fetchCustomEmojis(force = false) {
|
|||
}
|
||||
|
||||
customEmojis.value = res.emojis;
|
||||
triggerRef(customEmojis);
|
||||
set('emojis', res.emojis);
|
||||
set('lastEmojisFetchedAt', now);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export const instance: Misskey.entities.MetaDetailed = reactive(cachedMeta ?? {}
|
|||
|
||||
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
|
||||
|
||||
export const googleAnalyticsId = computed(() => instance.googleAnalyticsId ?? null);
|
||||
|
||||
export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL);
|
||||
|
||||
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
|
||||
|
|
|
|||
|
|
@ -147,6 +147,13 @@ export const navbarItemDef = reactive({
|
|||
miLocalStorage.setItem('ui', 'classic');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: 'twilike',
|
||||
active: ui === 'twilike',
|
||||
action: () => {
|
||||
miLocalStorage.setItem('ui', 'twilike');
|
||||
unisonReload();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
|
|
@ -177,6 +184,14 @@ export const navbarItemDef = reactive({
|
|||
show: computed(() => $i != null),
|
||||
to: `/@${$i?.username}`,
|
||||
},
|
||||
scheduledNotes: {
|
||||
title: i18n.ts._schedulePost.list,
|
||||
icon: 'ti ti-calendar-event',
|
||||
show: computed(() => $i && $i.policies?.canScheduleNote),
|
||||
action: (ev) => {
|
||||
os.listSchedulePost();
|
||||
},
|
||||
},
|
||||
cacheClear: {
|
||||
title: i18n.ts.clearCache,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type { Form, GetFormResultType } from '@/scripts/form.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
|
||||
import XPostFormDialog from '@/components/XPostFormDialog.vue';
|
||||
import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
|
||||
import MkPageWindow from '@/components/MkPageWindow.vue';
|
||||
import MkToast from '@/components/MkToast.vue';
|
||||
|
|
@ -24,6 +25,8 @@ import MkContextMenu from '@/components/MkContextMenu.vue';
|
|||
import { MenuItem } from '@/types/menu.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { ui } from '@/config.js';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
||||
|
|
@ -223,9 +226,8 @@ export function alert(props: {
|
|||
}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
export function confirm(props: {
|
||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'|'mksw';
|
||||
title?: string;
|
||||
text?: string;
|
||||
okText?: string;
|
||||
|
|
@ -242,6 +244,24 @@ export function confirm(props: {
|
|||
}, 'closed');
|
||||
});
|
||||
}
|
||||
export function switch1(props: {
|
||||
type: 'mksw';
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
}): Promise<{ canceled: boolean, result: boolean }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(MkDialog, {
|
||||
...props,
|
||||
showCancelButton: true,
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
},
|
||||
}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: const T extends ... にしたい
|
||||
// https://zenn.dev/general_link/articles/813e47b7a0eef7#const-type-parameters
|
||||
|
|
@ -528,11 +548,12 @@ export function form<F extends Form>(title: string, f: F): Promise<{ canceled: t
|
|||
});
|
||||
}
|
||||
|
||||
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
|
||||
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; multiple?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
|
||||
return new Promise(resolve => {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
|
||||
includeSelf: opts.includeSelf,
|
||||
localOnly: opts.localOnly,
|
||||
multiple: opts.multiple,
|
||||
}, {
|
||||
ok: user => {
|
||||
resolve(user);
|
||||
|
|
@ -540,7 +561,13 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
|
|||
}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSchedulePost() {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkSchedulePostListDialog.vue')), {
|
||||
}, {
|
||||
}, 'closed');
|
||||
});
|
||||
}
|
||||
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
return new Promise(resolve => {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
||||
|
|
@ -657,14 +684,25 @@ export function post(props: Record<string, any> = {}): Promise<void> {
|
|||
// 複数のpost formを開いたときに場合によってはエラーになる
|
||||
// もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
|
||||
let dispose;
|
||||
popup(MkPostFormDialog, props, {
|
||||
closed: () => {
|
||||
resolve();
|
||||
dispose();
|
||||
},
|
||||
}).then(res => {
|
||||
dispose = res.dispose;
|
||||
});
|
||||
if (ui !== 'twilike') {
|
||||
popup(MkPostFormDialog, props, {
|
||||
closed: () => {
|
||||
resolve();
|
||||
dispose();
|
||||
},
|
||||
}).then(res => {
|
||||
dispose = res.dispose;
|
||||
});
|
||||
} else {
|
||||
popup(XPostFormDialog, props, {
|
||||
closed: () => {
|
||||
resolve();
|
||||
dispose();
|
||||
},
|
||||
}).then(res => {
|
||||
dispose = res.dispose;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<img src="https://avatars.githubusercontent.com/u/22656849?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@anatawa12</span>
|
||||
</a>
|
||||
<a href="https://github.com/mattyatea" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/56515516?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@mattyatea</span>
|
||||
</a>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
<FormSection>
|
||||
|
|
|
|||
|
|
@ -4,52 +4,76 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkButton v-if="$i && ($i.isModerator || $i.policies.canManageCustomEmojis)" primary link to="/custom-emojis-manager">{{ i18n.ts.manageCustomEmojis }}</MkButton>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<MkButton v-if="$i && ($i.isModerator || $i.policies.canManageCustomEmojis)" primary link to="/custom-emojis-manager">{{ i18n.ts.manageCustomEmojis }}</MkButton>
|
||||
<MkButton v-if="$i && (!$i.isModerator || $i.policies.canRequestCustomEmojis)" primary @click="edit">{{ i18n.ts.requestCustomEmojis }}</MkButton>
|
||||
|
||||
<div class="query">
|
||||
<MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<div class="query" style="margin-top: 10px;">
|
||||
<MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<!-- たくさんあると邪魔
|
||||
<div class="tags">
|
||||
<span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="searchEmojis">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<div :class="$style.emojis">
|
||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
||||
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category">
|
||||
<template #header>{{ category || i18n.ts.other }}</template>
|
||||
<MkFoldableSection v-if="searchEmojis" :expanded="false">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<div :class="$style.emojis">
|
||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji" :request="emoji.request"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
||||
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category" :expanded="false">
|
||||
<template #header>{{ category || i18n.ts.other }}</template>
|
||||
<div :class="$style.emojis">
|
||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-if="tab === 'request'" :contentMax="1000" :marginMin="20">
|
||||
<div :class="$style.emojis">
|
||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/>
|
||||
<XEmoji v-for="emoji in requestEmojis.emojis" :key="emoji.name" :emoji="emoji" :request="true"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import { watch, defineAsyncComponent, ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XEmoji from './emojis.emoji.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
|
||||
import { customEmojis, customEmojiCategories } from '@/custom-emojis.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
let tab = ref('emojis');
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const customEmojiTags = getCustomEmojiTags();
|
||||
const q = ref('');
|
||||
const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null);
|
||||
const selectedTags = ref(new Set());
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'emojis',
|
||||
title: i18n.ts.list,
|
||||
}, {
|
||||
key: 'request',
|
||||
title: i18n.ts.requestingEmojis,
|
||||
}]);
|
||||
|
||||
definePageMetadata(ref({}));
|
||||
|
||||
let q = ref('');
|
||||
let searchEmojis = ref<Misskey.entities.CustomEmoji[]>(null);
|
||||
let selectedTags = ref(new Set());
|
||||
const requestEmojis = await misskeyApiGet('emoji-requests');
|
||||
|
||||
function search() {
|
||||
if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) {
|
||||
|
|
@ -65,28 +89,35 @@ function search() {
|
|||
queryarry.includes(`:${emoji.name}:`),
|
||||
);
|
||||
} else {
|
||||
searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
|
||||
searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q));
|
||||
}
|
||||
} else {
|
||||
searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t)));
|
||||
searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t)));
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTag(tag) {
|
||||
if (selectedTags.value.has(tag)) {
|
||||
selectedTags.value.delete(tag);
|
||||
} else {
|
||||
selectedTags.value.add(tag);
|
||||
}
|
||||
}
|
||||
const edit = () => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), {
|
||||
isRequest: true,
|
||||
}, {
|
||||
done: result => {
|
||||
window.location.reload();
|
||||
},
|
||||
}, 'closed');
|
||||
};
|
||||
|
||||
watch(q, () => {
|
||||
watch((q), () => {
|
||||
search();
|
||||
});
|
||||
|
||||
watch(selectedTags, () => {
|
||||
watch((selectedTags), () => {
|
||||
search();
|
||||
}, { deep: true });
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div :class="$style.items">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.item" :to="`/instance-info/${instance.host}`">
|
||||
<MkA
|
||||
v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`"
|
||||
:class="$style.item" :to="`/instance-info/${instance.host}`"
|
||||
>
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<img :src="iconUrl" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
</div>
|
||||
|
|
@ -31,10 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkKeyValue>
|
||||
<div v-html="i18n.tsx.poweredByType4nyDescription({ name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-type4ny">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
{{ i18n.ts.aboutType4ny }}
|
||||
</FormLink>
|
||||
<FormLink to="/about-type4ny">{{ i18n.ts.aboutType4ny }}</FormLink>
|
||||
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts.sourceCode }}
|
||||
|
|
@ -42,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInfo v-else warn>
|
||||
{{ i18n.ts.sourceCodeIsNotYetProvided }}
|
||||
</MkInfo>
|
||||
ソースコード含め問い合わせは下記のメールアドレスへよろしくお願いします。
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
|
@ -116,9 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<XEmojis/>
|
||||
</MkSpacer>
|
||||
<XEmojis v-else-if="tab === 'emojis'"/>
|
||||
<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
|
|
@ -130,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
import XFederation from './about.federation.vue';
|
||||
|
|
@ -150,6 +146,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { bannerDark, bannerLight, defaultStore, iconDark, iconLight } from '@/store.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialTab?: string;
|
||||
|
|
@ -165,6 +162,27 @@ watch(tab, () => {
|
|||
claimAchievement('viewInstanceChart');
|
||||
}
|
||||
});
|
||||
let bannerUrl = ref(defaultStore.state.bannerUrl);
|
||||
let iconUrl = ref(defaultStore.state.iconUrl);
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
|
||||
if (darkMode.value) {
|
||||
bannerUrl.value = bannerDark;
|
||||
iconUrl.value = iconDark;
|
||||
} else {
|
||||
bannerUrl.value = bannerLight;
|
||||
iconUrl.value = iconLight;
|
||||
}
|
||||
|
||||
watch(darkMode, () => {
|
||||
if (darkMode.value) {
|
||||
bannerUrl.value = bannerDark;
|
||||
iconUrl.value = iconDark;
|
||||
} else {
|
||||
bannerUrl.value = bannerLight;
|
||||
iconUrl.value = iconLight;
|
||||
}
|
||||
});
|
||||
|
||||
const initStats = () => misskeyApi('stats', {
|
||||
}).then((res) => {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>
|
||||
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
</div>
|
||||
<div>
|
||||
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="presentsPoints">ぷりずむを付与する</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
|
|
@ -312,6 +315,15 @@ async function resetPassword() {
|
|||
}
|
||||
}
|
||||
|
||||
async function presentsPoints() {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'ポイント',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (result === null) return;
|
||||
await misskeyApi('admin/accounts/present-points', { userId: user.value.id, points: parseInt(result) });
|
||||
}
|
||||
|
||||
async function toggleSuspend(v) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="iconDark" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (Dark)</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="iconLight" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (Light)</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="app192IconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
|
|
@ -41,6 +49,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="bannerDark" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }} (Dark)</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="bannerLight" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }} (Light)</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="backgroundImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
|
|
@ -61,6 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="googleAnalyticsId" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>googleAnal </template>
|
||||
</MkInput>
|
||||
|
||||
<MkColorInput v-model="themeColor">
|
||||
<template #label>{{ i18n.ts.themeColor }}</template>
|
||||
|
|
@ -128,10 +148,16 @@ const themeColor = ref<string | null>(null);
|
|||
const defaultLightTheme = ref<string | null>(null);
|
||||
const defaultDarkTheme = ref<string | null>(null);
|
||||
const serverErrorImageUrl = ref<string | null>(null);
|
||||
const googleAnalyticsId = ref<string | null>(null);
|
||||
|
||||
const infoImageUrl = ref<string | null>(null);
|
||||
const notFoundImageUrl = ref<string | null>(null);
|
||||
const repositoryUrl = ref<string | null>(null);
|
||||
const feedbackUrl = ref<string | null>(null);
|
||||
const iconDark = ref<string | null>(null);
|
||||
const iconLight = ref<string | null>(null);
|
||||
const bannerDark = ref<string | null>(null);
|
||||
const bannerLight = ref<string | null>(null);
|
||||
const manifestJsonOverride = ref<string>('{}');
|
||||
|
||||
async function init() {
|
||||
|
|
@ -150,6 +176,10 @@ async function init() {
|
|||
repositoryUrl.value = meta.repositoryUrl;
|
||||
feedbackUrl.value = meta.feedbackUrl;
|
||||
manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
|
||||
iconDark.value = meta.iconDark;
|
||||
iconLight.value = meta.iconLight;
|
||||
bannerDark.value = meta.bannerDark;
|
||||
bannerLight.value = meta.bannerLight;
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
@ -165,9 +195,14 @@ function save() {
|
|||
infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
|
||||
notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
|
||||
serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
|
||||
googleAnalyticsId: googleAnalyticsId.value === '' ? null : googleAnalyticsId.value,
|
||||
repositoryUrl: repositoryUrl.value === '' ? null : repositoryUrl.value,
|
||||
feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
|
||||
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
||||
iconDark: iconDark.value === '' ? null : iconDark.value,
|
||||
iconLight: iconLight.value === '' ? null : iconLight.value,
|
||||
bannerDark: bannerDark.value === '' ? null : bannerDark.value,
|
||||
bannerLight: bannerLight.value === '' ? null : bannerLight.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :contentMax="700" :marginMin="16">
|
||||
<div class="lxpfedzu">
|
||||
<div class="banner">
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
<img :src="iconUrl" alt="" class="icon"/>
|
||||
</div>
|
||||
|
||||
<div class="_gaps_s">
|
||||
|
|
@ -40,8 +40,9 @@ import { lookup } from '@/scripts/lookup.js';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { bannerDark, bannerLight, defaultStore, iconDark, iconLight } from '@/store.js';
|
||||
|
||||
const isEmpty = (x: string | null) => x == null || x === '';
|
||||
|
||||
|
|
@ -67,7 +68,20 @@ let noEmailServer = !instance.enableEmail;
|
|||
let noInquiryUrl = isEmpty(instance.inquiryUrl);
|
||||
const thereIsUnresolvedAbuseReport = ref(false);
|
||||
const currentPage = computed(() => router.currentRef.value.child);
|
||||
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
let iconUrl = ref();
|
||||
if (darkMode.value) {
|
||||
iconUrl.value = iconDark;
|
||||
} else {
|
||||
iconUrl.value = iconLight;
|
||||
}
|
||||
watch(darkMode, () => {
|
||||
if (darkMode.value) {
|
||||
iconUrl.value = iconDark;
|
||||
} else {
|
||||
iconUrl.value = iconLight;
|
||||
}
|
||||
});
|
||||
misskeyApi('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
limit: 1,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header>
|
||||
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<MkTextarea v-if="tab === 'block'" v-model="blockedHosts">
|
||||
|
|
@ -33,6 +35,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkSpacer from '@/components/global/MkSpacer.vue';
|
||||
import MkStickyContainer from '@/components/global/MkStickyContainer.vue';
|
||||
|
||||
const blockedHosts = ref<string>('');
|
||||
const silencedHosts = ref<string>('');
|
||||
|
|
@ -40,8 +44,8 @@ const tab = ref('block');
|
|||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
blockedHosts.value = meta.blockedHosts.join('\n');
|
||||
silencedHosts.value = meta.silencedHosts.join('\n');
|
||||
blockedHosts.value = meta.blockedHosts ? meta.blockedHosts.join('\n') : '';
|
||||
silencedHosts.value = meta.silencedHosts ? meta.silencedHosts.join('\n') : '';
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="enableGDPRMode">
|
||||
<template #label>{{ i18n.ts.enableGDPRMode }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||
|
||||
<MkInput v-model="tosUrl" type="url">
|
||||
|
|
@ -93,6 +97,7 @@ const preservedUsernames = ref<string>('');
|
|||
const tosUrl = ref<string | null>(null);
|
||||
const privacyPolicyUrl = ref<string | null>(null);
|
||||
const inquiryUrl = ref<string | null>(null);
|
||||
const enableGDPRMode = ref<boolean>(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
|
@ -105,6 +110,7 @@ async function init() {
|
|||
tosUrl.value = meta.tosUrl;
|
||||
privacyPolicyUrl.value = meta.privacyPolicyUrl;
|
||||
inquiryUrl.value = meta.inquiryUrl;
|
||||
enableGDPRMode.value = meta.enableGDPRMode;
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
@ -118,6 +124,7 @@ function save() {
|
|||
prohibitedWords: prohibitedWords.value.split('\n'),
|
||||
hiddenTags: hiddenTags.value.split('\n'),
|
||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||
enableGDPRMode: enableGDPRMode.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
<MkInput v-model="DiscordWebhookUrl" type="password">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Discord Webhook URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="DiscordWebhookUrlWordBlock" type="password">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Discord Webhook Url WordBlock</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="EmojiBotToken" type="password">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>EmojiBotToken</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ApiBase">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>ApiBase</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="requestEmojiAllOk">
|
||||
絵文字の申請全部許可
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
|
|
@ -52,11 +71,17 @@ import { fetchInstance } from '@/instance.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
||||
const enableServerMachineStats = ref<boolean>(false);
|
||||
const enableIdenticonGeneration = ref<boolean>(false);
|
||||
const enableChartsForRemoteUser = ref<boolean>(false);
|
||||
const enableChartsForFederatedInstances = ref<boolean>(false);
|
||||
const requestEmojiAllOk = ref(false);
|
||||
let DiscordWebhookUrl = ref(null);
|
||||
let DiscordWebhookUrlWordBlock = ref(null);
|
||||
let EmojiBotToken = ref(null);
|
||||
let ApiBase = ref(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
|
@ -64,6 +89,11 @@ async function init() {
|
|||
enableIdenticonGeneration.value = meta.enableIdenticonGeneration;
|
||||
enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser;
|
||||
enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances;
|
||||
requestEmojiAllOk.value = meta.requestEmojiAllOk;
|
||||
DiscordWebhookUrl.value = meta.DiscordWebhookUrl;
|
||||
DiscordWebhookUrlWordBlock.value = meta.DiscordWebhookUrlWordBlock;
|
||||
EmojiBotToken.value = meta.EmojiBotToken;
|
||||
ApiBase.value = meta.ApiBase;
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
@ -71,7 +101,12 @@ function save() {
|
|||
enableServerMachineStats: enableServerMachineStats.value,
|
||||
enableIdenticonGeneration: enableIdenticonGeneration.value,
|
||||
enableChartsForRemoteUser: enableChartsForRemoteUser.value,
|
||||
requestEmojiAllOk: requestEmojiAllOk.value,
|
||||
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
|
||||
DiscordWebhookUrl: DiscordWebhookUrl.value,
|
||||
EmojiBotToken: EmojiBotToken.value,
|
||||
ApiBase: ApiBase.value,
|
||||
DiscordWebhookUrlWordBlock: DiscordWebhookUrlWordBlock.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,7 +99,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.mentionLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mentionLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.mentionLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.mentionLimit.value" :disabled="role.policies.mentionLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.mentionLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(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>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])">
|
||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||
<template #suffix>
|
||||
|
|
@ -160,25 +177,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.mentionLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mentionLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.mentionLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.mentionLimit.value" :disabled="role.policies.mentionLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.mentionLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(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>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>
|
||||
|
|
@ -570,6 +568,63 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.emojiPickerProfileLimit, 'emojiPickerProfileLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.emojiPickerProfileLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.emojiPickerProfileLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.emojiPickerProfileLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.emojiPickerProfileLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.emojiPickerProfileLimit.value" type="number" :min="0" :disabled="role.policies.emojiPickerProfileLimit.useDefault" >
|
||||
<template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.emojiPickerProfileLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(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>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.listPinnedLimit, 'listPinnedLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.listPinnedLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.listPinnedLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.listPinnedLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.listPinnedLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.listPinnedLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.listPinnedLimit.value" type="number" :min="0" :disabled="role.policies.listPinnedLimit.useDefault" >
|
||||
<template #label>{{ i18n.ts._role._options.listPinnedLimit }}</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.listPinnedLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(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>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.localTimelineAnyLimit, 'localTimelineAnyLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.localTimelineAnyLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.localTimelineAnyLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.localTimelineAnyLimit.value}}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.localTimelineAnyLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.localTimelineAnyLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.localTimelineAnyLimit.value" type="number" :min="0" :disabled="role.policies.localTimelineAnyLimit.useDefault" >
|
||||
<template #label>{{ i18n.ts._role._options.localTimelineAnyLimit }}</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.localTimelineAnyLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(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>
|
||||
|
|
|
|||
|
|
@ -23,188 +23,261 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
|
||||
</MkRange>
|
||||
</MkFolder>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>タイムライン系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||
<template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.gtlAvailable">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])">
|
||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||
<template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.gtlAvailable">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
||||
<template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.ltlAvailable">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>ノート系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
|
||||
<template #suffix>{{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canPublicNote">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>{{ policies.mentionLimit }}</template>
|
||||
<MkInput v-model="policies.mentionLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
|
||||
<template #suffix>{{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canEditNote">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])">
|
||||
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
||||
<template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.ltlAvailable">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canScheduleNote, 'canScheduleNote'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canScheduleNote }}</template>
|
||||
<template #suffix>{{ policies.canScheduleNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canScheduleNote">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||
<template #suffix>{{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canSearchNotes">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])">
|
||||
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
|
||||
<template #suffix>{{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canPublicNote">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canSearchNotes'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUseTranslator">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #suffix>{{ policies.pinLimit }}</template>
|
||||
<MkInput v-model="policies.pinLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>招待系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canInvite">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||
<template #suffix>{{ policies.inviteLimit }}</template>
|
||||
<MkInput v-model="policies.inviteLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>{{ policies.mentionLimit }}</template>
|
||||
<MkInput v-model="policies.mentionLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canInvite">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canInvite">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||
<template #suffix>{{ policies.inviteLimit }}</template>
|
||||
<MkInput v-model="policies.inviteLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||
<template #suffix>{{ policies.inviteLimit }}</template>
|
||||
<MkInput v-model="policies.inviteLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||
<template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template>
|
||||
<MkInput v-model="policies.inviteLimitCycle" type="number">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||
<template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template>
|
||||
<MkInput v-model="policies.inviteLimitCycle" type="number">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||
<template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template>
|
||||
<MkInput v-model="policies.inviteExpirationTime" type="number">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>PrisMisskey独自機能系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.emojiPickerProfileLimit, 'pickerProfileDefault'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template>
|
||||
<template #suffix>{{ policies.emojiPickerProfileLimit }}</template>
|
||||
<MkInput v-model="policies.emojiPickerProfileLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||
<template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template>
|
||||
<MkInput v-model="policies.inviteExpirationTime" type="number">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.listPinnedLimit, 'listPinnedLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.listPinnedLimit }}</template>
|
||||
<template #suffix>{{ policies.listPinnedLimit }}</template>
|
||||
<MkInput v-model="policies.listPinnedLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.localTimelineAnyLimit, 'localTimelineAnyLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.localTimelineAnyLimit }}</template>
|
||||
<template #suffix>{{ policies.localTimelineAnyLimit }}</template>
|
||||
<MkInput v-model="policies.localTimelineAnyLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>カスタム絵文字系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canManageCustomEmojis">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||
<template #suffix>{{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canManageAvatarDecorations">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canRequestCustomEmojis, 'canRequestCustomEmojis'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canRequestCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canRequestCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canRequestCustomEmojis">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>ドライブ、ファイル系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>{{ policies.driveCapacityMb }}MB</template>
|
||||
<MkInput v-model="policies.driveCapacityMb" type="number">
|
||||
<template #suffix>MB</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canManageCustomEmojis">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
|
||||
<template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.alwaysMarkNsfw">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>アイコンデコレーション系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||
<template #suffix>{{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canManageAvatarDecorations">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #suffix>{{ policies.avatarDecorationLimit }}</template>
|
||||
<MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>クリップ系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
|
||||
<template #suffix>{{ policies.clipLimit }}</template>
|
||||
<MkInput v-model="policies.clipLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||
<template #suffix>{{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canSearchNotes">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
|
||||
<template #suffix>{{ policies.noteEachClipsLimit }}</template>
|
||||
<MkInput v-model="policies.noteEachClipsLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>リスト系</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
|
||||
<template #suffix>{{ policies.userListLimit }}</template>
|
||||
<MkInput v-model="policies.userListLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canSearchNotes'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUseTranslator">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
|
||||
<template #suffix>{{ policies.userEachUserListsLimit }}</template>
|
||||
<MkInput v-model="policies.userEachUserListsLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection :expanded="false">
|
||||
<template #header>その他</template>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
||||
<template #suffix>{{ policies.antennaLimit }}</template>
|
||||
<MkInput v-model="policies.antennaLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>{{ policies.driveCapacityMb }}MB</template>
|
||||
<MkInput v-model="policies.driveCapacityMb" type="number">
|
||||
<template #suffix>MB</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
|
||||
<template #suffix>{{ policies.wordMuteLimit }}</template>
|
||||
<MkInput v-model="policies.wordMuteLimit" type="number">
|
||||
<template #suffix>chars</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])">
|
||||
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
|
||||
<template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.alwaysMarkNsfw">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
|
||||
<template #suffix>{{ policies.webhookLimit }}</template>
|
||||
<MkInput v-model="policies.webhookLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #suffix>{{ policies.pinLimit }}</template>
|
||||
<MkInput v-model="policies.pinLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
||||
<template #suffix>{{ policies.antennaLimit }}</template>
|
||||
<MkInput v-model="policies.antennaLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
|
||||
<template #suffix>{{ policies.wordMuteLimit }}</template>
|
||||
<MkInput v-model="policies.wordMuteLimit" type="number">
|
||||
<template #suffix>chars</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
|
||||
<template #suffix>{{ policies.webhookLimit }}</template>
|
||||
<MkInput v-model="policies.webhookLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
|
||||
<template #suffix>{{ policies.clipLimit }}</template>
|
||||
<MkInput v-model="policies.clipLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
|
||||
<template #suffix>{{ policies.noteEachClipsLimit }}</template>
|
||||
<MkInput v-model="policies.noteEachClipsLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
|
||||
<template #suffix>{{ policies.userListLimit }}</template>
|
||||
<MkInput v-model="policies.userListLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
|
||||
<template #suffix>{{ policies.userEachUserListsLimit }}</template>
|
||||
<MkInput v-model="policies.userEachUserListsLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #suffix>{{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canHideAds">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #suffix>{{ policies.avatarDecorationLimit }}</template>
|
||||
<MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])" class="_margin">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #suffix>{{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canHideAds">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
</MkFoldableSection>
|
||||
|
||||
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -103,6 +103,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="bannedEmailDomains">
|
||||
<template #label>Banned Email Domains List</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label> Signup Protection</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="enableProxyCheckio">
|
||||
<template #label>Use ProxyCheck.io API</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="proxyCheckioApiKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>ProxyCheck.io API Key</template>
|
||||
</MkInput>
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
|
@ -153,11 +169,13 @@ const enableSensitiveMediaDetectionForVideos = ref<boolean>(false);
|
|||
const enableIpLogging = ref<boolean>(false);
|
||||
const enableActiveEmailValidation = ref<boolean>(false);
|
||||
const enableVerifymailApi = ref<boolean>(false);
|
||||
const enableProxyCheckio = ref<boolean>(false);
|
||||
const verifymailAuthKey = ref<string | null>(null);
|
||||
const enableTruemailApi = ref<boolean>(false);
|
||||
const truemailInstance = ref<string | null>(null);
|
||||
const truemailAuthKey = ref<string | null>(null);
|
||||
const bannedEmailDomains = ref<string>('');
|
||||
const proxyCheckioApiKey = ref<string | null>(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
|
@ -182,6 +200,8 @@ async function init() {
|
|||
truemailInstance.value = meta.truemailInstance;
|
||||
truemailAuthKey.value = meta.truemailAuthKey;
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
|
||||
enableProxyCheckio.value = meta.enableProxyCheckio;
|
||||
proxyCheckioApiKey.value = meta.proxyCheckioApiKey;
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
@ -204,6 +224,8 @@ function save() {
|
|||
truemailInstance: truemailInstance.value,
|
||||
truemailAuthKey: truemailAuthKey.value,
|
||||
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
|
||||
enableProxyCheckio: enableProxyCheckio.value,
|
||||
proxyCheckioApiKey: proxyCheckioApiKey.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,45 +7,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<MkSwitch v-model="select">SelectMode</MkSwitch>
|
||||
<MkButton @click="setCategoryBulk">Set Category</MkButton>
|
||||
<MkButton @click="deletes">Delete</MkButton>
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||
<template #label>{{ avatarDecoration.name }}</template>
|
||||
<template #caption>{{ avatarDecoration.description }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="avatarDecoration.name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="avatarDecoration.description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="avatarDecoration.url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<div class="buttons _buttons">
|
||||
<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<div :class="$style.decorations">
|
||||
<XDecoration
|
||||
v-for="avatarDecoration in avatarDecorations"
|
||||
:key="avatarDecoration.id"
|
||||
:class=" selectItemsId.includes(avatarDecoration.id) ? $style.selected : '' "
|
||||
:decoration="avatarDecoration"
|
||||
@click="select ? selectItems(avatarDecoration.id) : openDecorationEdit(avatarDecoration)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, defineAsyncComponent, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import XDecoration from '@/pages/settings/avatar-decoration.decoration.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
||||
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
||||
const select = ref(false);
|
||||
const selectItemsId = ref<string[]>([]);
|
||||
|
||||
function add() {
|
||||
avatarDecorations.value.unshift({
|
||||
|
|
@ -54,9 +47,19 @@ function add() {
|
|||
name: '',
|
||||
description: '',
|
||||
url: '',
|
||||
category: '',
|
||||
});
|
||||
}
|
||||
|
||||
function selectItems(decorationId) {
|
||||
if (selectItemsId.value.includes(decorationId)) {
|
||||
const index = selectItemsId.value.indexOf(decorationId);
|
||||
selectItemsId.value.splice(index, 1);
|
||||
} else {
|
||||
selectItemsId.value.push(decorationId);
|
||||
}
|
||||
}
|
||||
|
||||
function del(avatarDecoration) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
|
|
@ -73,10 +76,29 @@ async function save(avatarDecoration) {
|
|||
await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
|
||||
load();
|
||||
} else {
|
||||
os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
|
||||
selectItemsId.value.push(decorationId);
|
||||
}
|
||||
}
|
||||
|
||||
function openDecorationEdit(avatarDecoration) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkAvatarDecoEditDialog.vue')), {
|
||||
avatarDecoration: avatarDecoration,
|
||||
}, {
|
||||
del: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openDecorationCreate() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkAvatarDecoEditDialog.vue')), {
|
||||
}, {
|
||||
del: result => {
|
||||
avatarDecorations.value.unshift(result);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function load() {
|
||||
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
|
||||
avatarDecorations.value = _avatarDecorations;
|
||||
|
|
@ -84,12 +106,40 @@ function load() {
|
|||
}
|
||||
|
||||
load();
|
||||
watch(select, () => {
|
||||
selectItemsId.value = [];
|
||||
});
|
||||
|
||||
async function setCategoryBulk() {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Category',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (selectItemsId.value.length > 1) {
|
||||
for (let i = 0; i < selectItemsId.value.length; i++) {
|
||||
let decorationId = selectItemsId.value[i];
|
||||
await misskeyApi('admin/avatar-decorations/update', {
|
||||
id: decorationId,
|
||||
category: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deletes() {
|
||||
if (selectItemsId.value.length > 0) {
|
||||
selectItemsId.value.forEach(decorationId => {
|
||||
console.log(decorationId);
|
||||
misskeyApi('admin/avatar-decorations/delete', { id: decorationId });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.add,
|
||||
handler: add,
|
||||
handler: openDecorationCreate,
|
||||
}]);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
|
@ -99,3 +149,13 @@ definePageMetadata(() => ({
|
|||
icon: 'ti ti-sparkles',
|
||||
}));
|
||||
</script>
|
||||
<style module>
|
||||
.decorations {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
.selected{
|
||||
border: 0.1px solid var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
||||
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
||||
|
||||
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'featured'" key="featured">
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import MkClickerGame from '@/components/MkClickerGame.vue';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
title: '●👈',
|
||||
icon: 'ti ti-circle',
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,62 +10,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :contentMax="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||
<template #label>Select mode</template>
|
||||
</MkSwitch>
|
||||
<div v-if="selectMode" class="_buttons">
|
||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
<MkCustomEmojiEditLocal/>
|
||||
</div>
|
||||
<div v-if="tab === 'request'" class="request">
|
||||
<MkCustomEmojiEditRequest/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
<MkCustomEmojiEditRemote/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
|
@ -74,105 +25,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||
import MkCustomEmojiEditRequest from '@/components/MkCustomEmojiEditRequest.vue';
|
||||
import MkCustomEmojiEditLocal from '@/components/MkCustomEmojiEditLocal.vue';
|
||||
import MkCustomEmojiEditRemote from '@/components/MkCustomEmojiEditRemote.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const tab = ref('local');
|
||||
const query = ref<string | null>(null);
|
||||
const queryRemote = ref<string | null>(null);
|
||||
const host = ref<string | null>(null);
|
||||
const selectMode = ref(false);
|
||||
const selectedEmojis = ref<string[]>([]);
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'admin/emoji/list' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
query: (query.value && query.value !== '') ? query.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
const remotePagination = {
|
||||
endpoint: 'admin/emoji/list-remote' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
|
||||
host: (host.value && host.value !== '') ? host.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (selectedEmojis.value.length > 0) {
|
||||
selectedEmojis.value = [];
|
||||
} else {
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelect = (emoji) => {
|
||||
if (selectedEmojis.value.includes(emoji.id)) {
|
||||
selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
|
||||
} else {
|
||||
selectedEmojis.value.push(emoji.id);
|
||||
}
|
||||
};
|
||||
const tab = ref('request');
|
||||
|
||||
const add = async (ev: MouseEvent) => {
|
||||
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
|
||||
os.popup(defineAsyncComponent(() => import('../components/MkEmojiEditDialog.vue')), {
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
emojisPaginationComponent.value.prepend(result.created);
|
||||
}
|
||||
//TODO: emitにして追加を反映
|
||||
// if (result.created) {
|
||||
// emojisPaginationComponent.value.prepend(result.created);
|
||||
// emojisPaginationComponent.value.reload();
|
||||
// }
|
||||
},
|
||||
}, 'closed');
|
||||
};
|
||||
|
||||
const edit = (emoji) => {
|
||||
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
|
||||
emoji: emoji,
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
} else if (result.deleted) {
|
||||
emojisPaginationComponent.value.removeItem(emoji.id);
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
};
|
||||
|
||||
const importEmoji = (emoji) => {
|
||||
os.apiWithDialog('admin/emoji/copy', {
|
||||
emojiId: emoji.id,
|
||||
});
|
||||
};
|
||||
|
||||
const remoteMenu = (emoji, ev: MouseEvent) => {
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: ':' + emoji.name + ':',
|
||||
}, {
|
||||
text: i18n.ts.import,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => { importEmoji(emoji); },
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const menu = (ev: MouseEvent) => {
|
||||
os.popupMenu([{
|
||||
icon: 'ti ti-download',
|
||||
|
|
@ -215,78 +92,6 @@ const menu = (ev: MouseEvent) => {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const setCategoryBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Category',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-category-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
category: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const setLicenseBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'License',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-license-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const addTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const removeTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const setTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const delBulk = async () => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
|
|
@ -298,6 +103,9 @@ const headerActions = computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'request',
|
||||
title: i18n.ts.requestingEmojis,
|
||||
}, {
|
||||
key: 'local',
|
||||
title: i18n.ts.local,
|
||||
}, {
|
||||
|
|
@ -312,103 +120,4 @@ definePageMetadata(() => ({
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ogwlenmc {
|
||||
> .local {
|
||||
.empty {
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
.ldhfsamy {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
|
||||
> .emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
text-align: left;
|
||||
border: solid 1px var(--panel);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
> .img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
> .name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .info {
|
||||
opacity: 0.5;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .remote {
|
||||
.empty {
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
.ldhfsamy {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
|
||||
> .emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> .img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
> .name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .info {
|
||||
opacity: 0.5;
|
||||
font-size: 90%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ const NORAML_MONOS: FrontendMonoDefinition[] = [{
|
|||
img: '/client-assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png',
|
||||
imgSizeX: 256,
|
||||
imgSizeY: 256,
|
||||
spriteScale: 1.12,
|
||||
spriteScale: 1.0,
|
||||
}, {
|
||||
id: 'beb30459-b064-4888-926b-f572e4e72e0c',
|
||||
sfxPitch: 0.75,
|
||||
|
|
@ -508,9 +508,81 @@ const SWEETS_MONOS: FrontendMonoDefinition[] = [{
|
|||
imgSizeY: 32,
|
||||
spriteScale: 1,
|
||||
}];
|
||||
const PRISMISSKEY_MONOS: FrontendMonoDefinition[] = [{
|
||||
id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525',
|
||||
sfxPitch: 0.25,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fc4c7e430-bd92-415a-a7d3-031ddb7f0641.apng',
|
||||
imgSizeX: 3400,
|
||||
imgSizeY: 3400,
|
||||
spriteScale: 2.3,
|
||||
}, {
|
||||
id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1',
|
||||
sfxPitch: 0.5,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F5ba71ab2-1673-4eb0-bd3b-b44e063365ba.apng',
|
||||
|
||||
imgSizeX: 2000,
|
||||
imgSizeY: 2000,
|
||||
spriteScale: 1.65,
|
||||
}, {
|
||||
id: '41607ef3-b6d6-4829-95b6-3737bf8bb956',
|
||||
sfxPitch: 0.75,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fd6fe438b-c550-484f-95d1-1739a2b5173d.apng',
|
||||
|
||||
imgSizeX: 2000,
|
||||
imgSizeY: 2000,
|
||||
spriteScale: 1.8,
|
||||
}, {
|
||||
id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416',
|
||||
sfxPitch: 1,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F8226211b-58e2-46ba-be20-4ea635d614ab.webp',
|
||||
imgSizeX: 500,
|
||||
imgSizeY: 501,
|
||||
spriteScale: 1.0,
|
||||
}, {
|
||||
id: '1092e069-fe1a-450b-be97-b5d477ec398c',
|
||||
sfxPitch: 1.5,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fde30a66c-5c98-4c2e-a425-4d1f73d96899.png',
|
||||
imgSizeX: 340,
|
||||
imgSizeY: 351,
|
||||
spriteScale: 0.98,
|
||||
}, {
|
||||
id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0',
|
||||
sfxPitch: 2,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fdc8d893a-6d1f-4a86-847e-a30e56270249.png',
|
||||
imgSizeX: 1023,
|
||||
imgSizeY: 1000,
|
||||
spriteScale: 1.0,
|
||||
}, {
|
||||
id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a',
|
||||
sfxPitch: 2.5,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F56a303b9-4385-44bb-a4d4-2453450eef01.png',
|
||||
imgSizeX: 256,
|
||||
imgSizeY: 256,
|
||||
spriteScale: 0.4,
|
||||
}, {
|
||||
id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919',
|
||||
sfxPitch: 3,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F96a87c60-543d-4e83-a24d-c2a3247eb2ea.webp',
|
||||
imgSizeX: 630,
|
||||
imgSizeY: 620,
|
||||
spriteScale: 0.6,
|
||||
}, {
|
||||
id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d',
|
||||
sfxPitch: 3.5,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fdff2812b-2c80-4ecf-b5f1-b2874048899e.webp',
|
||||
imgSizeX: 1500,
|
||||
imgSizeY: 1500,
|
||||
spriteScale: 1.15,
|
||||
}, {
|
||||
id: '35e476ee-44bd-4711-ad42-87be245d3efd',
|
||||
sfxPitch: 4,
|
||||
img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fc4448bf6-d95f-49e5-844d-b6b9530e82cc.png',
|
||||
imgSizeX: 200,
|
||||
imgSizeY: 200,
|
||||
spriteScale: 1.5,
|
||||
}];
|
||||
const props = defineProps<{
|
||||
gameMode: 'normal' | 'square' | 'yen' | 'sweets' | 'space';
|
||||
gameMode: 'normal' | 'square' | 'yen' | 'sweets' | 'prismisskey' | 'space';
|
||||
mute: boolean;
|
||||
}>();
|
||||
|
||||
|
|
@ -524,6 +596,7 @@ const monoDefinitions = computed(() => {
|
|||
props.gameMode === 'yen' ? YEN_MONOS :
|
||||
props.gameMode === 'sweets' ? SWEETS_MONOS :
|
||||
props.gameMode === 'space' ? NORAML_MONOS :
|
||||
props.gameMode === 'prismisskey' ? PRISMISSKEY_MONOS :
|
||||
[] as never;
|
||||
});
|
||||
|
||||
|
|
@ -532,6 +605,7 @@ function getScoreUnit(gameMode: string) {
|
|||
gameMode === 'square' ? 'pt' :
|
||||
gameMode === 'yen' ? '円' :
|
||||
gameMode === 'sweets' ? 'kcal' :
|
||||
gameMode === 'prismisskey' ? 'pt' :
|
||||
'' as never;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="square">SQUARE</option>
|
||||
<option value="yen">YEN</option>
|
||||
<option value="sweets">SWEETS</option>
|
||||
<option value="prismisskey">PRISMISSKEY</option>
|
||||
<!--<option value="space">SPACE</option>-->
|
||||
</MkSelect>
|
||||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||
|
|
@ -72,8 +73,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>Credit</b></div>
|
||||
<div>
|
||||
<div>Ai-chan illustration: @poteriri@misskey.io</div>
|
||||
<div>BGM: @ys@misskey.design</div>
|
||||
<div>Ai-chan illustration: <MkA href="/@poteriri@misskey.io">@poteriri@misskey.io</MkA></div>
|
||||
<div>BGM: <MkA href="/@ys@misskey.design">@ys@misskey.design</MkA></div>
|
||||
<div>Emoji Thanks: <MkA to="/@User2_Moo@prismisskey.space">@User2_Moo@prismisskey.space</MkA></div>
|
||||
<div>Emoji Thanks: <MkA to="/@z_n_jin@prismisskey.space">@z_n_jin@prismisskey.space</MkA></div>
|
||||
<div>Emoji Thanks: <MkA to="/@nekomimi@prismisskey.space">@nekomimi@prismisskey.space</MkA></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -95,7 +99,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
|
||||
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
|
||||
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space' | 'prismisskey'>('normal');
|
||||
const gameStarted = ref(false);
|
||||
const mute = ref(false);
|
||||
const ranking = ref(null);
|
||||
|
|
@ -110,6 +114,7 @@ function getScoreUnit(gameMode: string) {
|
|||
gameMode === 'yen' ? '円' :
|
||||
gameMode === 'sweets' ? 'kcal' :
|
||||
gameMode === 'space' ? 'pt' :
|
||||
gameMode === 'prismisskey' ? 'pt' :
|
||||
'' as never;
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue