♻️ Interactive v2 #1
| @@ -7,6 +7,7 @@ type Article struct { | |||||||
| 	Hashtags    []Tag        `json:"tags" gorm:"many2many:article_tags"` | 	Hashtags    []Tag        `json:"tags" gorm:"many2many:article_tags"` | ||||||
| 	Categories  []Category   `json:"categories" gorm:"many2many:article_categories"` | 	Categories  []Category   `json:"categories" gorm:"many2many:article_categories"` | ||||||
| 	Reactions   []Reaction   `json:"reactions"` | 	Reactions   []Reaction   `json:"reactions"` | ||||||
|  | 	Attachments []Attachment `json:"attachments"` | ||||||
| 	Description string       `json:"description"` | 	Description string       `json:"description"` | ||||||
| 	Content     string       `json:"content"` | 	Content     string       `json:"content"` | ||||||
| 	RealmID     *uint        `json:"realm_id"` | 	RealmID     *uint        `json:"realm_id"` | ||||||
|   | |||||||
| @@ -2,8 +2,9 @@ package models | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/spf13/viper" |  | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  |  | ||||||
|  | 	"github.com/spf13/viper" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type AttachmentType = uint8 | type AttachmentType = uint8 | ||||||
|   | |||||||
| @@ -16,5 +16,7 @@ type Feed struct { | |||||||
| 	RealmID  *uint `json:"realm_id"` | 	RealmID  *uint `json:"realm_id"` | ||||||
|  |  | ||||||
| 	Author Account `json:"author" gorm:"embedded"` | 	Author Account `json:"author" gorm:"embedded"` | ||||||
|  |  | ||||||
|  | 	Attachments  []Attachment     `json:"attachments" gorm:"-"` | ||||||
| 	ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` | 	ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ type Moment struct { | |||||||
| 	Hashtags    []Tag        `json:"tags" gorm:"many2many:moment_tags"` | 	Hashtags    []Tag        `json:"tags" gorm:"many2many:moment_tags"` | ||||||
| 	Categories  []Category   `json:"categories" gorm:"many2many:moment_categories"` | 	Categories  []Category   `json:"categories" gorm:"many2many:moment_categories"` | ||||||
| 	Reactions   []Reaction   `json:"reactions"` | 	Reactions   []Reaction   `json:"reactions"` | ||||||
|  | 	Attachments []Attachment `json:"attachments"` | ||||||
| 	RealmID     *uint        `json:"realm_id"` | 	RealmID     *uint        `json:"realm_id"` | ||||||
| 	RepostID    *uint        `json:"repost_id"` | 	RepostID    *uint        `json:"repost_id"` | ||||||
| 	Realm       *Realm       `json:"realm"` | 	Realm       *Realm       `json:"realm"` | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ type PostBase struct { | |||||||
| 	BaseModel | 	BaseModel | ||||||
|  |  | ||||||
| 	Alias       string     `json:"alias" gorm:"uniqueIndex"` | 	Alias       string     `json:"alias" gorm:"uniqueIndex"` | ||||||
| 	Attachments []Attachment `json:"attachments"` |  | ||||||
| 	PublishedAt *time.Time `json:"published_at"` | 	PublishedAt *time.Time `json:"published_at"` | ||||||
|  |  | ||||||
| 	AuthorID uint    `json:"author_id"` | 	AuthorID uint    `json:"author_id"` | ||||||
|   | |||||||
| @@ -45,12 +45,12 @@ func createArticle(c *fiber.Ctx) error { | |||||||
| 	item := &models.Article{ | 	item := &models.Article{ | ||||||
| 		PostBase: models.PostBase{ | 		PostBase: models.PostBase{ | ||||||
| 			Alias:       data.Alias, | 			Alias:       data.Alias, | ||||||
| 			Attachments: data.Attachments, |  | ||||||
| 			PublishedAt: data.PublishedAt, | 			PublishedAt: data.PublishedAt, | ||||||
| 			AuthorID:    user.ID, | 			AuthorID:    user.ID, | ||||||
| 		}, | 		}, | ||||||
| 		Hashtags:    data.Hashtags, | 		Hashtags:    data.Hashtags, | ||||||
| 		Categories:  data.Categories, | 		Categories:  data.Categories, | ||||||
|  | 		Attachments: data.Attachments, | ||||||
| 		Title:       data.Title, | 		Title:       data.Title, | ||||||
| 		Description: data.Description, | 		Description: data.Description, | ||||||
| 		Content:     data.Content, | 		Content:     data.Content, | ||||||
|   | |||||||
| @@ -61,7 +61,6 @@ func createComment(c *fiber.Ctx) error { | |||||||
| 		PublishedAt *time.Time        `json:"published_at" form:"published_at"` | 		PublishedAt *time.Time        `json:"published_at" form:"published_at"` | ||||||
| 		Hashtags    []models.Tag      `json:"hashtags" form:"hashtags"` | 		Hashtags    []models.Tag      `json:"hashtags" form:"hashtags"` | ||||||
| 		Categories  []models.Category `json:"categories" form:"categories"` | 		Categories  []models.Category `json:"categories" form:"categories"` | ||||||
| 		Attachments []models.Attachment `json:"attachments" form:"attachments"` |  | ||||||
| 		ReplyTo     uint              `json:"reply_to" form:"reply_to"` | 		ReplyTo     uint              `json:"reply_to" form:"reply_to"` | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -74,7 +73,6 @@ func createComment(c *fiber.Ctx) error { | |||||||
| 	item := &models.Comment{ | 	item := &models.Comment{ | ||||||
| 		PostBase: models.PostBase{ | 		PostBase: models.PostBase{ | ||||||
| 			Alias:       data.Alias, | 			Alias:       data.Alias, | ||||||
| 			Attachments: data.Attachments, |  | ||||||
| 			PublishedAt: data.PublishedAt, | 			PublishedAt: data.PublishedAt, | ||||||
| 			AuthorID:    user.ID, | 			AuthorID:    user.ID, | ||||||
| 		}, | 		}, | ||||||
| @@ -138,7 +136,6 @@ func editComment(c *fiber.Ctx) error { | |||||||
| 		PublishedAt *time.Time        `json:"published_at" form:"published_at"` | 		PublishedAt *time.Time        `json:"published_at" form:"published_at"` | ||||||
| 		Hashtags    []models.Tag      `json:"hashtags" form:"hashtags"` | 		Hashtags    []models.Tag      `json:"hashtags" form:"hashtags"` | ||||||
| 		Categories  []models.Category `json:"categories" form:"categories"` | 		Categories  []models.Category `json:"categories" form:"categories"` | ||||||
| 		Attachments []models.Attachment `json:"attachments" form:"attachments"` |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := BindAndValidate(c, &data); err != nil { | 	if err := BindAndValidate(c, &data); err != nil { | ||||||
| @@ -160,7 +157,6 @@ func editComment(c *fiber.Ctx) error { | |||||||
| 	item.PublishedAt = data.PublishedAt | 	item.PublishedAt = data.PublishedAt | ||||||
| 	item.Hashtags = data.Hashtags | 	item.Hashtags = data.Hashtags | ||||||
| 	item.Categories = data.Categories | 	item.Categories = data.Categories | ||||||
| 	item.Attachments = data.Attachments |  | ||||||
|  |  | ||||||
| 	if item, err := services.EditPost(item); err != nil { | 	if item, err := services.EditPost(item); err != nil { | ||||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
|   | |||||||
| @@ -47,22 +47,28 @@ func listFeed(c *fiber.Ctx) error { | |||||||
| 	commentTable := viper.GetString("database.prefix") + "comments" | 	commentTable := viper.GetString("database.prefix") + "comments" | ||||||
| 	reactionTable := viper.GetString("database.prefix") + "reactions" | 	reactionTable := viper.GetString("database.prefix") + "reactions" | ||||||
|  |  | ||||||
| 	database.C.Raw(fmt.Sprintf(`SELECT feed.*, author.*,  | 	database.C.Raw( | ||||||
| 		COALESCE(comment_count, 0) as comment_count,  | 		fmt.Sprintf(`SELECT feed.*, author.*, | ||||||
| 		COALESCE(reaction_count, 0) as reaction_count | 		COALESCE(comment_count, 0) AS comment_count,  | ||||||
| 		FROM (? UNION ALL ?) as feed | 		COALESCE(reaction_count, 0) AS reaction_count | ||||||
| 		INNER JOIN %s as author ON author_id = author.id | 		FROM (? UNION ALL ?) AS feed | ||||||
| 		LEFT JOIN (SELECT article_id, moment_id, COUNT(*) as comment_count | 		INNER JOIN %s AS author ON author_id = author.id | ||||||
|  | 		LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS comment_count | ||||||
|             FROM %s |             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  |             ON (feed.model_type = 'article' AND feed.id = comments.article_id) OR  | ||||||
| 			   (feed.model_type = 'moment' AND feed.id = comments.moment_id) | 			   (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 |         	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  |             ON (feed.model_type = 'article' AND feed.id = reactions.article_id) OR  | ||||||
| 			   (feed.model_type = 'moment' AND feed.id = reactions.moment_id) | 			   (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(queryArticle).Model(&models.Article{}), | ||||||
| 		database.C.Select(queryMoment).Model(&models.Moment{}), | 		database.C.Select(queryMoment).Model(&models.Moment{}), | ||||||
| 		take, | 		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 | 	var count int64 | ||||||
| 	database.C.Raw(`SELECT COUNT(*) FROM (? UNION ALL ?) as feed`, | 	database.C.Raw(`SELECT COUNT(*) FROM (? UNION ALL ?) as feed`, | ||||||
| 		database.C.Select(queryArticle).Model(&models.Article{}), | 		database.C.Select(queryArticle).Model(&models.Article{}), | ||||||
|   | |||||||
| @@ -44,12 +44,12 @@ func createMoment(c *fiber.Ctx) error { | |||||||
| 	item := &models.Moment{ | 	item := &models.Moment{ | ||||||
| 		PostBase: models.PostBase{ | 		PostBase: models.PostBase{ | ||||||
| 			Alias:       data.Alias, | 			Alias:       data.Alias, | ||||||
| 			Attachments: data.Attachments, |  | ||||||
| 			PublishedAt: data.PublishedAt, | 			PublishedAt: data.PublishedAt, | ||||||
| 			AuthorID:    user.ID, | 			AuthorID:    user.ID, | ||||||
| 		}, | 		}, | ||||||
| 		Hashtags:    data.Hashtags, | 		Hashtags:    data.Hashtags, | ||||||
| 		Categories:  data.Categories, | 		Categories:  data.Categories, | ||||||
|  | 		Attachments: data.Attachments, | ||||||
| 		Content:     data.Content, | 		Content:     data.Content, | ||||||
| 		RealmID:     data.RealmID, | 		RealmID:     data.RealmID, | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| package services | package services | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"mime/multipart" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"mime/multipart" |  | ||||||
| 	"net/http" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewAttachment(user models.Account, header *multipart.FileHeader) (models.Attachment, error) { | 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) | 	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 | 	// Save into database | ||||||
| 	err = database.C.Save(&attachment).Error | 	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" /> |       <component :is="renderer[props.item?.model_type]" v-bind="props" /> | ||||||
|  |  | ||||||
|  |       <post-attachment v-if="props.item?.attachments" :attachments="props.item?.attachments" /> | ||||||
|  |  | ||||||
|       <post-reaction |       <post-reaction | ||||||
|         size="small" |         size="small" | ||||||
|         :item="props.item" |         :item="props.item" | ||||||
| @@ -34,6 +36,7 @@ import type { Component } from "vue" | |||||||
| import ArticleContent from "@/components/posts/ArticleContent.vue" | import ArticleContent from "@/components/posts/ArticleContent.vue" | ||||||
| import MomentContent from "@/components/posts/MomentContent.vue" | import MomentContent from "@/components/posts/MomentContent.vue" | ||||||
| import CommentContent from "@/components/posts/CommentContent.vue" | import CommentContent from "@/components/posts/CommentContent.vue" | ||||||
|  | import PostAttachment from "./PostAttachment.vue" | ||||||
| import PostReaction from "@/components/posts/PostReaction.vue" | import PostReaction from "@/components/posts/PostReaction.vue" | ||||||
|  |  | ||||||
| const props = defineProps<{ item: any; brief?: boolean }>() | const props = defineProps<{ item: any; brief?: boolean }>() | ||||||
|   | |||||||
| @@ -67,6 +67,20 @@ | |||||||
|                 </div> |                 </div> | ||||||
|               </template> |               </template> | ||||||
|             </v-expansion-panel> |             </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-expansion-panels> | ||||||
|         </v-container> |         </v-container> | ||||||
|       </v-card-text> |       </v-card-text> | ||||||
| @@ -74,8 +88,13 @@ | |||||||
|   </v-card> |   </v-card> | ||||||
|  |  | ||||||
|   <planned-publish v-model:show="dialogs.plan" v-model:value="data.publishedAt" /> |   <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="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 --> |   <!-- @vue-ignore --> | ||||||
|   <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> |   <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 { useEditor } from "@/stores/editor" | ||||||
| import { getAtk } from "@/stores/userinfo" | import { getAtk } from "@/stores/userinfo" | ||||||
| import { reactive, ref } from "vue" | import { reactive, ref } from "vue" | ||||||
| import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" |  | ||||||
| import { useRouter } from "vue-router" | import { useRouter } from "vue-router" | ||||||
|  | import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||||
|  | import Media from "@/components/publish/parts/Media.vue" | ||||||
|  |  | ||||||
| const editor = useEditor() | const editor = useEditor() | ||||||
|  |  | ||||||
| @@ -101,7 +121,8 @@ const data = reactive<any>({ | |||||||
|   title: "", |   title: "", | ||||||
|   content: "", |   content: "", | ||||||
|   description: "", |   description: "", | ||||||
|   publishedAt: null |   publishedAt: null, | ||||||
|  |   attachments: [] | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const router = useRouter() | const router = useRouter() | ||||||
| @@ -109,10 +130,13 @@ const router = useRouter() | |||||||
| const error = ref<string | null>(null) | const error = ref<string | null>(null) | ||||||
| const success = ref(false) | const success = ref(false) | ||||||
| const loading = ref(false) | const loading = ref(false) | ||||||
|  | const uploading = ref(false) | ||||||
|  |  | ||||||
| async function postArticle(evt: SubmitEvent) { | async function postArticle(evt: SubmitEvent) { | ||||||
|   const form = evt.target as HTMLFormElement |   const form = evt.target as HTMLFormElement | ||||||
|  |  | ||||||
|  |   if (uploading.value) return | ||||||
|  |  | ||||||
|   if (!data.content) return |   if (!data.content) return | ||||||
|   if (!data.title || !data.description) return |   if (!data.title || !data.description) return | ||||||
|   if (!data.publishedAt) data.publishedAt = new Date().toISOString() |   if (!data.publishedAt) data.publishedAt = new Date().toISOString() | ||||||
| @@ -149,4 +173,12 @@ async function postArticle(evt: SubmitEvent) { | |||||||
| .article-container { | .article-container { | ||||||
|   max-width: 720px; |   max-width: 720px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .snackbar-progress { | ||||||
|  |   margin-left: -16px; | ||||||
|  |   margin-right: -16px; | ||||||
|  |   margin-bottom: -14px; | ||||||
|  |   margin-top: 12px; | ||||||
|  |   width: calc(100% + 64px); | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -54,8 +54,13 @@ | |||||||
|   </v-card> |   </v-card> | ||||||
|  |  | ||||||
|   <planned-publish v-model:show="dialogs.plan" v-model:value="extras.publishedAt" /> |   <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="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 --> |   <!-- @vue-ignore --> | ||||||
|   <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> |   <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 { getAtk } from "@/stores/userinfo" | ||||||
| import { reactive, ref } from "vue" | import { reactive, ref } from "vue" | ||||||
| import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||||
|  | import Media from "@/components/publish/parts/Media.vue" | ||||||
|  |  | ||||||
| const editor = useEditor() | const editor = useEditor() | ||||||
|  |  | ||||||
| @@ -77,12 +83,14 @@ const dialogs = reactive({ | |||||||
| }) | }) | ||||||
|  |  | ||||||
| const extras = reactive({ | const extras = reactive({ | ||||||
|   publishedAt: null |   publishedAt: null, | ||||||
|  |   attachments: [] | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const error = ref<string | null>(null) | const error = ref<string | null>(null) | ||||||
| const success = ref(false) | const success = ref(false) | ||||||
| const loading = ref(false) | const loading = ref(false) | ||||||
|  | const uploading = ref(false) | ||||||
|  |  | ||||||
| async function postMoment(evt: SubmitEvent) { | async function postMoment(evt: SubmitEvent) { | ||||||
|   const form = evt.target as HTMLFormElement |   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()) |   if (!extras.publishedAt) data.set("published_at", new Date().toISOString()) | ||||||
|   else data.set("published_at", extras.publishedAt) |   else data.set("published_at", extras.publishedAt) | ||||||
|  |  | ||||||
|  |   extras.attachments.forEach((item) => data.append("attachments[]", item)) | ||||||
|  |  | ||||||
|   loading.value = true |   loading.value = true | ||||||
|   const res = await request("/api/p/moments", { |   const res = await request("/api/p/moments", { | ||||||
|     method: "POST", |     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" |           class="mt-2" | ||||||
|           label="Publish date" |           label="Publish date" | ||||||
|           hint="Your post will hidden for public before this time. Leave blank will publish immediately" |           hint="Your post will hidden for public before this time. Leave blank will publish immediately" | ||||||
|           variant="outlined" |           variant="solo-filled" | ||||||
|           type="datetime-local" |           type="datetime-local" | ||||||
|           :model-value="props.value" |           :model-value="props.value" | ||||||
|           @update:model-value="(val) => emits('update:value', val)" |           @update:model-value="(val) => emits('update:value', val)" | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
|       <div> |       <div> | ||||||
|         <v-alert type="info" variant="tonal" class="text-sm"> |         <v-alert type="info" variant="tonal" class="text-sm"> | ||||||
|           We just released the brand new design system and user interface! |           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> |         </v-alert> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user