mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-23 12:23:09 +02:00
ギャラリー投稿の編集と削除
This commit is contained in:
parent
8bce241170
commit
42539575a6
6 changed files with 301 additions and 111 deletions
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>
|
|
@ -1,110 +0,0 @@
|
||||||
<template>
|
|
||||||
<FormBase>
|
|
||||||
<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 @click="publish" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
|
|
||||||
</FormBase>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { 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 { 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,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
[symbols.PAGE_INFO]: {
|
|
||||||
title: this.$ts.postToGallery,
|
|
||||||
icon: 'fas fa-pencil-alt'
|
|
||||||
},
|
|
||||||
files: [],
|
|
||||||
description: null,
|
|
||||||
title: null,
|
|
||||||
isSensitive: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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 publish() {
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</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>
|
|
|
@ -19,6 +19,7 @@
|
||||||
<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>
|
<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>
|
||||||
<div class="other">
|
<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="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>
|
<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,6 +85,11 @@ export default defineComponent({
|
||||||
title: this.post.title,
|
title: this.post.title,
|
||||||
text: this.post.description,
|
text: this.post.description,
|
||||||
},
|
},
|
||||||
|
actions: [{
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
text: this.$ts.edit,
|
||||||
|
handler: this.edit
|
||||||
|
}]
|
||||||
} : null),
|
} : null),
|
||||||
otherPostsPagination: {
|
otherPostsPagination: {
|
||||||
endpoint: 'users/gallery/posts',
|
endpoint: 'users/gallery/posts',
|
||||||
|
@ -154,6 +160,10 @@ export default defineComponent({
|
||||||
this.post.likedCount--;
|
this.post.likedCount--;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
this.$router.push(`/gallery/${this.post.id}/edit`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -38,7 +38,8 @@ export const router = createRouter({
|
||||||
{ path: '/pages/new', component: page('page-editor/page-editor') },
|
{ 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: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
|
||||||
{ path: '/gallery', component: page('gallery/index') },
|
{ path: '/gallery', component: page('gallery/index') },
|
||||||
{ path: '/gallery/new', component: page('gallery/new') },
|
{ 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: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
|
||||||
{ path: '/channels', component: page('channels') },
|
{ path: '/channels', component: page('channels') },
|
||||||
{ path: '/channels/new', component: page('channel-editor') },
|
{ path: '/channels/new', component: page('channel-editor') },
|
||||||
|
|
40
src/server/api/endpoints/gallery/posts/delete.ts
Normal file
40
src/server/api/endpoints/gallery/posts/delete.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import define from '../../../define';
|
||||||
|
import { ApiError } from '../../../error';
|
||||||
|
import { GalleryPosts } from '../../../../../models';
|
||||||
|
import { ID } from '@/misc/cafy-id';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['gallery'],
|
||||||
|
|
||||||
|
requireCredential: true as const,
|
||||||
|
|
||||||
|
kind: 'write:gallery',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
postId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchPost: {
|
||||||
|
message: 'No such post.',
|
||||||
|
code: 'NO_SUCH_POST',
|
||||||
|
id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
const post = await GalleryPosts.findOne({
|
||||||
|
id: ps.postId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (post == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
await GalleryPosts.delete(post.id);
|
||||||
|
});
|
81
src/server/api/endpoints/gallery/posts/update.ts
Normal file
81
src/server/api/endpoints/gallery/posts/update.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import * as ms from 'ms';
|
||||||
|
import define from '../../../define';
|
||||||
|
import { ID } from '../../../../../misc/cafy-id';
|
||||||
|
import { DriveFiles, GalleryPosts } from '../../../../../models';
|
||||||
|
import { GalleryPost } from '../../../../../models/entities/gallery-post';
|
||||||
|
import { ApiError } from '../../../error';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['gallery'],
|
||||||
|
|
||||||
|
requireCredential: true as const,
|
||||||
|
|
||||||
|
kind: 'write:gallery',
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: ms('1hour'),
|
||||||
|
max: 300
|
||||||
|
},
|
||||||
|
|
||||||
|
params: {
|
||||||
|
postId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
validator: $.str.min(1),
|
||||||
|
},
|
||||||
|
|
||||||
|
description: {
|
||||||
|
validator: $.optional.nullable.str,
|
||||||
|
},
|
||||||
|
|
||||||
|
fileIds: {
|
||||||
|
validator: $.arr($.type(ID)).unique().range(1, 32),
|
||||||
|
},
|
||||||
|
|
||||||
|
isSensitive: {
|
||||||
|
validator: $.optional.bool,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object' as const,
|
||||||
|
optional: false as const, nullable: false as const,
|
||||||
|
ref: 'GalleryPost',
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
const files = (await Promise.all(ps.fileIds.map(fileId =>
|
||||||
|
DriveFiles.findOne({
|
||||||
|
id: fileId,
|
||||||
|
userId: user.id
|
||||||
|
})
|
||||||
|
))).filter(file => file != null);
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
await GalleryPosts.update({
|
||||||
|
id: ps.postId,
|
||||||
|
userId: user.id,
|
||||||
|
}, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
title: ps.title,
|
||||||
|
description: ps.description,
|
||||||
|
isSensitive: ps.isSensitive,
|
||||||
|
fileIds: files.map(file => file.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await GalleryPosts.findOneOrFail(ps.postId);
|
||||||
|
|
||||||
|
return await GalleryPosts.pack(post, user);
|
||||||
|
});
|
Loading…
Reference in a new issue