♻️ 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>
|
||||||
|
Loading…
Reference in New Issue
Block a user