Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49e10f8b60
|
||
|
|
6535c9c7b6
|
||
|
|
40f27d83e9
|
@@ -2,6 +2,8 @@ name: Build, Scan & Publish Image
|
|||||||
|
|
||||||
# Publishing a Gitea release (for example with tag 2026.09.25) builds the image,
|
# Publishing a Gitea release (for example with tag 2026.09.25) builds the image,
|
||||||
# scans it with Grype (report only), and pushes it to this repository's registry.
|
# scans it with Grype (report only), and pushes it to this repository's registry.
|
||||||
|
# A release pushes the release tag plus "latest" (non-prereleases only).
|
||||||
|
# A manual run builds and pushes only the tag supplied in the input.
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
@@ -52,7 +54,7 @@ jobs:
|
|||||||
echo "TAG=$SAFE_TAG"
|
echo "TAG=$SAFE_TAG"
|
||||||
echo "IMAGE_REF=${IMAGE}:${SAFE_TAG}"
|
echo "IMAGE_REF=${IMAGE}:${SAFE_TAG}"
|
||||||
} >> "$GITHUB_ENV"
|
} >> "$GITHUB_ENV"
|
||||||
if [ "${{ github.event.release.prerelease }}" != "true" ]; then
|
if [ -n "${EVENT_TAG:-}" ] && [ "${{ github.event.release.prerelease }}" != "true" ]; then
|
||||||
echo "PUSH_LATEST=1" >> "$GITHUB_ENV"
|
echo "PUSH_LATEST=1" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
echo "Resolved image reference: ${IMAGE}:${SAFE_TAG}"
|
echo "Resolved image reference: ${IMAGE}:${SAFE_TAG}"
|
||||||
|
|||||||
@@ -237,6 +237,12 @@ kovi does not implement web user accounts. Keep the web UI on a trusted LAN/priv
|
|||||||
|
|
||||||
kovi is inspired by [KoInsight](https://github.com/Ko-Insight/KoInsight), an MIT-licensed self-hosted KOReader statistics project, and by KOReader's own statistics model. It preserves the useful manual-SQLite + KOReader-plugin workflow while using a fresh application architecture and UX.
|
kovi is inspired by [KoInsight](https://github.com/Ko-Insight/KoInsight), an MIT-licensed self-hosted KOReader statistics project, and by KOReader's own statistics model. It preserves the useful manual-SQLite + KOReader-plugin workflow while using a fresh application architecture and UX.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you find **kovi** useful, consider supporting its development:
|
||||||
|
|
||||||
|
[](https://ko-fi.com/mdaleo404)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
kovi is released under the GNU General Public License v3.0; see [LICENSE](LICENSE). The container image also bundles Debian's unmodified `calibre` package (GPLv3); see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
kovi is released under the GNU General Public License v3.0; see [LICENSE](LICENSE). The container image also bundles Debian's unmodified `calibre` package (GPLv3); see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||||
|
|||||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -6,8 +6,13 @@
|
|||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta id="themeColor" name="theme-color" content="#f6f3ec">
|
<meta id="themeColor" name="theme-color" content="#f6f3ec">
|
||||||
<title>kovi — Your reading, beautifully understood</title>
|
<title>kovi — Your reading, beautifully understood</title>
|
||||||
<link rel="icon" href="/favicon.png" type="image/png">
|
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||||||
<link rel="apple-touch-icon" href="/favicon.png">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="kovi">
|
||||||
|
<meta name="application-name" content="kovi">
|
||||||
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<link rel="stylesheet" href="/styles.css">
|
<link rel="stylesheet" href="/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "kovi",
|
||||||
|
"short_name": "kovi",
|
||||||
|
"description": "Your reading, beautifully understood.",
|
||||||
|
"start_url": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#f6f3ec",
|
||||||
|
"background_color": "#f6f3ec",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 258 KiB |
@@ -307,7 +307,7 @@ async function handleApi(req,res,url) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIME={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.gif':'image/gif','.ico':'image/x-icon','.zip':'application/zip'};
|
const MIME={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.gif':'image/gif','.ico':'image/x-icon','.webmanifest':'application/manifest+json','.zip':'application/zip'};
|
||||||
async function serveFile(res,filePath,{cache=false}={}) {
|
async function serveFile(res,filePath,{cache=false}={}) {
|
||||||
try {
|
try {
|
||||||
const st=await fsp.stat(filePath); if(!st.isFile()) return false;
|
const st=await fsp.stat(filePath); if(!st.isFile()) return false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|||||||
const source=fs.readFileSync(new URL('../public/app.js', import.meta.url),'utf8');
|
const source=fs.readFileSync(new URL('../public/app.js', import.meta.url),'utf8');
|
||||||
const html=fs.readFileSync(new URL('../public/index.html', import.meta.url),'utf8');
|
const html=fs.readFileSync(new URL('../public/index.html', import.meta.url),'utf8');
|
||||||
const css=fs.readFileSync(new URL('../public/styles.css', import.meta.url),'utf8');
|
const css=fs.readFileSync(new URL('../public/styles.css', import.meta.url),'utf8');
|
||||||
|
const server=fs.readFileSync(new URL('../server.mjs', import.meta.url),'utf8');
|
||||||
|
|
||||||
test('manual SQLite upload attaches the browser CSRF token',()=>{
|
test('manual SQLite upload attaches the browser CSRF token',()=>{
|
||||||
assert.match(source,/setRequestHeader\('X-Kovi-CSRF',csrf\)/);
|
assert.match(source,/setRequestHeader\('X-Kovi-CSRF',csrf\)/);
|
||||||
@@ -159,6 +160,23 @@ test('sidebar and empty state use the kovi logo assets instead of split legacy b
|
|||||||
assert.match(html,/id="themeColor"/);assert.match(source,/themeColor/);
|
assert.match(html,/id="themeColor"/);assert.match(source,/themeColor/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the favicon uses the kovi mascot instead of the old svg mark',()=>{
|
test('the favicon set uses the kovi mascot assets instead of the old svg mark',()=>{
|
||||||
assert.match(html,/rel="icon" href="\/favicon\.png"/);assert.doesNotMatch(html,/logo\.svg/);
|
assert.match(html,/rel="icon" type="image\/png" sizes="96x96" href="\/favicon-96x96\.png"/);
|
||||||
|
assert.match(html,/rel="icon" type="image\/svg\+xml" href="\/favicon\.svg"/);
|
||||||
|
assert.match(html,/rel="shortcut icon" href="\/favicon\.ico"/);
|
||||||
|
assert.match(html,/rel="apple-touch-icon" sizes="180x180" href="\/apple-touch-icon\.png"/);
|
||||||
|
assert.match(html,/rel="manifest" href="\/site\.webmanifest"/);
|
||||||
|
assert.doesNotMatch(html,/favicon\.png/);assert.doesNotMatch(html,/logo\.svg/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the web app manifest brands kovi and points at the installed icons',()=>{
|
||||||
|
const manifest=JSON.parse(fs.readFileSync(new URL('../public/site.webmanifest', import.meta.url),'utf8'));
|
||||||
|
assert.equal(manifest.name,'kovi');assert.equal(manifest.short_name,'kovi');
|
||||||
|
assert.deepEqual(manifest.icons.map(icon=>icon.src),['/web-app-manifest-192x192.png','/web-app-manifest-512x512.png']);
|
||||||
|
for(const icon of manifest.icons){assert.equal(icon.type,'image/png');assert.match(icon.sizes,/^\d+x\d+$/);}
|
||||||
|
assert.match(manifest.theme_color,/^#[0-9a-f]{6}$/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the static server serves the web app manifest with its manifest MIME type',()=>{
|
||||||
|
assert.match(server,/\.webmanifest':'application\/manifest\+json'/);
|
||||||
});
|
});
|
||||||
|
|||||||