mirror of
https://github.com/Damillora/phoebe.git
synced 2025-04-15 19:33:12 +00:00
Compare commits
14 Commits
95d9a97db7
...
6bde5a6af6
Author | SHA1 | Date | |
---|---|---|---|
6bde5a6af6 | |||
2cb4d344cd | |||
4f39b8ab56 | |||
44075dbab9 | |||
3d40add1b2 | |||
7bb7a3389b | |||
413579e08d | |||
4d11c9c5fe | |||
675f12935c | |||
fba053e7b1 | |||
0a79956cca | |||
54ba5835b9 | |||
5e713d05c7 | |||
3ff0afda1c |
Dockerfile
pkg
web/app
package-lock.jsonpackage.json
src
app.scss
lib
routes
@ -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
|
||||
}
|
||||
|
490
web/app/package-lock.json
generated
490
web/app/package-lock.json
generated
@ -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>
|
||||
|
33
web/app/src/lib/components/ui/ShiorikoImage.svelte
Normal file
33
web/app/src/lib/components/ui/ShiorikoImage.svelte
Normal file
@ -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>
|
||||
|
12
web/app/src/lib/components/ui/TagTypeIndicator.svelte
Normal file
12
web/app/src/lib/components/ui/TagTypeIndicator.svelte
Normal file
@ -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>
|
||||
|
17
web/app/src/routes/auth/+layout.svelte
Normal file
17
web/app/src/routes/auth/+layout.svelte
Normal file
@ -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"
|
||||
>…</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"
|
||||
>…</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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user