mirror of
https://github.com/Damillora/Shioriko.git
synced 2024-11-21 20:07:33 +00:00
feat: redesign of some pages
This commit is contained in:
parent
fdb4250652
commit
3ed594ab88
@ -57,8 +57,9 @@ func postGet(c *gin.Context) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@ -67,7 +68,6 @@ func postGet(c *gin.Context) {
|
||||
ID: post.ID,
|
||||
ImageThumbnailPath: "/data/" + post.Blob.ThumbnailFilePath,
|
||||
ImagePath: "/data/" + post.Blob.FilePath,
|
||||
Tags: tagStrings,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, models.PostPaginationResponse{
|
||||
@ -75,6 +75,7 @@ func postGet(c *gin.Context) {
|
||||
TotalPage: totalPage,
|
||||
PostCount: postPages,
|
||||
Posts: postResult,
|
||||
Tags: tagStrings,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -31,4 +31,5 @@ type PostPaginationResponse struct {
|
||||
TotalPage int `json:"totalPage"`
|
||||
PostCount int `json:"postCount"`
|
||||
Posts []PostListItem `json:"posts"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
12
web/app/src/AuthCheck.svelte
Normal file
12
web/app/src/AuthCheck.svelte
Normal 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}
|
90
web/app/src/EditPostPanel.svelte
Normal file
90
web/app/src/EditPostPanel.svelte
Normal 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>
|
@ -8,4 +8,4 @@
|
||||
let tagDisplay = tagName.split("_").join(" ");
|
||||
</script>
|
||||
|
||||
<Link class="button is-rounded is-primary is-small m-1" to="/posts?tags={tagName}">{tagDisplay}</Link>
|
||||
<Link to="/posts?tags={tagName}">{tagDisplay}</Link>
|
||||
|
13
web/app/src/TagLinkNumbered.svelte
Normal file
13
web/app/src/TagLinkNumbered.svelte
Normal 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>
|
78
web/app/src/ViewPostPanel.svelte
Normal file
78
web/app/src/ViewPostPanel.svelte
Normal file
@ -0,0 +1,78 @@
|
||||
<script>
|
||||
import AuthCheck from "./AuthCheck.svelte";
|
||||
import TagLink from "./TagLink.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>
|
||||
<TagLink class="" {tag} />
|
||||
</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>
|
@ -12,5 +12,5 @@
|
||||
}
|
||||
|
||||
.svelte-tags-input-matchs-parent {
|
||||
z-index: 200;
|
||||
}
|
||||
z-index: 2000;
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
import TagLink from "../TagLink.svelte";
|
||||
import { getPost, postCreate, postDelete } from "../api.js";
|
||||
import { Link, navigate } from "svelte-routing";
|
||||
import EditPostPanel from "../EditPostPanel.svelte";
|
||||
import ViewPostPanel from "../ViewPostPanel.svelte";
|
||||
export let id;
|
||||
let post;
|
||||
const getData = async () => {
|
||||
@ -21,70 +23,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 +99,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}
|
||||
|
@ -3,47 +3,47 @@
|
||||
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;
|
||||
totalPages = data.totalPage;
|
||||
pagination = paginate(page, totalPages);
|
||||
} else {
|
||||
newBatch = [];
|
||||
posts = [];
|
||||
tags = [];
|
||||
totalPages = 0;
|
||||
pagination = paginate(page, totalPages);
|
||||
}
|
||||
};
|
||||
$: {
|
||||
posts = [...posts, ...newBatch];
|
||||
let catTags = tags.reduce(
|
||||
(acc, o) => ((acc[o] = (acc[o] || 0) + 1), acc),
|
||||
{}
|
||||
);
|
||||
categorizedTags = Object.entries(catTags).map(([k, v]) => ({
|
||||
tag: k,
|
||||
num: v,
|
||||
}));
|
||||
categorizedTags = categorizedTags.sort((a, b) => b.num - a.num);
|
||||
}
|
||||
let queryParams;
|
||||
|
||||
@ -64,9 +64,9 @@
|
||||
searchTerms = [];
|
||||
}
|
||||
posts = [];
|
||||
page = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
const onSearch = (i) => {
|
||||
if (searchTerms.length > 0) {
|
||||
navigate(`/posts?tags=${searchTerms.join("+")}`);
|
||||
@ -74,13 +74,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 +106,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 categorizedTags as tag (tag)}
|
||||
<li>
|
||||
<TagLinkNumbered
|
||||
class=""
|
||||
tag={tag.tag}
|
||||
num={tag.num}
|
||||
/>
|
||||
</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"
|
||||
>…</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>
|
||||
|
@ -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">
|
||||
|
@ -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}>
|
||||
|
64
web/app/src/simple-pagination.js
Normal file
64
web/app/src/simple-pagination.js
Normal 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]
|
||||
*/
|
Loading…
Reference in New Issue
Block a user