♻️ Interactive v2 #1
| @@ -7,6 +7,7 @@ type Article struct { | ||||
| 	Hashtags    []Tag        `json:"tags" gorm:"many2many:article_tags"` | ||||
| 	Categories  []Category   `json:"categories" gorm:"many2many:article_categories"` | ||||
| 	Reactions   []Reaction   `json:"reactions"` | ||||
| 	Attachments []Attachment `json:"attachments"` | ||||
| 	Description string       `json:"description"` | ||||
| 	Content     string       `json:"content"` | ||||
| 	RealmID     *uint        `json:"realm_id"` | ||||
|   | ||||
| @@ -2,8 +2,9 @@ package models | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/spf13/viper" | ||||
| ) | ||||
|  | ||||
| type AttachmentType = uint8 | ||||
|   | ||||
| @@ -16,5 +16,7 @@ type Feed struct { | ||||
| 	RealmID  *uint `json:"realm_id"` | ||||
|  | ||||
| 	Author Account `json:"author" gorm:"embedded"` | ||||
|  | ||||
| 	Attachments  []Attachment     `json:"attachments" gorm:"-"` | ||||
| 	ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ type Moment struct { | ||||
| 	Hashtags    []Tag        `json:"tags" gorm:"many2many:moment_tags"` | ||||
| 	Categories  []Category   `json:"categories" gorm:"many2many:moment_categories"` | ||||
| 	Reactions   []Reaction   `json:"reactions"` | ||||
| 	Attachments []Attachment `json:"attachments"` | ||||
| 	RealmID     *uint        `json:"realm_id"` | ||||
| 	RepostID    *uint        `json:"repost_id"` | ||||
| 	Realm       *Realm       `json:"realm"` | ||||
|   | ||||
| @@ -16,7 +16,6 @@ type PostBase struct { | ||||
| 	BaseModel | ||||
|  | ||||
| 	Alias       string     `json:"alias" gorm:"uniqueIndex"` | ||||
| 	Attachments []Attachment `json:"attachments"` | ||||
| 	PublishedAt *time.Time `json:"published_at"` | ||||
|  | ||||
| 	AuthorID uint    `json:"author_id"` | ||||
|   | ||||
| @@ -45,12 +45,12 @@ func createArticle(c *fiber.Ctx) error { | ||||
| 	item := &models.Article{ | ||||
| 		PostBase: models.PostBase{ | ||||
| 			Alias:       data.Alias, | ||||
| 			Attachments: data.Attachments, | ||||
| 			PublishedAt: data.PublishedAt, | ||||
| 			AuthorID:    user.ID, | ||||
| 		}, | ||||
| 		Hashtags:    data.Hashtags, | ||||
| 		Categories:  data.Categories, | ||||
| 		Attachments: data.Attachments, | ||||
| 		Title:       data.Title, | ||||
| 		Description: data.Description, | ||||
| 		Content:     data.Content, | ||||
|   | ||||
| @@ -61,7 +61,6 @@ func createComment(c *fiber.Ctx) error { | ||||
| 		PublishedAt *time.Time        `json:"published_at" form:"published_at"` | ||||
| 		Hashtags    []models.Tag      `json:"hashtags" form:"hashtags"` | ||||
| 		Categories  []models.Category `json:"categories" form:"categories"` | ||||
| 		Attachments []models.Attachment `json:"attachments" form:"attachments"` | ||||
| 		ReplyTo     uint              `json:"reply_to" form:"reply_to"` | ||||
| 	} | ||||
|  | ||||
| @@ -74,7 +73,6 @@ func createComment(c *fiber.Ctx) error { | ||||
| 	item := &models.Comment{ | ||||
| 		PostBase: models.PostBase{ | ||||
| 			Alias:       data.Alias, | ||||
| 			Attachments: data.Attachments, | ||||
| 			PublishedAt: data.PublishedAt, | ||||
| 			AuthorID:    user.ID, | ||||
| 		}, | ||||
| @@ -138,7 +136,6 @@ func editComment(c *fiber.Ctx) error { | ||||
| 		PublishedAt *time.Time        `json:"published_at" form:"published_at"` | ||||
| 		Hashtags    []models.Tag      `json:"hashtags" form:"hashtags"` | ||||
| 		Categories  []models.Category `json:"categories" form:"categories"` | ||||
| 		Attachments []models.Attachment `json:"attachments" form:"attachments"` | ||||
| 	} | ||||
|  | ||||
| 	if err := BindAndValidate(c, &data); err != nil { | ||||
| @@ -160,7 +157,6 @@ func editComment(c *fiber.Ctx) error { | ||||
| 	item.PublishedAt = data.PublishedAt | ||||
| 	item.Hashtags = data.Hashtags | ||||
| 	item.Categories = data.Categories | ||||
| 	item.Attachments = data.Attachments | ||||
|  | ||||
| 	if item, err := services.EditPost(item); err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
|   | ||||
| @@ -47,22 +47,28 @@ func listFeed(c *fiber.Ctx) error { | ||||
| 	commentTable := viper.GetString("database.prefix") + "comments" | ||||
| 	reactionTable := viper.GetString("database.prefix") + "reactions" | ||||
|  | ||||
| 	database.C.Raw(fmt.Sprintf(`SELECT feed.*, author.*,  | ||||
| 		COALESCE(comment_count, 0) as comment_count,  | ||||
| 		COALESCE(reaction_count, 0) as reaction_count | ||||
| 		FROM (? UNION ALL ?) as feed | ||||
| 		INNER JOIN %s as author ON author_id = author.id | ||||
| 		LEFT JOIN (SELECT article_id, moment_id, COUNT(*) as comment_count | ||||
| 	database.C.Raw( | ||||
| 		fmt.Sprintf(`SELECT feed.*, author.*, | ||||
| 		COALESCE(comment_count, 0) AS comment_count,  | ||||
| 		COALESCE(reaction_count, 0) AS reaction_count | ||||
| 		FROM (? UNION ALL ?) AS feed | ||||
| 		INNER JOIN %s AS author ON author_id = author.id | ||||
| 		LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS comment_count | ||||
|             FROM %s | ||||
|             GROUP BY article_id, moment_id) as comments | ||||
|             GROUP BY article_id, moment_id) AS comments | ||||
|             ON (feed.model_type = 'article' AND feed.id = comments.article_id) OR  | ||||
| 			   (feed.model_type = 'moment' AND feed.id = comments.moment_id) | ||||
|         LEFT JOIN (SELECT article_id, moment_id, COUNT(*) as reaction_count | ||||
|         LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS reaction_count | ||||
|         	FROM %s | ||||
|             GROUP BY article_id, moment_id) as reactions | ||||
|             GROUP BY article_id, moment_id) AS reactions | ||||
|             ON (feed.model_type = 'article' AND feed.id = reactions.article_id) OR  | ||||
| 			   (feed.model_type = 'moment' AND feed.id = reactions.moment_id) | ||||
| 		WHERE %s ORDER BY feed.created_at desc  LIMIT ? OFFSET ?`, userTable, commentTable, reactionTable, whereCondition), | ||||
| 		WHERE %s ORDER BY feed.created_at desc  LIMIT ? OFFSET ?`, | ||||
| 			userTable, | ||||
| 			commentTable, | ||||
| 			reactionTable, | ||||
| 			whereCondition, | ||||
| 		), | ||||
| 		database.C.Select(queryArticle).Model(&models.Article{}), | ||||
| 		database.C.Select(queryMoment).Model(&models.Moment{}), | ||||
| 		take, | ||||
| @@ -122,6 +128,56 @@ func listFeed(c *fiber.Ctx) error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if !c.QueryBool("noAttachment", false) { | ||||
| 		revertAttachment := func(dataset string) error { | ||||
| 			var attachments []struct { | ||||
| 				models.Attachment | ||||
|  | ||||
| 				PostID uint `json:"post_id"` | ||||
| 			} | ||||
|  | ||||
| 			itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) { | ||||
| 				return item, item.ModelType == dataset | ||||
| 			}), func(item *models.Feed) (uint, *models.Feed) { | ||||
| 				return item.ID, item | ||||
| 			}) | ||||
|  | ||||
| 			idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool { | ||||
| 				return item.ModelType == dataset | ||||
| 			}), func(item *models.Feed, index int) uint { | ||||
| 				return item.ID | ||||
| 			}) | ||||
|  | ||||
| 			if err := database.C. | ||||
| 				Model(&models.Attachment{}). | ||||
| 				Select(dataset+"_id as post_id, *"). | ||||
| 				Where(dataset+"_id IN (?)", idx). | ||||
| 				Scan(&attachments).Error; err != nil { | ||||
| 				return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||
| 			} | ||||
|  | ||||
| 			list := map[uint][]models.Attachment{} | ||||
| 			for _, info := range attachments { | ||||
| 				list[info.PostID] = append(list[info.PostID], info.Attachment) | ||||
| 			} | ||||
|  | ||||
| 			for k, v := range list { | ||||
| 				if post, ok := itemMap[k]; ok { | ||||
| 					post.Attachments = v | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		if err := revertAttachment("article"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := revertAttachment("moment"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var count int64 | ||||
| 	database.C.Raw(`SELECT COUNT(*) FROM (? UNION ALL ?) as feed`, | ||||
| 		database.C.Select(queryArticle).Model(&models.Article{}), | ||||
|   | ||||
| @@ -44,12 +44,12 @@ func createMoment(c *fiber.Ctx) error { | ||||
| 	item := &models.Moment{ | ||||
| 		PostBase: models.PostBase{ | ||||
| 			Alias:       data.Alias, | ||||
| 			Attachments: data.Attachments, | ||||
| 			PublishedAt: data.PublishedAt, | ||||
| 			AuthorID:    user.ID, | ||||
| 		}, | ||||
| 		Hashtags:    data.Hashtags, | ||||
| 		Categories:  data.Categories, | ||||
| 		Attachments: data.Attachments, | ||||
| 		Content:     data.Content, | ||||
| 		RealmID:     data.RealmID, | ||||
| 	} | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| package services | ||||
|  | ||||
| import ( | ||||
| 	"mime/multipart" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||
| 	"github.com/google/uuid" | ||||
| 	"mime/multipart" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| func NewAttachment(user models.Account, header *multipart.FileHeader) (models.Attachment, error) { | ||||
| @@ -33,6 +35,17 @@ func NewAttachment(user models.Account, header *multipart.FileHeader) (models.At | ||||
| 	} | ||||
| 	attachment.Mimetype = http.DetectContentType(fileHeader) | ||||
|  | ||||
| 	switch strings.Split(attachment.Mimetype, "/")[0] { | ||||
| 	case "image": | ||||
| 		attachment.Type = models.AttachmentPhoto | ||||
| 	case "video": | ||||
| 		attachment.Type = models.AttachmentVideo | ||||
| 	case "audio": | ||||
| 		attachment.Type = models.AttachmentAudio | ||||
| 	default: | ||||
| 		attachment.Type = models.AttachmentOthers | ||||
| 	} | ||||
|  | ||||
| 	// Save into database | ||||
| 	err = database.C.Save(&attachment).Error | ||||
|  | ||||
|   | ||||
							
								
								
									
										45
									
								
								pkg/views/src/components/posts/PostAttachment.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								pkg/views/src/components/posts/PostAttachment.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <template> | ||||
|   <v-card variant="tonal" class="max-w-[540px]" :ripple="canLightbox" @click="openLightbox"> | ||||
|     <div class="content"> | ||||
|       <img v-if="current.type === 1" :src="getUrl(current)" /> | ||||
|       <video v-if="current.type === 2" controls class="w-full"> | ||||
|         <source :src="getUrl(current)"></source> | ||||
|       </video> | ||||
|     </div> | ||||
|  | ||||
|     <vue-easy-lightbox teleport="#app" :visible="lightbox" :imgs="[getUrl(current)]" @hide="lightbox = false"> | ||||
|       <template v-slot:close-btn="{ close }"> | ||||
|         <v-btn class="fixed left-2 top-2" icon="mdi-close" variant="text" color="white" @click="close" /> | ||||
|       </template> | ||||
|     </vue-easy-lightbox> | ||||
|   </v-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { computed, ref } from "vue" | ||||
| import VueEasyLightbox from "vue-easy-lightbox" | ||||
|  | ||||
| const props = defineProps<{ attachments: any[] }>() | ||||
|  | ||||
| const lightbox = ref(false) | ||||
| const focus = ref(0) | ||||
|  | ||||
| const current = computed(() => props.attachments[focus.value]) | ||||
| const canLightbox = computed(() => current.value.type === 1) | ||||
|  | ||||
| function getUrl(item: any) { | ||||
|   return item.external_url ? item.external_url : `/api/attachments/o/${item.file_id}` | ||||
| } | ||||
|  | ||||
| function openLightbox() { | ||||
|   if (canLightbox.value) { | ||||
|     lightbox.value = true | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| .vel-model { | ||||
|   z-index: 10; | ||||
| } | ||||
| </style> | ||||
| @@ -18,6 +18,8 @@ | ||||
|  | ||||
|       <component :is="renderer[props.item?.model_type]" v-bind="props" /> | ||||
|  | ||||
|       <post-attachment v-if="props.item?.attachments" :attachments="props.item?.attachments" /> | ||||
|  | ||||
|       <post-reaction | ||||
|         size="small" | ||||
|         :item="props.item" | ||||
| @@ -34,6 +36,7 @@ import type { Component } from "vue" | ||||
| import ArticleContent from "@/components/posts/ArticleContent.vue" | ||||
| import MomentContent from "@/components/posts/MomentContent.vue" | ||||
| import CommentContent from "@/components/posts/CommentContent.vue" | ||||
| import PostAttachment from "./PostAttachment.vue" | ||||
| import PostReaction from "@/components/posts/PostReaction.vue" | ||||
|  | ||||
| const props = defineProps<{ item: any; brief?: boolean }>() | ||||
|   | ||||
| @@ -67,6 +67,20 @@ | ||||
|                 </div> | ||||
|               </template> | ||||
|             </v-expansion-panel> | ||||
|  | ||||
|             <v-expansion-panel title="Media"> | ||||
|               <template #text> | ||||
|                 <div class="flex justify-between items-center"> | ||||
|                   <div> | ||||
|                     <p class="text-xs">This article attached</p> | ||||
|                     <p class="text-lg font-medium"> | ||||
|                       {{ data.attachments.length }} attachment(s) | ||||
|                     </p> | ||||
|                   </div> | ||||
|                   <v-btn size="small" icon="mdi-camera-plus" variant="text" @click="dialogs.media = true" /> | ||||
|                 </div> | ||||
|               </template> | ||||
|             </v-expansion-panel> | ||||
|           </v-expansion-panels> | ||||
|         </v-container> | ||||
|       </v-card-text> | ||||
| @@ -74,8 +88,13 @@ | ||||
|   </v-card> | ||||
|  | ||||
|   <planned-publish v-model:show="dialogs.plan" v-model:value="data.publishedAt" /> | ||||
|   <media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" /> | ||||
|  | ||||
|   <v-snackbar v-model="success" :timeout="3000">Your article has been published.</v-snackbar> | ||||
|   <v-snackbar v-model="uploading" :timeout="-1"> | ||||
|     Uploading your media, please stand by... | ||||
|     <v-progress-linear class="snackbar-progress" indeterminate /> | ||||
|   </v-snackbar> | ||||
|  | ||||
|   <!-- @vue-ignore --> | ||||
|   <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> | ||||
| @@ -86,8 +105,9 @@ import { request } from "@/scripts/request" | ||||
| import { useEditor } from "@/stores/editor" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { reactive, ref } from "vue" | ||||
| import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||
| import { useRouter } from "vue-router" | ||||
| import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||
| import Media from "@/components/publish/parts/Media.vue" | ||||
|  | ||||
| const editor = useEditor() | ||||
|  | ||||
| @@ -101,7 +121,8 @@ const data = reactive<any>({ | ||||
|   title: "", | ||||
|   content: "", | ||||
|   description: "", | ||||
|   publishedAt: null | ||||
|   publishedAt: null, | ||||
|   attachments: [] | ||||
| }) | ||||
|  | ||||
| const router = useRouter() | ||||
| @@ -109,10 +130,13 @@ const router = useRouter() | ||||
| const error = ref<string | null>(null) | ||||
| const success = ref(false) | ||||
| const loading = ref(false) | ||||
| const uploading = ref(false) | ||||
|  | ||||
| async function postArticle(evt: SubmitEvent) { | ||||
|   const form = evt.target as HTMLFormElement | ||||
|  | ||||
|   if (uploading.value) return | ||||
|  | ||||
|   if (!data.content) return | ||||
|   if (!data.title || !data.description) return | ||||
|   if (!data.publishedAt) data.publishedAt = new Date().toISOString() | ||||
| @@ -149,4 +173,12 @@ async function postArticle(evt: SubmitEvent) { | ||||
| .article-container { | ||||
|   max-width: 720px; | ||||
| } | ||||
|  | ||||
| .snackbar-progress { | ||||
|   margin-left: -16px; | ||||
|   margin-right: -16px; | ||||
|   margin-bottom: -14px; | ||||
|   margin-top: 12px; | ||||
|   width: calc(100% + 64px); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -54,8 +54,13 @@ | ||||
|   </v-card> | ||||
|  | ||||
|   <planned-publish v-model:show="dialogs.plan" v-model:value="extras.publishedAt" /> | ||||
|   <media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="extras.attachments" /> | ||||
|  | ||||
|   <v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar> | ||||
|   <v-snackbar v-model="uploading" :timeout="-1"> | ||||
|     Uploading your media, please stand by... | ||||
|     <v-progress-linear class="snackbar-progress" indeterminate /> | ||||
|   </v-snackbar> | ||||
|  | ||||
|   <!-- @vue-ignore --> | ||||
|   <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> | ||||
| @@ -67,6 +72,7 @@ import { useEditor } from "@/stores/editor" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { reactive, ref } from "vue" | ||||
| import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||
| import Media from "@/components/publish/parts/Media.vue" | ||||
|  | ||||
| const editor = useEditor() | ||||
|  | ||||
| @@ -77,12 +83,14 @@ const dialogs = reactive({ | ||||
| }) | ||||
|  | ||||
| const extras = reactive({ | ||||
|   publishedAt: null | ||||
|   publishedAt: null, | ||||
|   attachments: [] | ||||
| }) | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
| const success = ref(false) | ||||
| const loading = ref(false) | ||||
| const uploading = ref(false) | ||||
|  | ||||
| async function postMoment(evt: SubmitEvent) { | ||||
|   const form = evt.target as HTMLFormElement | ||||
| @@ -91,6 +99,8 @@ async function postMoment(evt: SubmitEvent) { | ||||
|   if (!extras.publishedAt) data.set("published_at", new Date().toISOString()) | ||||
|   else data.set("published_at", extras.publishedAt) | ||||
|  | ||||
|   extras.attachments.forEach((item) => data.append("attachments[]", item)) | ||||
|  | ||||
|   loading.value = true | ||||
|   const res = await request("/api/p/moments", { | ||||
|     method: "POST", | ||||
|   | ||||
							
								
								
									
										109
									
								
								pkg/views/src/components/publish/parts/Media.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								pkg/views/src/components/publish/parts/Media.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| <template> | ||||
|   <v-dialog | ||||
|     eager | ||||
|     class="max-w-[540px]" | ||||
|     :model-value="props.show" | ||||
|     @update:model-value="(val) => emits('update:show', val)" | ||||
|   > | ||||
|     <v-card title="Media management"> | ||||
|       <template #text> | ||||
|         <v-file-input | ||||
|           prepend-icon="" | ||||
|           append-icon="mdi-upload" | ||||
|           variant="solo-filled" | ||||
|           label="File Picker" | ||||
|           v-model="picked" | ||||
|           :accept="['image/*', 'video/*']" | ||||
|           :loading="props.uploading" | ||||
|           @click:append="upload()" | ||||
|         /> | ||||
|  | ||||
|         <h2 class="px-2 mb-1">Media list</h2> | ||||
|         <v-card variant="tonal"> | ||||
|           <v-list> | ||||
|             <v-list-item v-for="item in props.value" :title="getFileName(item)"> | ||||
|               <template #subtitle> | ||||
|                 {{ getFileType(item) }} · {{ formatBytes(item.filesize) }} | ||||
|               </template> | ||||
|               <template #append> | ||||
|                 <v-btn icon="mdi-delete" size="small" variant="text" color="error" /> | ||||
|               </template> | ||||
|             </v-list-item> | ||||
|           </v-list> | ||||
|         </v-card> | ||||
|       </template> | ||||
|       <template #actions> | ||||
|         <v-btn class="ms-auto" text="Ok" @click="emits('update:show', false)"></v-btn> | ||||
|       </template> | ||||
|     </v-card> | ||||
|   </v-dialog> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { request } from "@/scripts/request" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { ref } from "vue" | ||||
|  | ||||
| const props = defineProps<{ show: boolean; uploading: boolean; value: any[] }>() | ||||
| const emits = defineEmits(["update:show", "update:uploading", "update:value"]) | ||||
|  | ||||
| const picked = ref<any[]>([]) | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
|  | ||||
| async function upload(file?: any) { | ||||
|   if (props.uploading) return | ||||
|  | ||||
|   const data = new FormData() | ||||
|   if (!file) { | ||||
|     if (!picked.value) return | ||||
|     data.set("attachment", picked.value[0]) | ||||
|   } else { | ||||
|     data.set("attachment", file) | ||||
|   } | ||||
|  | ||||
|   emits("update:uploading", true) | ||||
|   const res = await request("/api/attachments", { | ||||
|     method: "POST", | ||||
|     headers: { Authorization: `Bearer ${getAtk()}` }, | ||||
|     body: data | ||||
|   }) | ||||
|   if (res.status !== 200) { | ||||
|     error.value = await res.text() | ||||
|   } else { | ||||
|     const data = await res.json() | ||||
|     emits("update:value", props.value.concat([data.info])) | ||||
|     picked.value = [] | ||||
|   } | ||||
|   emits("update:uploading", false) | ||||
| } | ||||
|  | ||||
| function getFileName(item: any) { | ||||
|   return item.filename.replace(/\.[^/.]+$/, "") | ||||
| } | ||||
|  | ||||
| function getFileType(item: any) { | ||||
|   switch (item.type) { | ||||
|     case 1: | ||||
|       return "Photo" | ||||
|     case 2: | ||||
|       return "Video" | ||||
|     case 3: | ||||
|       return "Audio" | ||||
|     default: | ||||
|       return "Others" | ||||
|   } | ||||
| } | ||||
|  | ||||
| function formatBytes(bytes: number, decimals = 2) { | ||||
|   if (!+bytes) return "0 Bytes" | ||||
|  | ||||
|   const k = 1024 | ||||
|   const dm = decimals < 0 ? 0 : decimals | ||||
|   const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] | ||||
|  | ||||
|   const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||||
|  | ||||
|   return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` | ||||
| } | ||||
| </script> | ||||
| @@ -12,7 +12,7 @@ | ||||
|           class="mt-2" | ||||
|           label="Publish date" | ||||
|           hint="Your post will hidden for public before this time. Leave blank will publish immediately" | ||||
|           variant="outlined" | ||||
|           variant="solo-filled" | ||||
|           type="datetime-local" | ||||
|           :model-value="props.value" | ||||
|           @update:model-value="(val) => emits('update:value', val)" | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|       <div> | ||||
|         <v-alert type="info" variant="tonal" class="text-sm"> | ||||
|           We just released the brand new design system and user interface! | ||||
|           <a class="underline" href="https://forms.office.com/r/Uh8vYmRQ8f" target="_blank">Contribute our survey</a> | ||||
|           <a class="underline" href="https://tally.so/r/w2NM7g" target="_blank">Take a survey</a> | ||||
|         </v-alert> | ||||
|       </div> | ||||
|     </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user