Several updates

feat: New website UI
feat: Add few params
feat: Improve performance
This commit is contained in:
roffy3051 2024-10-20 05:11:14 +08:00
parent 81936a9883
commit cc1881cc4f
11 changed files with 2045 additions and 2280 deletions

View File

@ -7,6 +7,8 @@
<details>
<summary>More theme</summary>
### *Contribute themes is welcome!*
##### asoul
![asoul](https://count.getloli.com/get/@demo?theme=asoul)
@ -17,29 +19,35 @@
![Rule34](https://count.getloli.com/get/@demo?theme=rule34)
##### gelbooru
![Gelbooru](https://count.getloli.com/get/@demo?theme=gelbooru)</details>
![Gelbooru](https://count.getloli.com/get/@demo?theme=gelbooru)
</details>
## Demo
[https://count.getloli.com](https://count.getloli.com)
## How to use
About the counter usage, please see the [website homepage](https://count.getloli.com).
## Usage
### Install
#### Run on Replit
#### Run on Glitch
- Open the url [https://replit.com/@journeyad/Moe-Counter](https://replit.com/@journeyad/Moe-Counter)
- Just hit the **Fork** button
- And hit the **Run** button
- Open [Glitch project](https://glitch.com/~moe-counter-api)
- Just hit the **Remix your own** button
- That's it!
#### Deploying on your own server
```shell
$ git clone https://github.com/journey-ad/Moe-Counter.git
$ cd Moe-Counter
$ yarn install
$ pnpm install
$ yarn start
$ pnpm run start
```
### Configuration
@ -53,30 +61,27 @@ app:
db:
type: sqlite # sqlite or mongodb
interval: 60 # write to db interval in seconds (0 for realtime)
```
If you use mongodb, you need to specify the environment variable `DB_URL`
If using mongodb, you need to specify the environment variable `DB_URL`
```shell
# eg:
# e.g.
export DB_URL=mongodb+srv://account:passwd@***.***.***.mongodb.net/db_count
```
replit can use Secrets, [documentation](https://docs.replit.com/programming-ide/storing-sensitive-information-environment-variables)
```
DB_URL="mongodb+srv://account:passwd@***.***.***.mongodb.net/db_count"
```
Glitch can use `.env` file, [documentation](https://help.glitch.com/hc/en-us/articles/16287550167437-Adding-Private-Data)
## Credits
* [replit](https://replit.com/)
* [A-SOUL_Official](https://space.bilibili.com/703007996)
* [moebooru](https://github.com/moebooru/moebooru)
* rule34.xxx NSFW
* gelbooru.com NSFW
* [Icons8](https://icons8.com/icons/set/star)
* [Glitch](https://glitch.com/)
* [A-SOUL_Official](https://space.bilibili.com/703007996)
* [moebooru](https://github.com/moebooru/moebooru)
* rule34.xxx NSFW
* gelbooru.com NSFW
* [Icons8](https://icons8.com/icon/80355/star)
## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fjourney-ad%2FMoe-Counter.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fjourney-ad%2FMoe-Counter?ref=badge_large)
[MIT License](./LICENSE)

View File

@ -1,3 +1,39 @@
@media screen and (min-width: 800px) {
body {
max-width: min(90%, 800px);
}
}
details>summary {
list-style: none
}
details>summary::-webkit-details-marker,details>summary::marker {
display: none
}
summary:before {
border-bottom: 6px solid transparent;
border-left: 10px solid var(--b-txt);
border-top: 6px solid transparent;
content: "";
display: inline-block;
height: 0;
margin-right: 10px;
position: relative;
transition: .2s;
width: 0
}
details[open] summary:before {
transform: rotate(90deg)
}
h2, h3, h4, h5 {
margin-top: 1.5em;
margin-bottom: .6em;
}
@media screen and (max-width: 900px) {
iframe {
display: none;

View File

@ -4,3 +4,4 @@ app:
db:
type: sqlite # sqlite or mongodb
interval: 60 # write to db interval in seconds (0 for realtime)

View File

@ -1,55 +1,60 @@
'use strict'
"use strict";
const mongoose = require('mongoose')
const schema = require('./schema')
const mongoose = require("mongoose");
const schema = new mongoose.Schema(
{
name: { type: String, required: true },
num: { type: Number, required: true }
},
{ collection: 'tb_count', versionKey: false }
);
// the default mongodb url (local server)
const mongodbURL = process.env.DB_URL || 'mongodb://127.0.0.1:27017'
const mongodbURL = process.env.DB_URL || "mongodb://127.0.0.1:27017";
mongoose.connect(mongodbURL, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false
})
useFindAndModify: false,
});
const Count = mongoose.connection.model('Count', schema)
const Count = mongoose.connection.model("Count", schema);
function getNum(name) {
return Count
.findOne({ name }, '-_id -__v')
.exec()
return Count.findOne({ name }, "-_id -__v").exec();
}
function getAll() {
return Count
.find({ }, '-_id -__v')
.exec()
return Count.find({}, "-_id -__v").exec();
}
function setNum(name, num) {
return Count
.findOneAndUpdate({ name }, { name, num }, { upsert: true })
.exec()
return Count.findOneAndUpdate(
{ name },
{ name, num },
{ upsert: true }
).exec();
}
function setNumMulti(counters) {
const bulkOps = counters.map(obj => {
const { name, num } = obj
const bulkOps = counters.map((obj) => {
const { name, num } = obj;
return {
updateOne: {
filter: { name },
update: { name, num },
upsert: true
}
}
})
upsert: true,
},
};
});
return Count.bulkWrite(bulkOps, { ordered : false })
return Count.bulkWrite(bulkOps, { ordered: false });
}
module.exports = {
getNum,
getAll,
setNum,
setNumMulti
}
setNumMulti,
};

View File

@ -1,8 +0,0 @@
'use strict'
const mongoose = require('mongoose');
module.exports = new mongoose.Schema({
name: { type: String, required: true },
num: { type: Number, required: true }
}, { collection: 'tb_count', versionKey: false });

159
index.js
View File

@ -1,20 +1,17 @@
'use strict'
"use strict";
const fs = require('fs')
const config = require('config-yml')
const express = require('express')
const compression = require('compression')
const config = require("config-yml");
const express = require("express");
const compression = require("compression");
const db = require('./db')
const themify = require('./utils/themify')
const db = require("./db");
const themify = require("./utils/themify");
const PLACES = 7
const app = express();
const app = express()
app.use(express.static('assets'))
app.use(compression())
app.set('view engine', 'pug')
app.use(express.static("assets"));
app.use(compression());
app.set("view engine", "pug");
app.get('/', (req, res) => {
const site = config.app.site || `${req.protocol}://${req.get('host')}`
@ -22,104 +19,122 @@ app.get('/', (req, res) => {
});
// get the image
app.get('/get/@:name', async (req, res) => {
const { name } = req.params
const { theme = 'moebooru' } = req.query
let length = PLACES
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';
// This helps with GitHub's image cache
if (name.length > 32) {
res.status(400).send("name too long");
return;
}
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'
})
"content-type": "image/svg+xml",
"cache-control": "max-age=0, no-cache, no-store, must-revalidate",
});
const data = await getCountByName(name)
const data = await getCountByName(name);
if (name === 'demo') {
res.set({
'cache-control': 'max-age=31536000'
})
length = 10
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, length })
res.send(renderSvg)
const renderSvg = themify.getCountImage({
count: data.num,
theme,
padding,
darkmode,
pixelated: isPixelated
});
console.log(data, `theme: ${theme}`, `ref: ${req.get('Referrer') || null}`, `ua: ${req.get('User-Agent') || null}`)
})
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) => {
const { name } = req.params
app.get("/record/@:name", async (req, res) => {
const { name } = req.params;
const data = await getCountByName(name)
const data = await getCountByName(name);
res.json(data)
})
res.json(data);
});
app.get('/heart-beat', (req, res) => {
res.set({
'cache-control': 'max-age=0, no-cache, no-store, must-revalidate'
})
res.send('alive')
console.log('heart-beat')
app.get("/heart-beat", (req, res) => {
res.set("cache-control", "max-age=0, no-cache, no-store, must-revalidate");
res.send("alive");
console.log("heart-beat");
});
const listener = app.listen(config.app.port || 3000, () => {
console.log('Your app is listening on port ' + listener.address().port)
})
console.log("Your app is listening on port " + listener.address().port);
});
let __cache_counter = {}, shouldPush = false
let __cache_counter = {};
let enablePushDelay = config.db.interval > 0
let needPush = false;
setInterval(() => {
shouldPush = true
}, 1000 * 60);
if (enablePushDelay) {
setInterval(() => {
needPush = true;
}, 1000 * config.db.interval);
}
async function pushDB() {
if (!shouldPush) return
if (Object.keys(__cache_counter).length === 0) return;
if (enablePushDelay && !needPush) return;
try {
shouldPush = false
if (Object.keys(__cache_counter).length === 0) return
needPush = false;
console.log("pushDB", __cache_counter);
console.log("pushDB", __cache_counter)
const counters = Object.keys(__cache_counter).map(key => {
const counters = Object.keys(__cache_counter).map((key) => {
return {
name: key,
num: __cache_counter[key]
}
})
num: __cache_counter[key],
};
});
await db.setNumMulti(counters)
__cache_counter = {}
await db.setNumMulti(counters);
__cache_counter = {};
} catch (error) {
console.log("pushDB is error: ", error)
console.log("pushDB is error: ", error);
}
}
async function getCountByName(name) {
const defaultCount = { name, num: 0 }
const defaultCount = { name, num: 0 };
if (name === 'demo') return { name, num: '0123456789' }
if (name === "demo") return { name, num: "0123456789" };
try {
if (!(name in __cache_counter)) {
const counter = await db.getNum(name) || defaultCount
__cache_counter[name] = counter.num + 1
const counter = (await db.getNum(name)) || defaultCount;
__cache_counter[name] = counter.num + 1;
} else {
__cache_counter[name]++
__cache_counter[name]++;
}
pushDB()
return { name, num: __cache_counter[name] }
pushDB();
return { name, num: __cache_counter[name] };
} catch (error) {
console.log("get count by name is error: ", error)
return defaultCount
console.log("get count by name is error: ", error);
return defaultCount;
}
}

2091
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,10 +11,10 @@
"author": "journey-ad",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^8.1.0",
"better-sqlite3": "^8.2.0",
"compression": "^1.7.4",
"config-yml": "^0.10.3",
"express": "^4.17.1",
"express": "^4.18.2",
"image-size": "^0.8.3",
"mime-types": "^2.1.27",
"mongoose": "^5.9.28",

1667
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ const themePath = path.resolve(__dirname, '../assets/theme')
const themeList = {}
fs.readdirSync(themePath).forEach(theme => {
if(!(theme in themeList)) themeList[theme] = {}
if (!(theme in themeList)) themeList[theme] = {}
const imgList = fs.readdirSync(path.resolve(themePath, theme))
imgList.forEach(img => {
const imgPath = path.resolve(themePath, theme, img)
@ -25,39 +25,61 @@ fs.readdirSync(themePath).forEach(theme => {
})
})
function convertToDatauri(path){
function convertToDatauri(path) {
const mime = mimeType.lookup(path)
const base64 = fs.readFileSync(path).toString('base64')
return `data:${mime};base64,${base64}`
}
function getCountImage({ count, theme='moebooru', length=7 }) {
if(!(theme in themeList)) theme = 'moebooru'
function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = true, darkmode = 'auto' }) {
if (!(theme in themeList)) theme = 'moebooru'
// This is not the greatest way for generating an SVG but it'll do for now
const countArray = count.toString().padStart(length, '0').split('')
const countArray = count.toString().padStart(padding, '0').split('')
const uniqueChar = [...new Set(countArray)]
let x = 0, y = 0
const parts = countArray.reduce((acc, next, index) => {
const { width, height, data } = themeList[theme][next]
const image = `${acc}
<image x="${x}" y="0" width="${width}" height="${height}" xlink:href="${data}" />`
const defs = uniqueChar.reduce((ret, cur) => {
const { width, height, data } = themeList[theme][cur]
if (height > y) y = height
ret = `${ret}
<image id="${cur}" width="${width}" height="${height}" xlink:href="${data}" />`
return ret
}, '')
const parts = countArray.reduce((ret, cur) => {
const { width } = themeList[theme][cur]
const image = `${ret}
<use x="${x}" xlink:href="#${cur}" />`
x += width
if(height > y) y = height
return image
}, '')
const style = `
svg {
${pixelated ? 'image-rendering: pixelated;' : ''}
${darkmode === '1' ? 'filter: brightness(.6);' : ''}
}
${darkmode === 'auto' ? `@media (prefers-color-scheme: dark) { svg { filter: brightness(.6); } }` : ''}
`
return `<?xml version="1.0" encoding="UTF-8"?>
<svg width="${x}" height="${y}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="image-rendering: pixelated;">
<title>Moe Count</title>
<g>
${parts}
</g>
<!-- Generated by https://github.com/journey-ad/Moe-Counter -->
<svg viewBox="0 0 ${x} ${y}" width="${x}" height="${y}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Moe Counter!</title>
<style>${style}</style>
<defs>${defs}
</defs>
<g>${parts}
</g>
</svg>
`
}

View File

@ -3,7 +3,8 @@ html
title='Moe Counter!'
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='icon', type='image/png', href='favicon.png')
link(rel='stylesheet', href='https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/light.min.css')
link(rel='stylesheet', href='https://unpkg.com/normalize.css')
link(rel='stylesheet', href='https://unpkg.com/bamboo.css')
link(rel='stylesheet', href='style.css')
<!-- Global site tag (gtag.js) - Google Analytics -->
script(async, src='https://www.googletagmanager.com/gtag/js?id=G-2RLWN5JXRL')
@ -20,47 +21,55 @@ html
'event_label' : label
});
}
script(async, src='https://unpkg.com/party-js@2/bundle/party.min.js')
style.
html {
scroll-padding: 50px 0;
}
body
h3 How to use:
h1#main_title(style='margin-top: 0.5em;')
i Moe Counter!
h3 How to use
p Set a unique id for your counter, replace
code :name
| in the url, that's all.
h5 SVG address
code #{site}/get/@:name
code #{site}/@:name
h5 Img tag
code &lt;img src="#{site}/get/@:name" alt=":name" />
code &lt;img src="#{site}/@:name" alt=":name" />
h5 Markdown
code ![:name](#{site}/get/@:name)
code ![:name](#{site}/@:name)
h3 eg:
<img src="#{site}/get/@index" alt="Moe Count!" />
h5 e.g.
<img src="#{site}/@index" alt="Moe Count!" />
i Data can access by anyone, please
| <span style="color: #ff4500;"> DO NOT</span>
| enter personal information
details
summary(style='display: inline-block;', onclick='_evt_push("click", "normal", "more_theme")')
h3(style='display: inline-block; cursor: pointer;') More theme
p(style='margin: 0;') Just use the query parameters <code>theme</code>, like this: <code>#{site}/get/@:name?theme=moebooru</code>
details#themes
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 <code>theme</code>, like this: <code>#{site}/@:name?theme=moebooru</code>
h5 asoul
img(src='#{site}/get/@demo?theme=asoul', alt='A-SOUL')
<img src="#{site}/@demo?theme=asoul" alt="A-SOUL" />
h5 moebooru
img(src='#{site}/get/@demo?theme=moebooru', alt='Moebooru')
<img src="#{site}/@demo?theme=moebooru" alt="Moebooru" />
h5 moebooru-h
img(src='#{site}/get/@demo?theme=moebooru-h', alt='Moebooru-Hentai')
<img src="#{site}/@demo?theme=moebooru-h" alt="Moebooru-Hentai" />
h5 rule34
img(src='#{site}/get/@demo?theme=rule34', alt='Rule34')
<img src="#{site}/@demo?theme=rule34" alt="Rule34" />
h5 gelbooru
img(src='#{site}/get/@demo?theme=gelbooru', alt='Gelbooru')
<img src="#{site}/@demo?theme=gelbooru" alt="Gelbooru" />
h5 gelbooru-h
img(src='#{site}/get/@demo?theme=gelbooru-h', alt='Gelbooru-Hentai')
<img src="#{site}/@demo?theme=gelbooru-h" alt="Gelbooru-Hentai" />
h3 Credits
ul
li
a(href='https://repl.it/', target='_blank', rel='nofollow') repl.it
a(href='https://glitch.com/', target='_blank', rel='nofollow') Glitch
li
a(href='https://space.bilibili.com/703007996', target='_blank', title='A-SOUL_Official') A-SOUL
li
@ -72,41 +81,145 @@ html
a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') gelbooru.com
| NSFW
li
a(href='https://icons8.com/icons/set/star', target='_blank', rel='nofollow') Icons8
a(href='https://icons8.com/icon/80355/star', target='_blank', rel='nofollow') Icons8
h3 Tool
.tool
code #{site}/get/@
input#name(type='text', placeholder=':name', style='display: inline-block; width: 80px; height: 1.4em; line-height: 1.4em; margin: 0 4px; vertical-align: middle;')
code ?theme=
select#theme(style='display: inline-block; height: 1.6em; line-height: 1.6em; font-size: 14px; margin: 0 4px; padding: 0 4px; vertical-align: middle;')
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
button#get(style='margin: 10px 0;', onclick='_evt_push("click", "normal", "get_counter")') Get
img#result(style='display: block;')
table
thead
tr
th Param
th Value
tbody
tr
td
code name
td
input#name(type='text', placeholder=':name')
tr
td
code theme
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;')
tr
td
code padding
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 darkmode
td
select#darkmode(name="darkmode")
option(value="auto", selected) auto
option(value="1") yes
option(value="0") no
button#get(style='margin-bottom: 1em;', onclick='_evt_push("click", "normal", "get_counter")') Generate
div
code#code(style='visibility: hidden; display: inline-block; margin-bottom: 1em;')
img#result(style='display: block;')
script.
var btn = document.getElementById('get'),
img = document.getElementById('result')
img = document.getElementById('result'),
code = document.getElementById('code')
btn.addEventListener('click', function() {
var name = document.getElementById('name'),
themeEl = document.getElementById('theme')
var text = name.value ? name.value.trim() : ''
var theme = themeEl.value || 'moebooru'
if(!text) {
btn.addEventListener('click', throttle(function() {
var $name = document.getElementById('name'),
$theme = document.getElementById('theme'),
$pixelated = document.getElementById('pixelated'),
$padding = document.getElementById('padding'),
$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.')
return
}
img.src = '#{site}/get/@' + text + '?theme=' + theme
party.confetti(this, { count: party.variation.range(20, 40) });
img.src = `#{site}/@${name}?theme=${theme}&pixelated=${pixelated}&padding=${padding}&darkmode=${darkmode}`
code.textContent = img.src
code.style.visibility = 'visible'
img.onload = function() {
img.scrollIntoView({block:'start', behavior: 'smooth'})
}
}, 500))
code.addEventListener('click', function(e) {
e.preventDefault()
e.stopPropagation()
var target = e.target
var range, selection
if (document.body.createTextRange) {
range = document.body.createTextRange()
range.moveToElementText(target)
range.select()
} else if (window.getSelection) {
selection = window.getSelection()
range = document.createRange()
range.selectNodeContents(target)
selection.removeAllRanges()
selection.addRange(range)
}
})
iframe(src="https://chat.getloli.com/room/@Moe-counter?title=%E8%90%8C%E8%90%8C%E8%AE%A1%E6%95%B0%E5%99%A8%E7%9A%84%E7%95%99%E8%A8%80%E6%9D%BF", scrolling="no", frameborder="0", height="70%", width="26%", style="position: fixed;top: 2%;right: 5%;")
var $main_title = document.querySelector('#main_title i'),
$themes = document.querySelector('#themes'),
$more_theme = document.querySelector('#more_theme')
$main_title.addEventListener('click', throttle(function() {
party.sparkles(document.documentElement, { count: party.variation.range(40, 100) });
}, 1000))
$more_theme.addEventListener('click', function() {
if (!$themes.hasAttribute('open')) {
party.sparkles($more_theme.querySelector('h3'), { count: party.variation.range(20, 40) });
$themes.scrollIntoView({block:'start', behavior: 'smooth'})
}
})
p.copy
function throttle(fn, threshhold, scope) {
threshhold || (threshhold = 250)
var last
var deferTimer
return function () {
var context = scope || this
var now = +new Date
var args = arguments
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer)
deferTimer = setTimeout(function () {
last = now
fn.apply(context, args)
}, threshhold)
} else {
last = now
fn.apply(context, args)
}
}
}
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