Add video support

This commit is contained in:
Dominic Ferrando
2025-04-22 00:06:11 -04:00
parent fac517b28d
commit 741ac24e3e
15 changed files with 236 additions and 102 deletions
+37 -15
View File
@@ -7,6 +7,7 @@ import (
"log"
"net/http"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
@@ -155,13 +156,14 @@ func main() {
defer file.Close()
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + filepath.Ext(header.Filename)
if err := util.SavePostFile(&file, filename); err != nil {
err, savedMediaPath, savedThumbPath := util.SavePostFile(file, filename)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
log.Printf("savePostFile: %v", err)
return
}
if err := database.PutThread(db, slug, subject, body, filename, util.HashIp(ip)); err != nil {
if err := database.PutThread(db, slug, subject, body, savedMediaPath, savedThumbPath, util.HashIp(ip)); err != nil {
http.Error(w, "Failed to create thread", http.StatusInternalServerError)
log.Printf("PutThread: %v", err)
return
@@ -180,13 +182,6 @@ func main() {
return
}
threadIdStr := chi.URLParam(r, "threadId")
threadId, err := strconv.Atoi(threadIdStr)
if err != nil {
http.Error(w, "Invalid thread id", http.StatusBadRequest)
return
}
if err := r.ParseMultipartForm(util.FILE_MEM_LIMIT); err != nil {
http.Error(w, "Failed to parse form", http.StatusBadRequest)
log.Printf("ParseMultipartForm: %v", err)
@@ -195,6 +190,7 @@ func main() {
body := strings.TrimSpace(r.FormValue("body"))
mediaPath := ""
thumbPath := ""
if body == "" {
http.Error(w, "Malformed post body", http.StatusBadRequest)
@@ -204,22 +200,48 @@ func main() {
file, header, err := r.FormFile("file")
if err != nil {
if !errors.Is(err, http.ErrMissingFile) {
http.Error(w, "Failed to retrive file from form", http.StatusBadRequest)
http.Error(w, "Failed to retrieve file from form", http.StatusBadRequest)
log.Printf("FormFile: %v", err)
return
}
} else {
defer file.Close()
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + filepath.Ext(header.Filename)
if err := util.SavePostFile(&file, filename); err != nil {
isFileVideo := false
fileExt := strings.ToLower(filepath.Ext(header.Filename))
// file type
buffer := make([]byte, 512)
file.Read(buffer)
file.Seek(0, 0)
fileType := http.DetectContentType(buffer)
isFileVideo = strings.HasPrefix(fileType, "video/")
if isFileVideo && !slices.Contains(util.SUPPORTED_VID_FORMATS, fileExt) {
http.Error(w, "Unsupported file format", http.StatusBadRequest)
return
}
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + fileExt
err, savedMediaPath, savedThumbPath := util.SavePostFile(file, filename)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
log.Printf("savePostFile: %v", err)
return
}
mediaPath = filename
mediaPath = savedMediaPath
thumbPath = savedThumbPath
}
if err := database.PutPost(db, slug, threadId, body, mediaPath, util.HashIp(ip)); err != nil {
threadIdStr := chi.URLParam(r, "threadId")
threadId, err := strconv.Atoi(threadIdStr)
if err != nil {
http.Error(w, "Invalid thread id", http.StatusBadRequest)
return
}
if err := database.PutPost(db, slug, threadId, body, mediaPath, thumbPath, util.HashIp(ip)); err != nil {
http.Error(w, "Failed to create post", http.StatusInternalServerError)
log.Printf("PutPost: %v", err)
return
@@ -262,7 +284,7 @@ func main() {
Subject: thread.Subject,
Body: op.Body,
ThreadURL: fmt.Sprintf("/%s/threads/%d", slug, thread.Id),
MediaPath: op.MediaPath,
ThumbPath: op.ThumbPath,
ReplyCount: len(posts),
IpCount: SumUniquePostIps(posts),
})
+8
View File
@@ -220,6 +220,14 @@ body {
max-width: 100%;
}
.post-vid {
display: block;
float: left;
margin: 4px 12px 4px 0;
height: auto;
max-width: 100%;
}
.post-img-info {
display: inline-block;
font-size: 10px;

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

+24 -7
View File
@@ -36,15 +36,32 @@ function insertHeaderReplies() {
}
}
function togglePostImage(img) {
const filename = img.src.split("/").at(-1);
if (img.classList.contains("post-img-full")) {
img.classList.remove("post-img-full");
img.src = "/static/img/posts/thumb/" + filename;
function togglePostFile(imgEl) {
const isVideo = imgEl.dataset.full.endsWith(".mp4") || imgEl.dataset.full.endsWith(".webm") || imgEl.dataset.full.endsWith(".ogg");
if (isVideo) {
const vidEl = imgEl.parentElement.querySelector("video");
if (vidEl.style.display === "none") {
vidEl.src = "/static/media/posts/full/" + imgEl.dataset.full;
vidEl.style.display = "";
imgEl.style.display = "none";
}
else {
vidEl.src = "";
vidEl.style.display = "none";
imgEl.style.display = "";
}
}
else {
img.classList.add("post-img-full");
img.src = "/static/img/posts/full/" + filename;
if (imgEl.classList.contains("post-img-full")) {
imgEl.classList.remove("post-img-full");
imgEl.src = "/static/media/posts/thumb/" + imgEl.dataset.thumb;
}
else {
imgEl.classList.add("post-img-full");
imgEl.src = "/static/media/posts/full/" + imgEl.dataset.full;
}
}
}
+13 -5
View File
@@ -42,7 +42,15 @@ templ NewThreadForm(board database.Board) {
</tr>
<tr class="new-post-form-field">
<th>File</th>
<td><input id="newPostFile" name="file" type="file" required/></td>
<td>
<input
accept="image/*,video/mp4,video/webm,video/ogg"
id="newPostFile"
name="file"
type="file"
required
/>
</td>
</tr>
</tbody>
</table>
@@ -52,7 +60,7 @@ templ NewThreadForm(board database.Board) {
templ BoardHeader(board database.Board) {
<header class="board-header">
<img src={ fmt.Sprintf("/static/img/banners/%s.png", board.Slug) }/>
<img src={ fmt.Sprintf("/static/media/banners/%s.png", board.Slug) }/>
<h1>
{ fmt.Sprintf("/%s/ - %s", board.Slug, board.Name) }
</h1>
@@ -91,7 +99,7 @@ type CatalogThreadPreview struct {
Subject string
Body string
ThreadURL string
MediaPath string
ThumbPath string
ReplyCount int
IpCount int
}
@@ -106,8 +114,8 @@ templ ThreadsCatalog(previews []CatalogThreadPreview) {
<img
loading="lazy"
class="catalog-preview-img"
src={ fmt.Sprintf("/static/img/posts/thumb/%s",
preview.MediaPath) }
src={ fmt.Sprintf("/static/media/posts/thumb/%s",
preview.ThumbPath) }
/>
</a>
<strong class="catalog-preview-counts">R: { replyCount } / I: { ipCount }</strong>
+1 -1
View File
@@ -5,7 +5,7 @@ import "github.com/dominicf2001/comfychan/web/views/shared"
templ Index() {
@shared.Layout() {
<section class="container">
<img id="clavis" src="/static/img/clavis.png"/>
<img id="clavis" src="/static/media/clavis.png"/>
<div class="box">
<h2>Welcome to Comfychan</h2>
<div>
+23 -14
View File
@@ -9,24 +9,25 @@ import (
)
templ PostOriginal(post database.Post, thread *database.Thread) {
{{ imgInfo := util.GetPostImageInfo(post.MediaPath) }}
<article id={ fmt.Sprintf("post-%d", post.Number) } class="post-op">
<div>
File:
<a href={ templ.URL("/static/img/posts/full/" + post.MediaPath) } class="post-filename">
<a href={ templ.URL("/static/media/posts/full/" + post.MediaPath) } class="post-filename">
{ post.MediaPath }
</a>
<div class="post-img-info">
(<span>{ util.FormatBytes(imgInfo.Size) }</span>,
<span>{ strconv.Itoa(imgInfo.Width) }</span>x<span>{ strconv.Itoa(imgInfo.Height) }</span>)
<span>{ util.FormatFileInfo(util.GetPostFileInfo(post.MediaPath)) }</span>
</div>
</div>
<img
onclick="togglePostImage(this)"
onclick="togglePostFile(this)"
loading="lazy"
src={ fmt.Sprintf("/static/img/posts/thumb/%s", post.MediaPath) }
src={ fmt.Sprintf("/static/media/posts/thumb/%s", post.ThumbPath) }
data-full={ post.MediaPath }
data-thumb={ post.ThumbPath }
class="post-img"
/>
<video controls style="display: none;" class="post-vid"></video>
<header style="margin-top: 10px;" class="post-header">
<h1 class="thread-subject">{ thread.Subject } -</h1>
<span class="post-author">{ post.Author }</span>
@@ -61,25 +62,26 @@ templ PostReply(post database.Post) {
<span class="post-replies"></span>
</header>
if post.MediaPath != "" {
{{ imgInfo := util.GetPostImageInfo(post.MediaPath) }}
<div>
<div style="margin-bottom: 2px;">
File:
<a href={ templ.URL("/static/img/posts/full/" + post.MediaPath) } class="post-filename">
<a href={ templ.URL("/static/media/posts/full/" + post.MediaPath) } class="post-filename">
{ post.MediaPath }
</a>
<div class="post-img-info">
(<span>{ util.FormatBytes(imgInfo.Size) }</span>,
<span>{ strconv.Itoa(imgInfo.Width) }</span>x<span>{ strconv.Itoa(imgInfo.Height) }</span>)
<span>{ util.FormatFileInfo(util.GetPostFileInfo(post.MediaPath)) }</span>
</div>
</div>
<img
onclick="togglePostImage(this)"
onclick="togglePostFile(this)"
loading="lazy"
src={ fmt.Sprintf("/static/img/posts/thumb/%s",
post.MediaPath) }
src={ fmt.Sprintf("/static/media/posts/thumb/%s",
post.ThumbPath) }
data-full={ post.MediaPath }
data-thumb={ post.ThumbPath }
class="post-img"
/>
<video controls style="display: none;" class="post-vid"></video>
</div>
}
<p class="post-body">
@@ -128,7 +130,14 @@ templ Thread(board database.Board, thread database.Thread, posts []database.Post
</tr>
<tr class="new-post-form-field">
<th>File</th>
<td><input id="newPostFile" name="file" type="file"/></td>
<td>
<input
accept="image/*,video/mp4,video/webm,video/ogg"
id="newPostFile"
name="file"
type="file"
/>
</td>
</tr>
</tbody>
</table>