Merge branch 'develop' into sw-notification-action

This commit is contained in:
tamaina 2021-05-06 20:52:53 +09:00
commit 72fe859e03
129 changed files with 4778 additions and 906 deletions

View file

@ -6,7 +6,7 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
type Captcha = {
render(container: string | Node, options: {
@ -32,7 +32,7 @@ declare global {
export default defineComponent({
props: {
provider: {
type: String,
type: String as PropType<CaptchaProvider>,
required: true,
},
sitekey: {
@ -51,19 +51,25 @@ export default defineComponent({
},
computed: {
loaded() {
return !!window[this.provider as CaptchaProvider];
variable(): string {
switch (this.provider) {
case 'hcaptcha': return 'hcaptcha';
case 'recaptcha': return 'grecaptcha';
}
},
src() {
loaded(): boolean {
return !!window[this.variable];
},
src(): string {
const endpoint = ({
hcaptcha: 'https://hcaptcha.com/1',
recaptcha: 'https://www.recaptcha.net/recaptcha',
} as Record<PropertyKey, unknown>)[this.provider];
} as Record<CaptchaProvider, string>)[this.provider];
return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
},
captcha() {
return window[this.provider as CaptchaProvider] || {} as unknown as Captcha;
captcha(): Captcha {
return window[this.variable] || {} as unknown as Captcha;
},
},
@ -94,7 +100,7 @@ export default defineComponent({
methods: {
reset() {
this.captcha?.reset();
if (this.captcha?.reset) this.captcha.reset();
},
requestRender() {
if (this.captcha.render && this.$refs.captcha instanceof Element) {

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { defineComponent, h, TransitionGroup } from 'vue';
import MkAd from '@client/components/global/ad.vue';
export default defineComponent({
props: {
@ -22,6 +23,11 @@ export default defineComponent({
required: false,
default: false
},
ad: {
type: Boolean,
required: false,
default: false
},
},
methods: {
@ -58,11 +64,7 @@ export default defineComponent({
if (
i != this.items.length - 1 &&
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
!item._prId_ &&
!this.items[i + 1]._prId_ &&
!item._featuredId_ &&
!this.items[i + 1]._featuredId_
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
) {
const separator = h('div', {
class: 'separator',
@ -86,7 +88,15 @@ export default defineComponent({
return [el, separator];
} else {
return el;
if (this.ad && item._shouldInsertAd_) {
return [h(MkAd, {
class: 'a', // advertise()
key: item.id + ':ad',
prefer: ['horizontal', 'horizontal-big'],
}), el];
} else {
return el;
}
}
}));
},
@ -95,6 +105,10 @@ export default defineComponent({
<style lang="scss">
.sqadhkmv {
> *:empty {
display: none;
}
> *:not(:last-child) {
margin-bottom: var(--margin);
}

View file

@ -81,7 +81,7 @@ export default defineComponent({
getMenu() {
return [{
text: this.$ts.rename,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: this.rename
}, {
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,

View file

@ -247,7 +247,7 @@ export default defineComponent({
}
}, null, {
text: this.$ts.rename,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: this.rename
}, null, {
text: this.$ts.delete,

View file

@ -614,7 +614,7 @@ export default defineComponent({
type: 'label'
}, this.folder ? {
text: this.$ts.renameFolder,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: () => { this.renameFolder(this.folder); }
} : undefined, this.folder ? {
text: this.$ts.deleteFolder,

View file

@ -35,6 +35,7 @@
class="_button"
@click="chosen(emoji, $event)"
tabindex="0"
:key="emoji"
>
<MkEmoji :emoji="emoji" :normal="true"/>
</button>
@ -104,7 +105,7 @@ export default defineComponent({
return {
emojilist: markRaw(emojilist),
getStaticImageUrl,
pinned: this.$store.state.reactions,
pinned: this.$store.reactiveState.reactions,
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
big: this.asReactionPicker ? isDeviceTouch : false,

View file

@ -0,0 +1,71 @@
<template>
<XModalWindow ref="dialog"
:width="370"
:height="400"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template #header>{{ $ts.forgotPassword }}</template>
<form class="_monolithic_" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
<div class="_section">
<MkInput v-model:value="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
<span>{{ $ts.username }}</span>
<template #prefix>@</template>
</MkInput>
<MkInput v-model:value="email" type="email" spellcheck="false" required>
<span>{{ $ts.emailAddress }}</span>
<template #desc>{{ $ts._forgotPassword.enterEmail }}</template>
</MkInput>
<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
</div>
<div class="_section">
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
</div>
</form>
<div v-else>
{{ $ts._forgotPassword.contactAdmin }}
</div>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import * as os from '@client/os';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
emits: ['done', 'closed'],
data() {
return {
username: '',
email: '',
processing: false,
};
},
methods: {
async onSubmit() {
this.processing = true;
await os.apiWithDialog('request-reset-password', {
username: this.username,
email: this.email,
});
this.$emit('done');
this.$refs.dialog.close();
}
}
});
</script>

View file

@ -0,0 +1,126 @@
<template>
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
<div class="thumbnail">
<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
</div>
<article>
<header>
<MkAvatar :user="post.user" class="avatar"/>
</header>
<footer>
<span class="title">{{ post.title }}</span>
</footer>
</article>
</MkA>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { userName } from '@client/filters/user';
import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
import * as os from '@client/os';
export default defineComponent({
components: {
ImgWithBlurhash
},
props: {
post: {
type: Object,
required: true
},
},
methods: {
userName
}
});
</script>
<style lang="scss" scoped>
.ttasepnz {
display: block;
position: relative;
height: 200px;
&:hover {
text-decoration: none;
color: var(--accent);
> .thumbnail {
transform: scale(1.1);
}
> article {
> footer {
&:before {
opacity: 1;
}
}
}
}
> .thumbnail {
width: 100%;
height: 100%;
position: absolute;
transition: all 0.5s ease;
> .img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
> article {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
> header {
position: absolute;
top: 0;
width: 100%;
padding: 12px;
box-sizing: border-box;
display: flex;
> .avatar {
margin-left: auto;
width: 32px;
height: 32px;
}
}
> footer {
position: absolute;
bottom: 0;
width: 100%;
padding: 16px;
box-sizing: border-box;
color: #fff;
text-shadow: 0 0 8px #000;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
&:before {
content: "";
display: block;
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(0, 0, 0, 0.4), transparent);
opacity: 0;
transition: opacity 0.5s ease;
}
> .title {
font-weight: bold;
}
}
}
}
</style>

View file

@ -0,0 +1,163 @@
<template>
<div class="qiivuoyo" v-if="ad">
<div class="main" :class="ad.place" v-if="!showMenu">
<a :href="ad.url" target="_blank">
<img :src="ad.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
</a>
</div>
<div class="menu" v-else>
<div class="body">
<div>Ads by {{ host }}</div>
<!--<MkButton>{{ $ts.stopThisAd }}</MkButton>-->
<button class="_textButton" @click="toggleMenu">{{ $ts.close }}</button>
</div>
</div>
</div>
<div v-else></div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { instance } from '@client/instance';
import { host } from '@client/config';
import MkButton from '@client/components/ui/button.vue';
export default defineComponent({
components: {
MkButton
},
props: {
prefer: {
type: Array,
required: true
},
specify: {
type: Object,
required: false
},
},
setup(props) {
const showMenu = ref(false);
const toggleMenu = () => {
showMenu.value = !showMenu.value;
};
let ad = null;
if (props.specify) {
ad = props.specify;
} else {
let ads = instance.ads.filter(ad => props.prefer.includes(ad.place));
if (ads.length === 0) {
ads = instance.ads.filter(ad => ad.place === 'square');
}
const high = ads.filter(ad => ad.priority === 'high');
const middle = ads.filter(ad => ad.priority === 'middle');
const low = ads.filter(ad => ad.priority === 'low');
if (high.length > 0) {
ad = high[Math.floor(Math.random() * high.length)];
} else if (middle.length > 0) {
ad = middle[Math.floor(Math.random() * middle.length)];
} else if (low.length > 0) {
ad = low[Math.floor(Math.random() * low.length)];
}
}
return {
ad,
showMenu,
toggleMenu,
host,
};
}
});
</script>
<style lang="scss" scoped>
.qiivuoyo {
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
> .main {
text-align: center;
> a {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> img {
filter: contrast(120%);
}
}
> img {
display: block;
object-fit: contain;
margin: auto;
}
> .menu {
position: absolute;
top: 0;
right: 0;
background: var(--panel);
}
}
&.square {
> a ,
> a > img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
&.horizontal {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
&.horizontal-big {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.vertical {
> a ,
> a > img {
max-width: min(100px, 100%);
}
}
}
> .menu {
padding: 8px;
text-align: center;
> .body {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
}
}
}
</style>

View file

@ -113,8 +113,6 @@ export default defineComponent({
> .icon {
padding-left: 2px;
font-size: .9em;
font-weight: 400;
font-style: normal;
}
> .self {

View file

@ -12,8 +12,10 @@ import url from './global/url.vue';
import i18n from './global/i18n';
import loading from './global/loading.vue';
import error from './global/error.vue';
import ad from './global/ad.vue';
export default function(app: App) {
app.component('I18n', i18n);
app.component('Mfm', mfm);
app.component('MkA', a);
app.component('MkAcct', acct);
@ -25,5 +27,5 @@ export default function(app: App) {
app.component('MkUrl', url);
app.component('MkLoading', loading);
app.component('MkError', error);
app.component('I18n', i18n);
app.component('MkAd', ad);
}

View file

@ -3,12 +3,12 @@
<div class="szkkfdyq _popup">
<div class="main">
<template v-for="item in items">
<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }">
<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }" v-click-anime>
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
<MkA v-else :to="item.to" @click.passive="close()">
<MkA v-else :to="item.to" @click.passive="close()" v-click-anime>
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
@ -16,16 +16,16 @@
</template>
</div>
<div class="sub">
<MkA to="/docs" @click.passive="close()">
<MkA to="/docs" @click.passive="close()" v-click-anime>
<i class="fas fa-question-circle icon"></i>
<div class="text">{{ $ts.help }}</div>
</MkA>
<MkA to="/about" @click.passive="close()">
<MkA to="/about" @click.passive="close()" v-click-anime>
<i class="fas fa-info-circle icon"></i>
<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
</MkA>
<MkA to="/about-misskey" @click.passive="close()">
<i class="fas fa-info-circle icon"></i>
<MkA to="/about-misskey" @click.passive="close()" v-click-anime>
<img src="/static-assets/favicon.png" class="icon"/>
<div class="text">{{ $ts.aboutMisskey }}</div>
</MkA>
</div>
@ -101,6 +101,7 @@ export default defineComponent({
flex-direction: column;
align-items: center;
justify-content: center;
vertical-align: bottom;
width: 128px;
height: 128px;
border-radius: var(--radius);
@ -117,6 +118,7 @@ export default defineComponent({
> .icon {
font-size: 26px;
height: 32px;
}
> .text {

View file

@ -1,5 +1,5 @@
<template>
<div class="yohlumlk">
<div class="yohlumlk" v-size="{ min: [350, 500] }">
<MkAvatar class="avatar" :user="note.user"/>
<div class="main">
<XNoteHeader class="header" :note="note" :mini="true"/>
@ -50,18 +50,19 @@ export default defineComponent({
display: flex;
margin: 0;
padding: 0;
overflow: hidden;
overflow: clip;
font-size: 0.95em;
> .avatar {
@media (min-width: 350px) {
&.min-width_350px {
> .avatar {
margin: 0 10px 0 0;
width: 44px;
height: 44px;
}
}
@media (min-width: 500px) {
&.min-width_500px {
> .avatar {
margin: 0 12px 0 0;
width: 48px;
height: 48px;

View file

@ -17,7 +17,7 @@
</MkButton>
</div>
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap">
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true">
<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
</XList>

View file

@ -93,7 +93,7 @@ export default defineComponent({
if (this.menu) return;
this.menu = os.modalMenu([{
text: this.$ts.renameFile,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: () => { this.rename(file) }
}, {
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,

View file

@ -11,6 +11,7 @@
<MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
<span>{{ $ts.password }}</span>
<template #prefix><i class="fas fa-lock"></i></template>
<template #desc><button class="_textButton" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
</MkInput>
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
</div>
@ -49,8 +50,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from './ui/button.vue';
import MkInput from './ui/input.vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import { apiUrl, host } from '@client/config';
import { byteify, hexify } from '@client/scripts/2fa';
import * as os from '@client/os';
@ -197,6 +198,11 @@ export default defineComponent({
this.signing = false;
});
}
},
resetPassword() {
os.popup(import('@client/components/forgot-password.vue'), {}, {
}, 'closed');
}
}
});

View file

@ -12,14 +12,16 @@ export default defineComponent({
return withDirectives(h('div', {
class: 'pxhvhrfw',
}, options.map(option => h('button', {
}, options.map(option => withDirectives(h('button', {
class: ['_button', { active: this.value === option.props.value }],
key: option.props.value,
disabled: this.value === option.props.value,
onClick: () => {
this.$emit('update:value', option.props.value);
}
}, option.children))), [
}, option.children), [
[resolveDirective('click-anime')]
]))), [
[resolveDirective('size'), { max: [500] }]
]);
}

View file

@ -139,7 +139,8 @@ export default defineComponent({
}
&.primary {
color: #fff;
font-weight: bold;
color: #fff !important;
background: var(--accent);
&:not(:disabled):hover {
@ -200,10 +201,6 @@ export default defineComponent({
min-width: 100px;
}
&.primary {
font-weight: bold;
}
> .ripples {
position: absolute;
z-index: 0;

View file

@ -199,6 +199,7 @@ export default defineComponent({
> .fade {
display: block;
position: absolute;
z-index: 10;
bottom: 0;
left: 0;
width: 100%;

View file

@ -10,8 +10,8 @@
<div v-else class="cxiknjgy">
<slot :items="items"></slot>
<div class="more" v-show="more" key="_more_">
<MkButton class="button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
<div class="more _gap" v-show="more" key="_more_">
<MkButton class="button" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</MkButton>
@ -38,6 +38,12 @@ export default defineComponent({
pagination: {
required: true
},
disableAutoLoad: {
type: Boolean,
required: false,
default: false,
}
},
});
</script>

View file

@ -8,33 +8,35 @@
@closed="$emit('closed')"
>
<template #header>{{ $ts.selectUser }}</template>
<div class="tbhwbxda _section">
<div class="inputs">
<MkInput v-model:value="username" class="input" @update:value="search" ref="username"><span>{{ $ts.username }}</span><template #prefix>@</template></MkInput>
<MkInput v-model:value="host" class="input" @update:value="search"><span>{{ $ts.host }}</span><template #prefix>@</template></MkInput>
</div>
</div>
<div class="tbhwbxda _section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }">
<div class="users" v-if="users.length > 0">
<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
</div>
<div class="tbhwbxda _monolithic_">
<div class="_section">
<div class="inputs">
<MkInput v-model:value="username" class="input" @update:value="search" ref="username"><span>{{ $ts.username }}</span><template #prefix>@</template></MkInput>
<MkInput v-model:value="host" class="input" @update:value="search"><span>{{ $ts.host }}</span><template #prefix>@</template></MkInput>
</div>
</div>
<div v-else class="empty">
<span>{{ $ts.noUsers }}</span>
<div class="_section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }">
<div class="users" v-if="users.length > 0">
<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
</div>
</div>
</div>
<div v-else class="empty">
<span>{{ $ts.noUsers }}</span>
</div>
</div>
</div>
<div class="tbhwbxda _section recent" v-if="username == '' && host == ''">
<div class="users">
<div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
<div class="_section recent" v-if="username == '' && host == ''">
<div class="users">
<div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
</div>
</div>
</div>
</div>
@ -122,76 +124,78 @@ export default defineComponent({
<style lang="scss" scoped>
.tbhwbxda {
display: flex;
flex-direction: column;
overflow: auto;
height: 100%;
&.result.hit {
padding: 0;
}
&.recent {
padding: 0;
}
> .inputs {
> .input {
display: inline-block;
width: 50%;
margin: 0;
}
}
> .users {
flex: 1;
> ._section {
display: flex;
flex-direction: column;
overflow: auto;
padding: 8px 0;
height: 100%;
> .user {
display: flex;
align-items: center;
padding: 8px var(--root-margin);
font-size: 14px;
&.result.hit {
padding: 0;
}
&:hover {
background: var(--X7);
&.recent {
padding: 0;
}
> .inputs {
> .input {
display: inline-block;
width: 50%;
margin: 0;
}
}
&.selected {
background: var(--accent);
color: #fff;
}
> .users {
flex: 1;
overflow: auto;
padding: 8px 0;
> * {
pointer-events: none;
user-select: none;
}
> .user {
display: flex;
align-items: center;
padding: 8px var(--root-margin);
font-size: 14px;
> .avatar {
width: 45px;
height: 45px;
}
> .body {
padding: 0 8px;
min-width: 0;
> .name {
display: block;
font-weight: bold;
&:hover {
background: var(--X7);
}
> .acct {
opacity: 0.5;
&.selected {
background: var(--accent);
color: #fff;
}
> * {
pointer-events: none;
user-select: none;
}
> .avatar {
width: 45px;
height: 45px;
}
> .body {
padding: 0 8px;
min-width: 0;
> .name {
display: block;
font-weight: bold;
}
> .acct {
opacity: 0.5;
}
}
}
}
}
> .empty {
opacity: 0.7;
text-align: center;
> .empty {
opacity: 0.7;
text-align: center;
}
}
}
</style>

View file

@ -2,7 +2,10 @@ import { Directive } from 'vue';
export default {
mounted(el, binding, vn) {
el.classList.add('_anime_bounce_standBy');
el.addEventListener('mousedown', () => {
el.classList.add('_anime_bounce_standBy');
el.classList.add('_anime_bounce_ready');
el.addEventListener('mouseleave', () => {
@ -17,6 +20,7 @@ export default {
el.addEventListener('animationend', () => {
el.classList.remove('_anime_bounce_ready');
el.classList.remove('_anime_bounce');
el.classList.add('_anime_bounce_standBy');
});
}
} as Directive;

View file

@ -1,39 +1,57 @@
<template>
<FormBase class="mmnnbwxb" v-if="meta">
<div class="_formItem logo">
<img v-if="meta.logoImageUrl" :src="meta.logoImageUrl">
<span v-else class="text">{{ instanceName }}</span>
<FormBase>
<div class="_formItem">
<div class="_formPanel fwhjspax">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
<span class="name">{{ $instance.name || host }}</span>
</div>
</div>
<FormTextarea readonly :value="$instance.description">
</FormTextarea>
<FormGroup>
<FormKeyValueView>
<template #key>Misskey</template>
<template #value>v{{ version }}</template>
</FormKeyValueView>
<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
</FormGroup>
<FormGroup>
<FormKeyValueView>
<template #key>{{ $ts.administrator }}</template>
<template #value>{{ meta.maintainerName }}</template>
<template #value>{{ $instance.maintainerName }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $ts.contact }}</template>
<template #value>{{ meta.maintainerEmail }}</template>
<template #value>{{ $instance.maintainerEmail }}</template>
</FormKeyValueView>
</FormGroup>
<FormLink v-if="meta.tosUrl" :to="meta.tosUrl" external>{{ $ts.tos }}</FormLink>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" external>{{ $ts.tos }}</FormLink>
<FormGroup v-if="stats">
<template #label>{{ $ts.statistics }}</template>
<FormKeyValueView>
<template #key>{{ $ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</FormKeyValueView>
<FormSuspense :p="initStats">
<FormGroup>
<template #label>{{ $ts.statistics }}</template>
<FormKeyValueView>
<template #key>{{ $ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</FormKeyValueView>
</FormGroup>
</FormSuspense>
<FormGroup>
<template #label>Well-known resources</template>
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
</FormGroup>
</FormBase>
</template>
@ -45,9 +63,12 @@ import FormLink from '@client/components/form/link.vue';
import FormBase from '@client/components/form/base.vue';
import FormGroup from '@client/components/form/group.vue';
import FormKeyValueView from '@client/components/form/key-value-view.vue';
import FormTextarea from '@client/components/form/textarea.vue';
import FormSuspense from '@client/components/form/suspense.vue';
import * as os from '@client/os';
import number from '@client/filters/number';
import * as symbols from '@client/symbols';
import { host } from '@client/config';
export default defineComponent({
components: {
@ -55,6 +76,8 @@ export default defineComponent({
FormGroup,
FormLink,
FormKeyValueView,
FormTextarea,
FormSuspense,
},
data() {
@ -63,24 +86,17 @@ export default defineComponent({
title: this.$ts.instanceInfo,
icon: 'fas fa-info-circle'
},
host,
version,
instanceName,
stats: null,
initStats: () => os.api('stats', {
}).then((stats) => {
this.stats = stats;
})
}
},
computed: {
meta() {
return this.$instance;
},
},
created() {
os.api('stats').then(stats => {
this.stats = stats;
});
},
methods: {
number
}
@ -88,18 +104,20 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.mmnnbwxb {
max-width: 800px;
box-sizing: border-box;
margin: 0 auto;
.fwhjspax {
padding: 16px;
text-align: center;
> .logo {
text-align: center;
> .icon {
display: block;
margin: auto;
height: 64px;
border-radius: 8px;
}
> img {
vertical-align: bottom;
max-height: 100px;
}
> .name {
display: block;
margin-top: 12px;
}
}
</style>

View file

@ -0,0 +1,168 @@
<template>
<FormBase>
<FormSuspense :p="init">
<FormInput v-model:value="title">
<span>{{ $ts.title }}</span>
</FormInput>
<FormTextarea v-model:value="description" :max="500">
<span>{{ $ts.description }}</span>
</FormTextarea>
<FormGroup>
<div v-for="file in files" :key="file.id" class="_formItem _formPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
<div class="name">{{ file.name }}</div>
<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button>
</div>
<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
</FormGroup>
<FormSwitch v-model:value="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
<FormButton v-if="postId" @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
<FormButton v-else @click="save" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
<FormButton v-if="postId" @click="del" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
</FormSuspense>
</FormBase>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import FormButton from '@client/components/form/button.vue';
import FormInput from '@client/components/form/input.vue';
import FormTextarea from '@client/components/form/textarea.vue';
import FormSwitch from '@client/components/form/switch.vue';
import FormTuple from '@client/components/form/tuple.vue';
import FormBase from '@client/components/form/base.vue';
import FormGroup from '@client/components/form/group.vue';
import FormSuspense from '@client/components/form/suspense.vue';
import { selectFile } from '@client/scripts/select-file';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
FormButton,
FormInput,
FormTextarea,
FormSwitch,
FormBase,
FormGroup,
FormSuspense,
},
props: {
postId: {
type: String,
required: false,
default: null,
}
},
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.postId ? {
title: this.$ts.edit,
icon: 'fas fa-pencil-alt'
} : {
title: this.$ts.postToGallery,
icon: 'fas fa-pencil-alt'
}),
init: null,
files: [],
description: null,
title: null,
isSensitive: false,
}
},
watch: {
postId: {
handler() {
this.init = () => this.postId ? os.api('gallery/posts/show', {
postId: this.postId
}).then(post => {
this.files = post.files;
this.title = post.title;
this.description = post.description;
this.isSensitive = post.isSensitive;
}) : Promise.resolve(null);
},
immediate: true,
}
},
methods: {
selectFile(e) {
selectFile(e.currentTarget || e.target, null, true).then(files => {
this.files = this.files.concat(files);
});
},
remove(file) {
this.files = this.files.filter(f => f.id !== file.id);
},
async save() {
if (this.postId) {
await os.apiWithDialog('gallery/posts/update', {
postId: this.postId,
title: this.title,
description: this.description,
fileIds: this.files.map(file => file.id),
isSensitive: this.isSensitive,
});
this.$router.push(`/gallery/${this.postId}`);
} else {
const post = await os.apiWithDialog('gallery/posts/create', {
title: this.title,
description: this.description,
fileIds: this.files.map(file => file.id),
isSensitive: this.isSensitive,
});
this.$router.push(`/gallery/${post.id}`);
}
},
async del() {
const { canceled } = await os.dialog({
type: 'warning',
text: this.$ts.deleteConfirm,
showCancelButton: true
});
if (canceled) return;
await os.apiWithDialog('gallery/posts/delete', {
postId: this.postId,
});
this.$router.push(`/gallery`);
}
}
});
</script>
<style lang="scss" scoped>
.wqugxsfx {
height: 200px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
position: relative;
> .name {
position: absolute;
top: 8px;
left: 9px;
padding: 8px;
background: var(--panel);
}
> .remove {
position: absolute;
top: 8px;
right: 9px;
padding: 8px;
background: var(--panel);
}
}
</style>

View file

@ -0,0 +1,152 @@
<template>
<div class="xprsixdl _root">
<MkTab v-model:value="tab" v-if="$i">
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
</MkTab>
<div v-if="tab === 'explore'">
<MkFolder class="_gap">
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
<MkPagination :pagination="recentPostsPagination" #default="{items}" :disable-auto-load="true">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
</div>
</MkPagination>
</MkFolder>
<MkFolder class="_gap">
<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
<MkPagination :pagination="popularPostsPagination" #default="{items}" :disable-auto-load="true">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
</div>
</MkPagination>
</MkFolder>
</div>
<div v-else-if="tab === 'liked'">
<MkPagination :pagination="likedPostsPagination" #default="{items}">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="like in items" :post="like.post" :key="like.id" class="post"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'my'">
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
<MkPagination :pagination="myPostsPagination" #default="{items}">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
</div>
</MkPagination>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import XUserList from '@client/components/user-list.vue';
import MkFolder from '@client/components/ui/folder.vue';
import MkInput from '@client/components/ui/input.vue';
import MkButton from '@client/components/ui/button.vue';
import MkTab from '@client/components/tab.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
import number from '@client/filters/number';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
XUserList,
MkFolder,
MkInput,
MkButton,
MkTab,
MkPagination,
MkGalleryPostPreview,
},
props: {
tag: {
type: String,
required: false
}
},
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.gallery,
icon: 'fas fa-icons'
},
tab: 'explore',
recentPostsPagination: {
endpoint: 'gallery/posts',
limit: 6,
},
popularPostsPagination: {
endpoint: 'gallery/featured',
limit: 5,
},
myPostsPagination: {
endpoint: 'i/gallery/posts',
limit: 5,
},
likedPostsPagination: {
endpoint: 'i/gallery/likes',
limit: 5,
},
tags: [],
};
},
computed: {
meta() {
return this.$instance;
},
tagUsers(): any {
return {
endpoint: 'hashtags/users',
limit: 30,
params: {
tag: this.tag,
origin: 'combined',
sort: '+follower',
}
};
},
},
watch: {
tag() {
if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
},
},
created() {
},
methods: {
}
});
</script>
<style lang="scss" scoped>
.xprsixdl {
max-width: 1400px;
margin: 0 auto;
}
.vfpdbgtk {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: 0 var(--margin);
> .post {
}
}
</style>

View file

@ -0,0 +1,282 @@
<template>
<div class="_root">
<transition name="fade" mode="out-in">
<div v-if="post" class="rkxwuolj">
<div class="files">
<div class="file" v-for="file in post.files" :key="file.id">
<img :src="file.url"/>
</div>
</div>
<div class="body _block">
<div class="title">{{ post.title }}</div>
<div class="description"><Mfm :text="post.description"/></div>
<div class="info">
<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
</div>
<div class="actions">
<div class="like">
<MkButton class="button" @click="unlike()" v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton>
<MkButton class="button" @click="like()" v-else v-tooltip="$ts._gallery.like"><i class="far fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton>
</div>
<div class="other">
<button v-if="$i && $i.id === post.user.id" class="_button" @click="edit" v-tooltip="$ts.edit" v-click-anime><i class="fas fa-pencil-alt fa-fw"></i></button>
<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
</div>
</div>
<div class="user">
<MkAvatar :user="post.user" class="avatar"/>
<div class="name">
<MkUserName :user="post.user" style="display: block;"/>
<MkAcct :user="post.user"/>
</div>
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div>
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
<MkPagination :pagination="otherPostsPagination" #default="{items}">
<div class="sdrarzaf">
<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
</div>
</MkPagination>
</MkContainer>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
<MkLoading v-else/>
</transition>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import MkContainer from '@client/components/ui/container.vue';
import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
import MkFollowButton from '@client/components/follow-button.vue';
import { url } from '@client/config';
export default defineComponent({
components: {
MkContainer,
ImgWithBlurhash,
MkPagination,
MkGalleryPostPreview,
MkButton,
MkFollowButton,
},
props: {
postId: {
type: String,
required: true
}
},
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.post ? {
title: this.post.title,
avatar: this.post.user,
path: `/gallery/${this.post.id}`,
share: {
title: this.post.title,
text: this.post.description,
},
actions: [{
icon: 'fas fa-pencil-alt',
text: this.$ts.edit,
handler: this.edit
}]
} : null),
otherPostsPagination: {
endpoint: 'users/gallery/posts',
limit: 6,
params: () => ({
userId: this.post.user.id
})
},
post: null,
error: null,
};
},
watch: {
postId: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
this.post = null;
os.api('gallery/posts/show', {
postId: this.postId
}).then(post => {
this.post = post;
}).catch(e => {
this.error = e;
});
},
share() {
navigator.share({
title: this.post.title,
text: this.post.description,
url: `${url}/gallery/${this.post.id}`
});
},
shareWithNote() {
os.post({
initialText: `${this.post.title} ${url}/gallery/${this.post.id}`
});
},
like() {
os.apiWithDialog('gallery/posts/like', {
postId: this.postId,
}).then(() => {
this.post.isLiked = true;
this.post.likedCount++;
});
},
async unlike() {
const confirm = await os.dialog({
type: 'warning',
showCancelButton: true,
text: this.$ts.unlikeConfirm,
});
if (confirm.canceled) return;
os.apiWithDialog('gallery/posts/unlike', {
postId: this.postId,
}).then(() => {
this.post.isLiked = false;
this.post.likedCount--;
});
},
edit() {
this.$router.push(`/gallery/${this.post.id}/edit`);
}
}
});
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.125s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.rkxwuolj {
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .body {
padding: 32px;
> .title {
font-weight: bold;
font-size: 1.2em;
margin-bottom: 16px;
}
> .info {
margin-top: 16px;
font-size: 90%;
opacity: 0.7;
}
> .actions {
display: flex;
align-items: center;
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
> .like {
> .button {
--accent: rgb(241 97 132);
--X8: rgb(241 92 128);
--buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f;
::v-deep(.count) {
margin-left: 0.5em;
}
}
}
> .other {
margin-left: auto;
> button {
padding: 8px;
margin: 0 8px;
&:hover {
color: var(--fgHighlighted);
}
}
}
}
> .user {
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
display: flex;
align-items: center;
> .avatar {
width: 52px;
height: 52px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
margin-left: auto;
}
}
}
}
.sdrarzaf {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: var(--margin);
> .post {
}
}
</style>

View file

@ -147,7 +147,6 @@ import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
import * as symbols from '@client/symbols';
import { url } from '@client/config';
const chartLimit = 90;
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
@ -449,7 +448,7 @@ export default defineComponent({
.fnfelxur {
padding: 16px;
> img {
> .icon {
display: block;
margin: auto;
height: 64px;

View file

@ -0,0 +1,126 @@
<template>
<div class="uqshojas">
<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<section class="_card _gap ads" v-for="ad in ads">
<div class="_content ad">
<MkAd v-if="ad.url" :specify="ad"/>
<MkInput v-model:value="ad.url" type="url">
<span>URL</span>
</MkInput>
<MkInput v-model:value="ad.imageUrl">
<span>{{ $ts.imageUrl }}</span>
</MkInput>
<div style="margin: 32px 0;">
<MkRadio v-model="ad.place" value="square">square</MkRadio>
<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
</div>
<div style="margin: 32px 0;">
{{ $ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
</div>
<MkInput v-model:value="ad.expiresAt" type="date">
<span>{{ $ts.expiration }}</span>
</MkInput>
<MkTextarea v-model:value="ad.memo">
<span>{{ $ts.memo }}</span>
</MkTextarea>
<div class="buttons">
<MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import MkTextarea from '@client/components/ui/textarea.vue';
import MkRadio from '@client/components/ui/radio.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
MkRadio,
},
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.ads,
icon: 'fas fa-audio-description'
},
ads: [],
}
},
created() {
os.api('admin/ad/list').then(ads => {
this.ads = ads;
});
},
mounted() {
this.$emit('info', this[symbols.PAGE_INFO]);
},
methods: {
add() {
this.ads.unshift({
id: null,
memo: '',
place: 'square',
priority: 'middle',
url: '',
imageUrl: null,
expiresAt: null,
});
},
remove(ad) {
os.dialog({
type: 'warning',
text: this.$t('removeAreYouSure', { x: ad.url }),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.ads = this.ads.filter(x => x != ad);
os.apiWithDialog('admin/ad/delete', {
id: ad.id
});
});
},
save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
}
}
}
});
</script>
<style lang="scss" scoped>
.uqshojas {
margin: var(--margin);
}
</style>

View file

@ -23,6 +23,7 @@
<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
<FormLink :active="page === 'ads'" replace to="/instance/ads"><template #icon><i class="fas fa-audio-description"></i></template>{{ $ts.ads }}</FormLink>
<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
</FormGroup>
<FormGroup>
@ -102,6 +103,7 @@ export default defineComponent({
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));

View file

@ -25,7 +25,6 @@ export default defineComponent({
endpoint: 'notes/mentions',
limit: 10,
},
faAt
};
},

View file

@ -1,35 +1,61 @@
<template>
<div class="xcukqgmh _root" v-if="page" :key="page.id" v-size="{ max: [450] }">
<div class="_block main">
<!--
<div class="header">
<h1>{{ page.title }}</h1>
<div class="_root">
<transition name="fade" mode="out-in">
<div v-if="page" class="xcukqgmh" :key="page.id" v-size="{ max: [450] }">
<div class="_block main">
<!--
<div class="header">
<h1>{{ page.title }}</h1>
</div>
-->
<div class="banner">
<img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
</div>
<div class="content">
<XPage :page="page"/>
</div>
<div class="actions">
<div class="like">
<MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
<MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><i class="far fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
</div>
<div class="other">
<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
</div>
</div>
<div class="user">
<MkAvatar :user="page.user" class="avatar"/>
<div class="name">
<MkUserName :user="page.user" style="display: block;"/>
<MkAcct :user="page.user"/>
</div>
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div>
<div class="links">
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
<template v-if="$i && $i.id === page.userId">
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
<button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>
<button v-else @click="pin(true)" class="link _textButton">{{ $ts.pin }}</button>
</template>
</div>
</div>
<div class="footer">
<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
<MkPagination :pagination="otherPostsPagination" #default="{items}">
<MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/>
</MkPagination>
</MkContainer>
</div>
-->
<div class="banner">
<img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
</div>
<div class="content">
<XPage :page="page"/>
<small style="display: block; opacity: 0.7; margin-top: 1em;">@{{ page.user.username }}</small>
</div>
<div class="like">
<MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
<MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><i class="far fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
</div>
<div class="links">
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
<template v-if="$i && $i.id === page.userId">
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
<button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>
<button v-else @click="pin(true)" class="link _textButton">{{ $ts.pin }}</button>
</template>
</div>
</div>
<div class="footer">
<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
<MkLoading v-else/>
</transition>
</div>
</template>
@ -39,11 +65,20 @@ import XPage from '@client/components/page/page.vue';
import MkButton from '@client/components/ui/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { url } from '@client/config';
import MkFollowButton from '@client/components/follow-button.vue';
import MkContainer from '@client/components/ui/container.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import MkPagePreview from '@client/components/page-preview.vue';
export default defineComponent({
components: {
XPage,
MkButton,
MkFollowButton,
MkContainer,
MkPagination,
MkPagePreview,
},
props: {
@ -69,6 +104,14 @@ export default defineComponent({
},
} : null),
page: null,
error: null,
otherPostsPagination: {
endpoint: 'users/pages',
limit: 6,
params: () => ({
userId: this.page.user.id
})
},
};
},
@ -90,11 +133,28 @@ export default defineComponent({
methods: {
fetch() {
this.page = null;
os.api('pages/show', {
name: this.pageName,
username: this.username,
}).then(page => {
this.page = page;
}).catch(e => {
this.error = e;
});
},
share() {
navigator.share({
title: this.page.title || this.page.name,
text: this.page.summary,
url: `${url}/@${this.page.user.username}/pages/${this.page.name}`
});
},
shareWithNote() {
os.post({
initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}`
});
},
@ -132,6 +192,15 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.125s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.xcukqgmh {
--padding: 32px;
@ -140,6 +209,8 @@ export default defineComponent({
}
> .main {
padding: var(--padding);
> .header {
padding: 16px;
@ -150,35 +221,79 @@ export default defineComponent({
> .banner {
> img {
// TODO:
display: block;
width: 100%;
height: 120px;
height: 150px;
object-fit: cover;
}
}
> .content {
padding: var(--padding);
margin-top: 16px;
padding: 16px 0 0 0;
}
> .like {
padding: var(--padding);
> .actions {
display: flex;
align-items: center;
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
> .button {
--accent: rgb(216 71 106);
--X8: rgb(241 92 128);
--buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%);
> .like {
> .button {
--accent: rgb(241 97 132);
--X8: rgb(241 92 128);
--buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f;
::v-deep(.count) {
margin-left: 0.5em;
::v-deep(.count) {
margin-left: 0.5em;
}
}
}
> .other {
margin-left: auto;
> button {
padding: 8px;
margin: 0 8px;
&:hover {
color: var(--fgHighlighted);
}
}
}
}
> .user {
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
display: flex;
align-items: center;
> .avatar {
width: 52px;
height: 52px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
margin-left: auto;
}
}
> .links {
padding: var(--padding);
margin-top: 16px;
padding: 24px 0 0 0;
border-top: solid 0.5px var(--divider);
> .link {

View file

@ -0,0 +1,69 @@
<template>
<FormBase v-if="token">
<FormInput v-model:value="password" type="password">
<template #prefix><i class="fas fa-lock"></i></template>
<span>{{ $ts.newPassword }}</span>
</FormInput>
<FormButton primary @click="save">{{ $ts.save }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormLink from '@client/components/form/link.vue';
import FormBase from '@client/components/form/base.vue';
import FormGroup from '@client/components/form/group.vue';
import FormInput from '@client/components/form/input.vue';
import FormButton from '@client/components/form/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
FormBase,
FormGroup,
FormLink,
FormInput,
FormButton,
},
props: {
token: {
type: String,
required: false
}
},
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.resetPassword,
icon: 'fas fa-lock'
},
password: '',
}
},
mounted() {
if (this.token == null) {
os.popup(import('@client/components/forgot-password.vue'), {}, {}, 'closed');
this.$router.push('/');
}
},
methods: {
async save() {
await os.apiWithDialog('reset-password', {
token: this.token,
password: this.password,
});
this.$router.push('/');
}
}
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -157,7 +157,6 @@ export default defineComponent({
maps: maps,
form: null,
messages: [],
fasCircle, farCircle
};
},

View file

@ -10,6 +10,7 @@
</div>
<FormLink :active="page === 'accounts'" replace to="/settings/accounts"><template #icon><i class="fas fa-users"></i></template>{{ $ts.accounts }}</FormLink>
</FormGroup>
<FormInfo v-if="emailNotConfigured" warn>{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></FormInfo>
<FormGroup>
<template #label>{{ $ts.basicSettings }}</template>
<FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><i class="fas fa-user"></i></template>{{ $ts.profile }}</FormLink>
@ -58,10 +59,13 @@ import FormLink from '@client/components/form/link.vue';
import FormGroup from '@client/components/form/group.vue';
import FormBase from '@client/components/form/base.vue';
import FormButton from '@client/components/form/button.vue';
import FormInfo from '@client/components/form/info.vue';
import { scroll } from '@client/scripts/scroll';
import { signout } from '@client/account';
import { unisonReload } from '@client/scripts/unison-reload';
import * as symbols from '@client/symbols';
import { instance } from '@client/instance';
import { $i } from '@client/account';
export default defineComponent({
components: {
@ -69,6 +73,7 @@ export default defineComponent({
FormLink,
FormGroup,
FormButton,
FormInfo,
},
props: {
@ -173,6 +178,8 @@ export default defineComponent({
}
});
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
return {
[symbols.PAGE_INFO]: INFO,
page,
@ -182,6 +189,7 @@ export default defineComponent({
onInfo,
pageProps,
component,
emailNotConfigured,
logout: () => {
signout();
},

View file

@ -18,6 +18,7 @@
<button class="_button tab" @click="chooseList" :class="{ active: src === 'list' }" v-tooltip="$ts.lists"><i class="fas fa-list-ul"></i></button>
</div>
</div>
<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
<XTimeline ref="tl"
class="_gap"
:key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
@ -30,7 +31,6 @@
@after="after()"
@queue="queueUpdated"
/>
<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
</div>
</template>

View file

@ -0,0 +1,63 @@
<template>
<div>
<MkPagination :pagination="pagination" #default="{items}">
<div class="jrnovfpt">
<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
</div>
</MkPagination>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import { userPage, acct } from '../../filters/user';
export default defineComponent({
components: {
MkPagination,
MkGalleryPostPreview,
},
props: {
user: {
type: Object,
required: true
},
},
data() {
return {
pagination: {
endpoint: 'users/gallery/posts',
limit: 6,
params: () => ({
userId: this.user.id
})
},
};
},
watch: {
user() {
this.$refs.list.reload();
}
},
methods: {
userPage,
acct
}
});
</script>
<style lang="scss" scoped>
.jrnovfpt {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: var(--margin);
}
</style>

View file

@ -161,15 +161,15 @@
</dl>
</div>
<div class="status">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }" v-click-anime>
<b>{{ number(user.notesCount) }}</b>
<span>{{ $ts.notes }}</span>
</MkA>
<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }" v-click-anime>
<b>{{ number(user.followingCount) }}</b>
<span>{{ $ts.following }}</span>
</MkA>
<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }" v-click-anime>
<b>{{ number(user.followersCount) }}</b>
<span>{{ $ts.followers }}</span>
</MkA>
@ -179,18 +179,22 @@
<div class="contents">
<div class="nav _gap">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link" v-click-anime>
<i class="fas fa-comment-alt icon"></i>
<span>{{ $ts.notes }}</span>
</MkA>
<MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link">
<MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link" v-click-anime>
<i class="fas fa-paperclip icon"></i>
<span>{{ $ts.clips }}</span>
</MkA>
<MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link">
<MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link" v-click-anime>
<i class="fas fa-file-alt icon"></i>
<span>{{ $ts.pages }}</span>
</MkA>
<MkA :to="userPage(user, 'gallery')" :class="{ active: page === 'gallery' }" class="link" v-click-anime>
<i class="fas fa-icons icon"></i>
<span>{{ $ts.gallery }}</span>
</MkA>
</div>
<template v-if="page === 'index'">
@ -210,6 +214,7 @@
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
</div>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
@ -250,6 +255,7 @@ export default defineComponent({
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
XClips: defineAsyncComponent(() => import('./clips.vue')),
XPages: defineAsyncComponent(() => import('./pages.vue')),
XGallery: defineAsyncComponent(() => import('./gallery.vue')),
XPhotos: defineAsyncComponent(() => import('./index.photos.vue')),
XActivity: defineAsyncComponent(() => import('./index.activity.vue')),
},
@ -770,8 +776,7 @@ export default defineComponent({
> .nav {
display: flex;
align-items: center;
//font-size: 120%;
font-weight: bold;
font-size: 90%;
> .link {
flex: 1;

View file

@ -70,6 +70,8 @@ export default defineComponent({
border-radius: var(--radius);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
overflow: hidden;
max-width: 500px;
margin: 32px auto;
> h1 {
margin: 0;

View file

@ -23,6 +23,7 @@ export const router = createRouter({
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
{ path: '/@:acct/room', props: true, component: page('room/room') },
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
{ path: '/announcements', component: page('announcements') },
{ path: '/about', component: page('about') },
{ path: '/about-misskey', component: page('about-misskey') },
@ -37,6 +38,10 @@ export const router = createRouter({
{ path: '/pages', name: 'pages', component: page('pages') },
{ path: '/pages/new', component: page('page-editor/page-editor') },
{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/gallery', component: page('gallery/index') },
{ path: '/gallery/new', component: page('gallery/edit') },
{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
{ path: '/channels', component: page('channels') },
{ path: '/channels/new', component: page('channel-editor') },
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },

View file

@ -91,8 +91,10 @@ export default (opts) => ({
...params,
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
}).then(items => {
for (const item of items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
markRaw(item);
if (i === 3) item._shouldInsertAd_ = true;
}
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
items.pop();
@ -128,8 +130,10 @@ export default (opts) => ({
untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
}),
}).then(items => {
for (const item of items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
markRaw(item);
if (i === 10) item._shouldInsertAd_ = true;
}
if (items.length > SECOND_FETCH_LIMIT) {
items.pop();

View file

@ -18,9 +18,11 @@ export const builtinThemes = [
require('@client/themes/l-light.json5'),
require('@client/themes/l-apricot.json5'),
require('@client/themes/l-rainy.json5'),
require('@client/themes/l-vivid.json5'),
require('@client/themes/d-dark.json5'),
require('@client/themes/d-persimmon.json5'),
require('@client/themes/d-astro.json5'),
require('@client/themes/d-black.json5'),
] as Theme[];

View file

@ -97,6 +97,11 @@ export const sidebarDef = {
icon: 'fas fa-file-alt',
to: '/pages',
},
gallery: {
title: 'gallery',
icon: 'fas fa-icons',
to: '/gallery',
},
clips: {
title: 'clip',
icon: 'fas fa-paperclip',

View file

@ -62,8 +62,9 @@ export const defaultStore = markRaw(new Storage('base', {
'notifications',
'messaging',
'drive',
'-',
'followRequests',
'-',
'gallery',
'featured',
'explore',
'announcements',

View file

@ -11,6 +11,8 @@
@media (max-width: 500px) {
--margin: var(--marginHalf);
}
//--ad: rgb(255 169 0 / 10%);
}
::selection {
@ -337,7 +339,7 @@ hr {
}
._monolithic_ {
._section {
._section:not(:empty) {
box-sizing: border-box;
padding: var(--root-margin, 32px);
@ -522,13 +524,18 @@ hr {
}
._anime_bounce {
will-change: transform;
animation: bounce ease 0.7s;
animation-iteration-count: 1;
transform-origin: 50% 50%;
}
._anime_bounce_ready {
will-change: transform;
transform: scaleX(0.90) scaleY(0.90) ;
}
._anime_bounce_standBy {
transition: transform 0.1s ease;
}
@keyframes bounce{
0% {

View file

@ -0,0 +1,76 @@
{
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
base: 'dark',
name: 'Mi Astro',
author: 'syuilo',
props: {
bg: '#232125',
fg: '#efdab9',
cwBg: '#687390',
cwFg: '#393f4f',
link: '#78b0a0',
warn: '#ecb637',
badge: '#31b1ce',
error: '#ec4137',
focus: ':alpha<0.3<@accent',
navBg: '@panel',
navFg: '@fg',
panel: '#2a272b',
accent: '#81c08b',
header: ':alpha<0.7<@bg',
infoBg: '#253142',
infoFg: '#fff',
renote: '#659CC8',
shadow: 'rgba(0, 0, 0, 0.3)',
divider: 'rgba(255, 255, 255, 0.1)',
hashtag: '#ff9156',
mention: '#ffd152',
modalBg: 'rgba(0, 0, 0, 0.5)',
success: '#86b300',
buttonBg: 'rgba(255, 255, 255, 0.05)',
acrylicBg: ':alpha<0.5<@bg',
cwHoverBg: '#707b97',
indicator: '@accent',
mentionMe: '#fb5d38',
messageBg: ':lighten<5<@bg',
navActive: '@accent',
infoWarnBg: '#42321c',
infoWarnFg: '#ffbd3e',
navHoverFg: ':lighten<17<@fg',
dateLabelFg: '@fg',
inputBorder: '#959da2',
panelBorder: 'rgba(0, 0, 0, 0)',
panelShadow: '" 0 8px 24px rgba(0, 0, 0, 0.12)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':lighten<3<@fg',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',
panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.3)',
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
},
}

View file

@ -10,7 +10,7 @@
props: {
bg: '#f9f9f9',
fg: '#676767',
divider: 'rgb(223, 223, 223)',
divider: '#e8e8e8',
header: ':alpha<0.7<@panel',
navBg: '#fff',
panel: '#fff',

View file

@ -0,0 +1,82 @@
{
id: '6128c2a9-5c54-43fe-a47d-17942356470b',
name: 'Mi Vivid',
author: 'syuilo',
base: 'light',
props: {
bg: '#fafafa',
fg: '#444',
cwBg: '#b1b9c1',
cwFg: '#fff',
link: '#ff9400',
warn: '#ecb637',
badge: '#31b1ce',
error: '#ec4137',
focus: ':alpha<0.3<@accent',
navBg: '@panel',
navFg: '@fg',
panel: '#fff',
accent: '#008cff',
header: ':alpha<0.7<@panel',
infoBg: '#e5f5ff',
infoFg: '#72818a',
renote: '@accent',
shadow: 'rgba(0, 0, 0, 0.1)',
divider: 'rgba(0, 0, 0, 0.08)',
hashtag: '#92d400',
mention: '@accent',
modalBg: 'rgba(0, 0, 0, 0.3)',
success: '#86b300',
buttonBg: 'rgba(0, 0, 0, 0.05)',
acrylicBg: ':alpha<0.5<@bg',
cwHoverBg: '#bbc4ce',
indicator: '@accent',
mentionMe: '@mention',
messageBg: '@panel',
navActive: '@accent',
infoWarnBg: '#fff0db',
infoWarnFg: '#573c08',
navHoverFg: ':darken<17<@fg',
dateLabelFg: '@fg',
inputBorder: '#dae0e4',
panelBorder: 'rgba(0, 0, 0, 0)',
panelShadow: '" 0 8px 24px rgb(21 43 75 / 8%)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':darken<3<@fg',
fgTransparent: ':alpha<0.5<@fg',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',
panelHighlight: ':darken<3<@panel',
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
fgTransparentWeak: ':alpha<0.75<@fg',
panelHeaderDivider: '@divider',
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',
X4: 'rgba(0, 0, 0, 0.1)',
X5: 'rgba(0, 0, 0, 0.05)',
X6: 'rgba(0, 0, 0, 0.25)',
X7: 'rgba(0, 0, 0, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.1)',
X12: 'rgba(0, 0, 0, 0.1)',
X13: 'rgba(0, 0, 0, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
}

View file

@ -42,11 +42,7 @@ export default defineComponent({
if (
i != this.items.length - 1 &&
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
!item._prId_ &&
!this.items[i + 1]._prId_ &&
!item._featuredId_ &&
!this.items[i + 1]._featuredId_
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
) {
const separator = h('div', {
class: 'separator',

View file

@ -313,7 +313,7 @@ export default defineComponent({
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{

View file

@ -64,7 +64,7 @@ export default defineComponent({
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{

View file

@ -36,7 +36,6 @@ export default defineComponent({
endpoint: 'notes/mentions',
limit: 10,
},
faAt
}
},

View file

@ -31,8 +31,10 @@
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
</MkA>
<div class="divider"></div>
<div class="foo">
<MkEmoji :normal="true" :no-style="true" emoji="🍮"/>
<div class="about">
<MkA class="link" to="/about" v-click-anime>
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" class="_ghost"/>
</MkA>
</div>
<!--<MisskeyLogo class="misskey"/>-->
</div>
@ -260,14 +262,21 @@ export default defineComponent({
}
}
> .misskey {
> .about {
fill: currentColor;
}
> .foo {
text-align: center;
padding: 8px 0 16px 0;
opacity: 0.5;
text-align: center;
> .link {
display: block;
width: 32px;
margin: 0 auto;
img {
display: block;
width: 100%;
}
}
}
> .item {

View file

@ -165,7 +165,7 @@ export default defineComponent({
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{

View file

@ -1,6 +1,7 @@
<template>
<div class="efzpzdvf">
<div class="ddiqwdnk">
<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
<MkAd class="a" prefer="square"/>
<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
@ -56,13 +57,14 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.efzpzdvf {
.ddiqwdnk {
position: sticky;
height: min-content;
box-sizing: border-box;
padding-bottom: 8px;
> .widgets {
> .widgets,
> .a {
width: 300px;
}

View file

@ -191,7 +191,7 @@ export default defineComponent({
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{