From 8e8459fa559865428963b08e6422fba9617240f5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 15 Nov 2020 12:34:47 +0900
Subject: [PATCH] wip: clip

---
 locales/ja-JP.yml                        |   1 +
 src/client/pages/clip.vue                | 139 +++++++++++++++++++++++
 src/client/pages/my-clips/index.vue      |  32 +++++-
 src/client/router.ts                     |   1 +
 src/models/repositories/clip.ts          |  12 ++
 src/server/api/endpoints/clips/update.ts |  12 +-
 6 files changed, 193 insertions(+), 4 deletions(-)
 create mode 100644 src/client/pages/clip.vue

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ff39eec153..7d8ba1753f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -215,6 +215,7 @@ imageUrl: "画像URL"
 remove: "削除"
 removed: "削除しました"
 removeAreYouSure: "「{x}」を削除しますか?"
+deleteAreYouSure: "「{x}」を削除しますか?"
 resetAreYouSure: "リセットしますか?"
 saved: "保存しました"
 messaging: "チャット"
diff --git a/src/client/pages/clip.vue b/src/client/pages/clip.vue
new file mode 100644
index 0000000000..ad9e076fd6
--- /dev/null
+++ b/src/client/pages/clip.vue
@@ -0,0 +1,139 @@
+<template>
+<div v-if="clip" class="_section">
+	<div class="okzinsic _content _panel _vMargin">
+		<div class="description" v-if="clip.description">
+			<Mfm :text="clip.description" :is-note="false" :i="$store.state.i"/>
+		</div>
+	</div>
+
+	<XNotes class="_content _vMargin" :pagination="pagination" :detail="true"/>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { faEllipsisH, faPaperclip, faPencilAlt, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import MkContainer from '@/components/ui/container.vue';
+import XPostForm from '@/components/post-form.vue';
+import XNotes from '@/components/notes.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkContainer,
+		XPostForm,
+		XNotes,
+	},
+
+	props: {
+		clipId: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			INFO: computed(() => this.clip ? {
+				title: this.clip.name,
+				icon: faPaperclip,
+				action: {
+					icon: faEllipsisH,
+					handler: this.menu
+				}
+			} : null),
+			clip: null,
+			pagination: {
+				endpoint: 'clips/notes',
+				limit: 10,
+				params: () => ({
+					clipId: this.clipId,
+				})
+			},
+		};
+	},
+
+	computed: {
+		isOwned(): boolean {
+			return this.$store.getters.isSignedIn && this.clip && (this.$store.state.i.id === this.clip.userId);
+		}
+	},
+
+	watch: {
+		clipId: {
+			async handler() {
+				this.clip = await os.api('clips/show', {
+					clipId: this.clipId,
+				});
+			},
+			immediate: true
+		}
+	},
+
+	created() {
+
+	},
+
+	methods: {
+		menu(ev) {
+			os.modalMenu([this.isOwned ? {
+				icon: faPencilAlt,
+				text: this.$t('edit'),
+				action: async () => {
+					const { canceled, result } = await os.form(this.clip.name, {
+						name: {
+							type: 'string',
+							label: this.$t('name'),
+							default: this.clip.name
+						},
+						description: {
+							type: 'string',
+							required: false,
+							multiline: true,
+							label: this.$t('description'),
+							default: this.clip.description
+						},
+						isPublic: {
+							type: 'boolean',
+							label: this.$t('public'),
+							default: this.clip.isPublic
+						}
+					});
+					if (canceled) return;
+
+					os.apiWithDialog('clips/update', {
+						clipId: this.clip.id,
+						...result
+					});
+				}
+			} : undefined, this.isOwned ? {
+				icon: faTrashAlt,
+				text: this.$t('delete'),
+				danger: true,
+				action: async () => {
+					const { canceled } = await os.dialog({
+						type: 'warning',
+						text: this.$t('deleteAreYouSure', { x: this.clip.name }),
+						showCancelButton: true
+					});
+					if (canceled) return;
+
+					await os.apiWithDialog('clips/delete', {
+						clipId: this.clip.id,
+					});
+				}
+			} : undefined], ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.okzinsic {
+	position: relative;
+
+	> .description {
+		padding: 16px;
+	}
+}
+</style>
diff --git a/src/client/pages/my-clips/index.vue b/src/client/pages/my-clips/index.vue
index 93adb94a4b..efead39fcb 100644
--- a/src/client/pages/my-clips/index.vue
+++ b/src/client/pages/my-clips/index.vue
@@ -1,10 +1,13 @@
 <template>
-<div class="_section">
+<div class="_section qtcaoidl">
 	<MkButton @click="create" primary class="add"><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
 
 	<div class="_content">
-		<MkPagination :pagination="pagination" #default="{items}" ref="list">
-			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">{{ item.name }}</MkA>
+		<MkPagination :pagination="pagination" #default="{items}" ref="list" class="list">
+			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin">
+				<b>{{ item.name }}</b>
+				<div v-if="item.description" class="description">{{ item.description }}</div>
+			</MkA>
 		</MkPagination>
 	</div>
 </div>
@@ -76,3 +79,26 @@ export default defineComponent({
 	}
 });
 </script>
+
+<style lang="scss" scoped>
+.qtcaoidl {
+	> .add {
+		margin: 0 auto 16px auto;
+	}
+
+	> ._content {
+		> .list {
+			> .item {
+				display: block;
+				padding: 16px;
+
+				> .description {
+					margin-top: 8px;
+					padding-top: 8px;
+					border-top: solid 1px var(--divider);
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/router.ts b/src/client/router.ts
index da2945be2c..413e72c320 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -37,6 +37,7 @@ export const router = createRouter({
 		{ path: '/channels/new', component: page('channel-editor') },
 		{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
 		{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
+		{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
 		{ path: '/my/notifications', component: page('notifications') },
 		{ path: '/my/favorites', component: page('favorites') },
 		{ path: '/my/messages', component: page('messages') },
diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts
index 7cc3fb7110..f5c70a1829 100644
--- a/src/models/repositories/clip.ts
+++ b/src/models/repositories/clip.ts
@@ -15,8 +15,10 @@ export class ClipRepository extends Repository<Clip> {
 		return {
 			id: clip.id,
 			createdAt: clip.createdAt.toISOString(),
+			userId: clip.userId,
 			name: clip.name,
 			description: clip.description,
+			isPublic: clip.isPublic,
 		};
 	}
 }
@@ -38,6 +40,11 @@ export const packedClipSchema = {
 			format: 'date-time',
 			description: 'The date that the Clip was created.'
 		},
+		userId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
 		name: {
 			type: 'string' as const,
 			optional: false as const, nullable: false as const,
@@ -48,5 +55,10 @@ export const packedClipSchema = {
 			optional: false as const, nullable: true as const,
 			description: 'The description of the Clip.'
 		},
+		isPublic: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+			description: 'Whether this Clip is public.',
+		},
 	},
 };
diff --git a/src/server/api/endpoints/clips/update.ts b/src/server/api/endpoints/clips/update.ts
index 483941214c..4a1a31eb95 100644
--- a/src/server/api/endpoints/clips/update.ts
+++ b/src/server/api/endpoints/clips/update.ts
@@ -18,6 +18,14 @@ export const meta = {
 
 		name: {
 			validator: $.str.range(1, 100),
+		},
+
+		isPublic: {
+			validator: $.optional.bool
+		},
+
+		description: {
+			validator: $.optional.nullable.str.range(1, 2048)
 		}
 	},
 
@@ -42,7 +50,9 @@ export default define(meta, async (ps, user) => {
 	}
 
 	await Clips.update(clip.id, {
-		name: ps.name
+		name: ps.name,
+		description: ps.description,
+		isPublic: ps.isPublic,
 	});
 
 	return await Clips.pack(clip.id);