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:
mattyatea 2024-07-15 14:59:54 +09:00
commit 71382a6f85
297 changed files with 60420 additions and 4574 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}:`,

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

View file

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

View file

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

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,13 +94,6 @@ onUnmounted(() => {
if (connection) connection.dispose();
});
onDeactivated(() => {
if (connection) connection.dispose();
});
defineExpose({
reload,
});
</script>
<style lang="scss" module>

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;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;

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

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

File diff suppressed because it is too large Load diff

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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