diff --git a/Readme.md b/README.md
similarity index 97%
rename from Readme.md
rename to README.md
index 8fd1d61..9c6bdf8 100644
--- a/Readme.md
+++ b/README.md
@@ -84,4 +84,4 @@ Glitch can use `.env` file, [documentation](https://help.glitch.com/hc/en-us/art
## License
-[MIT License](./LICENSE)
+[MIT License](./LICENSE), excluding all themes
diff --git a/assets/img/back-to-top.png b/assets/img/back-to-top.png
new file mode 100644
index 0000000..2aec0d6
Binary files /dev/null and b/assets/img/back-to-top.png differ
diff --git a/assets/style.css b/assets/style.css
index da69e7a..0fdae66 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -34,6 +34,69 @@ h2, h3, h4, h5 {
margin-bottom: .6em;
}
+.back-to-top {
+ position: fixed;
+ z-index: 2;
+ right: -108px;
+ bottom: 0;
+ width: 108px;
+ height: 150px;
+ background: url('./img/back-to-top.png?v=1') no-repeat 0 0; /* artwork from https://www.pixiv.net/artworks/83996495 */
+ background-size: 108px 450px;
+ opacity: 0.6;
+ transition: opacity 0.3s, right 0.8s;
+ cursor: pointer;
+}
+.back-to-top:hover {
+ background-position: 0 -150px;
+ opacity: 1;
+}
+.back-to-top.load {
+ right: 0;
+}
+.back-to-top.ani-leave {
+ background-position: 0 -150px;
+ animation: ani-leave 390ms ease-in-out;
+ animation-fill-mode: forwards;
+}
+.back-to-top.leaved {
+ pointer-events: none;
+ background: none;
+ transition: none;
+}
+.back-to-top.ending {
+ pointer-events: none;
+}
+.back-to-top.ending::after {
+ opacity: 1;
+ transition-delay: 0.35s;
+}
+.back-to-top::after {
+ content: '';
+ position: fixed;
+ z-index: 2;
+ right: 0;
+ bottom: 0;
+ width: 108px;
+ height: 150px;
+ background: url('./img/back-to-top.png?v=1') no-repeat 0 0;
+ background-size: 108px 450px;
+ background-position: 0 -300px;
+ transition: opacity 0.3s;
+ opacity: 0;
+ pointer-events: none;
+}
+
+@keyframes ani-leave {
+ 0% {
+ transform: translateX(0);
+ }
+ 100% {
+ transform: translateX(108px);
+ }
+}
+
+
@media screen and (max-width: 900px) {
iframe {
display: none;
diff --git a/index.js b/index.js
index 8a08654..5931d81 100644
--- a/index.js
+++ b/index.js
@@ -3,9 +3,12 @@
const config = require("config-yml");
const express = require("express");
const compression = require("compression");
+const { z } = require("zod");
const db = require("./db");
-const themify = require("./utils/themify");
+const { themeList, getCountImage } = require("./utils/themify");
+const { ZodValid } = require("./utils/zod");
+const { randomArray } = require("./utils");
const app = express();
@@ -15,56 +18,65 @@ app.set("view engine", "pug");
app.get('/', (req, res) => {
const site = config.app.site || `${req.protocol}://${req.get('host')}`
- res.render('index', { site })
+ res.render('index', {
+ site,
+ themeList,
+ })
});
// get the image
-app.get(["/@:name", "/get/@:name"], async (req, res) => {
- const { name } = req.params;
- const { theme = "moebooru", padding = 7, pixelated = '1', darkmode = 'auto' } = req.query;
- const isPixelated = pixelated === '1';
+app.get(["/@:name", "/get/@:name"],
+ ZodValid({
+ params: z.object({
+ name: z.string().max(32),
+ }),
+ query: z.object({
+ theme: z.string().default("moebooru"),
+ padding: z.coerce.number().min(0).max(32).default(7),
+ offset: z.coerce.number().min(-500).max(500).default(0),
+ scale: z.coerce.number().min(0.1).max(2).default(1),
+ pixelated: z.enum(["0", "1"]).default("1"),
+ darkmode: z.enum(["0", "1", "auto"]).default("auto")
+ })
+ }),
+ async (req, res) => {
+ const { name } = req.params;
+ let { theme = "moebooru", ...rest } = req.query;
- if (name.length > 32) {
- res.status(400).send("name too long");
- return;
+ // This helps with GitHub's image cache
+ res.set({
+ "content-type": "image/svg+xml",
+ "cache-control": "max-age=0, no-cache, no-store, must-revalidate",
+ });
+
+ const data = await getCountByName(name);
+
+ if (name === "demo") {
+ res.set("cache-control", "max-age=31536000");
+ }
+
+ if (theme === "random") {
+ theme = randomArray(Object.keys(themeList));
+ }
+
+ // Send the generated SVG as the result
+ const renderSvg = getCountImage({
+ count: data.num,
+ theme,
+ ...rest
+ });
+
+ res.send(renderSvg);
+
+ console.log(
+ data,
+ `theme: ${theme}`,
+ `ip: ${req.headers['x-forwarded-for'] || req.connection.remoteAddress}`,
+ `ref: ${req.get("Referrer") || null}`,
+ `ua: ${req.get("User-Agent") || null}`
+ );
}
-
- if (padding > 32) {
- res.status(400).send("padding too long");
- return;
- }
-
- // This helps with GitHub's image cache
- res.set({
- "content-type": "image/svg+xml",
- "cache-control": "max-age=0, no-cache, no-store, must-revalidate",
- });
-
- const data = await getCountByName(name);
-
- if (name === "demo") {
- res.set("cache-control", "max-age=31536000");
- }
-
- // Send the generated SVG as the result
- const renderSvg = themify.getCountImage({
- count: data.num,
- theme,
- padding,
- darkmode,
- pixelated: isPixelated
- });
-
- res.send(renderSvg);
-
- console.log(
- data,
- `theme: ${theme}`,
- `ip: ${req.headers['x-forwarded-for'] || req.connection.remoteAddress}`,
- `ref: ${req.get("Referrer") || null}`,
- `ua: ${req.get("User-Agent") || null}`
- );
-});
+);
// JSON record
app.get("/record/@:name", async (req, res) => {
diff --git a/package.json b/package.json
index dc32288..2b6ec71 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,10 @@
"image-size": "^0.8.3",
"mime-types": "^2.1.27",
"mongoose": "^5.9.28",
- "pug": "^3.0.0"
+ "pug": "^3.0.0",
+ "zod": "^3.23.8"
+ },
+ "engines": {
+ "node": "16.x"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6d0a5e2..809153c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ dependencies:
pug:
specifier: ^3.0.0
version: 3.0.3
+ zod:
+ specifier: ^3.23.8
+ version: 3.23.8
packages:
@@ -1665,3 +1668,7 @@ packages:
y18n: 3.2.2
yargs-parser: 2.4.1
dev: false
+
+ /zod@3.23.8:
+ resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
+ dev: false
diff --git a/utils/index.js b/utils/index.js
new file mode 100644
index 0000000..a06a32b
--- /dev/null
+++ b/utils/index.js
@@ -0,0 +1,5 @@
+module.exports = {
+ randomArray: (arr) => {
+ return arr[Math.floor(Math.random() * arr.length)]
+ },
+}
diff --git a/utils/themify.js b/utils/themify.js
index 589205b..94047db 100644
--- a/utils/themify.js
+++ b/utils/themify.js
@@ -14,10 +14,10 @@ fs.readdirSync(themePath).forEach(theme => {
const imgList = fs.readdirSync(path.resolve(themePath, theme))
imgList.forEach(img => {
const imgPath = path.resolve(themePath, theme, img)
- const name = path.parse(img).name
+ const num = path.parse(img).name
const { width, height } = sizeOf(imgPath)
- themeList[theme][name] = {
+ themeList[theme][num] = {
width,
height,
data: convertToDatauri(imgPath)
@@ -32,8 +32,12 @@ function convertToDatauri(path) {
return `data:${mime};base64,${base64}`
}
-function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = true, darkmode = 'auto' }) {
+function getCountImage(params) {
+ let { count, theme = 'moebooru', padding = 7, offset = 0, scale = 1, pixelated = '1', darkmode = 'auto' } = params
+
if (!(theme in themeList)) theme = 'moebooru'
+ padding = parseInt(padding, 10)
+ offset = parseInt(offset, 10)
// This is not the greatest way for generating an SVG but it'll do for now
const countArray = count.toString().padStart(padding, '0').split('')
@@ -42,7 +46,9 @@ function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = tru
let x = 0, y = 0
const defs = uniqueChar.reduce((ret, cur) => {
- const { width, height, data } = themeList[theme][cur]
+ let { width, height, data } = themeList[theme][cur]
+ width *= scale
+ height *= scale
if (height > y) y = height
@@ -53,19 +59,23 @@ function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = tru
}, '')
const parts = countArray.reduce((ret, cur) => {
- const { width } = themeList[theme][cur]
+ let { width } = themeList[theme][cur]
+ width *= scale
const image = `${ret}
`
- x += width
+ x += width + offset
return image
}, '')
+ // Fix the last image offset
+ x -= offset
+
const style = `
svg {
- ${pixelated ? 'image-rendering: pixelated;' : ''}
+ ${pixelated === '1' ? 'image-rendering: pixelated;' : ''}
${darkmode === '1' ? 'filter: brightness(.6);' : ''}
}
${darkmode === 'auto' ? `@media (prefers-color-scheme: dark) { svg { filter: brightness(.6); } }` : ''}
@@ -85,5 +95,6 @@ function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = tru
}
module.exports = {
+ themeList,
getCountImage
}
diff --git a/utils/zod.js b/utils/zod.js
new file mode 100644
index 0000000..f5bcc89
--- /dev/null
+++ b/utils/zod.js
@@ -0,0 +1,50 @@
+function parseError(error) {
+ const err = JSON.parse(error)[0];
+
+ return {
+ code: 400,
+ message: `The field \`${err.path[0]}\` is invalid. ${err.message}`,
+ }
+}
+
+module.exports = {
+ ZodValid: ({ headers, params, query, body }) => {
+ const handler = (req, res, next) => {
+ if (headers) {
+ const result = headers.safeParse(req.headers);
+ if (!result.success) {
+ res.status(400).send(parseError(result.error));
+ return;
+ }
+ }
+
+ if (params) {
+ const result = params.safeParse(req.params);
+ if (!result.success) {
+ res.status(400).send(parseError(result.error));
+ return;
+ }
+ }
+
+ if (query) {
+ const result = query.safeParse(req.query);
+ if (!result.success) {
+ res.status(400).send(parseError(result.error));
+ return;
+ }
+ }
+
+ if (body) {
+ const result = body.safeParse(req.body);
+ if (!result.success) {
+ res.status(400).send(parseError(result.error));
+ return;
+ }
+ }
+
+ next();
+ }
+
+ return handler
+ }
+}
\ No newline at end of file
diff --git a/views/index.pug b/views/index.pug
index 8f60810..9b85f3f 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -35,7 +35,7 @@ html
h3 How to use
p Set a unique id for your counter, replace
code :name
- | in the url, that's all.
+ | in the url, That's it!
h5 SVG address
code #{site}/@:name
@@ -47,24 +47,15 @@ html
code ![:name](#{site}/@:name)
h5 e.g.
-
+
- details#themes
+ details#themes(style='margin-top: 2em;')
summary#more_theme(onclick='_evt_push("click", "normal", "more_theme")')
h3(style='display: inline-block; margin: 0; cursor: pointer;') More theme✨
p(style='margin: 0;') Just use the query parameters theme
, like this: #{site}/@:name?theme=moebooru
- h5 asoul
-
- h5 moebooru
-
- h5 moebooru-h
-
- h5 rule34
-
- h5 gelbooru
-
- h5 gelbooru-h
-
+ each theme in Object.keys(themeList)
+ h5 #{theme}
+
h3 Credits
ul
@@ -74,14 +65,13 @@ html
a(href='https://space.bilibili.com/703007996', target='_blank', title='A-SOUL_Official') A-SOUL
li
a(href='https://github.com/moebooru/moebooru', target='_blank', rel='nofollow') moebooru
- li
- a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') rule34.xxx
- | NSFW
li
a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') gelbooru.com
| NSFW
li
a(href='https://icons8.com/icon/80355/star', target='_blank', rel='nofollow') Icons8
+ span
+ i And all booru site...
h3 Tool
.tool
@@ -89,37 +79,58 @@ html
thead
tr
th Param
+ th Description
th Value
tbody
tr
td
code name
+ td Unique counter name
td
input#name(type='text', placeholder=':name')
tr
td
code theme
+ td Select a counter image theme, default is
+ code moebooru
td
select#theme
- option(value='asoul') asoul
- option(value='moebooru') moebooru
- option(value='moebooru-h') moebooru-h
- option(value='rule34') rule34
- option(value='gelbooru') gelbooru
- option(value='gelbooru-h') gelbooru-h
- tr
- td
- code pixelated
- td
- input#pixelated(type='checkbox', checked, style='margin: .5rem .75rem;')
+ option(value="random", selected) * random
+ each theme in Object.keys(themeList)
+
tr
td
code padding
+ td Set the minimum length, between 1-32, default is
+ code 7
td
input#padding(type='number', value='7', min='1', max='32', step='1', oninput='this.value = this.value.replace(/[^0-9]/g, "")')
+ tr
+ td
+ code offset
+ td Set the offset pixel value, between -500-500, default is
+ code 0
+ td
+ input#offset(type='number', value='0', min='-500', max='500', step='1', oninput='this.value = this.value.replace(/[^0-9|\-]/g, "")')
+ tr
+ td
+ code scale
+ td Set the image scale, between 0.1-2, default is
+ code 1
+ td
+ input#scale(type='number', value='1', min='0.1', max='2', step='0.1', oninput='this.value = this.value.replace(/[^0-9|\.]/g, "")')
+ tr
+ td
+ code pixelated
+ td Enable pixelated mode, Enum 0/1, default is
+ code 1
+ td
+ input#pixelated(type='checkbox', checked, style='margin: .5rem .75rem;')
tr
td
code darkmode
+ td Enable dark mode, Enum 0/1/auto, default is
+ code auto
td
select#darkmode(name="darkmode")
option(value="auto", selected) auto
@@ -132,7 +143,13 @@ html
code#code(style='visibility: hidden; display: inline-block; margin-bottom: 1em;')
img#result(style='display: block;')
- script.
+ p(style='margin-top: 2em;')
+ a(href='https://github.com/journey-ad/Moe-Counter', target='_blank', onclick='_evt_push("click", "normal", "go_github")') source code
+
+ div.back-to-top
+
+ script.
+ (function () {
var btn = document.getElementById('get'),
img = document.getElementById('result'),
code = document.getElementById('code')
@@ -140,15 +157,13 @@ html
btn.addEventListener('click', throttle(function() {
var $name = document.getElementById('name'),
$theme = document.getElementById('theme'),
- $pixelated = document.getElementById('pixelated'),
$padding = document.getElementById('padding'),
+ $offset = document.getElementById('offset'),
+ $scale = document.getElementById('scale'),
+ $pixelated = document.getElementById('pixelated'),
$darkmode = document.getElementById('darkmode')
var name = $name.value ? $name.value.trim() : ''
- var theme = $theme.value || 'moebooru'
- var pixelated = $pixelated.checked ? '1' : '0'
- var padding = $padding.value || '7'
- var darkmode = $darkmode.value || 'auto'
if(!name) {
alert('Please input counter name.')
@@ -157,7 +172,19 @@ html
party.confetti(this, { count: party.variation.range(20, 40) });
- img.src = `#{site}/@${name}?theme=${theme}&pixelated=${pixelated}&padding=${padding}&darkmode=${darkmode}`
+ var params = {
+ name: $name.value ? $name.value.trim() : '',
+ theme: $theme.value || 'moebooru',
+ padding: $padding.value || '7',
+ offset: $offset.value || '0',
+ scale: $scale.value || '1',
+ pixelated: $pixelated.checked ? '1' : '0',
+ darkmode: $darkmode.value || 'auto',
+ }
+
+ var query = new URLSearchParams(params).toString()
+
+ img.src = `#{site}/@${name}?${query}`
code.textContent = img.src
code.style.visibility = 'visible'
@@ -220,6 +247,50 @@ html
}
}
}
+ })();
- p(style='margin-top: 2em;')
- a(href='https://github.com/journey-ad/Moe-Counter', target='_blank', onclick='_evt_push("click", "normal", "go_github")') source code
+ script.
+ (function () {
+ var isShow = false, lock = false;
+ var btn = document.querySelector('.back-to-top');
+
+ window.addEventListener('scroll', function () {
+ if (lock) return;
+
+ if (document.body.scrollTop >= 1000) {
+ if (!isShow) btn.classList.add('load');
+ isShow = true;
+ } else {
+ if (isShow) {
+ btn.classList.remove('load');
+ isShow = false;
+ }
+ }
+ });
+
+ btn.addEventListener('click', function () {
+ lock = true;
+ btn.classList.add('ani-leave');
+
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+
+ setTimeout(function () {
+ btn.classList.remove('ani-leave');
+ btn.classList.add('leaved');
+ }, 390);
+
+ setTimeout(function () {
+ btn.classList.add('ending');
+ }, 120);
+
+ setTimeout(function () {
+ btn.classList.remove('load');
+ }, 1500);
+
+ setTimeout(function () {
+ lock = false;
+ isShow = false;
+ btn.classList.remove('leaved', 'ending');
+ }, 2000);
+ });
+ })();