Compare commits

...

5 Commits

16 changed files with 472 additions and 186 deletions

View File

@ -32,12 +32,16 @@ func postGet(c *gin.Context) {
pageParam := c.Query("page")
page, _ := strconv.Atoi(pageParam)
if page < 1 {
page = 1
}
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)
@ -47,9 +51,15 @@ func postGet(c *gin.Context) {
postPages = services.CountPostPages()
}
var totalPage = postPages / perPage
if postPages%perPage > 0 {
totalPage++
}
var postResult []models.PostListItem
var tagStrings []string
for _, post := range posts {
var tagStrings []string
for _, tag := range post.Tags {
tagStrings = append(tagStrings, tag.TagType.Name+":"+tag.Name)
}
@ -58,13 +68,16 @@ func postGet(c *gin.Context) {
ID: post.ID,
ImageThumbnailPath: "/data/" + post.Blob.ThumbnailFilePath,
ImagePath: "/data/" + post.Blob.FilePath,
Tags: tagStrings,
})
}
tagObjs := services.GetTagFilterString(tagStrings)
c.JSON(http.StatusOK, models.PostPaginationResponse{
CurrentPage: page,
TotalPage: totalPage,
PostCount: postPages,
Posts: postResult,
Tags: tagObjs,
})
}
@ -77,17 +90,15 @@ func postGetOne(c *gin.Context) {
Message: err.Error(),
})
}
var tagStrings []string
for _, tag := range post.Tags {
tagStrings = append(tagStrings, tag.TagType.Name+":"+tag.Name)
}
tagObjs := services.GetTagFilter(post.Tags)
c.JSON(http.StatusOK, models.PostReadModel{
ID: post.ID,
ImagePreviewPath: "/data/" + post.Blob.PreviewFilePath,
ImagePath: "/data/" + post.Blob.FilePath,
SourceURL: post.SourceURL,
Tags: tagStrings,
Tags: tagObjs,
Width: post.Blob.Width,
Height: post.Blob.Height,
Uploader: post.User.Username,

View File

@ -17,10 +17,9 @@ type TagAutocompleteListItem struct {
}
type PostListItem struct {
ID string `json:"id"`
ImagePath string `json:"image_path"`
ImageThumbnailPath string `json:"thumbnail_path"`
Tags []string `json:"tags"`
ID string `json:"id"`
ImagePath string `json:"image_path"`
ImageThumbnailPath string `json:"thumbnail_path"`
}
type PostSimilarityListItem struct {

View File

@ -1,12 +1,12 @@
package models
type PostReadModel struct {
ID string `json:"id"`
ImagePreviewPath string `json:"preview_path"`
ImagePath string `json:"image_path"`
SourceURL string `json:"source_url"`
Tags []string `json:"tags"`
Width int `json:"width"`
Height int `json:"height"`
Uploader string `json:"uploader"`
ID string `json:"id"`
ImagePreviewPath string `json:"preview_path"`
ImagePath string `json:"image_path"`
SourceURL string `json:"source_url"`
Tags []TagListItem `json:"tags"`
Width int `json:"width"`
Height int `json:"height"`
Uploader string `json:"uploader"`
}

View File

@ -28,6 +28,8 @@ type BlobSimilarResponse struct {
}
type PostPaginationResponse struct {
CurrentPage int `json:"currentPage"`
TotalPage int `json:"totalPage"`
PostCount int `json:"postCount"`
Posts []PostListItem `json:"posts"`
Tags []TagListItem `json:"tags"`
}

View File

@ -21,6 +21,27 @@ func GetTagAll() []models.TagListItem {
return tags
}
func GetTagFilterString(tagString []string) []models.TagListItem {
tagObjs, _ := ParseReadTags(tagString)
return GetTagFilter(tagObjs)
}
func GetTagFilter(tagObjs []database.Tag) []models.TagListItem {
var tagIds []string
for _, val := range tagObjs {
tagIds = append(tagIds, val.ID)
}
var tags []models.TagListItem
database.DB.Model(&tagObjs).
Joins("join tag_types on tag_types.id = tags.tag_type_id").
Joins("left join post_tags on post_tags.tag_id = tags.id").
Select("tags.id as tag_id, tags.name as tag_name, tag_types.name as tag_type, count(post_tags.post_id) as post_count").
Group("tags.id, tags.name, tag_types.name").
Order("post_count DESC").
Find(&tags, tagIds)
return tags
}
func GetTagAutocomplete() []string {
var tags []string
result := database.DB.Model(&database.Tag{}).

View File

@ -0,0 +1,12 @@
<script>
import { token } from "./stores.js";
let loggedIn = false;
token.subscribe((value) => {
loggedIn = value !== "";
});
</script>
{#if loggedIn == true}
<slot />
{/if}

View File

@ -0,0 +1,90 @@
<script>
import Tags from "svelte-tags-input";
import { onMount } from "svelte";
import { getPost, postUpdate, getTagAutocomplete } from "./api.js";
export let isActive = false;
export let post;
export let onSubmit;
const toggleEditModal = () => {
isActive = !isActive;
};
let form = {
source_url: "",
tags: [],
};
const getData = async () => {
form.source_url = post.source_url;
form.tags = post.tags;
};
const onTagChange = (value) => {
form.tags = value.detail.tags;
};
const onAutocomplete = async () => {
const list = await getTagAutocomplete();
return list;
};
const onFormSubmit = async () => {
const response = await postUpdate(post.id, form);
toggleEditModal();
onSubmit();
};
onMount(() => {
getData();
});
</script>
<form on:submit|preventDefault={onFormSubmit}>
<div class="panel is-warning">
<p class="panel-heading">Edit Post</p>
<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}
/>
</div>
</div>
</div>
</div>
<div class="panel-block column">
<button class="button is-primary" type="submit">Save</button>
<button class="button" on:click|preventDefault={toggleEditModal}
>Cancel</button
>
</div>
</div>
</form>

View File

@ -1,10 +0,0 @@
<script>
import { Link } from "svelte-routing";
export let tag;
let tagType = tag.split(":")[0] ?? "";
let tagName = tag.split(":")[1] ?? "";
</script>
<Link class="button is-rounded is-primary is-small m-1" to="/posts?tags={tagName}">{tagName}</Link>

View File

@ -0,0 +1,13 @@
<script>
import { Link } from "svelte-routing";
export let tag;
export let num;
let tagType = tag.split(":")[0] ?? "";
let tagName = tag.split(":")[1] ?? "";
let tagDisplay = tagName.split("_").join(" ");
</script>
<Link to="/posts?tags={tagName}">{tagDisplay} <span class="is-pulled-right">{num}</span></Link>

View File

@ -0,0 +1,82 @@
<script>
import AuthCheck from "./AuthCheck.svelte";
import TagLinkNumbered from "./TagLinkNumbered.svelte";
export let post;
export let toggleEditMenu;
export let toggleDeleteMenu;
const trimUrl = (str) => {
if (str.length > 30) {
return str.substring(0, 30) + "...";
}
return str;
};
</script>
<div class="panel is-primary">
<p class="panel-heading">Post</p>
<div class="panel-block column">
<div class="row">
<strong>Uploader:</strong>
</div>
<div class="row">{post.uploader}</div>
</div>
<div class="panel-block column">
<div class="row">
<strong>Source URL:</strong>
</div>
<div class="row">
<a href={post.source_url}>{trimUrl(post.source_url)}</a>
</div>
</div>
<div class="panel-block column">
<div class="row">
<strong>Original:</strong>
</div>
<div class="row">
<a href={post.image_path}>Image</a>
</div>
</div>
<div class="panel-block column">
<div class="row">
<strong>Dimensions:</strong>
</div>
<div class="row">
{post.width}x{post.height}
</div>
</div>
<div class="panel-block column">
<div class="row">
<p><strong>Tags:</strong></p>
</div>
<div class="row">
<div class="menu">
<ul class="menu-list">
{#if post.tags}
{#each post.tags as tag (tag)}
<li>
<TagLinkNumbered
class=""
tag={tag.tagType + ":" + tag.tagName}
num={tag.postCount}
/>
</li>
{/each}
{/if}
</ul>
</div>
</div>
</div>
<AuthCheck>
<p class="panel-block column">
<button
on:click|preventDefault={toggleEditMenu}
class="button is-primary">Edit</button
>
<button
on:click|preventDefault={toggleDeleteMenu}
class="button is-danger">Delete</button
>
</p>
</AuthCheck>
</div>

View File

@ -12,5 +12,5 @@
}
.svelte-tags-input-matchs-parent {
z-index: 200;
}
z-index: 2000;
}

View File

@ -1,8 +1,9 @@
<script>
import { onMount } from "svelte";
import TagLink from "../TagLink.svelte";
import { getPost, postCreate, postDelete } from "../api.js";
import { Link, navigate } from "svelte-routing";
import { getPost, postDelete } from "../api.js";
import { navigate } from "svelte-routing";
import EditPostPanel from "../EditPostPanel.svelte";
import ViewPostPanel from "../ViewPostPanel.svelte";
export let id;
let post;
const getData = async () => {
@ -21,70 +22,62 @@
getData();
});
let modal_shown = false;
let deleteMenuShown = false;
const deletePost = async () => {
toggleModal();
toggleDeleteMenu();
const success = await postDelete({ id });
if (success) {
navigate("/posts");
}
};
const toggleModal = () => {
modal_shown = !modal_shown;
const toggleDeleteMenu = () => {
deleteMenuShown = !deleteMenuShown;
};
let editMenuShown = false;
const toggleEditMenu = () => {
editMenuShown = !editMenuShown;
};
</script>
<section class="hero is-primary">
<div class="hero-body">
{#if post}
<p class="title">
Post ID: {post.id}
</p>
{/if}
</div>
</section>
{#if post}
<div class="container">
<section class="section">
<div class="columns">
<div class="column is-one-third box">
<div class="content">
<p>
<Link
class="button is-primary"
to="/post/edit/{post.id}">Edit</Link
>
<button
on:click|preventDefault={toggleModal}
class="button is-danger">Delete</button
>
</p>
<p>
Uploader: {post.uploader}
</p>
<p>
Source URL: <a href={post.source_url}
>{trimUrl(post.source_url)}</a
>
</p>
<p>
Original: <a href={post.image_path}>Image</a>
</p>
<p>
Dimensions: {post.width}x{post.height}
</p>
<p>
Tags:<br />
</p>
<p>
{#if post.tags}
{#each post.tags as tag (tag)}
<TagLink {tag} />
{/each}
{/if}
</p>
</div>
<div class="column is-one-third">
{#if editMenuShown == false && deleteMenuShown == false}
<ViewPostPanel
{post}
{toggleDeleteMenu}
{toggleEditMenu}
/>
{:else if editMenuShown == true}
<EditPostPanel
bind:isActive={editMenuShown}
{post}
onSubmit={getData}
/>
{:else if deleteMenuShown == true}
<div class="panel is-danger">
<p class="panel-heading">Delete Post</p>
<div class="panel-block">
Are you sure to delete post {post.id}?
</div>
<div class="panel-block column">
<button
on:click|preventDefault={deletePost}
class="button is-danger">Delete</button
>
<button
class="button"
on:click|preventDefault={toggleDeleteMenu}
>Cancel</button
>
</div>
</div>
{/if}
</div>
<div class="column box">
{#if post.width > 1000}
@ -105,26 +98,4 @@
</section>
</div>
<div class="modal" class:is-active={modal_shown}>
<div class="modal-background" />
<div class="modal-content">
Are you sure to delete post {post.id}?
</div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete post?</p>
<button class="delete" aria-label="close" />
</header>
<section class="modal-card-body" />
<footer class="modal-card-foot">
<button
on:click|preventDefault={deletePost}
class="button is-danger">Delete</button
>
<button class="button" on:click|preventDefault={toggleModal}
>Cancel</button
>
</footer>
</div>
</div>
{/if}

View File

@ -3,48 +3,37 @@
import { getPostSearchTag, getTagAutocomplete } from "../api.js";
import { Link, navigate } from "svelte-routing";
import InfiniteScroll from "svelte-infinite-scroll";
import TagLink from "../TagLink.svelte";
import TagLinkNumbered from "../TagLinkNumbered.svelte";
import queryString from "query-string";
import Tags from "svelte-tags-input";
import { add_attribute } from "svelte/internal";
import { paginate } from "../simple-pagination.js";
export let location;
let searchTerms = [];
let page = 1;
let totalPages = 1;
let pagination = [];
let posts = [];
let newBatch = [];
const splitToChunks = (array, parts) => {
let result = [];
for (let i = 0; i < parts; i++) {
let currentColumn = [];
for (let j = i; j < array.length; j += parts) {
currentColumn.push(array[j]);
}
result.push(currentColumn);
}
return result;
};
let postChunks = [];
// split posts into 4 columns
$: {
postChunks = splitToChunks(posts, 5);
}
let tags = [];
let categorizedTags = {};
const getData = async () => {
const data = await getPostSearchTag({ page, q: searchTerms.join("+") });
if (data.posts) {
newBatch = data.posts;
posts = data.posts;
tags = data.tags.sort((a, b) => b.postCount - a.postCount);
totalPages = data.totalPage;
pagination = paginate(page, totalPages);
} else {
newBatch = [];
posts = [];
tags = [];
totalPages = 0;
pagination = paginate(page, totalPages);
}
};
$: {
posts = [...posts, ...newBatch];
}
let queryParams;
const onTagChange = (value) => {
@ -64,9 +53,9 @@
searchTerms = [];
}
posts = [];
page = 1;
getData();
}
const onSearch = (i) => {
if (searchTerms.length > 0) {
navigate(`/posts?tags=${searchTerms.join("+")}`);
@ -74,13 +63,12 @@
navigate(`/posts`);
}
};
</script>
<section class="hero is-primary">
<div class="hero-body">
<p class="title">Posts</p>
</div>
</section>
const changePage = (i) => {
page = i;
getData();
}
</script>
<section class="section">
<div class="container">
@ -107,46 +95,101 @@
</div>
<div class="block">
<div class="columns">
{#each postChunks as postChunk}
<div class="column is-one-fifth">
{#each postChunk as post, i (post.id)}
<div class="block">
<div class="card">
<div class="card-image">
<figure class="image">
<Link to="/post/{post.id}">
<img
alt={post.id}
src={post.thumbnail_path}
/>
</Link>
</figure>
</div>
<div class="card-content">
{#if post.tags}
{#each post.tags as tag (tag)}
<TagLink {tag} />
{/each}
{:else}
<TagLink tag="tagme" />
{/if}
</div>
</div>
<div class="column is-one-third">
<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>
{/each}
</div>
</div>
{/each}
</div>
<div class="column is-two-thirds">
<div class="columns is-multiline">
<div class="column is-full">
<nav
class="pagination is-centered"
role="navigation"
aria-label="pagination"
>
<a
href={null}
on:click={changePage(page - 1)}
class="pagination-previous">Previous</a
>
<a
href={null}
on:click={changePage(page + 1)}
class="pagination-next">Next page</a
>
<ul class="pagination-list">
{#each pagination as pageEntry}
{#if pageEntry == "..."}
<li>
<span
class="pagination-ellipsis"
>&hellip;</span
>
</li>
{:else}
<li>
<a
href={null}
on:click={() =>
(changePage(pageEntry))}
class="pagination-link"
class:is-current={page ==
pageEntry}
aria-label="Goto page {pageEntry}"
>{pageEntry}</a
>
</li>
{/if}
{/each}
</ul>
</nav>
</div>
<div class="column is-full">
<div class="columns is-multiline">
{#each posts as post, i (post.id)}
<div class="column is-one-third">
<div class="block">
<div class="card">
<div class="card-image">
<figure class="image">
<Link
to="/post/{post.id}"
>
<img
alt={post.id}
src={post.thumbnail_path}
/>
</Link>
</figure>
</div>
<div class="card-content" />
</div>
</div>
</div>
{/each}
</div>
</div>
</div>
</div>
</div>
<InfiniteScroll
hasMore={newBatch.length}
elementScroll={document}
on:loadMore={() => {
page++;
getData();
}}
/>
</div>
{#if newBatch.length == 0}
{#if page >= totalPages}
<div class="notification is-primary">
<p class="has-text-centered">End of posts</p>
</div>

View File

@ -14,12 +14,6 @@
</script>
<section class="hero is-primary">
<div class="hero-body">
<p class="title">Tags</p>
</div>
</section>
<section class="section">
<div class="container">
<table class="table is-fullwidth">

View File

@ -51,12 +51,6 @@
<AuthRequired />
<section class="hero is-primary">
<div class="hero-body">
<p class="title">Upload</p>
</div>
</section>
<section class="section">
<div class="container">
<form on:submit|preventDefault={onSubmit}>

View File

@ -0,0 +1,64 @@
// Implementation in ES6
// https://gist.github.com/kottenator/9d936eb3e4e3c3e02598
const paginate = (c, m) => {
let current = c,
last = m,
delta = 2,
left = current - delta,
right = current + delta + 1,
range = [],
rangeWithDots = [],
l;
for (let i = 1; i <= last; i++) {
if (i == 1 || i == last || i >= left && i < right) {
range.push(i);
}
}
for (let i of range) {
if (l) {
if (i - l === 2) {
rangeWithDots.push(l + 1);
} else if (i - l !== 1) {
rangeWithDots.push('...');
}
}
rangeWithDots.push(i);
l = i;
}
return rangeWithDots;
}
export { paginate };
/*
Test it:
for (let i = 1, l = 20; i <= l; i++)
console.log(`Selected page ${i}:`, pagination(i, l));
Expected output:
Selected page 1: [1, 2, 3, "...", 20]
Selected page 2: [1, 2, 3, 4, "...", 20]
Selected page 3: [1, 2, 3, 4, 5, "...", 20]
Selected page 4: [1, 2, 3, 4, 5, 6, "...", 20]
Selected page 5: [1, 2, 3, 4, 5, 6, 7, "...", 20]
Selected page 6: [1, "...", 4, 5, 6, 7, 8, "...", 20]
Selected page 7: [1, "...", 5, 6, 7, 8, 9, "...", 20]
Selected page 8: [1, "...", 6, 7, 8, 9, 10, "...", 20]
Selected page 9: [1, "...", 7, 8, 9, 10, 11, "...", 20]
Selected page 10: [1, "...", 8, 9, 10, 11, 12, "...", 20]
Selected page 11: [1, "...", 9, 10, 11, 12, 13, "...", 20]
Selected page 12: [1, "...", 10, 11, 12, 13, 14, "...", 20]
Selected page 13: [1, "...", 11, 12, 13, 14, 15, "...", 20]
Selected page 14: [1, "...", 12, 13, 14, 15, 16, "...", 20]
Selected page 15: [1, "...", 13, 14, 15, 16, 17, "...", 20]
Selected page 16: [1, "...", 14, 15, 16, 17, 18, 19, 20]
Selected page 17: [1, "...", 15, 16, 17, 18, 19, 20]
Selected page 18: [1, "...", 16, 17, 18, 19, 20]
Selected page 19: [1, "...", 17, 18, 19, 20]
Selected page 20: [1, "...", 18, 19, 20]
*/