1
0
mirror of https://github.com/Damillora/phoebe.git synced 2025-04-15 19:33:12 +00:00

Compare commits

...

14 Commits

26 changed files with 1154 additions and 403 deletions

@ -11,7 +11,7 @@ FROM node:20-alpine AS node_build
WORKDIR /src
COPY . .
WORKDIR /src/web/app
RUN npm install && npm run build
RUN npm ci && npm run build
FROM scratch AS runtime

@ -11,8 +11,8 @@ import (
"os"
"path/filepath"
_ "golang.org/x/image/webp"
"golang.org/x/image/draw"
_ "golang.org/x/image/webp"
"github.com/Damillora/Shioriko/pkg/config"
"github.com/Damillora/Shioriko/pkg/database"
@ -107,17 +107,6 @@ func uploadBlob(c *gin.Context) {
hashSlice := make([]byte, 8)
binary.LittleEndian.PutUint64(hashSlice, hashInt)
if len(similarPosts) > 0 {
c.JSON(http.StatusOK,
models.BlobSimilarResponse{
ID: id,
Width: width,
Height: height,
Similar: similarPosts,
})
return
}
filename := id + filepath.Ext(file.Filename)
filePath := filepath.Join(dataDir, folder1, folder2, filename)
err = c.SaveUploadedFile(file, filePath)
@ -128,19 +117,19 @@ func uploadBlob(c *gin.Context) {
})
return
}
// Resize logic
previewWidth := 1000;
previewFactor := float32(previewWidth) / float32(width)
previewWidth := 1000
previewFactor := float32(previewWidth) / float32(width)
previewHeight := int(float32(height) * previewFactor)
if width <= previewWidth {
previewHeight = height
previewHeight = height
}
thumbnailWidth := 300;
thumbnailFactor := float32(thumbnailWidth) / float32(width)
thumbnailWidth := 300
thumbnailFactor := float32(thumbnailWidth) / float32(width)
thumbnailHeight := int(float32(height) * thumbnailFactor)
if width <= thumbnailWidth {
thumbnailHeight = height
thumbnailHeight = height
}
previewImage := image.NewRGBA(image.Rect(0, 0, previewWidth, previewHeight))
@ -197,10 +186,22 @@ func uploadBlob(c *gin.Context) {
database.DB.Create(&blob)
c.JSON(http.StatusOK, models.BlobResponse{
ID: id,
Width: width,
Height: height,
})
if len(similarPosts) > 0 {
c.JSON(http.StatusOK,
models.BlobSimilarResponse{
ID: id,
Width: width,
Height: height,
PreviewUrl: "/data/" + blob.PreviewFilePath,
Similar: similarPosts,
})
} else {
c.JSON(http.StatusOK, models.BlobResponse{
ID: id,
Width: width,
Height: height,
PreviewUrl: "/data/" + blob.PreviewFilePath,
})
}
return
}

@ -19,6 +19,10 @@ func InitializePostRoutes(g *gin.Engine) {
unprotected.GET("/", postGet)
unprotected.GET("/:id", postGetOne)
}
count := g.Group("/api/post-count")
{
count.GET("/", postCount)
}
protected := g.Group("/api/post").Use(middleware.AuthMiddleware())
{
protected.POST("/create", postCreate)
@ -30,24 +34,27 @@ func InitializePostRoutes(g *gin.Engine) {
func postGet(c *gin.Context) {
pageParam := c.Query("page")
perPageParam := c.Query("perPage")
page, _ := strconv.Atoi(pageParam)
perPage, _ := strconv.Atoi(perPageParam)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
tag := c.Query("tags")
tags := strings.Split(tag, " ")
var posts []database.Post
var postPages int
var perPage = 20
if tag != "" {
posts = services.GetPostTags(page, tags)
posts = services.GetPostTags(page, perPage, tags)
postPages = services.CountPostPagesTag(tags)
} else {
posts = services.GetPostAll(page)
posts = services.GetPostAll(page, perPage)
postPages = services.CountPostPages()
}
@ -59,10 +66,7 @@ func postGet(c *gin.Context) {
var postResult []models.PostListItem
var tagObjs []database.Tag
for _, post := range posts {
for _, tag := range post.Tags {
tagObjs = append(tagObjs, tag)
}
tagObjs = append(tagObjs, post.Tags...)
postResult = append(postResult, models.PostListItem{
ID: post.ID,
@ -80,7 +84,13 @@ func postGet(c *gin.Context) {
Tags: tagFilters,
})
}
func postCount(c *gin.Context) {
postPages := services.CountPostPages()
c.JSON(http.StatusOK, models.PostCountResponse{
PostCount: postPages,
})
}
func postGetOne(c *gin.Context) {
id := c.Param("id")
post, err := services.GetPost(id)

@ -15,16 +15,18 @@ type UserProfileResponse struct {
}
type BlobResponse struct {
ID string `json:"id"`
Width int `json:"width"`
Height int `json:"height"`
ID string `json:"id"`
Width int `json:"width"`
Height int `json:"height"`
PreviewUrl string `json:"previewUrl"`
}
type BlobSimilarResponse struct {
ID string `json:"id"`
Width int `json:"width"`
Height int `json:"height"`
Similar []PostSimilarityListItem `json:"similar"`
ID string `json:"id"`
Width int `json:"width"`
Height int `json:"height"`
PreviewUrl string `json:"previewUrl"`
Similar []PostSimilarityListItem `json:"similar"`
}
type PostPaginationResponse struct {
CurrentPage int `json:"currentPage"`
@ -33,3 +35,7 @@ type PostPaginationResponse struct {
Posts []PostListItem `json:"posts"`
Tags []TagListItem `json:"tags"`
}
type PostCountResponse struct {
PostCount int `json:"postCount"`
}

@ -9,15 +9,13 @@ import (
"github.com/google/uuid"
)
const perPage = 20
func GetPostAll(page int) []database.Post {
func GetPostAll(page int, perPage int) []database.Post {
var posts []database.Post
database.DB.Joins("Blob").Preload("Tags").Preload("Tags.TagType").Order("created_at desc").Offset((page - 1) * perPage).Limit(20).Find(&posts)
database.DB.Joins("Blob").Preload("Tags").Preload("Tags.TagType").Order("created_at desc").Offset((page - 1) * perPage).Limit(perPage).Find(&posts)
return posts
}
func GetPostTags(page int, tagSyntax []string) []database.Post {
func GetPostTags(page int, perPage int, tagSyntax []string) []database.Post {
positiveTagSyntax := []string{}
negativeTagSyntax := []string{}
@ -87,7 +85,7 @@ func GetPostTags(page int, tagSyntax []string) []database.Post {
}
query.Order("created_at desc").
Offset((page - 1) * perPage).
Limit(20).
Limit(perPage).
Find(&posts)
return posts
}

@ -20,7 +20,7 @@
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"eslint-plugin-svelte": "^2.45.1",
"sass": "^1.64.2",
"sass-embedded": "^1.85.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-tags-input": "^6.0.2",
@ -43,6 +43,13 @@
"node": ">=6.0.0"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz",
"integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==",
"dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -634,6 +641,7 @@
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
@ -676,6 +684,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -697,6 +706,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -718,6 +728,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -739,6 +750,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -760,6 +772,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -781,6 +794,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -802,6 +816,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -823,6 +838,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -844,6 +860,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -865,6 +882,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -886,6 +904,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -907,6 +926,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -928,6 +948,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
@ -1682,6 +1703,13 @@
"node": ">=8"
}
},
"node_modules/buffer-builder": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"dev": true,
"license": "MIT/X11"
},
"node_modules/bulma": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.3.tgz",
@ -1761,6 +1789,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/colorjs.io": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1878,6 +1913,7 @@
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
@ -2899,7 +2935,8 @@
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/once": {
"version": "1.4.0",
@ -3330,6 +3367,16 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@ -3344,11 +3391,13 @@
}
},
"node_modules/sass": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz",
"integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==",
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@ -3364,6 +3413,407 @@
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/sass-embedded": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.85.0.tgz",
"integrity": "sha512-x3Vv54g0jv1aPSW8OTA/0GzQCs/HMQOjIkLtZJ3Xsn/I4vnyjKbVTQmFTax9bQjldqLEEkdbvy6ES/cOOnYNwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"buffer-builder": "^0.2.0",
"colorjs.io": "^0.5.0",
"immutable": "^5.0.2",
"rxjs": "^7.4.0",
"supports-color": "^8.1.1",
"sync-child-process": "^1.0.2",
"varint": "^6.0.0"
},
"bin": {
"sass": "dist/bin/sass.js"
},
"engines": {
"node": ">=16.0.0"
},
"optionalDependencies": {
"sass-embedded-android-arm": "1.85.0",
"sass-embedded-android-arm64": "1.85.0",
"sass-embedded-android-ia32": "1.85.0",
"sass-embedded-android-riscv64": "1.85.0",
"sass-embedded-android-x64": "1.85.0",
"sass-embedded-darwin-arm64": "1.85.0",
"sass-embedded-darwin-x64": "1.85.0",
"sass-embedded-linux-arm": "1.85.0",
"sass-embedded-linux-arm64": "1.85.0",
"sass-embedded-linux-ia32": "1.85.0",
"sass-embedded-linux-musl-arm": "1.85.0",
"sass-embedded-linux-musl-arm64": "1.85.0",
"sass-embedded-linux-musl-ia32": "1.85.0",
"sass-embedded-linux-musl-riscv64": "1.85.0",
"sass-embedded-linux-musl-x64": "1.85.0",
"sass-embedded-linux-riscv64": "1.85.0",
"sass-embedded-linux-x64": "1.85.0",
"sass-embedded-win32-arm64": "1.85.0",
"sass-embedded-win32-ia32": "1.85.0",
"sass-embedded-win32-x64": "1.85.0"
}
},
"node_modules/sass-embedded-android-arm": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.85.0.tgz",
"integrity": "sha512-pPBT7Ad6G8Mlao8ypVNXW2ya7I/Bhcny+RYZ/EmrunEXfhzCNp4PWV2VAweitPO9RnPIJwvUTkLc8Fu6K3nVmw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.85.0.tgz",
"integrity": "sha512-4itDzRwezwrW8+YzMLIwHtMeH+qrBNdBsRn9lTVI15K+cNLC8z5JWJi6UCZ8TNNZr9LDBfsh5jUdjSub0yF7jg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.85.0.tgz",
"integrity": "sha512-bwqKq95hzbGbMTeXCMQhH7yEdc2xJVwIXj7rGdD3McvyFWbED6362XRFFPI5YyjfD2wRJd9yWLh/hn+6VyjcYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-riscv64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.85.0.tgz",
"integrity": "sha512-Fgkgay+5EePJXZFHR5Vlkutnsmox2V6nX4U3mfGbSN1xjLRm8F5ST72V2s5Z0mnIFpGvEu/v7hfptgViqMvaxg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.85.0.tgz",
"integrity": "sha512-/bG3JgTn3eoIDHCiJNVkLeJgUesat4ghxqYmKMZUJx++4e6iKCDj8XwQTJAgm+QDrsPKXHBacHEANJ9LEAuTqg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.85.0.tgz",
"integrity": "sha512-plp8TyMz97YFBCB3ndftEvoW29vyfsSBJILM5U84cGzr06SvLh/Npjj8psfUeRw+upEk1zkFtw5u61sRCdgwIw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.85.0.tgz",
"integrity": "sha512-LP8Zv8DG57Gn6PmSwWzC0gEZUsGdg36Ps3m0i1fVTOelql7N3HZIrlPYRjJvidL8ZlB3ISxNANebTREUHn/wkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.85.0.tgz",
"integrity": "sha512-18xOAEfazJt1MMVS2TRHV94n81VyMnywOoJ7/S7I79qno/zx26OoqqP4XvH107xu8+mZ9Gg54LrUH6ZcgHk08g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.85.0.tgz",
"integrity": "sha512-JRIRKVOY5Y8M1zlUOv9AQGju4P6lj8i5vLJZsVYVN/uY8Cd2dDJZPC8EOhjntp+IpF8AOGIHqCeCkHBceIyIjA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.85.0.tgz",
"integrity": "sha512-4JH+h+gLt9So22nNPQtsKojEsLzjld9ol3zWcOtMGclv+HojZGbCuhJUrLUcK72F8adXYsULmWhJPKROLIwYMA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.85.0.tgz",
"integrity": "sha512-Z1j4ageDVFihqNUBnm89fxY46pY0zD/Clp1D3ZdI7S+D280+AEpbm5vMoH8LLhBQfQLf2w7H++SZGpQwrisudQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.85.0.tgz",
"integrity": "sha512-aoQjUjK28bvdw9XKTjQeayn8oWQ2QqvoTD11myklGd3IHH7Jj0nwXUstI4NxDueCKt3wghuZoIQkjOheReQxlg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.85.0.tgz",
"integrity": "sha512-/cJCSXOfXmQFH8deE+3U9x+BSz8i0d1Tt9gKV/Gat1Xm43Oumw8pmZgno+cDuGjYQInr9ryW5121pTMlj/PBXQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.85.0.tgz",
"integrity": "sha512-l+FJxMXkmg42RZq5RFKXg4InX0IA7yEiPHe4kVSdrczP7z3NLxk+W9wVkPnoRKYIMe1qZPPQ25y0TgI4HNWouA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.85.0.tgz",
"integrity": "sha512-M9ffjcYfFcRvkFA6V3DpOS955AyvmpvPAhL/xNK45d/ma1n1ehTWpd24tVeKiNK5CZkNjjMEfyw2fHa6MpqmEA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-riscv64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.85.0.tgz",
"integrity": "sha512-yqPXQWfM+qiIPkfn++48GOlbmSvUZIyL9nwFstBk0k4x40UhbhilfknqeTUpxoHfQzylTGVhrm5JE7MjM+LNZA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.85.0.tgz",
"integrity": "sha512-NTDeQFZcuVR7COoaRy8pZD6/+QznwBR8kVFsj7NpmvX9aJ7TX/q+OQZHX7Bfb3tsfKXhf1YZozegPuYxRnMKAQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.85.0.tgz",
"integrity": "sha512-gO0VAuxC4AdV+uZYJESRWVVHQWCGzNs0C3OKCAdH4r1vGRugooMi7J/5wbwUdXDA1MV9ICfhlKsph2n3GiPdqA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.85.0.tgz",
"integrity": "sha512-PCyn6xeFIBUgBceNypuf73/5DWF2VWPlPqPuBprPsTvpZOMUJeBtP+Lf4mnu3dNy1z76mYVnpaCnQmzZ0zHZaA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.85.0.tgz",
"integrity": "sha512-AknE2jLp6OBwrR5hQ8pDsG94KhJCeSheFJ2xgbnk8RUjZX909JiNbgh2sNt9LG+RXf4xZa55dDL537gZoCx/iw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@ -3635,6 +4085,29 @@
"dev": true,
"license": "MIT"
},
"node_modules/sync-child-process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/sync-message-port": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -3752,6 +4225,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",

@ -18,7 +18,7 @@
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"eslint-plugin-svelte": "^2.45.1",
"sass": "^1.64.2",
"sass-embedded": "^1.85.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-tags-input": "^6.0.2",

@ -1,12 +1,12 @@
/* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */
// Path to Bulma's sass folder
@use "bulma/sass" with (
@use "../node_modules/bulma/sass/" as * with (
$family-primary: '"Nunito", sans-serif',
$primary: #00afcc,
$primary: #37b484,
);
// Import the Google Font
@import url("https://fonts.googleapis.com/css?family=Nunito:400,700");
@import "https://fonts.googleapis.com/css?family=Nunito:400,700";
// Others
@ -19,6 +19,9 @@
}
// Svelte Tags
#tags {
position: relative;
}
#tags .svelte-tags-input-layout {
@extend .input;
padding: 0;
@ -43,5 +46,28 @@
margin-right: var(--bulma-control-padding-horizontal);
}
#tags .svelte-tags-input-matchs-parent{
@extend .dropdown-menu;
display: block;
position: relative;
z-index: 2000;
}
#tags .svelte-tags-input-matchs {
@extend .dropdown-content;
position: absolute;
top: 0;
left: 0;
right: 0;
li {
@extend .dropdown-item;
background-color: hsl(var(--bulma-scheme-h), var(--bulma-scheme-s), calc(var(--bulma-scheme-main-l) + var(--bulma-dropdown-item-background-l-delta)));
--bulma-dropdown-item-background-l-delta: 0%;
&:hover {
--bulma-dropdown-item-background-l-delta: var(--bulma-hover-background-l-delta);
--bulma-dropdown-item-border-l-delta: var(--bulma-hover-border-l-delta);
}
transition-duration: var(--bulma-duration);
transition-property: background-color, border-color, color;
}
}

@ -62,23 +62,18 @@ export async function getTagAutocomplete({ tag, positive }) {
const response = await axios.get(endpoint);
return response.data;
}
export async function getPosts({ page }) {
const endpoint = url + "/api/post?page=" + page;
export async function getPosts({ page, q, perPage }: { page: any, q?: any, perPage?: any }) {
if (!perPage) {
perPage = 20;
}
let endpoint = url + "/api/post?page=" + page + "&perPage=" + perPage;
if (q) {
endpoint = url + "/api/post?tags=" + q + "&page=" + page + "&perPage=" + perPage;
}
const response = await axios.get(endpoint);
return response.data;
}
export async function getPostSearchTag({ page, q }) {
if (q) {
const endpoint = url + "/api/post?tags=" + q + "&page=" + page;
const response = await axios(endpoint);
return response.data;
} else {
const endpoint = url + "/api/post?page=" + page;
const response = await axios(endpoint);
return response.data;
}
}
export async function getPost({ id }) {
const endpoint = url + "/api/post/" + id;
@ -86,6 +81,12 @@ export async function getPost({ id }) {
return response.data;
}
export async function getPostCount() {
const endpoint = url + "/api/post-count";
const response = await axios(endpoint);
return response.data;
}
export async function uploadBlob({ file, onProgress }) {
var formData = new FormData();
formData.append("file", file);

@ -1,12 +1,14 @@
<script>
import { token } from "$lib/stores";
import { isTokenExpired } from "$lib/login-check";
import { isTokenExpired, getUsernameFromToken } from "$lib/login-check";
let menu_shown = $state(false);
let loggedIn = $state(false);
let username = $state("");
token.subscribe((value) => {
loggedIn = !isTokenExpired(value);
username = getUsernameFromToken(value);
});
const toggleMenu = () => {
@ -14,9 +16,11 @@
};
</script>
<nav class="navbar" role="navigation" aria-label="main navigation">
<nav class="navbar is-primary" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">Shioriko</a>
<a class="navbar-item" href="/">
<strong>shioriko</strong>
</a>
<a
href={"#"}
@ -42,29 +46,28 @@
</div>
<div class="navbar-end">
{#if loggedIn}
<div class="navbar-item">
<div class="buttons">
<a href="/user/profile" class="button is-primary">
<div class="navbar-item has-dropdown is-hoverable">
{#if loggedIn}
<div class="navbar-link">{username}</div>
<div class="navbar-dropdown">
<a href="/user/profile" class="navbar-item">
Profile
</a>
<a href="/auth/logout" class="button is-light">
Log out
</a>
<a href="/auth/logout" class="navbar-item">Log out</a>
</div>
</div>
{:else}
<div class="navbar-item">
<div class="buttons">
<a href="/auth/register" class="button is-primary">
{:else}
<div class="navbar-link">logged out</div>
<div class="navbar-dropdown">
<a href="/auth/register" class="navbar-item">
Register
</a>
<a href="/auth/login" class="button is-light">
Log in
</a>
<a href="/auth/login" class="navbar-item">Log in</a>
</div>
</div>
{/if}
{/if}
</div>
</div>
</div>
</nav>

@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import ShiorikoImage from "./ShiorikoImage.svelte";
let { posts = [] } = $props();
</script>
@ -12,7 +13,7 @@
<div class="card-image">
<figure class="image">
<a href="/post/{post.id}">
<img alt={post.id} src={post.thumbnail_path} />
<ShiorikoImage alt={post.id} src={post.thumbnail_path} />
</a>
</figure>
</div>

@ -0,0 +1,33 @@
<script>
import { onMount } from "svelte";
let { alt, src } = $props();
let loading = $state(false);
let failed = $state(false);
onMount(() => {
const img = new Image();
img.src = src;
loading = true;
img.onload = () => {
loading = false;
failed = false;
};
img.onerror = () => {
loading = true;
failed = true;
};
})
</script>
{#if !failed}
<figure class:is-skeleton="{loading}">
<img {src} {alt} />
</figure>
{:else}
<div class="notification is-danger is-light">
There was an error loading this image.
</div>
{/if}

@ -1,4 +1,6 @@
<script lang="ts">
import TagTypeIndicator from "$lib/components/ui/TagTypeIndicator.svelte";
let { tag, num } = $props();
let tagType = tag.split(":")[0] ?? "";
@ -6,6 +8,10 @@
let tagDisplay = tagName.split("_").join(" ");
</script>
<a href="/posts?tags={tagName}"
>{tagDisplay} <span class="is-pulled-right">{num}</span></a
>
<a href="/posts?tags={tagName}">
<span>
{tagDisplay}
</span>
<TagTypeIndicator tagType={tagType}></TagTypeIndicator>
<span class="is-pulled-right">{num}</span>
</a>

@ -0,0 +1,12 @@
<script>
let { tagType } = $props();
</script>
{#if tagType == "character"}
<span class="tag is-link is-light">{tagType}</span>
{:else if tagType == "series"}
<span class="tag is-warning is-light">{tagType}</span>
{:else}
<span class="tag">{tagType}</span>
{/if}

@ -1,8 +1,20 @@
const isTokenExpired = (token) => {
if (token === "") return true;
const expiry = (JSON.parse(atob(token.split('.')[1]))).exp;
const tokenData = (JSON.parse(atob(token.split('.')[1])));
const expiry = tokenData.exp;
return (Math.floor((new Date).getTime() / 1000)) >= expiry;
}
const getUsernameFromToken = (token) => {
if (token === "") return "logged out";
export { isTokenExpired }
const isExpired = isTokenExpired(token);
if (!isExpired) {
const tokenData = (JSON.parse(atob(token.split('.')[1])));
return tokenData.name;
}
return "logged out";
}
export { isTokenExpired, getUsernameFromToken }

@ -17,3 +17,11 @@
<Navbar />
{@render children?.()}
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong><a href="https://github.com/Damillora/Shioriko">shioriko</a></strong>: a booru-style image gallery written in Go and Svelte
</p>
</div>
</footer>

@ -1 +0,0 @@
export const ssr = false;

@ -1,19 +1,23 @@
<script lang="ts">
import Tags from "svelte-tags-input";
import { getTagAutocomplete } from "$lib/api";
import { getPostCount, getPosts, getTagAutocomplete } from "$lib/api";
import { goto } from '$app/navigation';
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import PostGallery from "$lib/components/ui/PostGallery.svelte";
let searchTerms: string[] = $state([]);
let postCount: number = $state(0);
let postCountLoaded: boolean = $state(false);
const onTagChange = (value) => {
searchTerms = value.detail.tags;
};
const onAutocomplete = async (tag) => {
const list = await getTagAutocomplete({ tag });
return list;
};
const list = await getTagAutocomplete({ tag });
return list;
};
const onSearch = (e) => {
e.preventDefault();
@ -23,35 +27,55 @@
goto(`/posts`);
}
};
const getCounts = async () => {
const response = await getPostCount();
postCount = response.postCount;
postCountLoaded = true;
};
onMount(() => {
getCounts();
});
</script>
<section class="hero is-small">
<section class="hero is-primary is-fullheight-with-navbar">
<div class="hero-body">
<div class="container has-text-centered">
<p class="title">Shioriko</p>
<p class="subtitle">Booru-style gallery written in Go and Svelte</p>
</div>
</div>
<div class="hero-foot">
<div class="container has-text-centered">
<form onsubmit={onSearch}>
<div class="field has-addons">
<div class="control has-text-left is-expanded">
<div class="control" id="tags">
<Tags
tags={searchTerms}
addKeys={[9, 32]}
on:tags={onTagChange}
autoComplete={onAutocomplete}
autoCompleteFilter={false}
/>
<div class="container">
<div class="columns is-centered">
<div class="column is-12-tablet is-8-desktop is-8-widescreen">
<div class="box has-text-centered">
<p class="title">shioriko</p>
<p class="subtitle">a booru-style gallery written in Go and Svelte</p>
<div class="block">
<form onsubmit={onSearch}>
<div class="field">
<div class="control has-text-left is-expanded" id="tags">
<Tags
tags={searchTerms}
addKeys={[9, 32]}
on:tags={onTagChange}
autoComplete={onAutocomplete}
autoCompleteFilter={false}
/>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-primary">
Search
</button>
</div>
</div>
</form>
</div>
{#if postCountLoaded}
<p class="block">serving <span class="is-primary"><strong>{postCount}</strong></span> images</p>
{:else}
<p class="block">serving <span class="is-primary"><strong>...</strong></span> images</p>
{/if}
</div>
</div>
<div class="control">
<button type="submit" class="button is-primary"> Search </button>
</div>
</form>
</div>
</div>
</div>
</section>

@ -0,0 +1,17 @@
<script lang="ts">
import Navbar from "$lib/components/ui/Navbar.svelte";
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
export const ssr = false;
</script>
<section class="hero is-primary is-fullheight-with-navbar">
<div class="hero-body">
{@render children?.()}
</div>
</section>

@ -19,51 +19,50 @@
};
</script>
<section class="hero is-primary">
<div class="hero-body">
<p class="title">Login</p>
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
<div class="box">
<p class="title">Login</p>
<form onsubmit={doLogin}>
<div class="field">
<label for="username" class="label">Username</label>
<div class="control">
<input
id="username"
class="input"
type="text"
placeholder="Username"
bind:value={username}
required
/>
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input
id="password"
class="input"
type="password"
placeholder="Password"
bind:value={password}
required
/>
</div>
</div>
{#if error}
<div class="field">
<p class="has-text-danger">{error}</p>
</div>
{/if}
<div class="field">
<div class="control">
<button class="button is-link">Login</button>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<form onsubmit={doLogin}>
<div class="field">
<label for="username" class="label">Username</label>
<div class="control">
<input
id="username"
class="input"
type="text"
placeholder="Username"
bind:value={username}
required
/>
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input
id="password"
class="input"
type="password"
placeholder="Password"
bind:value={password}
required
/>
</div>
</div>
{#if error}
<div class="field">
<p class="has-text-danger">{error}</p>
</div>
{/if}
<div class="field">
<div class="control">
<button class="button is-link">Login</button>
</div>
</div>
</form>
</div>
</section>
</div>

@ -18,57 +18,58 @@
};
</script>
<section class="hero is-primary">
<div class="hero-body">
<p class="title">Register</p>
</div>
</section>
<div class="container">
<form onsubmit={doRegister}>
<div class="field">
<label for="email" class="label">Email</label>
<div class="control">
<input
id="email"
class="input"
type="text"
placeholder="Email"
bind:value={email}
required
/>
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
<div class="box">
<p class="title">Register</p>
<form onsubmit={doRegister}>
<div class="field">
<label for="email" class="label">Email</label>
<div class="control">
<input
id="email"
class="input"
type="text"
placeholder="Email"
bind:value={email}
required
/>
</div>
</div>
<div class="field">
<label for="username" class="label">Username</label>
<div class="control">
<input
id="username"
class="input"
type="text"
placeholder="Username"
bind:value={username}
required
/>
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input
id="password"
class="input"
type="password"
placeholder="Password"
bind:value={password}
required
/>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-link">Login</button>
</div>
</div>
</form>
</div>
</div>
<div class="field">
<label for="username" class="label">Username</label>
<div class="control">
<input
id="username"
class="input"
type="text"
placeholder="Username"
bind:value={username}
required
/>
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input
id="password"
class="input"
type="password"
placeholder="Password"
bind:value={password}
required
/>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-link">Login</button>
</div>
</div>
</form>
</div>
</div>

@ -1,6 +1,4 @@
<script lang="ts">
import { run } from "svelte/legacy";
import { onMount } from "svelte";
import { getPost, postDelete } from "$lib/api";
import { afterNavigate, goto } from "$app/navigation";
@ -8,6 +6,7 @@
import ViewPostPanel from "$lib/components/panels/ViewPostPanel.svelte";
import { page } from "$app/stores";
import ShiorikoImage from "$lib/components/ui/ShiorikoImage.svelte";
const { id } = $page.params;
let post: any = $state();
@ -16,8 +15,10 @@
post = data;
imagePercentage = ((1000 * 100) / post.width).toFixed(0) + "%";
};
let loading = $state(false);
let isOriginal = $state(false);
const trimUrl = (str) => {
const trimUrl = (str: string) => {
if (str.length > 30) {
return str.substring(0, 30) + "...";
}
@ -53,11 +54,11 @@
let imagePercentage = $state("0%");
</script>
{#if post}
<div class="container">
<section class="section">
<div class="columns">
<div class="column is-one-third">
<div class="container">
<section class="section">
<div class="columns">
<div class="column is-one-third">
{#if post}
{#if editMenuShown == false && deleteMenuShown == false}
<ViewPostPanel
{post}
@ -88,25 +89,38 @@
</div>
</div>
{/if}
</div>
<div class="column box">
{#if post.width > 1000}
{:else}
<div class="skeleton-block">
</div>
{/if}
</div>
<div class="column box">
{#if post}
{#if post.width > 1000 && isOriginal == false}
<div class="notification is-info">
Resized to {imagePercentage} of the original image.
<a href={post.image_path} target="_blank"
<a onclick="{() => { isOriginal = true; }}"
>View original</a
>
</div>
<figure class="image">
<img alt={post.id} src={post.preview_path} />
<ShiorikoImage alt={post.id} src={post.preview_path} />
</figure>
{:else}
<div class="notification is-primary">
Currently viewing original image.
</div>
<figure class="image">
<img alt={post.id} src={post.image_path} />
<ShiorikoImage alt={post.id} src={post.image_path} />
</figure>
{/if}
</div>
{:else}
<div class="skeleton-block">
</div>
{/if}
</div>
</section>
</div>
{/if}
</div>
</section>
</div>

@ -1,15 +1,15 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { run } from "svelte/legacy";
import { getPostSearchTag, getTag, getTagAutocomplete } from "$lib/api";
import { getPosts, getTag, getTagAutocomplete } from "$lib/api";
import TagLinkNumbered from "$lib/components/ui/TagLinkNumbered.svelte";
import PostGallery from "$lib/components/ui/PostGallery.svelte";
import queryString from "query-string";
import Tags from "svelte-tags-input";
import { paginate } from "$lib/simple-pagination";
import { afterNavigate, beforeNavigate, goto } from "$app/navigation";
import { page as currentPage } from '$app/stores';
import { onMount } from 'svelte';
import { page as currentPage } from "$app/stores";
import { onMount } from "svelte";
let url = $derived($currentPage.url);
@ -23,16 +23,17 @@
let tags = $state([]);
let tagInfo = $state(null);
let categorizedTags = {};
let loading = $state(false);
const getData = async () => {
const data = await getPostSearchTag({ page, q: searchTerms.join("+") });
const data = await getPosts({ page, q: searchTerms.join("+") });
if (data.posts) {
posts = data.posts;
tags = data.tags
.filter(
(x) =>
!searchTerms.includes(x.tagName) &&
!searchTerms.includes(x.tagType + ":" + x.tagName)
!searchTerms.includes(x.tagType + ":" + x.tagName),
)
.sort((a, b) => b.postCount - a.postCount);
totalPages = data.totalPage;
@ -46,9 +47,10 @@
pagination = paginate(page, totalPages);
}
if (searchTerms.filter(x => !x.startsWith("-")).length == 1) {
if (searchTerms.filter((x) => !x.startsWith("-")).length == 1) {
tagInfo = await getTag({ tag: searchTerms[0] });
}
loading = false;
};
let tagQuery = $state();
@ -62,7 +64,8 @@
};
afterNavigate(() => {
tagQuery = url.searchParams.get('tags');
loading = true;
tagQuery = url.searchParams.get("tags");
if (tagQuery) {
searchTerms = tagQuery.split(" ");
} else {
@ -72,7 +75,7 @@
posts = [];
page = 1;
getData();
})
});
const onSearch = (e) => {
e.preventDefault();
if (searchTerms.length > 0) {
@ -95,14 +98,14 @@
<div class="block">
<div class="columns is-multiline">
<div class="column is-full">
<div class="block">
</div>
<div class="block"></div>
</div>
<div class="column is-one-third">
<div class="panel is-primary">
<div class="panel-heading">Menu</div>
<div class="panel-block column">
<form onsubmit={onSearch}>
<div class="field has-addons">
<div class="field">
<div class="control is-expanded">
<div class="control" id="tags">
<Tags
@ -115,16 +118,44 @@
</div>
</div>
</div>
<div class="control">
<button
type="submit"
class="button is-primary"
>
Search
</button>
<div class="field">
<div class="control">
<button
type="submit"
class="button is-primary"
>
Search
</button>
</div>
</div>
</form>
</div>
<div class="panel-block column">
{#if !loading}
<div class="row">
<strong>Tags:</strong>
</div>
<div class="row">
<div class="menu">
<ul class="menu-list">
{#each tags as tag (tag)}
<li>
<TagLinkNumbered
class=""
tag={tag.tagType +
":" +
tag.tagName}
num={tag.postCount}
/>
</li>
{/each}
</ul>
</div>
</div>
{:else}
<div class="skeleton-block"></div>
{/if}
</div>
</div>
{#if tagInfo}
<div class="panel is-info">
@ -141,84 +172,73 @@
{/if}
<div class="panel-block column">
<a
class="button is-primary"
class="button is-primary is-fullwidth is-outlined"
href="/tags/{tagInfo.tagName}">View Tag</a
>
</div>
</div>
{/if}
<div class="panel is-primary">
<div class="panel-heading">Tags</div>
<div class="panel-block column">
<div class="menu">
<ul class="menu-list">
{#each tags as tag (tag)}
<li>
<TagLinkNumbered
class=""
tag={tag.tagType +
":" +
tag.tagName}
num={tag.postCount}
/>
</li>
{/each}
</ul>
</div>
</div>
</div>
</div>
<div class="column is-two-thirds">
<div class="columns is-multiline">
<div class="column is-full">
<PostGallery {posts} />
</div>
{#if !loading}
<div class="column is-full">
<PostGallery {posts} />
</div>
<div class="column is-full">
<nav
class="pagination is-centered"
aria-label="pagination"
>
<a
href={null}
onclick={changePage(page - 1)}
class="pagination-previous"
class:is-disabled={page == 1}>Previous</a
<div class="column is-full">
<nav
class="pagination is-centered"
aria-label="pagination"
>
<a
href={null}
onclick={changePage(page + 1)}
class="pagination-next"
class:is-disabled={page == totalPages}
>Next</a
>
<ul class="pagination-list">
{#each pagination as pageEntry}
{#if pageEntry == "..."}
<li>
<span
class="pagination-ellipsis"
>&hellip;</span
>
</li>
{:else}
<li>
<a
href={null}
onclick={() =>
changePage(pageEntry)}
class="pagination-link"
class:is-current={page ==
pageEntry}
aria-label="Goto page {pageEntry}"
>{pageEntry}</a
>
</li>
{/if}
{/each}
</ul>
</nav>
</div>
<a
href={null}
onclick={() => changePage(page - 1)}
class="pagination-previous"
class:is-disabled={page == 1}
>Previous</a
>
<a
href={null}
onclick={() => changePage(page + 1)}
class="pagination-next"
class:is-disabled={page == totalPages}
>Next</a
>
<ul class="pagination-list">
{#each pagination as pageEntry}
{#if pageEntry == "..."}
<li>
<span
class="pagination-ellipsis"
>&hellip;</span
>
</li>
{:else}
<li>
<a
href={null}
onclick={() =>
changePage(
pageEntry,
)}
class="pagination-link"
class:is-current={page ==
pageEntry}
aria-label="Goto page {pageEntry}"
>{pageEntry}</a
>
</li>
{/if}
{/each}
</ul>
</nav>
</div>
{:else}
<div class="column">
<div class="skeleton-block"></div>
</div>
{/if}
</div>
</div>
</div>

@ -3,15 +3,19 @@
import { getTags } from "$lib/api";
import { afterNavigate } from '$app/navigation';
import TagTypeIndicator from '$lib/components/ui/TagTypeIndicator.svelte';
let tags = $state([]);
let loading = $state(false);
const getData = async () => {
const data = await getTags();
tags = data;
loading = false;
};
afterNavigate(() => {
loading = true;
getData();
})
</script>
@ -20,6 +24,7 @@
<section class="section">
<div class="container">
<h1 class="title">Tag List</h1>
{#if !loading}
<table class="table is-fullwidth">
<thead>
<tr>
@ -34,11 +39,14 @@
<td>
<a href="/tags/{tag.tagName}">{tag.tagName}</a>
</td>
<td>{tag.tagType}</td>
<td><TagTypeIndicator tagType={tag.tagType} /></td>
<td>{tag.postCount}</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<div class="skeleton-block"></div>
{/if}
</div>
</section>

@ -1,14 +1,13 @@
<script>
import { onMount } from "svelte";
import { getTag, getPostSearchTag } from "$lib/api";
import { getTag, getPosts } from "$lib/api";
import EditTagNotesPanel from "$lib/components/panels/EditTagNotesPanel.svelte";
import ViewTagNotesPanel from "$lib/components/panels/ViewTagNotesPanel.svelte";
import ViewTagPanel from "$lib/components/panels/ViewTagPanel.svelte";
import EditTagPanel from "$lib/components/panels/EditTagPanel.svelte";
import PostGallery from "$lib/components/ui/PostGallery.svelte";
import { page } from "$app/stores";
let { tag } = $state($page.params);
@ -18,7 +17,7 @@
const getData = async () => {
if (tag) {
data = await getTag({ tag });
const response = await getPostSearchTag({
const response = await getPosts({
page: 1,
q: tag,
});
@ -51,9 +50,9 @@
<section class="section">
<div class="container">
{#if data}
<div class="columns">
<div class="column is-one-third">
<div class="columns">
<div class="column is-one-third">
{#if data}
{#if renameMenuShown}
<EditTagPanel
{tag}
@ -64,8 +63,12 @@
{:else}
<ViewTagPanel {tag} {data} {toggleRenameMenu} />
{/if}
</div>
<div class="column is-two-thirds">
{:else}
<div class="skeleton-block"></div>
{/if}
</div>
<div class="column is-two-thirds">
{#if data}
{#if editMenuShown}
<EditTagNotesPanel
{tag}
@ -78,8 +81,10 @@
{/if}
<h1 class="title">Posts</h1>
<PostGallery {posts} />
</div>
{:else}
<div class="skeleton-block"></div>
{/if}
</div>
{/if}
</div>
</div>
</section>

@ -3,11 +3,13 @@
import { goto } from "$app/navigation";
import Tags from "svelte-tags-input";
import AuthRequired from "$lib/components/checks/AuthRequired.svelte";
import ShiorikoImage from "$lib/components/ui/ShiorikoImage.svelte";
let currentProgress = $state(0);
let fileName = $state("");
let similar = $state([]);
let previewUrl = $state("");
let form = $state({
blob_id: "",
@ -21,16 +23,20 @@
};
const onFileChange = async (e) => {
fileName = "";
similar = [];
var file = e.target.files[0];
fileName = "";
previewUrl = "";
similar = [];
if (file) {
var response = await uploadBlob({ file, onProgress });
if (response.similar) {
fileName = "";
previewUrl = "";
similar = response.similar;
}
form.blob_id = response.id;
fileName = file.name;
previewUrl = response.previewUrl;
}
};
@ -54,72 +60,133 @@
<section class="section">
<div class="container">
<h1 class="title">Upload Image</h1>
<form onsubmit={onSubmit}>
<div class="field">
<label for="file" class="label">Image File</label>
<div class="control">
<div class="file">
<label class="file-label">
<input
id="file"
class="file-input"
type="file"
name="resume"
onchange={onFileChange}
/>
<span class="file-cta">
<span class="file-icon"></span>
<span class="file-label"> Choose a file… </span>
</span>
</label>
</div>
</div>
{#if currentProgress > 0 && currentProgress < 100}
<p class="help">{currentProgress}%</p>
{/if}
{#if fileName !== ""}
<p class="help">{fileName} uploaded</p>
{/if}
{#if similar.length > 0}
<p class="help">
Similar posts:
{#each similar as post, i}
<a href="/post/{post.id}">{post.id}</a>
{#if i < similar.length - 1}
,
{/if}
{/each}
</p>
{/if}
</div>
<div class="field">
<label for="source" class="label">Source URL</label>
<div class="control">
<input
id="source"
class="input"
type="url"
placeholder="Source URL"
bind:value={form.source_url}
/>
<div class="columns">
<div class="column is-one-third">
<div class="panel is-primary">
<form onsubmit={onSubmit}>
<p class="panel-heading">Upload Image</p>
<div class="panel-block column">
<div class="row">
<label for="file" class="label">Image:</label>
</div>
<div class="row">
<div class="field">
<div class="control">
<div class="file">
<label class="file-label">
<input
id="file"
class="file-input"
type="file"
name="resume"
onchange={onFileChange}
/>
<span class="file-cta">
<span class="file-icon"
></span>
<span class="file-label">
Choose a file…
</span>
</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="panel-block column">
<div class="row">
<label for="source" class="label"
>Source URL:</label
>
</div>
<div class="row">
<div class="field">
<div class="control">
<input
id="source"
class="input"
type="url"
placeholder="Source URL"
bind:value={form.source_url}
/>
</div>
</div>
</div>
</div>
<div class="panel-block column">
<div class="row">
<label for="tags" class="label">Tags</label>
</div>
<div class="row">
<div class="field">
<div class="control" id="tags">
<Tags
tags={form.tags}
addKeys={[9, 32]}
on:tags={onTagChange}
autoComplete={onAutocomplete}
autoCompleteFilter={false}
/>
</div>
</div>
</div>
</div>
<div class="panel-block column">
<button
type="submit"
class="button is-primary is-fullwidth is-outlined"
>Submit</button
>
</div>
</form>
</div>
</div>
<div class="field">
<label for="tags" class="label">Tags</label>
<div class="control" id="tags">
<Tags
tags={form.tags}
addKeys={[9, 32]}
on:tags={onTagChange}
autoComplete={onAutocomplete}
autoCompleteFilter={false}
/>
<div class="column is-two-thirds">
<div class="box">
{#if fileName}
{#if similar.length > 0}
<div class="notification is-warning">
{fileName} has been succesfully uploaded. There are
similar images existing:
{#each similar as post, i}
<a href="/post/{post.id}">{post.id}</a>
{#if i < similar.length - 1}
,
{/if}
{/each}
</div>
{:else}
<div class="notification is-primary">
{fileName} has been succesfully uploaded.
</div>
{/if}
<figure class="image">
<ShiorikoImage alt={fileName} src={previewUrl} />
</figure>
{:else if currentProgress > 0 && currentProgress < 100}
<progress
class="progress is-primary"
value={currentProgress}
max="100"
>
{currentProgress}%
</progress>
<div class="notification is-info">
Your image is currently uploading...
</div>
{:else}
<div class="notification is-primary">
Your image will appear here when you upload it.
</div>
{/if}
</div>
</div>
<div class="control">
<button type="submit" class="button is-primary">Submit</button>
</div>
</form>
</div>
</div>
</section>
<section class="section">
<div class="container">
<h1 class="title">Upload Image</h1>
</div>
</section>