Merge branch 'develop' into sw-notification-action
This commit is contained in:
@@ -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>
|
||||
|
168
src/client/pages/gallery/edit.vue
Normal file
168
src/client/pages/gallery/edit.vue
Normal 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>
|
152
src/client/pages/gallery/index.vue
Normal file
152
src/client/pages/gallery/index.vue
Normal 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>
|
282
src/client/pages/gallery/post.vue
Normal file
282
src/client/pages/gallery/post.vue
Normal 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>
|
@@ -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;
|
||||
|
126
src/client/pages/instance/ads.vue
Normal file
126
src/client/pages/instance/ads.vue
Normal 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>
|
@@ -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'));
|
||||
|
@@ -25,7 +25,6 @@ export default defineComponent({
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
},
|
||||
faAt
|
||||
};
|
||||
},
|
||||
|
||||
|
@@ -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 {
|
||||
|
69
src/client/pages/reset-password.vue
Normal file
69
src/client/pages/reset-password.vue
Normal 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>
|
@@ -157,7 +157,6 @@ export default defineComponent({
|
||||
maps: maps,
|
||||
form: null,
|
||||
messages: [],
|
||||
fasCircle, farCircle
|
||||
};
|
||||
},
|
||||
|
||||
|
@@ -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();
|
||||
},
|
||||
|
@@ -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>
|
||||
|
||||
|
63
src/client/pages/user/gallery.vue
Normal file
63
src/client/pages/user/gallery.vue
Normal 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>
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user