Compare commits

..

No commits in common. "V1.2" and "V1.1" have entirely different histories.
V1.2 ... V1.1

164 changed files with 4704 additions and 6437 deletions

View file

@ -15,7 +15,6 @@
"Chuudoku",
"Codepen",
"commonmark",
"cooldown",
"cout",
"dompurify",
"fontawesome",

143
LICENSE
View file

@ -1,5 +1,5 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@ -7,15 +7,17 @@
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
@ -60,7 +72,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -1,4 +1,4 @@
![Logo](https://github.com/KUN1007/kun-galgame-vue/blob/unfixed-loli/src/assets/images/favicon.png)
![Logo](https://github.com/KUN1007/kun-galgame-vue/blob/layout/src/assets/images/favicon.png)Logo
The image is sourced from the game [Ark Order](https://apps.qoo-app.com/en/app/9593), featuring the character '鲲' (Kun).

View file

@ -7,96 +7,13 @@
type="image/svg+xml"
href="/src/assets/images/favicon.webp"
/>
<link rel="stylesheet" href="/kungalgame-loading.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="KUN Visual Novel Forum. 鲲 Galgame 论坛。 The CUTEST Visual Novel Forum!世界上最萌的 Galgame 论坛. Topic, Technique. NO ADs Forever. Free Forever"
content="The CUTEST Visual Novel Forum!世界上最萌的 Galgame 论坛. Topic, Technique. NO ADs Forever. Free Forever"
/>
<title>KUN Visual Novel Forum| 鲲 Galgame 论坛</title>
<style>
:root {
--kungalgame-blue-1: #b6e3ff;
--kungalgame-blue-4: #218bff;
--kungalgame-trans-blue-1: #b6e3ff77;
--kungalgame-shadow-0: 0px 0px 17px 5px var(--kungalgame-blue-1);
}
#kungalgame-loading-container {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
}
#kungalgame-loading {
height: 4.8px;
background: var(--kungalgame-blue-4);
box-shadow: var(--kungalgame-shadow-0);
box-sizing: border-box;
animation: kungalgame-loading 7.7s linear infinite;
position: relative;
}
#kungalgame-loading::after,
#kungalgame-loading::before {
content: '';
width: 10px;
height: 1px;
background: var(--kungalgame-blue-4);
position: absolute;
top: 9px;
right: -2px;
opacity: 0;
transform: rotate(-45deg) translateX(0px);
box-sizing: border-box;
animation: coli1 0.3s linear infinite;
}
#kungalgame-loading::before {
top: -4px;
transform: rotate(45deg);
animation: coli2 0.3s linear infinite;
}
#kungalgame-loading h2 {
position: absolute;
top: 17px;
color: var(--kungalgame-blue-4);
}
@keyframes kungalgame-loading {
0% {
width: 0;
}
100% {
width: 100%;
}
}
@keyframes coli1 {
0% {
transform: rotate(-45deg) translateX(0px);
opacity: 0.7;
}
100% {
transform: rotate(-45deg) translateX(-45px);
opacity: 0;
}
}
@keyframes coli2 {
0% {
transform: rotate(45deg) translateX(0px);
opacity: 1;
}
100% {
transform: rotate(45deg) translateX(-45px);
opacity: 0.7;
}
}
</style>
<title>KUNGalgame</title>
</head>
<body>
<div id="app">

View file

@ -18,43 +18,46 @@
"preview": "vite preview"
},
"dependencies": {
"@milkdown/core": "7.3.6",
"@milkdown/ctx": "7.3.6",
"@milkdown/plugin-clipboard": "7.3.6",
"@milkdown/plugin-history": "7.3.6",
"@milkdown/plugin-indent": "7.3.6",
"@milkdown/plugin-listener": "7.3.6",
"@milkdown/plugin-prism": "7.3.6",
"@milkdown/plugin-tooltip": "7.3.6",
"@milkdown/plugin-trailing": "7.3.6",
"@milkdown/preset-commonmark": "7.3.6",
"@milkdown/preset-gfm": "7.3.6",
"@milkdown/prose": "7.3.6",
"@milkdown/transformer": "7.3.6",
"@milkdown/utils": "7.3.6",
"@milkdown/vue": "7.3.6",
"@prosemirror-adapter/vue": "0.2.6",
"@milkdown/core": "^7.3.1",
"@milkdown/ctx": "^7.3.1",
"@milkdown/plugin-clipboard": "^7.3.1",
"@milkdown/plugin-history": "^7.3.1",
"@milkdown/plugin-indent": "^7.3.1",
"@milkdown/plugin-listener": "^7.3.1",
"@milkdown/plugin-prism": "^7.3.1",
"@milkdown/plugin-tooltip": "^7.3.1",
"@milkdown/plugin-trailing": "^7.3.1",
"@milkdown/preset-commonmark": "^7.3.1",
"@milkdown/preset-gfm": "^7.3.1",
"@milkdown/prose": "^7.3.1",
"@milkdown/transformer": "^7.3.1",
"@milkdown/utils": "^7.3.1",
"@milkdown/vue": "^7.3.1",
"@prosemirror-adapter/vue": "^0.2.6",
"animate.css": "^4.1.1",
"dayjs": "^1.11.10",
"dompurify": "^3.0.6",
"localforage": "^1.10.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"pinia-plugin-persistedstate": "^3.2.0",
"refractor": "^4.8.1",
"vue": "^3.4.24",
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.2"
"vue": "^3.3.7",
"vue-i18n": "^9.6.2",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@iconify/vue": "^4.1.2",
"@types/node": "^20.12.7",
"@types/nprogress": "^0.2.3",
"@vitejs/plugin-vue": "^5.0.4",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.75.0",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vue-tsc": "^2.0.14"
"@iconify/vue": "^4.1.1",
"@types/dompurify": "^3.0.4",
"@types/js-cookie": "^3.0.5",
"@types/node": "^20.8.10",
"@types/nprogress": "^0.2.2",
"@vitejs/plugin-vue": "^4.4.0",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.69.5",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vue-tsc": "^1.8.22"
},
"keywords": [
"kun",
@ -68,4 +71,4 @@
"visual novel"
],
"license": "LGPL-3.0-or-later"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
/* When page blank, loading */
:root {
--kungalgame-blue-1: #b6e3ff;
--kungalgame-blue-4: #218bff;
--kungalgame-trans-blue-1: #b6e3ff77;
--kungalgame-shadow-0: 0px 0px 17px 5px var(--kungalgame-blue-1);
}
#kungalgame-loading-container {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
}
#kungalgame-loading {
height: 4.8px;
background: var(--kungalgame-blue-4);
box-shadow: var(--kungalgame-shadow-0);
box-sizing: border-box;
animation: loading 7.7s linear infinite;
position: relative;
}
#kungalgame-loading::after,
#kungalgame-loading::before {
content: '';
width: 10px;
height: 1px;
background: var(--kungalgame-blue-4);
position: absolute;
top: 9px;
right: -2px;
opacity: 0;
transform: rotate(-45deg) translateX(0px);
box-sizing: border-box;
animation: coli1 0.3s linear infinite;
}
#kungalgame-loading::before {
top: -4px;
transform: rotate(45deg);
animation: coli2 0.3s linear infinite;
}
#kungalgame-loading h2 {
position: absolute;
top: 17px;
color: var(--kungalgame-blue-4);
}
@keyframes loading {
0% {
width: 0;
}
100% {
width: 100%;
}
}
@keyframes coli1 {
0% {
transform: rotate(-45deg) translateX(0px);
opacity: 0.7;
}
100% {
transform: rotate(-45deg) translateX(-45px);
opacity: 0;
}
}
@keyframes coli2 {
0% {
transform: rotate(45deg) translateX(0px);
opacity: 1;
}
100% {
transform: rotate(45deg) translateX(-45px);
opacity: 0.7;
}
}

View file

@ -9,9 +9,6 @@ import Info from '@/components/alert/Info.vue'
const Capture = defineAsyncComponent(
() => import('@/components/capture/Capture.vue')
)
const KUNGalgameSearchBox = defineAsyncComponent(
() => import('@/components/search/KUNGalgameSearchBox.vue')
)
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
@ -47,9 +44,6 @@ onBeforeMount(() => {
<!-- Global capture component -->
<Capture />
<!-- Global search component -->
<KUNGalgameSearchBox />
<RouterView />
</template>

View file

@ -17,7 +17,6 @@ export interface EditUpdateTopicRequestData {
content: string
tags: string[]
category: string[]
edited: number
}
// Request data format for getting hot tags

View file

@ -5,25 +5,11 @@ import type * as Home from './types/home'
// URLs to be requested
const homeURLs = {
search: `/home/search`,
home: `/home/topic`,
navHot: `/home/nav/hot`,
navNew: `/home/nav/new`,
}
// Home page topic list
export async function getHomeSearchTopicApi(
requestData: Home.HomeSearchTopicRequestData
): Promise<Home.HomeSearchTopicResponseData> {
const queryParams = objectToQueryParams(requestData)
const response = await fetchGet<Home.HomeSearchTopicResponseData>(
`${homeURLs.search}?${queryParams}`
)
return response
}
// Home page topic list
export async function getHomeTopicApi(
requestData: Home.HomeTopicRequestData

View file

@ -1,19 +1,3 @@
export interface HomeSearchTopic {
tid: number
title: string
content: string
category: string[]
}
export interface HomeSearchTopicRequestData {
keywords: string
category: string
page: number
limit: number
sortField: string
sortOrder: string
}
interface HomeUserInfo {
uid: number
avatar: string
@ -33,6 +17,7 @@ export interface HomeNewTopic {
}
export interface HomeTopicRequestData {
keywords: string
category: string
page: number
limit: number
@ -59,12 +44,11 @@ export interface HomeTopic {
upvote_time: number
}
export type HomeSearchTopicResponseData = KUNGalgameResponseData<
HomeSearchTopic[]
>
// 10 hot topics on the left side
export type HomeHotTopicResponseData = KUNGalgameResponseData<HomeHotTopic[]>
// 10 latest topics on the left side
export type HomeNewTopicResponseData = KUNGalgameResponseData<HomeNewTopic[]>
// Topics displayed in the middle
export type HomeTopicResponseData = KUNGalgameResponseData<HomeTopic[]>

View file

@ -16,8 +16,6 @@ export * from './non-moe/types/nonMoe'
export * from './ranking/types/ranking'
export * from './topic/types'
export * from './update-log/types/updateLog'
export * from './pool/types/pool'
export * from './technique/types/technique'
// Expose all APIs
export * from './balance'
@ -29,5 +27,3 @@ export * from './non-moe'
export * from './ranking'
export * from './topic'
export * from './update-log'
export * from './pool'
export * from './technique'

View file

@ -1,19 +0,0 @@
import { fetchGet } from '@/utils/request'
import objectToQueryParams from '@/utils/objectToQueryParams'
import type * as Pool from './types/pool'
const poolURLs = {
topic: `/pool/topic`,
}
export async function getPoolTopicApi(
requestData: Pool.PoolTopicsRequestData
): Promise<Pool.PoolTopicResponseData> {
const queryParams = objectToQueryParams(requestData)
const response = await fetchGet<Pool.PoolTopicResponseData>(
`${poolURLs.topic}?${queryParams}`
)
return response
}

View file

@ -1,17 +0,0 @@
export interface PoolTopic {
tid: number
title: string
views: number
likesCount: number
time: number
content: string
}
export interface PoolTopicsRequestData {
page: number
limit: number
sortField: string
sortOrder: string
}
export type PoolTopicResponseData = KUNGalgameResponseData<PoolTopic[]>

View file

@ -1,19 +0,0 @@
import { fetchGet } from '@/utils/request'
import objectToQueryParams from '@/utils/objectToQueryParams'
import type * as Technique from './types/technique'
const techniqueURLs = {
topic: `/technique/topic`,
}
export async function getTechniqueTopicApi(
requestData: Technique.TechniqueTopicsRequestData
): Promise<Technique.TechniqueTopicResponseData> {
const queryParams = objectToQueryParams(requestData)
const response = await fetchGet<Technique.TechniqueTopicResponseData>(
`${techniqueURLs.topic}?${queryParams}`
)
return response
}

View file

@ -1,20 +0,0 @@
export interface TechniqueTopic {
tid: number
title: string
views: number
likesCount: number
replyCount: number
content: string
tags: string[]
}
export interface TechniqueTopicsRequestData {
page: number
limit: number
sortField: string
sortOrder: string
}
export type TechniqueTopicResponseData = KUNGalgameResponseData<
TechniqueTopic[]
>

View file

@ -4,7 +4,7 @@ import objectToQueryParams from '@/utils/objectToQueryParams'
import * as Reply from './types/reply'
// Get topic replies by tid
export async function getRepliesByTidApi(
export async function getRepliesByPidApi(
request: Reply.TopicReplyRequestData
): Promise<Reply.TopicReplyResponseData> {
const queryParams = objectToQueryParams(request, 'tid')
@ -16,7 +16,7 @@ export async function getRepliesByTidApi(
}
// Create a reply by tid
export async function postReplyByTidApi(
export async function postReplyByPidApi(
request: Reply.TopicCreateReplyRequestData
): Promise<Reply.TopicCreateReplyResponseData> {
const url = `/topics/${request.tid}/reply`

View file

@ -37,7 +37,6 @@ export interface TopicCreateReplyRequestData {
to_floor: number
tags: string[]
content: string
time: number
}
// Upvote reply, the upvote operation is irreversible
@ -45,7 +44,6 @@ export interface TopicUpvoteReplyRequestData {
tid: number
to_uid: number
rid: number
time: number
}
// Like reply
@ -70,7 +68,6 @@ export interface TopicUpdateReplyRequestData {
rid: number
content: string
tags: string[]
edited: number
}
// Response data format for a single topic reply, returning multiple reply data in an array

View file

@ -37,7 +37,6 @@ export interface TopicDetail {
export interface TopicUpvoteTopicRequestData {
tid: number
to_uid: number
time: number
}
// Request data for liking a topic

View file

@ -1,802 +0,0 @@
[
{
"name": "汗",
"left": 287,
"top": 425,
"width": 12,
"height": 14,
"visible": 1,
"layer_id": 2961,
"group_layer_id": 2931
},
{
"name": "追真剣",
"left": 234,
"top": 383,
"width": 76,
"height": 27,
"visible": 1,
"layer_id": 3022,
"group_layer_id": 2923
},
{
"name": "笑1",
"left": 232,
"top": 380,
"width": 78,
"height": 29,
"visible": 1,
"layer_id": 2842,
"group_layer_id": 2923
},
{
"name": "笑2",
"left": 235,
"top": 385,
"width": 76,
"height": 32,
"visible": 1,
"layer_id": 2845,
"group_layer_id": 2923
},
{
"name": "怒1",
"left": 239,
"top": 382,
"width": 63,
"height": 28,
"visible": 1,
"layer_id": 2848,
"group_layer_id": 2923
},
{
"name": "怒2",
"left": 240,
"top": 382,
"width": 60,
"height": 26,
"visible": 1,
"layer_id": 2864,
"group_layer_id": 2923
},
{
"name": "悲1",
"left": 237,
"top": 386,
"width": 70,
"height": 28,
"visible": 1,
"layer_id": 2855,
"group_layer_id": 2923
},
{
"name": "悲2",
"left": 238,
"top": 391,
"width": 66,
"height": 28,
"visible": 1,
"layer_id": 2859,
"group_layer_id": 2923
},
{
"name": "泣1",
"left": 243,
"top": 388,
"width": 58,
"height": 30,
"visible": 1,
"layer_id": 2865,
"group_layer_id": 2923
},
{
"name": "泣2",
"left": 240,
"top": 386,
"width": 65,
"height": 31,
"visible": 1,
"layer_id": 2870,
"group_layer_id": 2923
},
{
"name": "驚1",
"left": 232,
"top": 379,
"width": 78,
"height": 31,
"visible": 1,
"layer_id": 2877,
"group_layer_id": 2923
},
{
"name": "驚2",
"left": 232,
"top": 379,
"width": 78,
"height": 33,
"visible": 1,
"layer_id": 2882,
"group_layer_id": 2923
},
{
"name": "照1",
"left": 234,
"top": 382,
"width": 77,
"height": 31,
"visible": 1,
"layer_id": 2887,
"group_layer_id": 2923
},
{
"name": "照2",
"left": 239,
"top": 385,
"width": 70,
"height": 31,
"visible": 1,
"layer_id": 2893,
"group_layer_id": 2923
},
{
"name": "えっへん",
"left": 235,
"top": 384,
"width": 72,
"height": 25,
"visible": 1,
"layer_id": 2899,
"group_layer_id": 2923
},
{
"name": "ギャグ笑",
"left": 234,
"top": 383,
"width": 76,
"height": 33,
"visible": 1,
"layer_id": 2905,
"group_layer_id": 2923
},
{
"name": "ギャグ怒",
"left": 239,
"top": 384,
"width": 61,
"height": 29,
"visible": 1,
"layer_id": 2910,
"group_layer_id": 2923
},
{
"name": "ギャグ泣",
"left": 239,
"top": 389,
"width": 63,
"height": 26,
"visible": 1,
"layer_id": 2915,
"group_layer_id": 2923
},
{
"name": "ギャグ驚",
"left": 235,
"top": 383,
"width": 66,
"height": 23,
"visible": 1,
"layer_id": 2918,
"group_layer_id": 2923
},
{
"name": "追真剣",
"left": 238,
"top": 392,
"width": 78,
"height": 41,
"visible": 0,
"layer_id": 3020,
"group_layer_id": 2925
},
{
"name": "笑1",
"left": 238,
"top": 390,
"width": 78,
"height": 43,
"visible": 0,
"layer_id": 2843,
"group_layer_id": 2925
},
{
"name": "笑2",
"left": 244,
"top": 395,
"width": 69,
"height": 37,
"visible": 0,
"layer_id": 2846,
"group_layer_id": 2925
},
{
"name": "怒1",
"left": 238,
"top": 391,
"width": 78,
"height": 42,
"visible": 0,
"layer_id": 2850,
"group_layer_id": 2925
},
{
"name": "怒2",
"left": 238,
"top": 391,
"width": 78,
"height": 42,
"visible": 0,
"layer_id": 2853,
"group_layer_id": 2925
},
{
"name": "悲1",
"left": 238,
"top": 392,
"width": 78,
"height": 41,
"visible": 0,
"layer_id": 2856,
"group_layer_id": 2925
},
{
"name": "悲2",
"left": 240,
"top": 396,
"width": 75,
"height": 37,
"visible": 0,
"layer_id": 2861,
"group_layer_id": 2925
},
{
"name": "泣1",
"left": 243,
"top": 396,
"width": 71,
"height": 37,
"visible": 1,
"layer_id": 3025,
"group_layer_id": 2925
},
{
"name": "泣2",
"left": 238,
"top": 394,
"width": 78,
"height": 60,
"visible": 0,
"layer_id": 3024,
"group_layer_id": 2925
},
{
"name": "驚1",
"left": 238,
"top": 390,
"width": 78,
"height": 43,
"visible": 0,
"layer_id": 2880,
"group_layer_id": 2925
},
{
"name": "驚2",
"left": 238,
"top": 390,
"width": 78,
"height": 43,
"visible": 0,
"layer_id": 2885,
"group_layer_id": 2925
},
{
"name": "照1",
"left": 238,
"top": 392,
"width": 78,
"height": 41,
"visible": 0,
"layer_id": 2891,
"group_layer_id": 2925
},
{
"name": "照2",
"left": 244,
"top": 395,
"width": 69,
"height": 37,
"visible": 0,
"layer_id": 2896,
"group_layer_id": 2925
},
{
"name": "えっへん",
"left": 244,
"top": 396,
"width": 68,
"height": 38,
"visible": 0,
"layer_id": 2902,
"group_layer_id": 2925
},
{
"name": "ギャグ笑",
"left": 244,
"top": 395,
"width": 69,
"height": 37,
"visible": 0,
"layer_id": 2908,
"group_layer_id": 2925
},
{
"name": "ギャグ怒",
"left": 244,
"top": 401,
"width": 66,
"height": 33,
"visible": 0,
"layer_id": 2913,
"group_layer_id": 2925
},
{
"name": "ギャグ泣",
"left": 244,
"top": 405,
"width": 66,
"height": 47,
"visible": 0,
"layer_id": 2916,
"group_layer_id": 2925
},
{
"name": "ギャグ驚",
"left": 248,
"top": 395,
"width": 57,
"height": 37,
"visible": 0,
"layer_id": 2920,
"group_layer_id": 2925
},
{
"name": "追開け",
"left": 270,
"top": 430,
"width": 19,
"height": 16,
"visible": 1,
"layer_id": 2980,
"group_layer_id": 2927
},
{
"name": "追閉じ",
"left": 271,
"top": 430,
"width": 18,
"height": 14,
"visible": 0,
"layer_id": 2969,
"group_layer_id": 2927
},
{
"name": "追真剣",
"left": 273,
"top": 433,
"width": 12,
"height": 13,
"visible": 0,
"layer_id": 3023,
"group_layer_id": 2927
},
{
"name": "笑1",
"left": 269,
"top": 429,
"width": 21,
"height": 18,
"visible": 0,
"layer_id": 2844,
"group_layer_id": 2927
},
{
"name": "笑2",
"left": 268,
"top": 428,
"width": 24,
"height": 22,
"visible": 0,
"layer_id": 2847,
"group_layer_id": 2927
},
{
"name": "怒1",
"left": 272,
"top": 433,
"width": 15,
"height": 12,
"visible": 0,
"layer_id": 2851,
"group_layer_id": 2927
},
{
"name": "怒2",
"left": 268,
"top": 428,
"width": 25,
"height": 23,
"visible": 0,
"layer_id": 2854,
"group_layer_id": 2927
},
{
"name": "悲1",
"left": 272,
"top": 434,
"width": 14,
"height": 11,
"visible": 0,
"layer_id": 2857,
"group_layer_id": 2927
},
{
"name": "悲2",
"left": 272,
"top": 435,
"width": 15,
"height": 12,
"visible": 0,
"layer_id": 2862,
"group_layer_id": 2927
},
{
"name": "泣1",
"left": 270,
"top": 431,
"width": 21,
"height": 17,
"visible": 0,
"layer_id": 2869,
"group_layer_id": 2927
},
{
"name": "泣2",
"left": 270,
"top": 433,
"width": 22,
"height": 13,
"visible": 0,
"layer_id": 2876,
"group_layer_id": 2927
},
{
"name": "驚1",
"left": 272,
"top": 432,
"width": 14,
"height": 16,
"visible": 0,
"layer_id": 2881,
"group_layer_id": 2927
},
{
"name": "驚2",
"left": 268,
"top": 427,
"width": 26,
"height": 25,
"visible": 0,
"layer_id": 2886,
"group_layer_id": 2927
},
{
"name": "照1",
"left": 269,
"top": 431,
"width": 20,
"height": 15,
"visible": 0,
"layer_id": 2892,
"group_layer_id": 2927
},
{
"name": "照2",
"left": 268,
"top": 430,
"width": 23,
"height": 19,
"visible": 0,
"layer_id": 2897,
"group_layer_id": 2927
},
{
"name": "えっへん",
"left": 269,
"top": 429,
"width": 22,
"height": 20,
"visible": 0,
"layer_id": 2903,
"group_layer_id": 2927
},
{
"name": "ギャグ笑",
"left": 267,
"top": 427,
"width": 26,
"height": 24,
"visible": 0,
"layer_id": 2909,
"group_layer_id": 2927
},
{
"name": "ギャグ怒",
"left": 270,
"top": 430,
"width": 20,
"height": 16,
"visible": 0,
"layer_id": 2914,
"group_layer_id": 2927
},
{
"name": "ギャグ泣",
"left": 270,
"top": 434,
"width": 19,
"height": 11,
"visible": 0,
"layer_id": 2917,
"group_layer_id": 2927
},
{
"name": "ギャグ驚",
"left": 267,
"top": 427,
"width": 25,
"height": 24,
"visible": 0,
"layer_id": 2921,
"group_layer_id": 2927
},
{
"name": "G頬",
"left": 249,
"top": 415,
"width": 59,
"height": 28,
"visible": 0,
"layer_id": 813,
"group_layer_id": 2314
},
{
"name": "頬1",
"left": 246,
"top": 410,
"width": 65,
"height": 35,
"visible": 0,
"layer_id": 2369,
"group_layer_id": 2314
},
{
"name": "頬2",
"left": 245,
"top": 410,
"width": 66,
"height": 35,
"visible": 0,
"layer_id": 2364,
"group_layer_id": 2314
},
{
"name": "頬3",
"left": 245,
"top": 408,
"width": 66,
"height": 38,
"visible": 0,
"layer_id": 2360,
"group_layer_id": 2314
},
{
"name": "頬4",
"left": 242,
"top": 406,
"width": 69,
"height": 44,
"visible": 1,
"layer_id": 2356,
"group_layer_id": 2314
},
{
"name": "頬5",
"left": 245,
"top": 410,
"width": 66,
"height": 36,
"visible": 0,
"layer_id": 2352,
"group_layer_id": 2314
},
{
"name": "斜め1",
"left": 148,
"top": 323,
"width": 309,
"height": 600,
"visible": 1,
"layer_id": 2346,
"group_layer_id": 2086
},
{
"name": "斜め2",
"left": 148,
"top": 323,
"width": 309,
"height": 600,
"visible": 0,
"layer_id": 2345,
"group_layer_id": 2086
},
{
"name": "斜め1",
"left": 147,
"top": 323,
"width": 313,
"height": 600,
"visible": 1,
"layer_id": 2934,
"group_layer_id": 2090
},
{
"name": "斜め2",
"left": 147,
"top": 323,
"width": 313,
"height": 600,
"visible": 1,
"layer_id": 2933,
"group_layer_id": 2090
},
{
"name": "斜め1",
"left": 137,
"top": 323,
"width": 367,
"height": 602,
"visible": 1,
"layer_id": 2340,
"group_layer_id": 2094
},
{
"name": "斜め2",
"left": 137,
"top": 323,
"width": 367,
"height": 602,
"visible": 1,
"layer_id": 2339,
"group_layer_id": 2094
},
{
"name": "斜め1",
"left": 148,
"top": 323,
"width": 294,
"height": 599,
"visible": 0,
"layer_id": 2338,
"group_layer_id": 2098
},
{
"name": "斜め2",
"left": 148,
"top": 323,
"width": 290,
"height": 599,
"visible": 1,
"layer_id": 2337,
"group_layer_id": 2098
},
{
"name": "汗",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 0,
"layer_id": 2931,
"group_layer_id": 974
},
{
"name": "眉",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 0,
"layer_id": 2923,
"group_layer_id": 974
},
{
"name": "目",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 1,
"layer_id": 2925,
"group_layer_id": 974
},
{
"name": "口",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 0,
"layer_id": 2927,
"group_layer_id": 974
},
{
"name": "頬",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 0,
"layer_id": 2314,
"group_layer_id": 975
},
{
"name": "喫茶店服",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 0,
"layer_id": 2086,
"group_layer_id": 975
},
{
"name": "パジャマ",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 0,
"layer_id": 2090,
"group_layer_id": 975
},
{
"name": "私服2",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 0,
"layer_id": 2094,
"group_layer_id": 975
},
{
"name": "私服1",
"left": 0,
"top": 0,
"width": 0,
"height": 0,
"visible": 1,
"layer_id": 2098,
"group_layer_id": 975
}
]

View file

@ -1,32 +0,0 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
</script>
<template>
<RouterLink to="/kun" class="return">
<Icon class="icon" icon="line-md:home-md-twotone" />
<span>{{ $tm('back.home') }}</span>
</RouterLink>
</template>
<style lang="scss" scoped>
.return {
position: absolute;
bottom: 1%;
right: 2%;
color: var(--kungalgame-font-color-0);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
.icon {
font-size: 18px;
margin-right: 5px;
}
}
.return:hover {
color: var(--kungalgame-blue-4);
}
</style>

View file

@ -5,6 +5,7 @@ const router = useRouter()
</script>
<template>
<!-- Back to homepage -->
<div class="return" @click="router.back()">
<span>{{ `< ${$tm('back.back')}` }}</span>
</div>

View file

@ -1,27 +1,9 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
</script>
<script setup lang="ts"></script>
<template>
<div class="footer">
<div>{{ $tm('footer.copyright') }}</div>
<div>
<span>{{ $tm('footer.openSource') }}</span>
<a
href="http://github.com/KUN1007/kun-galgame-vue"
target="_blank"
rel="noopener noreferrer"
aria-label="KUN Visual Novel Open Source GitHub Repository. 鲲 Galgame 开源 GitHub 仓库."
>
<Icon icon="line-md:github-loop" />
</a>
</div>
<div>
<span>{{ $tm('footer.reserved') }} 1.3.0</span>
</div>
<span>Copyright © 2023 KUNGalgame</span>
<span>All rights reserved | Version 1.2.0</span>
</div>
</template>
@ -34,25 +16,5 @@ import { Icon } from '@iconify/vue'
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 14px;
div {
display: flex;
justify-self: center;
align-items: center;
a {
display: flex;
justify-self: center;
align-items: center;
margin-left: 5px;
color: var(--kungalgame-blue-5);
&:hover {
transition: all 0.2s;
transform: scale(1.5);
}
}
}
}
</style>

View file

@ -0,0 +1,309 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
// Import debounce function
import { debounce } from '@/utils/debounce'
import { ref, onBeforeMount } from 'vue'
// Import user store
import { useKUNGalgameHomeStore } from '@/store/modules/home'
import { storeToRefs } from 'pinia'
const { keywords, searchHistory, category } = storeToRefs(
useKUNGalgameHomeStore()
)
// Value of the input field
const inputValue = ref('')
// Whether to show search history
const isShowSearchHistory = ref(false)
// Style when the input field is active
const inputActiveClass = ref({})
// Define props to tell the input field in which category to search for topics
const props = defineProps(['category'])
// Initialize the content of the search box to prevent content from persisting after page refresh
// Assign the topic category to be searched because the search box will be rendered on three pages
// , corresponding to three categories
onBeforeMount(() => {
keywords.value = ''
category.value = props.category
})
// Define the debounce handling function
const debouncedSearch = debounce((inputValue: string) => {
// Reset page status and loading state before searching
useKUNGalgameHomeStore().resetPageStatus()
keywords.value = inputValue
}, 300) // 300 milliseconds debounce delay
// When the search box is focused
const handleInputFocus = () => {
if (searchHistory.value.length !== 0) {
isShowSearchHistory.value = true
}
inputActiveClass.value = {
backgroundColor: 'var(--kungalgame-white)',
}
}
// When the search box is blurred
const handleInputBlur = () => {
// Delay hiding the search history so that clicking on the search history can trigger the fill event
setTimeout(() => {
isShowSearchHistory.value = false
inputActiveClass.value = {}
}, 100)
}
// Search function logic
const search = () => {
debouncedSearch(inputValue.value)
if (!searchHistory.value.includes(inputValue.value)) {
// Push the element into the array only when there are no identical elements in the array
searchHistory.value.push(inputValue.value)
}
}
// When the user presses Enter
const handleClickEnter = (event: KeyboardEvent) => {
event.preventDefault()
search()
}
// Clicking the search button triggers the search logic
const handleClickSearch = () => {
if (inputValue.value.trim()) {
search()
}
}
// Clicking on search history
const handleClickHistory = (index: number) => {
inputValue.value = searchHistory.value[index]
}
// Clear search history
const clearSearchHistory = () => {
searchHistory.value = []
}
// Delete search history
const handleDeleteHistory = (historyIndex: number) => {
searchHistory.value.splice(historyIndex, 1)
}
</script>
<template>
<!-- Interactive area search box -->
<div class="container">
<!-- Search box form -->
<form class="search-form">
<!-- Search box content -->
<div class="content">
<!-- Input field -->
<input
v-model="inputValue"
type="search"
class="input"
:style="inputActiveClass"
:placeholder="`${$tm('mainPage.header.search')}`"
@focus="handleInputFocus"
@blur="handleInputBlur"
@input="debouncedSearch(inputValue)"
@keydown.enter="handleClickEnter"
/>
</div>
<!-- Search box icon -->
<div class="search-btn" @click="handleClickSearch">
<Icon icon="line-md:search" />
</div>
</form>
<!-- Search history container -->
<div v-if="isShowSearchHistory" class="history">
<!-- Search history title -->
<div class="title">
<span>{{ $tm('mainPage.header.history') }}</span>
<span @click="clearSearchHistory">
{{ $tm('mainPage.header.clear') }}
</span>
</div>
<!-- Search history -->
<div class="history-container">
<div
class="single-history"
v-for="(history, index) in searchHistory"
:key="index"
@click="handleClickHistory(index)"
>
<span>{{ history }} </span>
<span @click="handleDeleteHistory(index)">
<Icon class="delete" icon="line-md:close-circle" />
</span>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
/* Search topics */
.container {
height: 39px;
width: 1px;
justify-content: center;
align-items: center;
/* Prevent line breaks when the page is resized */
white-space: nowrap;
background-color: var(--kungalgame-trans-blue-2);
border: 1px solid var(--kungalgame-blue-4);
flex-grow: 2;
/* Position relative to the secondary menu */
position: relative;
display: flex;
color: var(--kungalgame-font-color-3);
}
/* Search box form */
.search-form {
display: flex;
height: 39px;
/* Grows with the page */
width: 1px;
flex-grow: 1;
/* Centered */
justify-content: center;
align-items: center;
}
/* Search content area */
.content {
width: 100%;
}
/* Input field */
.input {
padding: 0 15px;
height: 39px;
width: 100%;
/* Font size for input during search */
font-size: 16px;
border: none;
background-color: var(--kungalgame-trans-white-5);
color: var(--kungalgame-font-color-3);
transition: all 0.2s;
&::placeholder {
color: var(--kungalgame-font-color-1);
}
}
/* Search button */
.search-btn {
/* Square shape, does not shrink */
height: 39px;
width: 39px;
flex-shrink: 0;
border-left: 1px solid var(--kungalgame-trans-blue-4);
/* Center the search icon */
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--kungalgame-red-1);
}
&:active {
background-color: var(--kungalgame-red-2);
}
}
/* Search history container */
.history {
width: 100%;
/* Absolute positioning relative to the search area in the nav */
position: absolute;
/* Tight positioning under the topic search area */
top: 39px;
left: 0;
flex-direction: column;
background-color: var(--kungalgame-white);
color: var(--kungalgame-font-color-3);
border: 1px solid var(--kungalgame-red-1);
border-radius: 7px;
box-shadow: var(--shadow);
}
/* Text for the search history title */
.title {
display: flex;
margin: 10px;
/* Distribute two hint texts left and right */
justify-content: space-between;
span {
font-size: 14px;
&:nth-child(2) {
cursor: pointer;
border-bottom: 1.5px solid var(--kungalgame-trans-white-5);
&:hover {
border-bottom: 1.5px solid var(--kungalgame-blue-4);
}
}
}
}
/* Container for storing search history tags */
.history-container {
display: flex;
flex-direction: column;
/* Font for individual search records */
font-size: 13px;
/* Blank space on the left and right sides of the search record */
margin: 10px;
}
.single-history {
width: 100%;
display: flex;
justify-content: space between;
padding: 7px 3px;
margin: 2px 0;
&:hover {
color: var(--kungalgame-blue-4);
.delete {
display: flex;
}
}
span:nth-child(1) {
cursor: default;
position: relative;
display: flex;
overflow: hidden;
}
span:nth-child(2) {
width: 17px;
}
}
/* Delete button */
.delete {
width: 30px;
right: 5px;
font-size: 17px;
position: absolute;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--kungalgame-font-color-0);
background-color: var(--kungalgame-white);
display: none;
}
</style>

View file

@ -1,17 +1,19 @@
<script setup lang="ts">
import { useTempMessageStore } from '@/store/temp/message'
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { storeToRefs } from 'pinia'
const { showAlert, alertMsg, isShowCancel } = storeToRefs(useTempMessageStore())
const { showAlert, alertMsg, isShowCancel } = storeToRefs(
useKUNGalgameMessageStore()
)
const handleClose = () => {
showAlert.value = false
useTempMessageStore().handleClose()
useKUNGalgameMessageStore().handleClose()
}
const handleConfirm = () => {
showAlert.value = false
useTempMessageStore().handleConfirm()
useKUNGalgameMessageStore().handleConfirm()
}
</script>

View file

@ -1,11 +1,11 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { useTempMessageStore } from '@/store/temp/message'
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { storeToRefs } from 'pinia'
import img from './loli'
import 'animate.css'
const { showInfo, infoMsg } = storeToRefs(useTempMessageStore())
const { showInfo, infoMsg } = storeToRefs(useKUNGalgameMessageStore())
const { loli, name } = img

View file

@ -1,17 +1,18 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, computed, watch } from 'vue'
// Import questions
import { questionsEN, Question } from './questionsEN'
import { questionsCN } from './questionsCN'
import Message from '@/components/alert/Message'
import { useTempMessageStore } from '@/store/temp/message'
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
const { isShowCapture, isCaptureSuccessful } = storeToRefs(
useTempMessageStore()
useKUNGalgameMessageStore()
)
// Current language
const questions = ref<Question[]>([])

View file

@ -27,21 +27,15 @@ import { Plugin } from '@milkdown/prose/state'
import '@/styles/editor/index.scss'
// Syntax highlight
import c from 'refractor/lang/c'
import cpp from 'refractor/lang/cpp'
import csharp from 'refractor/lang/csharp'
import css from 'refractor/lang/css'
import go from 'refractor/lang/go'
import haskell from 'refractor/lang/haskell'
import python from 'refractor/lang/python'
import java from 'refractor/lang/java'
import javascript from 'refractor/lang/javascript'
import typescript from 'refractor/lang/typescript'
import jsx from 'refractor/lang/jsx'
import kotlin from 'refractor/lang/kotlin'
import r from 'refractor/lang/r'
import rust from 'refractor/lang/rust'
import scala from 'refractor/lang/scala'
import sql from 'refractor/lang/sql'
import tsx from 'refractor/lang/tsx'
import markdown from 'refractor/lang/markdown'
@ -92,22 +86,16 @@ const editorInfo = useEditor((root) =>
ctx.set(prismConfig.key, {
configureRefractor: (refractor) => {
refractor.register(c)
refractor.register(cpp)
refractor.register(csharp)
refractor.register(css)
refractor.register(go)
refractor.register(haskell)
refractor.register(python)
refractor.register(markdown)
refractor.register(java)
refractor.register(javascript)
refractor.register(typescript)
refractor.register(jsx)
refractor.register(kotlin)
refractor.register(r)
refractor.register(rust)
refractor.register(scala)
refractor.register(sql)
refractor.register(tsx)
},
@ -197,10 +185,6 @@ const editorInfo = useEditor((root) =>
scrollbar-color: var(--kungalgame-blue-4) var(--kungalgame-blue-1); /* Firefox 64+ */
}
img {
max-width: 100%;
}
del {
text-decoration: line-through;
}

View file

@ -8,25 +8,22 @@ import { ProsemirrorAdapterProvider } from '@prosemirror-adapter/vue'
import MilkdownEditor from './MilkdownEditor.vue'
// KUN Visual Novel store
import { useTempEditStore } from '@/store/temp/edit'
import { useKUNGalgameEditStore } from '@/store/modules/edit'
import { useTempReplyStore } from '@/store/temp/topic/reply'
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
import { storeToRefs } from 'pinia'
const { content: rewriteContent, isTopicRewriting } = storeToRefs(
useTempEditStore()
)
const {
editorHeight: editEditorHeight,
isSaveTopic,
content: editContent,
content,
topicRewrite,
} = storeToRefs(useKUNGalgameEditStore())
const { isReplyRewriting, replyRewrite } = storeToRefs(useTempReplyStore())
const {
editorHeight: replyEditorHeight,
isSaveReply,
isReplyRewriting,
replyDraft,
replyRewrite,
} = storeToRefs(usePersistKUNGalgameReplyStore())
const props = defineProps<{
@ -50,14 +47,14 @@ onBeforeMount(() => {
*/
// Load topic data before mounting if not saved (and must be on the Edit page)
if (isSaveTopic.value && routeName.value === 'Edit') {
valueMarkdown.value = editContent.value
valueMarkdown.value = content.value
}
/**
* Editor is in the re-editing edit mode
*/
// Load data for re-editing a topic before mounting
if (isTopicRewriting.value && routeName.value === 'Edit') {
valueMarkdown.value = rewriteContent.value
if (topicRewrite.value.isTopicRewriting && routeName.value === 'Edit') {
valueMarkdown.value = topicRewrite.value.content
}
/**
* Editor is in the reply mode
@ -74,21 +71,22 @@ onBeforeMount(() => {
}
})
const saveMarkdown = (editorMarkdown: string) =>
debounce(() => {
const saveMarkdown = (editorMarkdown: string) => {
// Create a debounce function
const debouncedUpdateContent = debounce(() => {
/**
* Editor is in edit mode
*/
// Save to the edit store if not in topic re-edit mode
if (!isTopicRewriting.value && routeName.value === 'Edit') {
editContent.value = editorMarkdown
if (!topicRewrite.value.isTopicRewriting && routeName.value === 'Edit') {
content.value = editorMarkdown
}
/**
* Editor is in re-editing edit mode
*/
// Load data for re-editing a topic before mounting
if (isTopicRewriting.value && routeName.value === 'Edit') {
rewriteContent.value = editorMarkdown
if (topicRewrite.value.isTopicRewriting && routeName.value === 'Edit') {
topicRewrite.value.content = editorMarkdown
}
/**
* Editor is in reply mode
@ -104,6 +102,9 @@ const saveMarkdown = (editorMarkdown: string) =>
replyRewrite.value.content = editorMarkdown
}
}, 1007)
debouncedUpdateContent()
}
</script>
<!-- MilkdownEditorWrapper.vue -->

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed } from 'vue'
// Milkdown core
import {
Editor,
rootCtx,
@ -11,28 +11,22 @@ import {
import { Milkdown, useEditor } from '@milkdown/vue'
import { commonmark } from '@milkdown/preset-commonmark'
import { gfm } from '@milkdown/preset-gfm'
// Milkdown Plugins
import { prism, prismConfig } from '@milkdown/plugin-prism'
import { replaceAll } from '@milkdown/utils'
// KUN Visual Novel style
import '@/styles/editor/index.scss'
// Syntax highlight
import c from 'refractor/lang/c'
import cpp from 'refractor/lang/cpp'
import csharp from 'refractor/lang/csharp'
import css from 'refractor/lang/css'
import go from 'refractor/lang/go'
import haskell from 'refractor/lang/haskell'
import python from 'refractor/lang/python'
import java from 'refractor/lang/java'
import javascript from 'refractor/lang/javascript'
import typescript from 'refractor/lang/typescript'
import jsx from 'refractor/lang/jsx'
import kotlin from 'refractor/lang/kotlin'
import r from 'refractor/lang/r'
import rust from 'refractor/lang/rust'
import scala from 'refractor/lang/scala'
import sql from 'refractor/lang/sql'
import tsx from 'refractor/lang/tsx'
import markdown from 'refractor/lang/markdown'
@ -46,7 +40,7 @@ const valueMarkdown = computed(() => props.valueMarkdown)
const editable = () => !props.isReadonly
const editor = useEditor((root) =>
useEditor((root) =>
Editor.make()
.config((ctx) => {
ctx.set(rootCtx, root)
@ -63,22 +57,16 @@ const editor = useEditor((root) =>
ctx.set(prismConfig.key, {
configureRefractor: (refractor) => {
refractor.register(c)
refractor.register(cpp)
refractor.register(csharp)
refractor.register(css)
refractor.register(go)
refractor.register(haskell)
refractor.register(python)
refractor.register(markdown)
refractor.register(java)
refractor.register(javascript)
refractor.register(typescript)
refractor.register(jsx)
refractor.register(kotlin)
refractor.register(r)
refractor.register(rust)
refractor.register(scala)
refractor.register(sql)
refractor.register(tsx)
},
@ -88,13 +76,6 @@ const editor = useEditor((root) =>
.use(gfm)
.use(prism)
)
watch(
() => valueMarkdown.value,
() => {
editor.get()?.action(replaceAll(valueMarkdown.value))
}
)
</script>
<!-- MilkdownEditor.vue -->
@ -135,10 +116,6 @@ watch(
scrollbar-color: var(--kungalgame-blue-4) var(--kungalgame-blue-1);
}
img {
max-width: 100%;
}
del {
text-decoration: line-through;
}

View file

@ -1,16 +1,14 @@
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue'
import { useTempEditStore } from '@/store/temp/edit'
import { useKUNGalgameEditStore } from '@/store/modules/edit'
import { storeToRefs } from 'pinia'
import { debounce } from '@/utils/debounce'
const { title: rewriteTitle, isTopicRewriting } = storeToRefs(
useTempEditStore()
const { isSaveTopic, title, topicRewrite } = storeToRefs(
useKUNGalgameEditStore()
)
const { isSaveTopic, title: editTitle } = storeToRefs(useKUNGalgameEditStore())
// Topic title text
const topicTitle = ref('')
@ -22,14 +20,14 @@ onBeforeMount(() => {
* Editor is in edit mode
*/
if (isSaveTopic.value) {
topicTitle.value = editTitle.value
topicTitle.value = title.value
}
/**
* Editor is in re-editing edit mode
*/
// Load data for re-editing a topic before mounting
if (isTopicRewriting.value) {
topicTitle.value = rewriteTitle.value
if (topicRewrite.value.isTopicRewriting) {
topicTitle.value = topicRewrite.value.title
}
})
@ -41,27 +39,30 @@ const handleInput = () => {
}
if (topicTitle.value.trim() === '') {
rewriteTitle.value = ''
editTitle.value = ''
title.value = ''
topicRewrite.value.title = ''
return
}
return debounce(() => {
// Create a debounce handling function
const debouncedInput = debounce(() => {
/**
* Editor is in reply mode
*/
// Save to the edit store if not in topic re-edit mode
if (!isTopicRewriting.value) {
editTitle.value = topicTitle.value
if (!topicRewrite.value.isTopicRewriting) {
title.value = topicTitle.value
}
/**
* Editor is in re-editing edit mode
*/
// Save to the re-editing page's store if in re-edit mode
if (isTopicRewriting.value) {
rewriteTitle.value = topicTitle.value
if (topicRewrite.value.isTopicRewriting) {
topicRewrite.value.title = topicTitle.value
}
}, 300)
debouncedInput()
}
</script>

View file

@ -1,27 +1,17 @@
<!-- Custom plugins, calculate text size -->
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import Settings from '../components/Settings.vue'
import { usePluginViewContext } from '@prosemirror-adapter/vue'
import { useTempEditStore } from '@/store/temp/edit'
import { useKUNGalgameEditStore } from '@/store/modules/edit'
import { useTempReplyStore } from '@/store/temp/topic/reply'
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
import { storeToRefs } from 'pinia'
const { textCount: textCountEditRewrite, isTopicRewriting } = storeToRefs(
useTempEditStore()
)
const { textCount: textCountEdit } = storeToRefs(useKUNGalgameEditStore())
const { textCount: textCountReplyRewrite, isReplyRewriting } = storeToRefs(
useTempReplyStore()
)
const { textCount: textCountReply } = storeToRefs(
usePersistKUNGalgameReplyStore()
)
const { textCount: textCountReply } = storeToRefs(useTempReplyStore())
const { view } = usePluginViewContext()
@ -35,14 +25,6 @@ const size = computed(() => {
watch(
() => size.value,
() => {
if (routeName.value === 'Edit' && isTopicRewriting.value) {
textCountEditRewrite.value = size.value
return
}
if (routeName.value === 'Topic' && isReplyRewriting.value) {
textCountReplyRewrite.value = size.value
return
}
if (routeName.value === 'Edit') {
textCountEdit.value = size.value
}
@ -51,16 +33,6 @@ watch(
}
}
)
onMounted(() => {
if (routeName.value === 'Edit' && isTopicRewriting.value) {
textCountEditRewrite.value = size.value
return
}
if (routeName.value === 'Topic' && isReplyRewriting.value) {
textCountReplyRewrite.value = size.value
}
})
</script>
<template>

View file

@ -1,164 +0,0 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
import SearchBox from './SearchBox.vue'
import SearchHistory from './SearchHistory.vue'
import SearchResult from './SearchResult.vue'
import { useTempHomeStore } from '@/store/temp/home'
import { storeToRefs } from 'pinia'
import { HomeSearchTopic } from '@/api'
const { search, isShowSearch } = storeToRefs(useTempHomeStore())
const topics = ref<HomeSearchTopic[]>([])
const container = ref<HTMLElement>()
const searchTopics = async () => {
return (await useTempHomeStore().searchTopic()).data
}
watch(
() => search.value.keywords,
async () => {
if (search.value.keywords) {
topics.value = await searchTopics()
} else {
topics.value = []
useTempHomeStore().resetSearchStatus()
}
}
)
watch(
() => container.value,
() => {
const element = container.value
if (element) {
element.addEventListener('scroll', scrollHandler)
}
}
)
const scrollHandler = async () => {
if (isScrollAtBottom() && search.value.isLoading && search.value.keywords) {
search.value.page++
const lazyLoadTopics = await searchTopics()
if (!lazyLoadTopics.length) {
search.value.isLoading = false
}
topics.value = [...topics.value, ...lazyLoadTopics]
}
}
const isScrollAtBottom = () => {
if (container.value) {
const scrollHeight = container.value.scrollHeight
const scrollTop = container.value.scrollTop
const clientHeight = container.value.clientHeight
const errorMargin = 1.007
return Math.abs(scrollHeight - scrollTop - clientHeight) < errorMargin
}
}
onBeforeUnmount(() => {
const element = container.value
if (element) {
element.removeEventListener('scroll', scrollHandler)
}
})
</script>
<template>
<Teleport to="body" :disabled="isShowSearch">
<Transition name="search">
<div class="mask" v-if="isShowSearch" @click="isShowSearch = false">
<div ref="container" class="container" @click.stop>
<SearchBox />
<SearchHistory v-if="!search.keywords" />
<SearchResult :topics="topics" v-if="topics.length" />
<span class="empty" v-if="!topics.length && search.keywords">
{{ $tm('mainPage.header.emptyResult') }}
</span>
</div>
</div>
</Transition>
</Teleport>
</template>
<style lang="scss" scoped>
.mask {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--kungalgame-mask-color-0);
display: flex;
transition: opacity 0.3s ease;
justify-content: center;
align-items: center;
color: var(--kungalgame-font-color-3);
}
.container {
display: flex;
flex-direction: column;
align-items: center;
white-space: nowrap;
position: relative;
color: var(--kungalgame-font-color-3);
background-color: var(--kungalgame-trans-white-2);
box-shadow: var(--kungalgame-shadow-0);
border-radius: 17px;
padding: 10px;
width: 40vw;
max-width: 500px;
min-height: 200px;
max-height: 600px;
overflow-y: scroll;
}
.empty {
display: flex;
justify-content: center;
color: var(--kungalgame-blue-2);
font-style: oblique;
margin-top: 20px;
}
.search-enter-from {
opacity: 0;
}
.search-leave-to {
opacity: 0;
}
.search-enter-from .container,
.search-leave-to .container {
transition: all 0.3s ease;
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
@media (max-width: 1000px) {
.container {
width: 60vw;
}
}
@media (max-width: 700px) {
.container {
width: 90vw;
}
}
</style>

View file

@ -1,90 +0,0 @@
<script setup lang="ts">
import { ref, onBeforeMount, onMounted, watch } from 'vue'
import { debounce } from '@/utils/debounce'
import { useTempHomeStore } from '@/store/temp/home'
import { storeToRefs } from 'pinia'
const { search } = storeToRefs(useTempHomeStore())
const input = ref<HTMLElement | null>(null)
const inputValue = ref('')
onBeforeMount(() => {
search.value.keywords = ''
})
const debouncedSearch = debounce((inputValue: string) => {
if (inputValue.trim()) {
search.value.keywords = inputValue
} else {
search.value.keywords = ''
}
}, 300)
watch(
() => search.value.keywords,
() => {
if (!inputValue.value) {
inputValue.value = search.value.keywords
}
}
)
onMounted(() => {
if (input) {
input.value?.focus()
}
})
</script>
<template>
<div class="search-form">
<input
ref="input"
v-model="inputValue"
type="search"
class="input"
:placeholder="`${$tm('mainPage.header.search')}`"
@input="debouncedSearch(inputValue)"
@keydown.enter="debouncedSearch(inputValue)"
/>
</div>
</template>
<style lang="scss" scoped>
.search-form {
position: sticky;
top: 0;
width: 100%;
max-width: 777px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 17px;
background-color: var(--kungalgame-trans-white-2);
backdrop-filter: blur(5px);
}
.input {
padding: 0 15px;
height: 40px;
width: 100%;
font-size: 16px;
border: none;
background-color: var(--kungalgame-trans-white-9);
border: 2px solid var(--kungalgame-blue-4);
border-radius: 17px;
color: var(--kungalgame-font-color-3);
transition: all 0.2s;
&:focus {
border: 2px solid var(--kungalgame-pink-4);
}
&::placeholder {
color: var(--kungalgame-font-color-1);
}
}
</style>

View file

@ -1,136 +0,0 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { usePersistKUNGalgameHomeStore } from '@/store/modules/home'
import { useTempHomeStore } from '@/store/temp/home'
import { storeToRefs } from 'pinia'
const { searchHistory } = storeToRefs(usePersistKUNGalgameHomeStore())
const { search } = storeToRefs(useTempHomeStore())
const handleClickHistory = (index: number) => {
search.value.keywords = searchHistory.value[index]
}
const clearSearchHistory = () => {
searchHistory.value = []
}
const handleDeleteHistory = (historyIndex: number) => {
searchHistory.value.splice(historyIndex, 1)
}
</script>
<template>
<div class="history">
<div class="title">
<span>{{ $tm('mainPage.header.history') }}</span>
<span @click="clearSearchHistory">
{{ $tm('mainPage.header.clear') }}
</span>
</div>
<div class="history-container" v-if="searchHistory.length">
<div
class="single-history"
v-for="(history, index) in searchHistory"
:key="index"
@click="handleClickHistory(index)"
>
<span>{{ history }} </span>
<span>
<Icon
@click="handleDeleteHistory(index)"
class="delete"
icon="line-md:close-circle"
/>
</span>
</div>
</div>
<span class="empty" v-if="!searchHistory.length">
{{ $tm('mainPage.header.emptyHistory') }}
</span>
</div>
</template>
<style lang="scss" scoped>
.history {
width: 100%;
top: 70px;
left: 0;
flex-direction: column;
color: var(--kungalgame-font-color-3);
border-radius: 7px;
}
.title {
display: flex;
margin: 10px;
justify-content: space-between;
span {
font-size: 14px;
&:nth-child(2) {
cursor: pointer;
&:hover {
color: var(--kungalgame-blue-4);
}
}
}
}
.history-container {
display: flex;
flex-direction: column;
font-size: 13px;
margin: 10px;
}
.single-history {
width: 100%;
display: flex;
justify-content: space between;
padding: 7px 3px;
margin: 2px 0;
&:hover {
color: var(--kungalgame-blue-4);
.delete {
display: flex;
}
}
span:nth-child(1) {
cursor: default;
position: relative;
display: flex;
overflow: hidden;
}
span:nth-child(2) {
width: 17px;
}
}
.delete {
width: 30px;
right: 5px;
font-size: 17px;
position: absolute;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--kungalgame-font-color-0);
display: none;
}
.empty {
display: flex;
justify-content: center;
color: var(--kungalgame-blue-2);
font-style: oblique;
}
</style>

View file

@ -1,94 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { HomeSearchTopic } from '@/api'
import { usePersistKUNGalgameHomeStore } from '@/store/modules/home'
const { searchHistory } = storeToRefs(usePersistKUNGalgameHomeStore())
import { useTempHomeStore } from '@/store/temp/home'
import { storeToRefs } from 'pinia'
const router = useRouter()
const { search, isShowSearch } = storeToRefs(useTempHomeStore())
const props = defineProps<{
topics: HomeSearchTopic[]
}>()
const topics = computed(() => props.topics)
const handleClickTopic = (tid: number) => {
router.push(`/topic/${tid}`)
if (!searchHistory.value.includes(search.value.keywords)) {
searchHistory.value.push(search.value.keywords)
}
isShowSearch.value = false
}
</script>
<template>
<div class="result">
<div
v-for="(topic, index) in topics"
:key="index"
:to="`/topic/${topic.tid}`"
class="topic"
@click="handleClickTopic(topic.tid)"
>
<span class="title">{{ topic.title }}</span>
<span class="content">{{ topic.content }}</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.result {
width: 100%;
top: 70px;
left: 0;
display: flex;
flex-direction: column;
.topic {
color: var(--kungalgame-font-color-3);
border: 1px solid var(--kungalgame-blue-4);
background-color: var(--kungalgame-trans-blue-0);
padding: 10px;
margin-bottom: 10px;
border-radius: 17px;
cursor: pointer;
&:first-child {
margin-top: 10px;
}
&:hover {
transition: all 0.2s;
background-color: var(--kungalgame-white);
box-shadow: var(--kungalgame-shadow-1);
}
}
span {
&:nth-child(1) {
white-space: wrap;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
overflow: hidden;
-webkit-box-orient: vertical;
color: var(--kungalgame-blue-5);
}
&:nth-child(2) {
white-space: wrap;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
-webkit-box-orient: vertical;
}
}
}
</style>

View file

@ -1,3 +1,4 @@
<!-- Settings panel component, displaying the entire forum's settings panel -->
<script setup lang="ts">
import { Icon } from '@iconify/vue'
@ -18,16 +19,19 @@ const emits = defineEmits<{
close: [showKUNGalgamePanel: boolean]
}>()
// Restore all settings to default
const handleRecover = () => {
settingsStore.setKUNGalgameSettingsRecover()
}
// Close the settings panel
const handelCloseSettingsPanel = () => {
emits('close', false)
}
</script>
<template>
<!-- Root element -->
<div class="root">
<div class="container">
<div class="title">
@ -35,8 +39,10 @@ const handelCloseSettingsPanel = () => {
<span><Icon class="settings-icon" icon="uiw:setting-o" /></span>
</div>
<!-- Mode switch component -->
<Mode />
<!-- Language switch component -->
<SwitchLanguage />
<div class="switch">
@ -57,15 +63,18 @@ const handelCloseSettingsPanel = () => {
<TransitionGroup name="item" tag="div">
<div class="item" v-if="isShowPageWidth">
<!-- Page width adjustment component -->
<PageWidth />
</div>
<div class="item" v-else-if="!isShowPageWidth">
<!-- Set the page font -->
<Font />
</div>
</TransitionGroup>
</div>
<!-- Background settings component -->
<Background />
<button class="reset" @click="handleRecover">
@ -73,15 +82,19 @@ const handelCloseSettingsPanel = () => {
</button>
</div>
<!-- Mascot component -->
<Loli class="loli" />
<!-- Close panel -->
<div class="close">
<!-- showKUNGalgamePanel exists in the settings, false to close the settings panel -->
<Icon @click="handelCloseSettingsPanel" icon="line-md:close" />
</div>
</div>
</template>
<style lang="scss" scoped>
/* Root container */
.root {
top: 65px;
right: 0;
@ -113,6 +126,7 @@ const handelCloseSettingsPanel = () => {
}
}
// Keep the settings button rotating
.settings-icon {
animation: settings 3s linear infinite;
}
@ -126,6 +140,7 @@ const handelCloseSettingsPanel = () => {
}
}
/* Menu for switching settings options */
.switch {
display: flex;
flex-direction: column;
@ -189,7 +204,7 @@ const handelCloseSettingsPanel = () => {
cursor: pointer;
}
.item-move,
.item-move, /* Transition applied to moving elements */
.item-enter-active,
.item-leave-active {
transition: all 0.5s ease;
@ -201,6 +216,8 @@ const handelCloseSettingsPanel = () => {
transform: translateY(77px);
}
/* Ensure the leaving element is removed from the layout flow
to correctly calculate the animated movement. */
.item-leave-active {
position: absolute;
}

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import CustomBackground from './CustomBackground.vue'
import Message from '@/components/alert/Message'
import BackgroundImageSkeleton from '@/components/skeleton/settings-panel/BackgroundImageSkeleton.vue'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
@ -11,16 +11,35 @@ import { getBackgroundURL } from '@/hooks/useBackgroundPicture'
import { restoreBackground } from '@/hooks/useBackgroundPicture'
const imageArray = ref<string[]>([])
const { showKUNGalgameBackground } = storeToRefs(useKUNGalgameSettingsStore())
// Use the settings panel store
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
storeToRefs(useKUNGalgameSettingsStore())
// Get background image thumbnails
const getBackground = async (imageNumber: number) => {
return await getBackgroundURL(`bg${imageNumber}-m`)
}
// Change the background image
const handleChangeImage = (index: number) => {
showKUNGalgameBackground.value = `bg${index}`
}
// Custom background
const url = ref('')
const handleCustomBackground = () => {
if (url.value) {
showKUNGalgameCustomBackground.value = url.value
showKUNGalgameBackground.value = 'bg1007'
url.value = ''
} else {
Message('Please input a valid image URL', '请输入合法的图片链接', 'warn')
}
}
const handleHoverBackgroundImage = () => {}
onMounted(async () => {
for (const background of backgroundImages) {
const backgroundURL = await getBackground(background.index)
@ -37,6 +56,7 @@ onMounted(async () => {
<ul class="kungalgame-background-container">
<li>
<span>{{ $tm('header.settings.preset') }}</span>
<!-- Preset background collection -->
<ul class="kungalgame-restore-bg">
<li
v-for="kun in backgroundImages"
@ -47,6 +67,7 @@ onMounted(async () => {
v-if="kun"
:src="imageArray[kun.index - 1]"
@click="handleChangeImage(kun.index)"
@hover="handleHoverBackgroundImage"
/>
<BackgroundImageSkeleton v-if="!imageArray[kun.index - 1]" />
@ -54,9 +75,29 @@ onMounted(async () => {
</ul>
</li>
<!-- User-customized background -->
<li>
<CustomBackground />
<!-- Title -->
<span>{{ $tm('header.settings.custom') }}</span>
<!-- Input field -->
<div class="kungalgamer-bg">
<div class="bg-url-input">
<input
:placeholder="`${$tm('header.settings.url')}`"
type="text"
v-model="url"
required
/>
<!-- Confirm background URL -->
<button @click="handleCustomBackground">
{{ $tm('header.settings.confirm') }}
</button>
</div>
</div>
<!-- Reset blank background -->
<button class="restore-bg" @click="restoreBackground">
{{ $tm('header.settings.restore') }}
</button>
@ -66,19 +107,23 @@ onMounted(async () => {
</template>
<style lang="scss" scoped>
/* Background settings */
.kungalgame-background-container {
margin: 0;
padding: 0;
list-style: none;
text-decoration: none;
display: block;
/* Height of the background menu */
height: 100%;
font-size: 15px;
font-weight: normal;
color: var(--kungalgame-font-color-3);
/* Font for the title of the background container */
span {
height: 30px;
/* Centered */
display: flex;
justify-content: center;
align-items: center;
@ -89,6 +134,7 @@ onMounted(async () => {
margin: 10px 0;
}
/* Grid of background image thumbnails, three rows and three columns */
.kungalgame-restore-bg {
margin: 0;
padding: 0;
@ -99,18 +145,22 @@ onMounted(async () => {
grid-template-columns: repeat(3, 80px);
grid-template-rows: repeat(3, 50px);
position: relative;
/* Distance from the lower area */
margin-bottom: 10px;
/* Center individual images */
li {
display: flex;
justify-content: center;
align-items: center;
/* Spacing for individual images */
img {
cursor: pointer;
width: 70px;
position: relative;
/* Image hover effect */
&:hover {
transform: scale(3);
transition: 0.2s;
@ -123,6 +173,54 @@ onMounted(async () => {
}
}
}
.kungalgamer-bg {
display: flex;
flex-direction: column;
}
/* URL input box */
.bg-url-input {
display: flex;
justify-content: center;
align-items: center;
color: var(--kungalgame-font-color-3);
input {
width: 100%;
padding-left: 5px;
height: 25px;
border: 1px solid var(--kungalgame-blue-4);
background-color: var(--kungalgame-trans-white-9);
color: var(--kungalgame-font-color-3);
/* Focus on the input box */
&:focus {
outline: none;
background-color: var(--kungalgame-trans-blue-0);
}
}
/* Confirm button */
button {
flex-shrink: 0;
padding: 0 10px;
height: 25px;
width: 70px;
color: var(--kungalgame-font-color-3);
border: 1px solid var(--kungalgame-blue-4);
border-left: none;
background-color: var(--kungalgame-trans-white-5);
cursor: pointer;
/* Confirm button hover effect */
&:hover {
background-color: var(--kungalgame-trans-red-1);
/* Confirm button active effect */
&:active {
background-color: var(--kungalgame-trans-red-3);
}
}
}
}
.restore-bg {
font-size: 15px;

View file

@ -1,88 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import Message from '@/components/alert/Message'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
import { saveImage, getImage } from '@/hooks/useLocalforage'
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
storeToRefs(useKUNGalgameSettingsStore())
const props = defineProps<{
isMobile?: boolean
}>()
const input = ref<HTMLElement>()
const handleCustomBackground = () => {
input.value?.click()
}
const handleFileChange = async (event: Event) => {
const input = event.target as HTMLInputElement
if (!input.files || !input.files[0]) {
return
}
const file = input.files[0]
await saveImage(file, 'kun-galgame-custom-bg')
const backgroundImageBlobData = await getImage('kun-galgame-custom-bg')
if (backgroundImageBlobData) {
showKUNGalgameBackground.value = 'bg1007'
showKUNGalgameCustomBackground.value = URL.createObjectURL(
backgroundImageBlobData
)
} else {
Message('Upload image failed!', '上传图片错误!', 'error')
}
}
</script>
<template>
<div class="kungalgamer-bg">
<input
ref="input"
hidden
type="file"
accept=".jpg, .jpeg, .png"
@change="handleFileChange($event)"
/>
<button
:class="props.isMobile ? 'mobile' : ''"
@click="handleCustomBackground"
>
{{ $tm('header.settings.custom') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.kungalgamer-bg {
display: flex;
flex-direction: column;
button {
font-size: 15px;
cursor: pointer;
height: 30px;
width: 100%;
color: var(--kungalgame-font-color-3);
border: 1px solid var(--kungalgame-blue-4);
background-color: var(--kungalgame-trans-white-9);
transition: all 0.2s;
color: var(--kungalgame-blue-4);
&:hover {
color: var(--kungalgame-white);
background-color: var(--kungalgame-blue-4);
}
}
}
.mobile {
border-radius: 14px;
}
</style>

View file

@ -1,5 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
// Global message component (top)
import Message from '@/components/alert/Message'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
@ -21,6 +23,7 @@ const setFont = () => {
</script>
<template>
<!-- Set the width of certain pages -->
<div class="font">
<div class="title">
<span>{{ $tm('header.settings.font') }}</span>
@ -31,7 +34,6 @@ const setFont = () => {
{{ showKUNGalgameFontStyle }}
</span>
</div>
<div class="font-input">
<input
:placeholder="`${$tm('header.settings.fontInput')}`"
@ -53,6 +55,7 @@ const setFont = () => {
margin-bottom: 10px;
}
/* URL input box */
.font-input {
display: flex;
justify-content: center;
@ -68,12 +71,13 @@ const setFont = () => {
background-color: var(--kungalgame-trans-white-9);
color: var(--kungalgame-font-color-3);
/* Focus on the input box */
&:focus {
outline: none;
background-color: var(--kungalgame-trans-blue-0);
}
}
/* Confirm button */
button {
flex-shrink: 0;
padding: 0 10px;
@ -85,9 +89,11 @@ const setFont = () => {
background-color: var(--kungalgame-trans-white-5);
cursor: pointer;
/* Confirm button hover effect */
&:hover {
background-color: var(--kungalgame-trans-red-1);
/* Confirm button active effect */
&:active {
background-color: var(--kungalgame-trans-red-3);
}

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useLoliDataURL } from '@/hooks/useLoli'
import { onMounted, ref } from 'vue'
import { getLoli } from './loli'
import LoliSkeleton from '@/components/skeleton/settings-panel/LoliSkeleton.vue'
import KUNGalgameLoading from '@/components/loading/KUNGalgameLoading.vue'
@ -26,12 +26,12 @@ const isShowLoading = ref(false)
const reGetLoli = async () => {
isShowLoading.value = true
loliData.value = await getLoli()
loliData.value = await useLoliDataURL()
isShowLoading.value = false
}
onMounted(async () => {
await reGetLoli()
loliData.value = await useLoliDataURL()
})
</script>

View file

@ -1,25 +1,30 @@
<script setup lang="ts">
// Import the icon font
import { Icon } from '@iconify/vue'
// Import the settings store
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
// Use the settings store
const settingsStore = useKUNGalgameSettingsStore()
</script>
<template>
<div class="mode">
<!-- Day / Night mode toggle -->
<span>{{ $tm('header.settings.mode') }}</span>
<div class="mode-container">
<span>
<Icon
class="sun"
icon="line-md:moon-filled-alt-to-sunny-filled-loop-transition"
@click="useKUNGalgameSettingsStore().setKUNGalgameTheme('')"
@click="settingsStore.setKUNGalgameTheme('')"
/>
</span>
<span>
<Icon
class="moon"
icon="line-md:sunny-outline-to-moon-loop-transition"
@click="useKUNGalgameSettingsStore().setKUNGalgameTheme('dark')"
@click="settingsStore.setKUNGalgameTheme('dark')"
/>
</span>
</div>
@ -33,7 +38,6 @@ import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
align-items: center;
justify-content: space-between;
}
.mode-container {
font-size: 25px;
width: 60%;

View file

@ -9,10 +9,14 @@ const route = useRoute()
const settingsStore = useKUNGalgameSettingsStore()
const { showKUNGalgamePageWidth } = storeToRefs(settingsStore)
// Page width
const pageWidth = ref(0)
// Current route name
const routeName = computed(() => route.name as string)
// Whether page width adjustment is disabled
const isDisabled = ref(false)
// Pages where width adjustment is allowed
const pageNameArray = [
'KUN',
'Topic',
@ -24,9 +28,12 @@ const pageNameArray = [
'ThanksList',
]
// Initialize page width
const initPageWidth = () => {
if (pageNameArray.includes(routeName.value)) {
// Page width value equals store width value
pageWidth.value = showKUNGalgamePageWidth.value[routeName.value]
// Enable input
isDisabled.value = false
} else {
isDisabled.value = true
@ -35,6 +42,7 @@ const initPageWidth = () => {
watch(pageWidth, () => {
if (pageNameArray.includes(routeName.value)) {
// Store user-input width
showKUNGalgamePageWidth.value[routeName.value] = pageWidth.value
}
})
@ -49,6 +57,7 @@ onActivated(() => {
</script>
<template>
<!-- Set the width for specific pages -->
<div
class="width"
:class="isDisabled ? 'disabled' : ''"
@ -58,7 +67,6 @@ onActivated(() => {
<span>{{ $tm('header.settings.width') }}</span>
<span>{{ pageWidth }}%</span>
</div>
<div class="page-width">
<span>50%</span>
<input
@ -85,7 +93,7 @@ onActivated(() => {
margin-top: 15px;
}
}
/* Main page width slider */
.main {
width: 100%;
height: 10px;
@ -97,6 +105,7 @@ onActivated(() => {
justify-content: space-between;
}
/* Styles when page width adjustment is disabled */
.disabled {
cursor: not-allowed;
color: var(--kungalgame-font-color-0);

View file

@ -1,11 +1,18 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
// Import the settings store
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
import { watch } from 'vue'
// Import i18n
import { useI18n } from 'vue-i18n'
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
// Use the settings store
const settingsStore = useKUNGalgameSettingsStore()
const { showKUNGalgameLanguage } = storeToRefs(settingsStore)
/*
* Website language settings
*/
const { locale } = useI18n({ useScope: 'global' })
watch(showKUNGalgameLanguage, () => {
@ -24,12 +31,13 @@ watch(showKUNGalgameLanguage, () => {
</template>
<style lang="scss" scoped>
// Language settings
.set-lang {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
// Language selection box
.select {
width: 100px;
font-size: 16px;

View file

@ -1,72 +0,0 @@
import loliData from '@/assets/ren/ren.json'
import { randomNum } from '@/utils/random'
export const getLoli = async () => {
const linkPreset = `https://cdn.jsdelivr.net/gh/kun-moe/kun-image@main/ren/`
const getAssetsFile = (name: number) => `${linkPreset}${name}.webp`
const randomBrow = randomNum(1, 18)
const randomEye = randomNum(19, 36)
const randomMouth = randomNum(37, 56)
const randomFace = randomNum(57, 62)
const randomSkirt = randomNum(63, 70)
const loli = {
lass: loliData[randomSkirt],
eye: loliData[randomEye],
brow: loliData[randomBrow],
mouth: loliData[randomMouth],
face: loliData[randomFace],
}
const loliBodyLeft = `${loli.lass.left}px`
const loliBodyTop = `${loli.lass.top}px`
const loliEyeLeft = `${loli.eye.left}px`
const loliEyeTop = `${loli.eye.top}px`
const loliBrowLeft = `${loli.brow.left}px`
const loliBrowTop = `${loli.brow.top}px`
const loliMouthLeft = `${loli.mouth.left}px`
const loliMouthTop = `${loli.mouth.top}px`
const loliFaceLeft = `${loli.face.left}px`
const loliFaceTop = `${loli.face.top}px`
const promises = [
getAssetsFile(loli.lass.layer_id),
getAssetsFile(loli.eye.layer_id),
getAssetsFile(loli.brow.layer_id),
getAssetsFile(loli.mouth.layer_id),
getAssetsFile(loli.face.layer_id),
].map((url) => fetch(url))
const responses = await Promise.all(promises)
const results = await Promise.all(
responses.map((response) => response.blob())
)
const [body, eye, brow, mouth, face] = results.map((blob) =>
URL.createObjectURL(blob)
)
return {
loliBodyLeft,
loliBodyTop,
loliEyeLeft,
loliEyeTop,
loliBrowLeft,
loliBrowTop,
loliMouthLeft,
loliMouthTop,
loliFaceLeft,
loliFaceTop,
body,
eye,
brow,
mouth,
face,
}
}

View file

@ -1,70 +0,0 @@
<script setup lang="ts">
const count = 10
</script>
<template>
<div v-for="(_, index) in count" :key="index" class="skeleton">
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.skeleton {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
background-color: var(--kungalgame-trans-white-5);
border-radius: 3px;
margin: 0 auto;
padding: 12px;
}
ul {
padding: 0;
margin: 0;
width: 100%;
li {
background-image: linear-gradient(
90deg,
var(--kungalgame-trans-blue-2) 25%,
var(--kungalgame-pink-0) 37%,
var(--kungalgame-trans-blue-2) 63%
);
border-radius: 3px;
width: 100%;
height: 10px;
list-style: none;
background-size: 400% 100%;
margin-top: 10px;
background-position: 100% 50%;
animation: skeleton 1.7s ease infinite;
&:first-child {
margin-top: 0;
width: 50%;
height: 20px;
}
&:last-child {
width: 77%;
}
}
}
@keyframes skeleton {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
</style>

View file

@ -1,85 +0,0 @@
<script setup lang="ts"></script>
<template>
<div class="skeleton">
<div class="container">
<span></span>
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</template>
<style lang="scss" scoped>
.skeleton {
margin-top: 30px;
width: 100%;
height: 150px;
display: flex;
justify-content: center;
}
.container {
background-color: var(--kungalgame-trans-white-5);
border-radius: 3px;
margin: 0 auto;
padding: 20px;
width: 100%;
display: flex;
span {
flex-shrink: 0;
width: 100px;
height: 100px;
background-color: var(--kungalgame-trans-blue-2);
margin-right: 20px;
}
}
ul {
padding: 0;
margin: 0;
width: 100%;
li {
background-image: linear-gradient(
90deg,
var(--kungalgame-trans-blue-2) 25%,
var(--kungalgame-pink-0) 37%,
var(--kungalgame-trans-blue-2) 63%
);
border-radius: 3px;
width: 100%;
height: 17px;
list-style: none;
background-size: 400% 100%;
margin-top: 10px;
background-position: 100% 50%;
animation: skeleton 1.7s ease infinite;
&:first-child {
margin-top: 0;
height: 30px;
width: 23%;
}
&:last-child {
width: 77%;
}
}
}
@keyframes skeleton {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
</style>

View file

@ -0,0 +1,94 @@
<script setup lang="ts">
import { ref, defineProps } from 'vue'
const props = defineProps({
content: String,
position: String, // 'top', 'bottom', 'left', 'right'
})
</script>
<template>
<div class="tooltip">
<div class="tooltip-content">
{{ content }}
</div>
<div class="tooltip-arrow"></div>
</div>
</template>
<style lang="scss" scoped>
.tooltip {
position: relative;
display: inline-block;
}
.tooltip-content {
display: none;
position: absolute;
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 4px;
z-index: 1;
}
.tooltip-arrow {
position: absolute;
width: 0;
height: 0;
border-width: 6px;
border-style: solid;
}
.tooltip.top .tooltip-content {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
}
.tooltip.top .tooltip-arrow {
top: 100%;
left: 50%;
margin-left: -6px;
border-color: transparent transparent #333 transparent;
}
.tooltip.bottom .tooltip-content {
top: 100%;
left: 50%;
transform: translateX(-50%);
}
.tooltip.bottom .tooltip-arrow {
bottom: 100%;
left: 50%;
margin-left: -6px;
border-color: #333 transparent transparent transparent;
}
.tooltip.left .tooltip-content {
top: 50%;
right: 100%;
transform: translateY(-50%);
}
.tooltip.left .tooltip-arrow {
top: 50%;
right: 100%;
margin-top: -6px;
border-color: transparent #333 transparent transparent;
}
.tooltip.right .tooltip-content {
top: 50%;
left: 100%;
transform: translateY(-50%);
}
.tooltip.right .tooltip-arrow {
top: 50%;
left: 100%;
margin-top: -6px;
border-color: transparent transparent transparent #333;
}
</style>

View file

@ -1,8 +1,8 @@
<!-- This file is for adapting the top navigation bar for mobile devices -->
<script setup lang="ts">
import Mode from '../setting-panel/components/Mode.vue'
import SwitchLanguage from '../setting-panel/components/SwitchLanguage.vue'
import CustomBackground from '../setting-panel/components/CustomBackground.vue'
import { hamburgerItem } from './hamburgerItem'
import { topBarItem } from './topBarItem'
defineEmits(['showKUNGalgameHamburger'])
</script>
@ -20,22 +20,18 @@ defineEmits(['showKUNGalgameHamburger'])
</div>
<!-- Interactive items -->
<div class="item" style="font-size: 17px">
<span v-for="kun in hamburgerItem" :key="kun.index">
<span v-for="kun in topBarItem" :key="kun.index">
<RouterLink :to="kun.router">
{{ $tm(`header.hamburger.${kun.name}`) }}
{{ $tm(`header['${kun.name}']`) }}
</RouterLink>
</span>
</div>
<Mode style="font-size: 15px" />
<!-- Day and night mode switch component -->
<Mode style="font-size: 20px" />
<SwitchLanguage style="font-size: 15px" />
<CustomBackground :is-mobile="true" />
<div class="home">
<RouterLink to="/kun">{{ $tm('header.hamburger.home') }}</RouterLink>
</div>
<!-- Language switch component -->
<SwitchLanguage style="font-size: 20px" />
</div>
</Transition>
</div>
@ -58,9 +54,8 @@ defineEmits(['showKUNGalgameHamburger'])
}
.container {
height: 100vh;
position: absolute;
width: 247px;
width: 277px;
padding: 10px;
background-color: var(--kungalgame-trans-white-2);
border: 1px solid var(--kungalgame-blue-1);
@ -93,21 +88,4 @@ defineEmits(['showKUNGalgameHamburger'])
font-size: 20px;
}
}
.home {
width: 100%;
margin-top: 50px;
a {
padding: 5px 10px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 17px;
font-size: 17px;
border: 1px solid var(--kungalgame-blue-4);
color: var(--kungalgame-blue-4);
}
}
</style>

View file

@ -4,29 +4,33 @@ import { Icon } from '@iconify/vue'
import 'animate.css'
import { topBarItem } from './topBarItem'
import { onBeforeRouteLeave } from 'vue-router'
// Mobile version hamburger
const Hamburger = defineAsyncComponent(() => import('./Hamburger.vue'))
// Settings panel
const KUNGalgameSettingsPanel = defineAsyncComponent(
() => import('../setting-panel/KUNGalgameSettingPanel.vue')
)
// Panel when clicking on the user's avatar
const KUNGalgameUserInfo = defineAsyncComponent(
() => import('./KUNGalgameUserInfo.vue')
)
import { useTempHomeStore } from '@/store/temp/home'
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { storeToRefs } from 'pinia'
const { isShowSearch } = storeToRefs(useTempHomeStore())
const { name, avatarMin } = storeToRefs(useKUNGalgameUserStore())
const showKUNGalgameHamburger = ref(false)
// Show settings panel state
const showKUNGalgamePanel = ref(false)
// Show mobile mode hamburger state
const showKUNGalgameHamburger = ref(false)
// Show user panel when clicking on the user's avatar
const showKUNGalgameUserPanel = ref(false)
// Set the navigation bar width based on the number of navigation items
const navItemNum = topBarItem.length
const navItemLength = `${navItemNum}00px`
// Destroy the SettingsPanel and Hamburger before leaving the route
onBeforeRouteLeave(() => {
showKUNGalgamePanel.value = false
showKUNGalgameHamburger.value = false
@ -35,6 +39,7 @@ onBeforeRouteLeave(() => {
<template>
<div class="header">
<!-- Top left interactive bar -->
<div class="nav-top">
<div class="hamburger">
<Icon
@ -50,32 +55,32 @@ onBeforeRouteLeave(() => {
</Transition>
</div>
<!-- Website name and logo -->
<div class="kungalgame">
<RouterLink to="/kun">
<img
src="@/assets/images/favicon.webp"
alt="KUN Visual Novel | 鲲 Galgame"
alt="KUN Visual Novel 鲲 Galgame"
/>
<span>{{ $tm('header.name') }}</span>
</RouterLink>
</div>
<!-- Navigation bar -->
<div class="top-bar">
<!-- Top individual sections -->
<span v-for="kun in topBarItem" :key="kun.index">
<RouterLink :to="{ path: kun.router }">
{{ $tm(`header.${kun.name}`) }}
{{ $tm(`header['${kun.name}']`) }}
</RouterLink>
</span>
<!-- Hover effect under the top section -->
<div class="box"></div>
</div>
</div>
<div class="kungalgamer-info">
<span class="search" @click="isShowSearch = true">
<Icon icon="line-md:search" />
</span>
<!-- showKUNGalgamePanel is a boolean value in the store, true/false controls the display and close of the settings panel -->
<span
class="settings"
@click="showKUNGalgamePanel = !showKUNGalgamePanel"
@ -125,6 +130,7 @@ onBeforeRouteLeave(() => {
display: flex;
align-items: center;
justify-content: space-between;
/* 相对于设置面板定位 */
position: relative;
z-index: 1;
margin-bottom: 7px;
@ -236,16 +242,6 @@ $navNumber: v-bind(navItemNum);
align-items: center;
margin-right: 50px;
.search {
display: flex;
justify-content: center;
align-items: center;
color: var(--kungalgame-font-color-2);
font-size: 25px;
cursor: pointer;
margin-right: 20px;
}
.settings {
display: flex;
justify-content: center;
@ -311,13 +307,12 @@ $navNumber: v-bind(navItemNum);
margin-right: 0 !important;
}
}
.settings {
display: none !important;
}
}
@media (max-width: 700px) {
.settings {
display: none !important;
}
.top-bar {
display: none;
}
@ -332,11 +327,5 @@ $navNumber: v-bind(navItemNum);
.kungalgamer-info {
margin-right: 30px;
}
.avatar {
img {
margin-left: 0;
}
}
}
</style>

View file

@ -5,7 +5,7 @@ import { useRouter } from 'vue-router'
// Global message component (top)
import Message from '@/components/alert/Message'
// Global message component (bottom)
import { useTempMessageStore } from '@/store/temp/message'
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { storeToRefs } from 'pinia'
// Reset store
@ -33,7 +33,10 @@ const handlePanelBlur = async () => {
// Log out - for simplicity, the code here does not communicate with the backend to remove the token from Redis.
const logOut = async () => {
// Get the user's response
const res = await useTempMessageStore().alert('AlertInfo.edit.logout', true)
const res = await useKUNGalgameMessageStore().alert(
'AlertInfo.edit.logout',
true
)
if (res) {
kungalgameStoreReset()
router.push('/login')

View file

@ -1,22 +0,0 @@
// Interface for individual items in the top navigation bar
interface Hamburger {
index: number
name: string
router: string
}
// Items in the top navigation bar
// (be sure to include '/' here, or child routes may have issues!!!)
export const hamburgerItem: Hamburger[] = [
{ index: 1, name: 'pool', router: '/pool' },
{ index: 2, name: 'create', router: '/edit' },
{ index: 3, name: 'technique', router: '/technique' },
{ index: 4, name: 'about', router: '/kungalgame' },
{ index: 5, name: 'ranking', router: '/ranking' },
{ index: 6, name: 'update', router: '/update-log' },
{ index: 7, name: 'bylaw', router: '/bylaw' },
{ index: 8, name: 'balance', router: '/balance' },
{ index: 9, name: 'nonMoe', router: '/non-moe' },
{ index: 10, name: 'thanks', router: '/thanks-list' },
{ index: 11, name: 'join', router: '/contact' },
]

View file

@ -2,23 +2,21 @@
import { ref, watch } from 'vue'
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { useTempMessageStore } from '@/store/temp/message'
import { storeToRefs } from 'pinia'
import { useKUNGalgameMessageStore } from '@/store/modules/message'
const info = useKUNGalgameMessageStore()
// The parent component instructs it to send the verification code, and it will do so.
const props = defineProps<{
email: string
isSendCode: boolean
}>()
const { isCaptureSuccessful } = storeToRefs(useTempMessageStore())
const info = useTempMessageStore()
const isSendCode = ref(false)
const isSending = ref(false)
const countdown = ref(0)
watch(
() => isSendCode.value,
() => props.isSendCode,
async () => {
if (!isSending.value) {
isSending.value = true
@ -39,16 +37,10 @@ watch(
}
}
)
const handleSendCode = () => {
if (isCaptureSuccessful.value) {
isSendCode.value = !isSendCode.value
}
}
</script>
<template>
<button @click="handleSendCode" :disabled="isSending">
<button :disabled="isSending">
{{ isSending ? countdown : $tm('login.register.send') }}
</button>
</template>

View file

@ -1,86 +1,29 @@
/* -B means backend, message error code is defined by backend */
const errorMessagesEN: Record<number, string> = {
// User Part
10101: `User not found (-B)`,
10102: `User password error (-B)`,
10103: `Email verification code error (-B)`,
10104: `Email is already registered, please change it (-B)`,
10105: `Username is already registered, please change it (-B)`,
10106: `User bio is too long (-B)`,
10107: `Invalid Email, Name, Password, or Verification Code Format (-B)`,
10108: `Invalid password format (-B)`,
10109: `Invalid Email or Verification Code Format (-B)`,
10110: `Avatar image upload error. The image is an array. (-B)`,
10111: `Avatar image upload error. The final compressed size of the image exceeds 50KB. (-B)`,
10112: `In cooldown for login, two identical login attempts should have a one-minute interval. (-B)`,
10113: `In cooldown for register, two identical register attempts should have a one-minute interval. (-B)`,
10101: 'User not found',
10102: 'User password error',
10103: 'Email verification code error',
10104: 'Email is already registered, please change it',
10105: 'Username is already registered, please change it',
// Topic Part
10201: `Your daily topic limit has been reached for today. (-B)`,
10202: `Your moemoepoints are less than 1100, so you can't use the topic suggestion feature (-B)`,
10204: `Topic title length exceed 40 characters. Or empty. (-B)`,
10205: `Topic content length exceed 100007 characters. Or empty. (-B)`,
10206: `Topic with a maximum of 7 tags. Minimum one tag. (-B)`,
10207: `Topic with a maximum of 2 categories. Minimum one category. (-B)`,
10208: `Invalid topics timestamp. (-B)`,
// Auth Part
10301: `Sending emails too frequently, please waiting 30s (-B)`,
10302: `Invalid Email Format (-B)`,
10303: `Invalid Email, Password, or Verification Code Format (-B)`,
// Comment Part
10401: `Comment length exceed 1007 characters. Or empty. (-B)`,
// Reply Part
10501: `Reply with a maximum of 7 tags. (-B)`,
10502: `Single tag maximum length is 17 characters (-B)`,
10503: `Reply content is empty (-B)`,
10504: `Reply maximum length is 10007 characters (-B)`,
10505: `Invalid reply timestamp. (-B)`,
10201: 'Your daily topic limit has been reached for today.',
10202: `Your moemoepoints are less than 1100, so you can't use the topic suggestion feature`,
}
const errorMessagesCN: Record<number, string> = {
10101: `用户未找到 (-B)`,
10102: `用户密码错误 (-B)`,
10103: `邮箱验证码错误 (-B)`,
10104: `邮箱已被注册,请更改 (-B)`,
10105: `用户名已被注册,请修改 (-B)`,
10106: `用户签名过长 (-B)`,
10107: `非法的邮箱, 用户名, 密码, 或验证码 (-B)`,
10108: `非法的密码格式 (-B)`,
10109: `非法的邮箱或验证码格式 (-B)`,
10110: `头像上传错误. 图片为数组 (-B)`,
10111: `头像上传错误. 图片最终压缩大小超过 50kb (-B)`,
10112: `登陆冷却中,两次相同登陆时间间隔一分钟 (-B)`,
10113: `注册冷却中,两次相同注册时间间隔一分钟 (-B)`,
10101: '用户未找到',
10102: '用户密码错误',
10103: '邮箱验证码错误',
10104: '邮箱已被注册,请更改',
10105: '用户名已被注册,请修改',
10201: `您今日可以发表的话题数已达上限 (-B)`,
10202: `您的萌萌点不足 1100, 无法使用推话题功能 (-B)`,
10204: `话题标题长度超过 40 个字符, 或为空 (-B)`,
10205: `话题内容长度超过 100007 个字符, 或为空 (-B)`,
10206: `话题最多 7 个标签, 最少一个标签 (-B)`,
10207: `话题最多 2 个分类, 最少一个分类 (-B)`,
10208: `非法的话题时间戳. (-B)`,
10301: `发送邮件频率过快, 请等待 30 秒 (-B)`,
10302: `非法的邮箱格式 (-B)`,
10303: `非法的邮箱, 密码, 或验证码 (-B)`,
10401: `评论内容长度超过 1007 个字符, 或为空 (-B)`,
10501: `回复最多 7 个标签 (-B)`,
10502: `单个标签最长 17 个字符 (-B)`,
10503: `回复内容不可为空 (-B)`,
10504: `回复内容最大长度为 10007 个字符 (-B)`,
10505: `非法的回复时间戳 (-B)`,
10201: '您今日可以发表的话题数已达上限',
10202: '您的萌萌点不足 1100, 无法使用推话题功能',
}
export const getErrorMessageEN = (errorCode: number) => {
return errorMessagesEN[errorCode] || `Unknown server error (-B)`
return errorMessagesEN[errorCode] || 'Unknown server error'
}
export const getErrorMessageCN = (errorCode: number) => {
return errorMessagesCN[errorCode] || `未知的服务器错误 (-B)`
return errorMessagesCN[errorCode] || '未知的服务器错误'
}

View file

@ -1,6 +1,11 @@
// Global message component (top)
import Message from '@/components/alert/Message'
import { generateTokenByRefreshTokenApi } from '@/api'
// Use the user store
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
// Import the router
import router from '@/router'
// Import known error handling functions
import { kungalgameErrorHandler } from './errorHandler'
interface ErrorResponseData {
@ -13,15 +18,27 @@ interface ErrorResponseData {
* Then identifies errors based on custom backend status codes
* If unable to recognize, it throws an error.
*/
export const onRequestError = async (response: Response) => {
export async function onRequestError(response: Response) {
// Identify errors based on status codes
if (response.status === 401) {
Message(
'Login expired, please log in again.',
'登陆过期,请重新登陆',
'error'
)
useKUNGalgameUserStore().removeToken()
router.push('/login')
// Attempt to obtain a new token using the refresh token
const accessTokenResponse = await generateTokenByRefreshTokenApi()
// If a new token is successfully obtained, set the token
if (accessTokenResponse.code === 200 && accessTokenResponse.data.token) {
useKUNGalgameUserStore().setToken(accessTokenResponse.data.token)
// Set the page to reload with the new token applied
location.reload()
} else {
// Otherwise, prompt the user to log in again
Message(
'Login expired, please log in again.',
'登陆过期,请重新登陆',
'error'
)
useKUNGalgameUserStore().removeToken()
router.push('/login')
}
return
}
@ -34,6 +51,8 @@ export const onRequestError = async (response: Response) => {
return
}
// Get the error response data
const data: ErrorResponseData = await response.json()
// Handle known errors
kungalgameErrorHandler(data.code)
}

81
src/hooks/useLoli.ts Normal file
View file

@ -0,0 +1,81 @@
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
// It's a bit strange here; there's a 'data' property on the buffer
// , not sure where it comes from
interface LoliBuffer {
data: Uint8Array
}
export interface LoliData {
loliBodyLeft: string
loliBodyTop: string
loliEyeLeft: string
loliEyeTop: string
loliBrowLeft: string
loliBrowTop: string
loliMouthLeft: string
loliMouthTop: string
loliFaceLeft: string
loliFaceTop: string
body: LoliBuffer
eye: LoliBuffer
brow: LoliBuffer
mouth: LoliBuffer
face: LoliBuffer
}
export type LoliDataResponseData = KUNGalgameResponseData<LoliData>
const fetchGetLoliData = async (): Promise<
KUNGalgameResponseData<LoliData>
> => {
const baseUrl = import.meta.env.VITE_API_BASE_URL
const url = `/loli/image`
const fullUrl = `${baseUrl}${url}`
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${useKUNGalgameUserStore().getToken()}`,
},
})
return await response.json()
}
// Not caching loli data here, as it would clutter the indexdb with too much data
const createImageUrl = (imageBuffer: LoliBuffer) => {
const uint8Array = new Uint8Array(imageBuffer.data)
const blob = new Blob([uint8Array], { type: 'image/webp' })
return URL.createObjectURL(blob)
}
export const useLoliDataURL = async () => {
const loliData = (await fetchGetLoliData()).data
const imageUrls = [
createImageUrl(loliData.body),
createImageUrl(loliData.eye),
createImageUrl(loliData.brow),
createImageUrl(loliData.mouth),
createImageUrl(loliData.face),
]
return {
loliBodyLeft: loliData.loliBodyLeft,
loliBodyTop: loliData.loliBodyTop,
loliEyeLeft: loliData.loliEyeLeft,
loliEyeTop: loliData.loliEyeTop,
loliBrowLeft: loliData.loliBrowLeft,
loliBrowTop: loliData.loliBrowTop,
loliMouthLeft: loliData.loliMouthLeft,
loliMouthTop: loliData.loliMouthTop,
loliFaceLeft: loliData.loliFaceLeft,
loliFaceTop: loliData.loliFaceTop,
body: imageUrls[0],
eye: imageUrls[1],
brow: imageUrls[2],
mouth: imageUrls[3],
face: imageUrls[4],
}
}

View file

@ -6,21 +6,6 @@ export default {
technique: 'TECHNIQUE',
about: 'ABOUT',
return: 'HOME',
hamburger: {
name: 'KUN Visual Novel',
pool: 'Pool',
create: 'Create Topic',
technique: 'Technique',
about: 'About Us',
ranking: 'Ranking',
update: 'Update Log',
bylaw: 'Regulations',
balance: 'P & L',
nonMoe: 'Non-moe',
thanks: 'Thanks List',
join: 'Join / Contacts',
home: 'Back Home',
},
settings: {
name: 'Settings',
mode: 'Mode',
@ -33,7 +18,8 @@ export default {
background: 'Background Setting',
preset: 'Use our preset background',
custom: 'Custom Background',
confirm: 'Confirm',
url: 'Paste the picture url here',
confirm: 'confirm',
restore: 'Restore blank background',
recover: 'Recover all settings to default',
},
@ -44,16 +30,14 @@ export default {
},
back: {
back: 'Back',
home: 'Home',
},
mainPage: {
header: {
filter: 'Filter',
category: 'Category',
galgame: 'Visual Novel',
technique: 'Technique',
others: 'Others',
search: 'Search Topics',
all: 'All Topics',
history: 'Search History',
clear: 'Clear all history',
updated: 'Default',
time: 'Time',
popularity: 'Popularity',
@ -61,15 +45,10 @@ export default {
likes: 'Likes',
replies: 'Replies',
comments: 'Comments',
search: 'Input to Auto Search',
history: 'Search History',
clear: 'Clear all history',
emptyHistory: `This loli hasn't searched for anything`,
emptyResult: 'No results found...',
},
asideActive: {
fold: 'Fold Aside',
create: 'CREATE TOPIC',
create: 'CREATE Topic!',
update: 'Update',
balance: 'P & L',
ranking: 'Ranking',
@ -98,7 +77,6 @@ export default {
acgngame: 'ACGNGAME',
shinnku: `Shinnku's Visual Novel`,
ymgal: 'YM galgame',
kun: `KUN's Blog`,
},
describe: {
title: 'KUN Visual Novel',
@ -107,13 +85,17 @@ export default {
kun3: 'NO ADs Forever',
kun4: 'Free Forever',
},
contact: 'Contact Us',
},
},
topic: {
aside: {
floor: 'Sort by Floor Number',
like: 'Sort by Likes Count',
comment: 'Sort by Comment Count',
top: 'Back Top',
floorSort: 'Floor Sort',
timeSort: 'Time Sort',
likeSort: 'Like Sort',
commentSort: 'Reply Sort',
updatedSort: 'Update Sort',
tags: 'Topics Under the Same Tags',
tagsEmpty: 'The tags currently has no other topics',
master: 'Other Topics of The Master',
@ -365,18 +347,6 @@ export default {
donate: 'Donate Us',
home: 'Back Home',
},
pool: {
load: 'Click to Load More Topics',
complete: `Already there's nothing left...`,
view: 'Sort by Views',
like: 'Sort by Likes',
time: 'Sort by Time',
},
technique: {
prev: 'Prev',
next: 'Next',
KKKKK: `We're not sure how this page should be written. If you have any suggestions, please contact us.`,
},
donate: {
donate: 'Donate Us',
no: 'comes with no Moemoepoint rewards',
@ -397,11 +367,6 @@ export default {
success: 'Login successful',
home: 'You will be redirected to the home page in 3 seconds',
},
footer: {
copyright: 'Copyright © 2023 KUN Visual Novel (except for images)',
openSource: 'GitHub Open Source',
reserved: 'All rights reserved | Version',
},
// 非页面组件这里统一用大驼峰
ComponentAlert: {
confirm: 'OK',
@ -411,12 +376,14 @@ export default {
edit: {
publish: 'Confirm to publish?',
publishSuccess: 'Publish Successfully',
publishCancel: 'Cancel Publish',
upvoteTopic:
'Are you sure you want to upvote this topic? This will cost you 17 Moe Moe Points',
upvoteReply:
'Are you sure you want to upvote this reply? This will cost you 3 Moe Moe Points',
rewrite: 'Confirm to Rewrite?',
rewriteSuccess: 'Rewrite Successfully',
rewriteCancel: 'Cancel Rewrite',
closePanel: 'Confirm closing the panel? Your changes will not be saved.',
draft: 'The draft has been saved successfully!',
leave: 'Confirm leaving the page? Your changes will not be saved.',

View file

@ -6,21 +6,6 @@ export default {
technique: '技术交流',
about: '关于我们',
return: '返回主页',
hamburger: {
name: '鲲 Galgame',
pool: '所有话题',
create: '发布话题',
technique: '技术交流',
about: '关于我们',
ranking: '排行榜单',
update: '更新日志',
bylaw: '执行条例',
balance: '收支公示',
nonMoe: '不萌记录',
thanks: '感谢名单',
join: '加入 / 联系',
home: '返回主页',
},
settings: {
name: '设置面板',
mode: '模式切换',
@ -33,6 +18,7 @@ export default {
background: '背景设置',
preset: '点击使用我们预设的背景',
custom: '自定义背景',
url: '请在这里粘贴图片的URL',
confirm: '确定',
restore: '恢复空白背景',
recover: '恢复所有设置为默认',
@ -44,16 +30,14 @@ export default {
},
back: {
back: '返回',
home: '主页',
},
mainPage: {
header: {
filter: '筛选',
category: '分类',
galgame: 'Galgame',
technique: '技术交流',
others: '其它',
search: '搜索话题',
all: '全部话题',
history: '搜索历史',
clear: '清除所有历史',
updated: '恢复默认排序',
time: '按照时间排序',
popularity: '按热度值排序',
@ -61,11 +45,6 @@ export default {
likes: '按点赞数排序',
replies: '按回复数排序',
comments: '按评论数排序',
search: '输入内容以自动搜索',
history: '搜索历史',
clear: '清除所有历史',
emptyHistory: '这只萝莉什么也没搜索过',
emptyResult: '什么也没有搜索到。。。',
},
asideActive: {
fold: '折叠左侧区域',
@ -98,7 +77,6 @@ export default {
acgngame: 'ACGNGAME',
shinnku: '失落的小站',
ymgal: '月幕 galgame',
kun: '鲲的博客',
},
describe: {
title: '鲲 Galgame',
@ -107,13 +85,17 @@ export default {
kun3: '鲲 Galgame 永远不会有广告',
kun4: '鲲 Galgame 永远不会收费',
},
contact: '联系我们',
},
},
topic: {
aside: {
floor: '按照楼层数排序',
like: '按照点赞数排序',
comment: '按照评论数排序',
top: '返回到顶端',
floorSort: '按楼层排序',
timeSort: '按时间排序',
likeSort: '按点赞排序',
commentSort: '按评论排序',
updatedSort: '按更新排序',
tags: '相同标签下的其它话题',
tagsEmpty: '该标签下暂无其它话题',
master: '楼主的其它话题',
@ -364,18 +346,6 @@ export default {
donate: '赞助我们',
home: '返回主页',
},
pool: {
load: '点击继续加载话题',
complete: '已经。。。一滴也不剩了',
view: '按浏览数排序',
like: '按点赞数排序',
time: '按照时间排序',
},
technique: {
prev: '上一页',
next: '下一页',
KKKKK: `我们不知道这个页面怎么写了,如果有建议,请联系我们`,
},
donate: {
donate: '赞助我们',
no: '没有任何的萌萌点奖励',
@ -395,11 +365,6 @@ export default {
success: '登陆成功',
home: '3 秒后你将会进入主页',
},
footer: {
copyright: '版权所有 © 2023 鲲 Galgame (图片除外)',
openSource: 'GitHub 开源',
reserved: '保留所有权利 | 版本',
},
// 非页面组件这里统一用大驼峰
ComponentAlert: {
confirm: '确定',
@ -409,10 +374,12 @@ export default {
edit: {
publish: '确认发布吗?',
publishSuccess: '发布成功',
publishCancel: '取消发布',
upvoteTopic: '您确定推这个话题吗,这将会消耗您 17 萌萌点',
upvoteReply: '您确定推这个回复吗,这将会消耗您 3 萌萌点',
rewrite: '确认 Rewrite 吗?',
rewriteSuccess: 'Rewrite 成功',
rewriteCancel: '取消 Rewrite',
closePanel: '确认关闭面板吗?您的更改将不会被保存',
draft: '草稿已经保存成功!',
leave: '确认离开界面吗?您的更改将不会保存',

View file

@ -1,12 +1,12 @@
<script setup lang="ts">
import { onMounted, watch, ref } from 'vue'
// Import animations
import 'animate.css'
import { getCurrentBackground } from '@/hooks/useBackgroundPicture'
import KUNGalgameTopBar from '@/components/top-bar/KUNGalgameTopBar.vue'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
import { getImage } from '@/hooks/useLocalforage'
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
storeToRefs(useKUNGalgameSettingsStore())
@ -14,12 +14,6 @@ const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
const imageURL = ref('')
onMounted(async () => {
const backgroundImageBlobData = await getImage('kun-galgame-custom-bg')
if (showKUNGalgameBackground.value === 'bg1007' && backgroundImageBlobData) {
showKUNGalgameCustomBackground.value = URL.createObjectURL(
backgroundImageBlobData
)
}
imageURL.value = await getCurrentBackground()
})
@ -37,7 +31,7 @@ watch(
<div class="top-bar">
<KUNGalgameTopBar />
</div>
<!-- <RouterView /> -->
<RouterView #default="{ route, Component }">
<Transition
:enter-active-class="`animate__animated ${route.meta.transition}`"

View file

@ -6,31 +6,62 @@ import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import type { App } from 'vue'
// Import store for the income and expense public disclosure page
import { useKUNGalgameBalanceStore } from './modules/balance'
// Import store for the editing interface
import { useKUNGalgameEditStore } from './modules/edit'
import { usePersistKUNGalgameHomeStore } from './modules/home'
// Import home store
import { useKUNGalgameHomeStore } from './modules/home'
// Import user store
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
// Import message store
import { useKUNGalgameMessageStore } from './modules/message'
// Import non-moe records store
import { useKUNGalgameNonMoeStore } from './modules/nonMoe'
// Import ranking store
import { useKUNGalgameRankingStore } from './modules/ranking'
// Import website settings panel store
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
// Import store for the topic detail page
import { usePersistKUNGalgameTopicStore } from '@/store/modules/topic/topic'
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
const store = createPinia()
// Function to set up Pinia, to be called in main.ts
export function setupKUNGalgamePinia(app: App<Element>) {
store.use(piniaPluginPersistedstate)
app.use(store)
}
// Reset all stores, used for logging out
export function kungalgameStoreReset() {
const balanceStore = useKUNGalgameBalanceStore()
const editStore = useKUNGalgameEditStore()
const homeStore = usePersistKUNGalgameHomeStore()
const homeStore = useKUNGalgameHomeStore()
const userStore = useKUNGalgameUserStore()
const messageStore = useKUNGalgameMessageStore()
const nonMoeStore = useKUNGalgameNonMoeStore()
const rankingStore = useKUNGalgameRankingStore()
const settingsStore = useKUNGalgameSettingsStore()
const topicStore = usePersistKUNGalgameTopicStore()
const replyStore = usePersistKUNGalgameReplyStore()
balanceStore.$reset()
editStore.$reset()
homeStore.$reset()
userStore.$reset()
messageStore.$reset()
nonMoeStore.$reset()
rankingStore.$reset()
settingsStore.$reset()
topicStore.$reset()
replyStore.$reset()

View file

@ -15,8 +15,8 @@ interface BalanceStore {
expenditure: BalanceExpenditureRequestData
}
export const useTempBalanceStore = defineStore({
id: 'tempBalance',
export const useKUNGalgameBalanceStore = defineStore({
id: 'KUNGalgameBalance',
persist: false,
state: (): BalanceStore => ({
income: {

View file

@ -1,19 +1,20 @@
import { defineStore } from 'pinia'
import { postNewTopicApi, getTopTagsApi } from '@/api'
import { postNewTopicApi, updateNewTopicApi, getTopTagsApi } from '@/api'
import {
EditCreateTopicRequestData,
EditCreateTopicResponseData,
EditUpdateTopicRequestData,
EditUpdateTopicResponseData,
EditGetHotTagsRequestData,
EditGetHotTagsResponseData,
} from '@/api'
import { EditStore } from '../types/edit'
import { checkTopicPublish } from '../utils/checkTopicPublish'
import type { EditStorePersist } from '../types/edit'
export const useKUNGalgameEditStore = defineStore({
id: 'KUNGalgameEdit',
persist: true,
state: (): EditStorePersist => ({
state: (): EditStore => ({
editorHeight: 300,
textCount: 0,
@ -23,6 +24,16 @@ export const useKUNGalgameEditStore = defineStore({
category: [],
isShowHotKeywords: true,
isSaveTopic: false,
topicRewrite: {
tid: 0,
title: '',
content: '',
tags: [],
category: [],
isTopicRewriting: false,
},
}),
getters: {},
actions: {
@ -44,6 +55,24 @@ export const useKUNGalgameEditStore = defineStore({
return await postNewTopicApi(requestData)
},
// Update a topic
async rewriteTopic(): Promise<EditUpdateTopicResponseData | undefined> {
const requestData: EditUpdateTopicRequestData = {
tid: this.topicRewrite.tid,
title: this.topicRewrite.title,
content: this.topicRewrite.content,
tags: this.topicRewrite.tags,
category: this.topicRewrite.category,
}
// If the topic data is invalid, return directly
if (!checkTopicPublish(this.textCount, requestData)) {
return
}
return await updateNewTopicApi(requestData)
},
// Get popular tags
async getHotTags(limit: number): Promise<EditGetHotTagsResponseData> {
const requestData: EditGetHotTagsRequestData = { limit }
@ -53,7 +82,6 @@ export const useKUNGalgameEditStore = defineStore({
// Reset topic draft data for publishing
resetTopicData() {
this.textCount = 0
this.title = ''
this.content = ''
this.tags = []
@ -61,5 +89,16 @@ export const useKUNGalgameEditStore = defineStore({
this.isSaveTopic = false
},
// Reset data for re-editing a topic
resetRewriteTopicData() {
this.textCount = 0
this.topicRewrite.title = ''
this.topicRewrite.content = ''
this.topicRewrite.tags = []
this.topicRewrite.category = []
this.topicRewrite.isTopicRewriting = false
},
},
})

View file

@ -1,12 +1,61 @@
/* Home Page Store */
import { defineStore } from 'pinia'
import type { HomeStorePersist } from '../types/home'
// API
import { getHomeTopicApi } from '@/api/index'
// Data interface types
import { HomeTopicRequestData, HomeTopicResponseData } from '@/api/index'
export const usePersistKUNGalgameHomeStore = defineStore({
// Home store type
import { HomeStore } from '../types/home'
export const useKUNGalgameHomeStore = defineStore({
id: 'KUNGalgameHome',
persist: true,
state: (): HomeStorePersist => ({
state: (): HomeStore => ({
// Search box store
/**
* @param {String} keywords - Search keywords, default to all if not provided
* @param {Array} category - Topic categories, currently there are three: Galgame, Technique, Others
* @param {Number} page - Page number for pagination
* @param {Number} limit - Number of data per page
* @param {String} sortField - Field to sort by
* @param {String} sortOrder - Sorting order, can be 'asc' or 'desc'
* @returns {HomeTopicResponseData} topicData
*/
keywords: '',
category: '',
page: 1,
limit: 17,
sortField: 'updated',
sortOrder: 'desc',
isLoading: true,
// Other store
// Whether to activate the left interaction panel on the main page
isActiveMainPageAside: true,
// Search history storage
searchHistory: [],
}),
getters: {},
actions: {
// Get home topics
async getHomeTopic(): Promise<HomeTopicResponseData> {
// The values here are used for initialization
const requestData: HomeTopicRequestData = {
keywords: this.keywords,
category: this.category,
page: this.page,
limit: this.limit,
sortField: this.sortField,
sortOrder: this.sortOrder,
}
return await getHomeTopicApi(requestData)
},
// Reset page number and loading status for sorting to take effect
resetPageStatus() {
this.page = 1
this.isLoading = true
},
},
})

View file

@ -51,8 +51,10 @@ import {
updateUserPasswordByEmailApi,
} from '@/api'
import type { KUNGalgamerStore } from '../types/kungalgamer'
// KUNGalgamer store type
import { KUNGalgamerStore } from '../types/kungalgamer'
// Here, pinia-plugin-persistedstate is used, so storage is automatic
export const useKUNGalgameUserStore = defineStore({
id: 'KUNGalgameUser',
persist: true,
@ -107,6 +109,10 @@ export const useKUNGalgameUserStore = defineStore({
res.data.roles
)
this.setToken(res.data.token)
} else if (res.code === 500) {
console.log(res.message)
} else {
throw new Error('500 Server ERROR')
}
return res
},

View file

@ -3,8 +3,8 @@ import { defineStore } from 'pinia'
// Type of message store
import { MessageStore } from '../types/message'
export const useTempMessageStore = defineStore({
id: 'tempMessage',
export const useKUNGalgameMessageStore = defineStore({
id: 'KUNGalgameMessage',
// No need to persist any message components
persist: false,
state: (): MessageStore => ({

View file

@ -4,8 +4,8 @@ import type { NonMoeLogRequestData, NonMoeGetLogsResponseData } from '@/api'
import { getNonMoeLogsApi } from '@/api'
export const useTempNonMoeStore = defineStore({
id: 'tempNonMoe',
export const useKUNGalgameNonMoeStore = defineStore({
id: 'KUNGalgameNonMoe',
persist: false,
state: (): NonMoeLogRequestData => ({
page: 1,

View file

@ -14,8 +14,8 @@ interface RankingStore {
user: RankingGetUserRequestData
}
export const useTempRankingStore = defineStore({
id: 'tempRanking',
export const useKUNGalgameRankingStore = defineStore({
id: 'KUNGalgameRanking',
persist: false,
state: (): RankingStore => ({
topic: {

View file

@ -1,14 +1,16 @@
import { defineStore } from 'pinia'
import { postReplyByTidApi } from '@/api'
// Replies
import { postReplyByPidApi, updateReplyApi } from '@/api'
import type {
TopicCreateReplyRequestData,
TopicCreateReplyResponseData,
TopicUpdateReplyRequestData,
TopicUpdateReplyResponseData,
} from '@/api'
import { checkReplyPublish } from '@/store/utils/checkReplyPublish'
import type { ReplyStorePersist } from '@/store/types/topic/reply'
import { ReplyStorePersist } from '@/store/types/topic/reply'
export const usePersistKUNGalgameReplyStore = defineStore({
id: 'KUNGalgameReply',
@ -18,9 +20,10 @@ export const usePersistKUNGalgameReplyStore = defineStore({
isSaveReply: false,
isShowHotKeywords: true,
editorHeight: 200,
textCount: 0,
// Whether the reply is being rewritten
isReplyRewriting: false,
replyDraft: {
tid: 0,
@ -30,10 +33,17 @@ export const usePersistKUNGalgameReplyStore = defineStore({
tags: [],
toFloor: 0,
},
replyRewrite: {
tid: 0,
rid: 0,
content: '',
tags: [],
edited: 0,
},
}),
actions: {
// Create a new reply
async postNewReply(): Promise<TopicCreateReplyResponseData | undefined> {
async postNewReply(): Promise<TopicCreateReplyResponseData> {
// The values here are used to initialize the reply
const requestData: TopicCreateReplyRequestData = {
tid: this.replyDraft.tid,
@ -41,20 +51,23 @@ export const usePersistKUNGalgameReplyStore = defineStore({
to_floor: this.replyDraft.toFloor,
tags: this.replyDraft.tags,
content: this.replyDraft.content,
time: Date.now(),
}
return await postReplyByPidApi(requestData)
},
if (!checkReplyPublish(requestData.tags, requestData.content)) {
return
// Update a reply
async updateReply(): Promise<TopicUpdateReplyResponseData> {
const requestData: TopicUpdateReplyRequestData = {
tid: this.replyRewrite.tid,
rid: this.replyRewrite.rid,
content: this.replyRewrite.content,
tags: this.replyRewrite.tags,
}
return await postReplyByTidApi(requestData)
return await updateReplyApi(requestData)
},
// Reset reply draft to its original value, used for the reply publish button
resetReplyDraft() {
this.textCount = 0
this.replyDraft.tid = 0
this.replyDraft.toUserName = ''
this.replyDraft.toUid = 0
@ -64,5 +77,15 @@ export const usePersistKUNGalgameReplyStore = defineStore({
this.isSaveReply = false
},
// Reset data for re-editing a reply
resetRewriteReplyData() {
this.replyRewrite.tid = 0
this.replyRewrite.rid = 0
this.replyRewrite.content = ''
this.replyRewrite.tags = []
this.isReplyRewriting = false
},
},
})

View file

@ -1,57 +0,0 @@
import { defineStore } from 'pinia'
import { updateNewTopicApi } from '@/api'
import type {
EditUpdateTopicRequestData,
EditUpdateTopicResponseData,
} from '@/api'
import { checkTopicPublish } from '@/store/utils/checkTopicPublish'
import { EditStoreTemp } from '@/store/types/edit'
export const useTempEditStore = defineStore({
id: 'tempEdit',
persist: false,
state: (): EditStoreTemp => ({
tid: 0,
title: '',
content: '',
tags: [],
category: [],
textCount: 0,
isTopicRewriting: false,
}),
actions: {
// Update a topic
async rewriteTopic(): Promise<EditUpdateTopicResponseData | undefined> {
const requestData: EditUpdateTopicRequestData = {
tid: this.tid,
title: this.title,
content: this.content,
tags: this.tags,
category: this.category,
edited: Date.now(),
}
// If the topic data is invalid, return directly
if (!checkTopicPublish(this.textCount, requestData)) {
return
}
return await updateNewTopicApi(requestData)
},
// Reset data for re-editing a topic
resetRewriteTopicData() {
this.textCount = 0
this.title = ''
this.content = ''
this.tags = []
this.category = []
this.isTopicRewriting = false
},
},
})

View file

@ -1,72 +0,0 @@
import { defineStore } from 'pinia'
import { getHomeSearchTopicApi, getHomeTopicApi } from '@/api/index'
import type {
HomeSearchTopicRequestData,
HomeSearchTopicResponseData,
HomeTopicRequestData,
HomeTopicResponseData,
} from '@/api/index'
import type { HomeStoreTemp } from '../types/home'
export const useTempHomeStore = defineStore({
id: 'tempHome',
persist: false,
state: (): HomeStoreTemp => ({
search: {
keywords: '',
category: ['Galgame', 'Technique', 'Others'],
page: 1,
limit: 7,
sortField: 'updated',
sortOrder: 'desc',
isLoading: true,
},
topic: {
category: ['Galgame'],
page: 1,
limit: 17,
sortField: 'updated',
sortOrder: 'desc',
isLoading: true,
},
isShowSearch: false,
}),
getters: {},
actions: {
async searchTopic(): Promise<HomeSearchTopicResponseData> {
const requestData: HomeSearchTopicRequestData = {
keywords: this.search.keywords.trim().slice(0, 40),
category: JSON.stringify(this.search.category),
page: this.search.page,
limit: this.search.limit,
sortField: this.search.sortField,
sortOrder: this.search.sortOrder,
}
return await getHomeSearchTopicApi(requestData)
},
async getHomeTopic(): Promise<HomeTopicResponseData> {
const requestData: HomeTopicRequestData = {
category: JSON.stringify(this.topic.category),
page: this.topic.page,
limit: this.topic.limit,
sortField: this.topic.sortField,
sortOrder: this.topic.sortOrder,
}
return await getHomeTopicApi(requestData)
},
resetSearchStatus() {
this.search.page = 1
this.search.limit = 7
this.search.isLoading = true
},
resetHomePageStatus() {
this.topic.page = 1
this.topic.limit = 17
this.topic.isLoading = true
},
},
})

View file

@ -1,36 +0,0 @@
import { defineStore } from 'pinia'
import { getPoolTopicApi } from '@/api'
import type { PoolTopicsRequestData, PoolTopicResponseData } from '@/api'
import type { PoolStoreTemp } from '@/store/types/pool'
export const useTempPoolStore = defineStore({
id: 'tempPool',
persist: false,
state: (): PoolStoreTemp => ({
page: 1,
limit: 10,
sortField: 'time',
sortOrder: 'desc',
isScrollToTop: false,
}),
actions: {
async getTopics(): Promise<PoolTopicResponseData> {
const requestData: PoolTopicsRequestData = {
page: this.page,
limit: this.limit,
sortField: this.sortField,
sortOrder: this.sortOrder,
}
return await getPoolTopicApi(requestData)
},
resetPageStatus() {
this.page = 1
this.limit = 10
},
},
})

View file

@ -1,37 +0,0 @@
import { defineStore } from 'pinia'
import { getTechniqueTopicApi } from '@/api'
import type {
TechniqueTopicsRequestData,
TechniqueTopicResponseData,
} from '@/api'
import type { TechniqueStoreTemp } from '@/store/types/technique'
export const useTempTechniqueStore = defineStore({
id: 'tempTechnique',
persist: false,
state: (): TechniqueStoreTemp => ({
page: 1,
limit: 10,
sortField: 'time',
sortOrder: 'desc',
}),
actions: {
async getTopics(): Promise<TechniqueTopicResponseData> {
const requestData: TechniqueTopicsRequestData = {
page: this.page,
limit: this.limit,
sortField: this.sortField,
sortOrder: this.sortOrder,
}
return await getTechniqueTopicApi(requestData)
},
resetPageStatus() {
this.page = 1
this.limit = 10
},
},
})

View file

@ -1,11 +1,10 @@
import { defineStore } from 'pinia'
// Replies
import {
getRepliesByTidApi,
getRepliesByPidApi,
updateReplyUpvoteApi,
updateReplyLikeApi,
updateReplyDislikeApi,
updateReplyApi,
} from '@/api'
import type {
@ -17,11 +16,8 @@ import type {
TopicLikeReplyResponseData,
TopicDislikeReplyRequestData,
TopicDislikeReplyResponseData,
TopicUpdateReplyRequestData,
TopicUpdateReplyResponseData,
} from '@/api'
import { checkReplyPublish } from '@/store/utils/checkReplyPublish'
import type { ReplyStoreTemp } from '@/store/types/topic/reply'
export const useTempReplyStore = defineStore({
@ -32,9 +28,8 @@ export const useTempReplyStore = defineStore({
isEdit: false,
isScrollToTop: false,
isLoading: true,
// Reply ID starts from 0, -1 is just for monitoring data changes
scrollToReplyId: -1,
isReplyRewriting: false,
replyRequest: {
page: 1,
@ -43,20 +38,13 @@ export const useTempReplyStore = defineStore({
sortOrder: 'asc',
},
replyRewrite: {
tid: 0,
rid: 0,
content: '',
tags: [],
edited: 0,
},
tempReply: {
rid: 0,
tid: 0,
// Floor where the reply is located
floor: 0,
// Floor where the replied reply is located
to_floor: 0,
r_user: {
uid: 0,
name: '',
@ -91,7 +79,7 @@ export const useTempReplyStore = defineStore({
sortField: this.replyRequest.sortField || 'floor',
sortOrder: this.replyRequest.sortOrder || 'desc',
}
return await getRepliesByTidApi(requestData)
return await getRepliesByPidApi(requestData)
},
// Upvote a reply
@ -104,7 +92,6 @@ export const useTempReplyStore = defineStore({
tid: tid,
to_uid: toUid,
rid: rid,
time: Date.now(),
}
return await updateReplyUpvoteApi(requestData)
},
@ -140,37 +127,5 @@ export const useTempReplyStore = defineStore({
}
return await updateReplyDislikeApi(requestData)
},
// Update a reply
async updateReply(): Promise<TopicUpdateReplyResponseData | undefined> {
const requestData: TopicUpdateReplyRequestData = {
tid: this.replyRewrite.tid,
rid: this.replyRewrite.rid,
content: this.replyRewrite.content,
tags: this.replyRewrite.tags,
edited: Date.now(),
}
if (!checkReplyPublish(requestData.tags, requestData.content)) {
return
}
return await updateReplyApi(requestData)
},
// Reset data for re-editing a reply
resetRewriteReplyData() {
this.replyRewrite.tid = 0
this.replyRewrite.rid = 0
this.replyRewrite.content = ''
this.replyRewrite.tags = []
this.isReplyRewriting = false
},
resetPageStatus() {
this.replyRequest.page = 1
this.isLoading = true
},
},
})

View file

@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
// Topics
import {
getTopicByTidApi,
getRelatedTopicsByTagsApi,
@ -53,7 +54,6 @@ export const useTempTopicStore = defineStore({
const requestData: TopicUpvoteTopicRequestData = {
tid: tid,
to_uid: toUid,
time: Date.now(),
}
return await updateTopicUpvoteApi(requestData)
},

View file

@ -1,26 +1,45 @@
export interface EditStorePersist {
// Ahahaha, I just want to use "rewrite" in the name to tease you
interface TopicRewrite {
// Topic ID
tid: number
// Topic title
title: string
// Topic content
content: string
// Topic tags
tags: Array<string>
// Topic category
category: Array<string>
// Whether the topic is being rewritten
isTopicRewriting: boolean
}
export interface EditStore {
editorHeight: number
textCount: number
/**
* Topic related
* @param {string} title - Topic title
* @param {string} content - Topic content (rich text)
* @param {Array<string>} tags - Topic tags
* @param {Array<string>} category - Topic category
* @param {boolean} isSave - Whether to save the topic draft
*/
// Topic title
title: string
// Topic content
content: string
// Topic tags
tags: Array<string>
// Topic category
category: Array<string>
// Whether to display hot keywords
isShowHotKeywords: boolean
// Whether to save the topic
isSaveTopic: boolean
}
export interface EditStoreTemp {
tid: number
title: string
content: string
tags: Array<string>
category: Array<string>
textCount: number
// Whether the topic is being rewritten
isTopicRewriting: boolean
/* Rewrite a topic */
topicRewrite: TopicRewrite
}

View file

@ -1,34 +1,13 @@
interface HomeSearchTemp {
export interface HomeStore {
keywords: string
category: string[]
category: string
page: number
limit: number
sortField: string
sortOrder: string
// Whether to continue loading after it's done
isLoading: boolean
}
export interface HomeTopicTemp {
category: string[]
page: number
limit: number
sortField: string
sortOrder: string
// Whether to continue loading after it's done
isLoading: boolean
}
export interface HomeStoreTemp {
search: HomeSearchTemp
topic: HomeTopicTemp
isShowSearch: boolean
}
export interface HomeStorePersist {
// Other stores
// Whether to activate the left interactive panel of the main page
isActiveMainPageAside: boolean

View file

@ -1,8 +0,0 @@
export interface PoolStoreTemp {
page: number
limit: number
sortField: string
sortOrder: string
isScrollToTop: boolean
}

View file

@ -1,6 +0,0 @@
export interface TechniqueStoreTemp {
page: number
limit: number
sortField: string
sortOrder: string
}

View file

@ -42,10 +42,7 @@ export interface ReplyStoreTemp {
// Reply ID to scroll to
scrollToReplyId: number
isReplyRewriting: boolean
replyRequest: ReplyRequest
replyRewrite: ReplyRewrite
tempReply: TopicReply
tempReplyRewrite: ReplyRewriteTemp
}
@ -55,9 +52,10 @@ export interface ReplyStorePersist {
isSaveReply: boolean
isShowHotKeywords: boolean
editorHeight: number
textCount: number
isReplyRewriting: boolean
replyDraft: ReplyDraft
replyRewrite: ReplyRewrite
}

View file

@ -1,35 +0,0 @@
import Message from '@/components/alert/Message'
export const checkReplyPublish = (tags: string[], content: string) => {
if (tags.length > 7) {
Message('Reply with a maximum of 7 tags', '回复最多 7 个标签', 'warn')
return false
}
for (const tag of tags) {
if (tag.length > 17) {
Message(
'Single tag maximum length is 17 characters',
'单个标签最长 17 个字符',
'warn'
)
return false
}
}
if (!content.trim()) {
Message('Reply content cannot be empty', '回复内容不可为空', 'warn')
return false
}
if (content.length > 10007) {
Message(
'Reply maximum length is 10007 characters',
'回复内容最大长度为 10007 个字符',
'warn'
)
return false
}
return true
}

View file

@ -29,29 +29,14 @@ export const checkTopicPublish = (
return false
}
if (topicData.title.trim().length > 40) {
// If the title is empty, show a warning
Message(
'Title maximum length is 40 characters!',
'标题最大长度为 40 个字符!',
'warn'
)
return false
}
// Check content character count
if (!textCount) {
// If the content is empty, show a warning
Message('Content cannot be empty!', '内容不可为空!', 'warn')
return false
}
if (textCount > 100007) {
Message(
'Content maximum length is 100007!',
'内容最大长度为100007',
'warn'
)
Message('Content max length is 100007!', '内容最大长度为100007', 'warn')
return false
}
@ -61,22 +46,6 @@ export const checkTopicPublish = (
return false
}
if (topicData.tags.length > 7) {
Message('Reply with a maximum of 7 tags', '回复最多 7 个标签', 'warn')
return false
}
for (const tag of topicData.tags) {
if (tag.length > 17) {
Message(
'Single tag maximum length is 17 characters',
'单个标签最长 17 个字符',
'warn'
)
return false
}
}
// Check category
if (!topicData.category.length) {
Message(
@ -87,15 +56,6 @@ export const checkTopicPublish = (
return false
}
if (topicData.category.length > 2) {
Message(
'Topic with a maximum of 2 categories!',
'最多选择两个分类!',
'warn'
)
return false
}
// If all checks pass, return true
return true
}

View file

@ -62,11 +62,3 @@ button {
); /* Background color of selected text */
color: var(--kungalgame-blue-4); /* Text color of selected text */
}
/* Milkdown */
p code {
background-color: var(--kungalgame-trans-blue-2);
padding: 2px 7px;
font-size: 13px;
border-radius: 7px;
}

49
src/types/pool/tags.ts Normal file
View file

@ -0,0 +1,49 @@
// Temp data
interface tag {
index: number
name: string
}
export const tags: tag[] = [
{
index: 1,
name: '啊这可海星',
},
{
index: 2,
name: '啊这可海星',
},
{
index: 3,
name: '啊这可海星',
},
{
index: 4,
name: '啊这可海星啊这可海星',
},
{
index: 5,
name: '啊这可海星',
},
{
index: 6,
name: '啊这可海星啊这可海星',
},
{
index: 7,
name: '啊这可海星',
},
{
index: 8,
name: '啊这可海星啊这可',
},
{
index: 9,
name: '啊这可海星',
},
{
index: 10,
name: '啊这可海星',
},
]

13
src/utils/cookie.ts Normal file
View file

@ -0,0 +1,13 @@
import Cookies from 'js-cookie'
export const getToken = () => {
return Cookies.get('kungalgame-moemoe-access-token')
}
export const setToken = (token: string) => {
Cookies.set('kungalgame-moemoe-access-token', token)
}
export const removeToken = () => {
Cookies.remove('kungalgame-moemoe-access-token')
}

View file

@ -1,16 +1,20 @@
export const debounce = <F extends (...args: any[]) => any>(
fn: F,
time: number
): ((...args: Parameters<F>) => void) => {
let timeoutID: NodeJS.Timeout | null = null
/*
* Debounce function that takes a function and a delay time as parameters
*/
export type DebouncedFunction<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => void
return function (...args: Parameters<F>) {
if (timeoutID !== null) {
clearTimeout(timeoutID)
}
export function debounce<T extends (...args: any[]) => any>(
func: T,
delay: number
): DebouncedFunction<T> {
let timeoutId: ReturnType<typeof setTimeout>
timeoutID = setTimeout(() => {
fn(...args)
}, time)
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
func(...args)
}, delay)
}
}

View file

@ -37,7 +37,7 @@ const languageOptions = {
},
}
const replaceTimeUnits = (input: string, language: string) => {
function replaceTimeUnits(input: string, language: string) {
const languageOption =
(languageOptions as Record<string, any>)[language] || languageOptions.en
@ -66,7 +66,7 @@ const replaceTimeUnits = (input: string, language: string) => {
}
// Format time difference
export const formatTimeDifference = (pastTime: number, language: string) => {
export function formatTimeDifference(pastTime: number, language: string) {
const now = dayjs()
const diffInSeconds = now.diff(pastTime, 'second')
const hint = language === 'en' ? ' ago' : '前'

View file

@ -1,10 +0,0 @@
import dayjs from 'dayjs'
import 'dayjs/locale/en'
dayjs.locale('en')
export const formatTimeI18n = (time: number) => {
const formattedENDate = dayjs(time).format('MMMM D, YYYY - h:mm:ss A')
const formattedCNDate = dayjs(time).format('YYYY年MM月DD日-HH:mm:ss 发布')
return { formattedENDate, formattedCNDate }
}

12
src/utils/getPlainText.ts Normal file
View file

@ -0,0 +1,12 @@
export function getPlainText(html: string): string {
// Remove HTML tags
const plainText = html.replace(/<[^>]*>/g, '')
// Decode HTML entities
const textWithEntitiesDecoded = new DOMParser().parseFromString(
plainText,
'text/html'
).body.textContent
return textWithEntitiesDecoded ? textWithEntitiesDecoded : ''
}

View file

@ -1,10 +0,0 @@
export const markdownToText = (markdown: string) => {
return markdown
.replace(/(\*\*|__)(.*?)\1/gs, '$2')
.replace(/(\*|_)(.*?)\1/gs, '$2')
.replace(/#+\s*(.*?)\n/g, '$1\n')
.replace(/!?\[(.*?)\]\(.*?\)/gs, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/```[\s\S]+?```/gs, '')
.replace(/[<>~\\]/g, '')
}

View file

@ -1,9 +1,12 @@
// Using the user store
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { requestRefresh } from './requestRefresh'
// Error handling function
import { onRequestError } from '@/error/onRequestError'
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
const successResponseArray = [200, 201, 202, 204, 205, 206]
export type FetchOptions = {
method: HttpMethod
credentials: 'include'
@ -11,6 +14,7 @@ export type FetchOptions = {
body?: BodyInit
}
// Fetch request function
const kunFetchRequest = async <T>(
url: string,
options: FetchOptions
@ -18,6 +22,7 @@ const kunFetchRequest = async <T>(
const baseUrl = import.meta.env.VITE_API_BASE_URL
const fullUrl = `${baseUrl}${url}`
// Add the token to the request headers
const headers = {
...options.headers,
Authorization: `Bearer ${useKUNGalgameUserStore().getToken()}`,
@ -25,18 +30,15 @@ const kunFetchRequest = async <T>(
const response = await fetch(fullUrl, { ...options, headers })
if (response.status === 205) {
const newResponseData = await requestRefresh(fullUrl, options)
const data: T = await newResponseData.json()
return data
} else if (response.status === 233 || !response.ok) {
// Handle some known backend error
// If not 20X, then throw an error
if (!successResponseArray.includes(response.status)) {
// Handle errors, such as token expiration
await onRequestError(response)
return {} as T
} else {
const data: T = await response.json()
return data
throw new Error('KUNGalgame Fetch Error occurred, but no problem')
}
const data: T = await response.json()
return data
}
const fetchGet = async <T>(

View file

@ -1,23 +0,0 @@
import Message from '@/components/alert/Message'
import router from '@/router'
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { generateTokenByRefreshTokenApi } from '@/api'
import type { FetchOptions } from './request'
export const requestRefresh = async (
fullUrl: string,
options: FetchOptions
) => {
const accessTokenResponse = await generateTokenByRefreshTokenApi()
useKUNGalgameUserStore().setToken(accessTokenResponse.data.token)
const headers = {
...options.headers,
Authorization: `Bearer ${useKUNGalgameUserStore().getToken()}`,
}
const response = await fetch(fullUrl, { ...options, headers })
return response
}

View file

@ -1,30 +1,29 @@
export const isValidTimestamp = (timestamp: number) => {
return (
timestamp.toString().length === 10 || timestamp.toString().length === 13
)
}
// Regular expression to match a valid URL
export const isValidURL = (url: string) => {
const regex =
/^(https?|http):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|net|org|biz|moe|info|name|pro|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return regex.test(url)
}
// Regular expression to match a valid email
export const isValidEmail = (email: string) => {
const regex = /^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{1,24}$/
return regex.test(email)
}
// Match a username of 1 to 17 characters containing Chinese, English, Japanese, numbers, underscores, or tildes
export const isValidName = (name: string) => {
const regex = /^[\p{L}\p{N}~_]{1,17}$/u
return regex.test(name)
}
// Regular expression to match a password of 6 to 107 characters, containing at least one letter and one number, and optionally including special characters \w!@#$%^&*()-+=
export const isValidPassword = (pwd: string) => {
const regex = /^(?=.*[a-zA-Z])(?=.*[0-9])[\w!@#$%^&*()-+=]{6,107}$/
return regex.test(pwd)
}
// Regular expression to match a 7-character alphanumeric email confirmation code
export const isValidMailConfirmCode = (code: string) => {
const regex = /^[a-zA-Z0-9]{7}$/
return regex.test(code)

View file

@ -16,10 +16,4 @@ import MainPageFooter from './footer/MainPageFooter.vue'
display: flex;
flex-direction: column;
}
@media (max-width: 700px) {
.root {
height: 100vh;
}
}
</style>

View file

@ -38,7 +38,7 @@ const mainPageWidth = computed(() => {
.content-container {
width: v-bind(mainPageWidth);
transition: width 0.2s;
transition: all 0.2s;
height: 100%;
margin: 0 auto;
display: flex;
@ -58,6 +58,7 @@ const mainPageWidth = computed(() => {
.content-container {
width: 80%;
border: none;
background-color: var(--kungalgame-trans-white-9);
}
}
@ -69,6 +70,7 @@ const mainPageWidth = computed(() => {
.content-container {
width: 100%;
border: none;
background-color: var(--kungalgame-trans-white-9);
}
}
</style>

View file

@ -22,6 +22,7 @@ import ArticleContent from './components/ArticleContent.vue'
.article-container {
width: 100%;
height: 100%;
border: 1px solid var(--kungalgame-trans-blue-4);
border-radius: 5px;
display: flex;
flex-direction: column;

View file

@ -6,66 +6,86 @@ import { hourDiff } from '@/utils/time'
import { HomeTopic } from '@/api'
import { useTempHomeStore } from '@/store/temp/home'
// Import the homepage store
import { useKUNGalgameHomeStore } from '@/store/modules/home'
import { storeToRefs } from 'pinia'
const { topic } = storeToRefs(useTempHomeStore())
const homeTopics = ref<HomeTopic[]>([])
const content = ref<HTMLElement>()
const getTopics = async (): Promise<HomeTopic[]> => {
return (await useTempHomeStore().getHomeTopic()).data
}
watch(
() => [topic.value.category, topic.value.sortField, topic.value.sortOrder],
async () => {
homeTopics.value = await getTopics()
}
const { page, keywords, sortField, sortOrder, isLoading } = storeToRefs(
useKUNGalgameHomeStore()
)
const scrollHandler = async () => {
if (isScrollAtBottom() && topic.value.isLoading) {
topic.value.page++
// Define reactive topic data in the component
const topics = ref<HomeTopic[]>([])
// Page container for calculating whether it has reached the bottom
const content = ref<HTMLElement>()
// Function to get page topics
const getTopics = async (): Promise<HomeTopic[]> => {
return (await useKUNGalgameHomeStore().getHomeTopic()).data
}
// Call fetchTopics to get topic data (watch is great!)
watch([keywords, sortField, sortOrder], async () => {
topics.value = await getTopics()
})
// Scroll event handler
const scrollHandler = async () => {
// Handling logic when scrolling to the bottom
if (isScrollAtBottom() && isLoading.value) {
// Automatically increment the page number
page.value++
// Get the topics for the next page
const lazyLoadTopics = await getTopics()
// Check if data has already been loaded, if so, no need to load more
if (!lazyLoadTopics.length) {
topic.value.isLoading = false
isLoading.value = false
}
homeTopics.value = [...homeTopics.value, ...lazyLoadTopics]
// Append the newly loaded reply data to the existing reply data
topics.value = [...topics.value, ...lazyLoadTopics]
}
}
// Check if it has scrolled to the bottom
const isScrollAtBottom = () => {
if (content.value) {
const scrollHeight = content.value.scrollHeight
const scrollTop = content.value.scrollTop
const clientHeight = content.value.clientHeight
// Compare with a margin of error, as JavaScript floating-point numbers are not precise
// Why 1007? Because I got KUN san on October 7th, ahahaha
const errorMargin = 1.007
return Math.abs(scrollHeight - scrollTop - clientHeight) < errorMargin
}
}
onBeforeMount(async () => {
useTempHomeStore().resetHomePageStatus()
// Reset page number, loading status, etc. before mounting
useKUNGalgameHomeStore().resetPageStatus()
})
// Add a scroll event listener after the component is mounted
onMounted(async () => {
// Get a reference to the scrolling element
const element = content.value
// If the element is found, start the listener to track scroll behavior
if (element) {
element.addEventListener('scroll', scrollHandler)
}
homeTopics.value = await getTopics()
// Load topics for the first time
topics.value = await getTopics()
})
// Remove the scroll event listener before the component is unmounted
onBeforeUnmount(() => {
const element = content.value
// If the page element is found, remove the listener
if (element) {
element.removeEventListener('scroll', scrollHandler)
}
@ -74,9 +94,10 @@ onBeforeUnmount(() => {
<template>
<div class="topic-container" ref="content">
<TransitionGroup name="list" tag="div" v-if="homeTopics.length">
<TransitionGroup name="list" tag="div" v-if="topics.length">
<!-- Posted within 10 hours -->
<div
v-for="topic in homeTopics"
v-for="topic in topics"
:key="topic.tid"
:class="
hourDiff(topic.upvote_time, 10) ? 'kungalgame-comet-surround' : ''
@ -92,9 +113,9 @@ onBeforeUnmount(() => {
</TransitionGroup>
<!-- Skeleton -->
<HomeTopicSkeleton :count="7" v-if="!homeTopics.length" />
<HomeTopicSkeleton :count="7" v-if="!topics.length" />
<HomeTopicSkeleton v-if="topic.isLoading && homeTopics.length >= 16" />
<HomeTopicSkeleton v-if="isLoading && topics.length >= 16" />
</div>
</template>

View file

@ -1,5 +1,4 @@
<script setup lang="ts">
import { computed } from 'vue'
import UserPart from './UserPart.vue'
import TopicPart from './TopicPart.vue'
@ -8,16 +7,14 @@ import { HomeTopic } from '@/api'
const props = defineProps<{
topic: HomeTopic
}>()
const topic = computed(() => props.topic)
</script>
<template>
<div class="topic">
<UserPart :user="topic.user" />
<UserPart :user="props.topic.user" />
<RouterLink :to="`/topic/${topic.tid}`">
<TopicPart :topic="topic" />
<RouterLink :to="`/topic/${props.topic.tid}`">
<TopicPart :topic="props.topic" />
</RouterLink>
</div>
</template>
@ -25,7 +22,7 @@ const topic = computed(() => props.topic)
<style lang="scss" scoped>
.topic {
width: 100%;
height: 77px;
height: 74px;
flex-shrink: 0;
border-radius: 3px;
background-color: var(--kungalgame-trans-blue-0);

View file

@ -3,12 +3,13 @@ import { computed } from 'vue'
import { Icon } from '@iconify/vue'
import { formatTimeDifference } from '@/utils/formatTime'
import { markdownToText } from '@/utils/markdownToText'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
const settingsStore = storeToRefs(useKUNGalgameSettingsStore())
import { getPlainText } from '@/utils/getPlainText'
import { HomeTopic } from '@/api'
const props = defineProps<{
@ -30,6 +31,8 @@ const {
popularity,
} = props.topic
const plainText = getPlainText(content)
const getRepliesCount = computed(() => {
return repliesCount + comments
})
@ -72,7 +75,7 @@ const getRepliesCount = computed(() => {
<div class="introduction">
<p>
{{ markdownToText(content) }}
{{ plainText }}
</p>
</div>
</div>
@ -120,7 +123,7 @@ const getRepliesCount = computed(() => {
font-size: smaller;
li {
margin-right: 5px;
margin-left: 5px;
display: flex;
align-items: center;
cursor: pointer;
@ -147,6 +150,7 @@ const getRepliesCount = computed(() => {
width: 100%;
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
@ -160,10 +164,4 @@ const getRepliesCount = computed(() => {
color: var(--kungalgame-font-color-2);
}
}
@media (max-width: 700px) {
.time {
display: none;
}
}
</style>

View file

@ -1,58 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Icon } from '@iconify/vue'
import KUNGalgameSearchBox from '@/components/KUNGalgameSearchBox.vue'
import SortTopic from './SortTopic.vue'
import { useTempHomeStore } from '@/store/temp/home'
import { storeToRefs } from 'pinia'
import { categoryItem } from './navItem'
const { topic } = storeToRefs(useTempHomeStore())
const categoryIcon = ref('galgame')
const handleSortByCategory = (name: string) => {
useTempHomeStore().resetHomePageStatus()
topic.value.category = []
categoryIcon.value = name
// Because category is [Galgame, Technique, Others], need to capitalize first letter
const capitalizeFirstLetter = name.charAt(0).toUpperCase() + name.slice(1)
topic.value.category.push(capitalizeFirstLetter)
}
const iconMap: Record<string, string> = {
galgame: 'icon-park-outline:game',
technique: 'mingcute:tool-line',
others: 'basil:other-1-outline',
}
const category = ['Galgame']
</script>
<template>
<!-- Top interactive area of the article section -->
<div class="nav-article">
<div class="category">
<span>{{ $tm('mainPage.header.category') }}</span>
<span><Icon :icon="iconMap[categoryIcon]" /></span>
<div class="category-container">
<div class="category-submenu">
<div
class="item"
v-for="(kun, _) in categoryItem"
:key="kun.index"
@click="handleSortByCategory(kun.name)"
>
<span><Icon class="icon-item" :icon="kun.icon" /></span>
<span>
{{ $tm(`mainPage.header.${kun.name}`) }}
</span>
</div>
</div>
</div>
</div>
<!-- Sorting area container -->
<SortTopic />
<!-- Search box, only Galgame on the homepage -->
<KUNGalgameSearchBox :category="category" style="border: none" />
<!-- Enter all topics in the interactive area -->
<RouterLink to="/pool" class="more">
<span>{{ $tm('mainPage.header.all') }}</span>
<Icon class="all-topic" icon="line-md:chevron-triple-right" />
@ -66,94 +28,11 @@ const iconMap: Record<string, string> = {
height: 40px;
display: flex;
flex-shrink: 0;
border-bottom: 1px solid var(--kungalgame-trans-blue-4);
color: var(--kungalgame-font-color-3);
z-index: 1;
}
.category {
display: flex;
justify-content: center;
align-items: center;
width: 1px;
flex-grow: 1;
position: relative;
background-color: var(--kungalgame-trans-blue-0);
border: 1px solid var(--kungalgame-blue-4);
border-radius: 5px;
cursor: pointer;
& > span:nth-child(2) {
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
margin-left: 7px;
color: var(--kungalgame-blue-4);
}
&:hover {
transition: all 0.2s;
border: 1px solid var(--kungalgame-blue-4);
background-color: var(--kungalgame-blue-4);
color: var(--kungalgame-white);
& > span:nth-child(2) {
color: var(--kungalgame-white);
}
}
}
.category-container {
width: 100%;
top: 40px;
position: absolute;
}
.category-submenu {
display: none;
flex-direction: column;
background-color: var(--kungalgame-trans-white-2);
box-shadow: var(--shadow);
border-radius: 5px;
.item {
padding: 10px 0;
font-size: 14px;
color: var(--kungalgame-font-color-3);
text-decoration: none;
display: flex;
justify-content: space-around;
cursor: pointer;
&:hover {
background-color: var(--kungalgame-trans-blue-1);
backdrop-filter: blur(5px);
}
&:active {
background-color: var(--kungalgame-trans-blue-2);
}
.icon-item {
color: var(--kungalgame-blue-4);
padding-right: 3px;
font-size: 20px;
}
&:first-child {
border-radius: 5px 5px 0 0;
}
&:last-child {
border-radius: 0 0 5px 5px;
}
}
}
.category:hover .category-submenu {
display: flex;
}
.more {
height: 100%;
width: 1px;
@ -161,23 +40,19 @@ const iconMap: Record<string, string> = {
justify-content: center;
align-items: center;
white-space: nowrap;
background-color: var(--kungalgame-trans-blue-0);
border: 1px solid var(--kungalgame-blue-4);
background-color: var(--kungalgame-trans-blue-3);
flex-grow: 1;
border-radius: 5px;
border-radius: 0 5px 0 0;
cursor: pointer;
border-left: 1px solid var(--kungalgame-trans-blue-4);
color: var(--kungalgame-font-color-3);
margin-left: 7px;
&:hover {
transition: all 0.2s;
border: 1px solid var(--kungalgame-blue-4);
background-color: var(--kungalgame-blue-4);
color: var(--kungalgame-white);
background-color: var(--kungalgame-trans-blue-2);
}
& > span:nth-child(2) {
color: var(--kungalgame-white);
}
&:active {
background-color: var(--kungalgame-trans-blue-4);
}
}

View file

@ -1,31 +1,33 @@
<script setup lang="ts">
// Import icons
import { Icon } from '@iconify/vue'
import { ref } from 'vue'
import { useTempHomeStore } from '@/store/temp/home'
import { useKUNGalgameHomeStore } from '@/store/modules/home'
import { storeToRefs } from 'pinia'
import { sortItem } from './navItem'
// Import sorting list fields
import { navSortItem } from './navSortItem'
import { ref } from 'vue'
// Styles for ascending and descending orders
const ascClass = ref('')
const { topic } = storeToRefs(useTempHomeStore())
const { sortField, sortOrder } = storeToRefs(useKUNGalgameHomeStore())
const handleSortByField = (field: string) => {
useTempHomeStore().resetHomePageStatus()
topic.value.sortField = field
useKUNGalgameHomeStore().resetPageStatus()
sortField.value = field
}
const orderAscending = () => {
useTempHomeStore().resetHomePageStatus()
topic.value.sortOrder = 'asc'
useKUNGalgameHomeStore().resetPageStatus()
sortOrder.value = 'asc'
// Change style
ascClass.value = 'active'
}
const orderDescending = () => {
useTempHomeStore().resetHomePageStatus()
topic.value.sortOrder = 'desc'
useKUNGalgameHomeStore().resetPageStatus()
sortOrder.value = 'desc'
ascClass.value = ''
}
@ -34,24 +36,29 @@ const iconMap: Record<string, string> = {
time: 'eos-icons:hourglass',
popularity: 'bi:fire',
views: 'ic:outline-remove-red-eye',
likes_count: 'line-md:thumbs-up-twotone',
replies_count: 'ri:reply-line',
likes: 'line-md:thumbs-up-twotone',
replies: 'ri:reply-line',
comments: 'fa-regular:comment-dots',
}
const isSortField = () => {
return Object.keys(iconMap).includes(sortField.value)
}
</script>
<template>
<div class="container" :class="ascClass">
<span>{{ $tm('mainPage.header.filter') }}</span>
<span class="filter">
<Icon :icon="iconMap[topic.sortField]" />
<Icon v-if="isSortField()" :icon="iconMap[sortField]" />
</span>
<!-- Secondary menu for sorting -->
<div class="sort-container">
<div class="sort-submenu">
<div
class="sort-item"
v-for="kun in sortItem"
v-for="kun in navSortItem"
:key="kun.index"
@click="handleSortByField(kun.sortField)"
>
@ -83,27 +90,15 @@ const iconMap: Record<string, string> = {
}
.container {
display: flex;
justify-content: center;
align-items: center;
width: 1px;
background-color: var(--kungalgame-trans-blue-3);
flex-grow: 1;
border-radius: 5px 0 0 0;
position: relative;
background-color: var(--kungalgame-trans-blue-0);
border: 1px solid var(--kungalgame-blue-4);
border-radius: 5px;
border-right: 1px solid var(--kungalgame-trans-blue-4);
cursor: pointer;
margin-left: 7px;
&:hover {
transition: all 0.2s;
border: 1px solid var(--kungalgame-blue-4);
background-color: var(--kungalgame-blue-4);
color: var(--kungalgame-white);
& > span:nth-child(2) {
color: var(--kungalgame-white);
}
background-color: var(--kungalgame-trans-white-5);
}
}
@ -125,9 +120,7 @@ const iconMap: Record<string, string> = {
.sort-submenu {
display: none;
flex-direction: column;
background-color: var(--kungalgame-trans-white-2);
box-shadow: var(--shadow);
border-radius: 5px;
box-shadow: 1px 2px 1px 1px var(--kungalgame-trans-blue-4);
}
.container:hover .sort-submenu {
@ -136,6 +129,7 @@ const iconMap: Record<string, string> = {
.sort-item {
padding: 10px 0;
background-color: var(--kungalgame-trans-white-2);
font-size: 14px;
color: var(--kungalgame-font-color-3);
text-decoration: none;
@ -150,10 +144,6 @@ const iconMap: Record<string, string> = {
&:active {
background-color: var(--kungalgame-trans-blue-2);
}
&:first-child {
border-radius: 5px 5px 0 0;
}
}
.icon-item {
@ -164,10 +154,10 @@ const iconMap: Record<string, string> = {
.sort-order {
width: 100%;
padding: 10px 0;
display: flex;
cursor: default;
background-color: var(--kungalgame-trans-white-2);
border-radius: 0 0 5px 5px;
span {
color: var(--kungalgame-blue-4);
@ -176,8 +166,6 @@ const iconMap: Record<string, string> = {
justify-content: center;
align-items: center;
font-size: 17px;
padding: 10px 0;
cursor: pointer;
&:hover {
transition: all 0.2s;
@ -189,26 +177,25 @@ const iconMap: Record<string, string> = {
}
}
}
.active {
border: 1px solid var(--kungalgame-pink-4);
background-color: var(--kungalgame-pink-4);
color: var(--kungalgame-white);
& > span:nth-child(2) {
color: var(--kungalgame-white);
}
background-color: var(--kungalgame-trans-red-3);
.filter {
color: var(--kungalgame-pink-4);
color: var(--kungalgame-red-4);
}
}
@media (max-width: 700px) {
.sort-item {
display: flex;
justify-content: space-around;
justify-content: center;
align-items: center;
span {
&:nth-child(1) {
display: none;
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show more