Compare commits

...
18 Commits
Author SHA1 Message Date
polv 23e4be8b15 Merge pull request 'feat/tauri-mobile' (#1) from feat/tauri-mobile into main
Reviewed-on: #1
2026-07-28 06:55:00 +00:00
polv f4a0c8d883 build with redirected images 2026-07-28 13:53:21 +07:00
polv 5a8edf9330 add images to git 2026-07-28 13:20:46 +07:00
polv 412905ade6 add video 2026-07-28 12:12:59 +07:00
polv 3937086dbe add video 2026-07-28 11:57:30 +07:00
polv 92173199c9 add video and test dedup 2026-07-17 18:21:18 +07:00
polv c475960219 add video to mobile via YouTube 2026-07-15 23:05:19 +07:00
polv 8e6c5e2bda image compression 2026-07-13 20:12:51 +07:00
polv e76a45f1d6 add video 2026-07-13 17:16:44 +07:00
polv d632c946e4 add video; fix group-similar.js for public folder 2026-07-13 17:02:35 +07:00
polv 05e45cd936 add video and some fixes 2026-07-01 21:47:34 +07:00
polv 40edfe44e9 add Android build; remove bulk video compression as it's impractical 2026-06-27 19:52:00 +07:00
polv f2f6c28989 tauri build fails of large vdo 2026-06-26 00:25:35 +07:00
polv 87a3c1b641 video compress to webm 2026-06-25 17:56:03 +07:00
polv d72377b0bb ensure it builds 2026-06-25 16:51:22 +07:00
polv 3df6d87ab5 convert all images to webp 2026-06-25 16:34:27 +07:00
polv 3e3d2dcbcb tauri to manage and kill ws-server 2026-06-25 15:18:23 +07:00
polv 126a3749f9 add tauri 2026-06-25 13:18:21 +07:00
12482 changed files with 28981 additions and 10354 deletions

No files matched your search

+2 -1
View File
@@ -1 +1,2 @@
node_modules
node_modules
.env
+4
View File
@@ -6,6 +6,10 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.maxTokenizationLineLength": 2500,
},
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
},
"[vue]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
+5 -3
View File
@@ -89,9 +89,11 @@ docs/ # VitePress documentation root
Downloader.vue # Vue component for entering a video filename (web interface / video input UI)
Video.vue # Vue component to display videos and linked timestamps
public/ # Directory to put downloaded videos
*.md # Markdown files with content
[video-filename]/ # Screenshot folders (auto-generated)
*.png # Timestamped screenshots
images/
[video-filename]/ # Screenshot folders (auto-generated)
*.webp # Timestamped screenshots
[video-filename].webm # Downloaded videos
[video-filename].md # Markdown files with content
server.js # WebSocket server for handling downloads
group-similar.js # Script for deduplicating similar screenshots
```
+93
View File
@@ -0,0 +1,93 @@
import fs from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
const SRC_DIR = path.resolve("docs/public"); // change if needed
const OUT_DIR = path.resolve("docs/public/images"); // change if needed
const MAX_W = 800;
const MAX_H = 600;
const FORMAT = "webp";
const WEBP_QUALITY = 75; // tweak 60-85 if you want smaller/larger
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
console.log(dir, entries.filter((e) => !e.isDirectory()).length);
const files = [];
for (const e of entries) {
const p = path.join(dir, e.name);
if (p === OUT_DIR) continue;
if (e.isDirectory()) files.push(...(await walk(p)));
else files.push(p);
}
return files;
}
function isImage(file) {
return /\.(png|jpe?g|webp|avif)$/i.test(file);
}
async function ensureDir(p) {
await fs.mkdir(p, { recursive: true });
}
async function processOne(inPath, outPathBase) {
await ensureDir(path.dirname(outPathBase));
await sharp(inPath)
.rotate() // respects EXIF orientation
.resize({
width: MAX_W,
height: MAX_H,
fit: "inside", // preserve aspect, fit within box
withoutEnlargement: true, // don't upscale small images
})
.toFormat(FORMAT, {
quality: WEBP_QUALITY,
effort: 4,
})
.toFile(`${outPathBase}.${FORMAT}`);
}
async function main() {
const files = (await walk(SRC_DIR)).filter(isImage);
await ensureDir(OUT_DIR);
let done = 0;
for (const inPath of files) {
const rel = path.relative(SRC_DIR, inPath);
// strip original extension and write same relative path
const outBase = path
.join(OUT_DIR, rel)
.replace(/\.(png|jpe?g|webp|avif)$/i, "");
const outPath = `${outBase}.${FORMAT}`;
// simple cache: skip if output exists and is newer than input
try {
const [stIn, stOut] = await Promise.all([
fs.stat(inPath),
fs.stat(outPath),
]);
if (stOut.mtimeMs >= stIn.mtimeMs) continue;
} catch {
// output missing -> process
}
await processOne(inPath, outBase);
done++;
}
console.log(
`Optimized ${done} image(s) to max ${MAX_W}x${MAX_H} (${FORMAT}).`,
);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
-2
View File
@@ -1,2 +0,0 @@
**/*.png
**/*.jpg
+1
View File
@@ -1,2 +1,3 @@
/dist
/cache
/.temp
+71 -14
View File
@@ -1,6 +1,13 @@
import { defineConfig } from 'vitepress'
import { defineConfig } from "vitepress";
import { execSync } from "node:child_process";
const TAURI_HOST = ''
const host = process.env.TAURI_DEV_HOST;
const {
IMG_ORIGIN = `https://git.polv.cc/polv/yt2press/raw/branch/${
execSync("git branch --show-current").toString().trimEnd()
}/docs/public`,
} = process.env;
// https://vitepress.dev/reference/site-config
export default defineConfig({
@@ -8,39 +15,89 @@ export default defineConfig({
description: "video to vitepress",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
],
nav: [{ text: "Home", link: "/" }],
sidebar: [
{
text: 'Preview',
text: "Preview",
// items: [
// { text: 'Markdown Examples', link: '/markdown-examples' },
// { text: 'Runtime API Examples', link: '/api-examples' }
// ]
}
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
]
{ icon: "github", link: "https://git.polv.cc/polv/yt2press" },
],
},
markdown: {
image: {
lazyLoading: true,
},
config(md) {
// production-only gate
const isProd =
process.env.NODE_ENV === "production" ||
process.env.VITEPRESS_ENV === "production"; // extra safety
if (isProd) {
md.core.ruler.push("rewrite-image-urls", (state) => {
if (!state.tokens) return;
for (const token of state.tokens) {
if (token.type !== "inline") continue;
for (const child of token.children ?? []) {
// images: ![alt](src)
if (
child.type === "image" &&
typeof child.attrGet("src") === "string"
) {
const src = child.attrGet("src");
// rewrite only /images/* (adjust if your paths differ)
if (src && src.startsWith("/images/")) {
child.attrSet("src", `${IMG_ORIGIN}${src}`);
}
}
}
}
});
}
},
},
vite: {
clearScreen: false,
server: {
// make sure this port matches the devUrl port in tauri.conf.json file
port: 5173,
// Tauri expects a fixed port, fail if that port is not available
strictPort: true,
// if the host Tauri is expecting is set, use it
host: host || false,
watch: {
// tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
proxy: {
'/socket': {
target: 'ws://localhost:8080',
"/socket": {
target: "ws://localhost:8080",
ws: true,
rewriteWsOrigin: true,
}
},
},
},
}
})
// Env variables starting with the item of `envPrefix` will be exposed in tauri's source code through `import.meta.env`.
envPrefix: ["VITE_", "TAURI_ENV_*"],
build: {
// Tauri uses Chromium on Windows and WebKit on macOS and Linux
target:
process.env.TAURI_ENV_PLATFORM == "windows" ? "chrome105" : "safari13",
// don't minify for debug builds
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_ENV_DEBUG,
},
},
});
@@ -24,7 +24,7 @@ function onSubmit(ev: SubmitEvent) {
ev.preventDefault();
if (!q.value) return;
const src = "/" + q.value;
const src = "/" + encodeURIComponent(q.value);
const isSameSrc = videoSrc.value === src;
videoSrc.value = src;
+31 -5
View File
@@ -1,17 +1,29 @@
<template>
<Teleport to="body">
<video v-if="videoURL" controls :src="videoURL" ref="video">Hello</video>
<div :class="$style.container">
<YouTube v-if="youtubeID" :id="youtubeID" />
<video
v-else-if="videoURL"
controls
:src="videoURL"
ref="video"
@error="onVideoNotFound"
/>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { useData } from "vitepress";
import { computed, useTemplateRef, watch } from "vue";
import { computed, ref, useTemplateRef, watch } from "vue";
import YouTube from "./YouTube.vue";
const { frontmatter } = useData<{ video?: string }>();
const videoURL = computed(() => frontmatter.value?.video);
const videoEl = useTemplateRef("video");
const youtubeID = ref("");
function setImgClick() {
document.removeEventListener("click", imgClickListener);
document.addEventListener("click", imgClickListener);
@@ -43,16 +55,30 @@ watch(videoURL, () => {
setImgClick();
});
setTimeout(() => setImgClick(), 500);
function onVideoNotFound() {
const m = /\[(.+?)\]\.\S+$/.exec(videoURL.value || "") || [];
youtubeID.value = m[1] || "";
}
//@ts-ignore
const { SSR } = import.meta.env;
if (!SSR) {
setTimeout(() => setImgClick(), 500);
}
</script>
<style scoped>
video {
<style module>
.container {
width: 400px;
overflow: visible;
position: fixed;
bottom: 10px;
left: 10px;
z-index: 100;
> * {
width: 100%;
}
}
</style>
@@ -0,0 +1,39 @@
<template>
<div :class="$style.container">
<iframe
:class="$style.iframe"
width="100%"
height="100%"
:src="'https://www.youtube.com/embed/' + $props.id"
frameborder="0"
allow="
accelerometer;
autoplay;
encrypted-media;
gyroscope;
picture-in-picture;
fullscreen;
"
>
</iframe>
</div>
</template>
<script setup lang="ts">
defineProps<{ id: string; title?: string }>();
</script>
<style module>
.container {
padding-bottom: 56.25%;
position: relative;
}
.iframe {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
</style>
+19 -8
View File
@@ -1,5 +1,6 @@
import { type Plugin, type ViteDevServer } from "vite";
import { readFileSync, mkdirSync, writeFileSync } from "fs";
import sharp from "sharp";
export default function ytParsePlugin(): Plugin {
return {
@@ -8,19 +9,22 @@ export default function ytParsePlugin(): Plugin {
server.ws.on("download-q", ({ q }) => {
try {
mkdirSync(`docs/${q}`);
mkdirSync(`docs/public/images/${q}`);
} catch (e) {}
let outMd = "";
const re = /^(?<min>\d{1,2}):(?<sec>\d{2})\s*$/;
for (const line of readFileSync(`docs/${q}.md`, "utf-8").trimEnd().split("\n")) {
for (const line of readFileSync(`docs/${q}.md`, "utf-8")
.trimEnd()
.split("\n")) {
const m = re.exec(line);
if (m) {
const { min, sec } = m.groups!;
const t = parseInt(min) * 60 + parseInt(sec);
server.ws.send("seek-q", { t });
outMd += `[${line}](./${q}/${t}.png)`;
outMd += `[${line}](/images/${q}/${t}.webp)`;
} else {
outMd += line + "\n";
}
@@ -30,12 +34,19 @@ export default function ytParsePlugin(): Plugin {
});
server.ws.on("timed-q", ({ q, t, data }) => {
writeFileSync(
`docs/${q}/${t}.png`,
data.replace(/^data:image\/png;base64,/, ""),
"base64",
);
sharp(
Buffer.from(data.replace(/^data:image\/png;base64,/, ""), "base64"),
)
.rotate()
.resize({
width: 800,
height: 600,
fit: "inside",
withoutEnlargement: true,
})
.webp({ quality: 75 })
.toFile(`docs/public/images/${q}/${t}.png`);
});
},
};
}
}
File diff suppressed because it is too large. Load diff
+6
View File
@@ -9,6 +9,12 @@ hero:
<Downloader />
- [【2026年最新版】タイ入国前に必ず見て!入国前・入国審査・入国後の注意事項まとめ](./【2026年最新版】タイ入国前に必ず見て!入国前・入国審査・入国後の注意事項まとめ.md)
- [比西藏、新疆、蒙古近 【韓國為啥不是中國的?】文化最像 卻沒被併吞?](./比西藏、新疆、蒙古近_【韓國為啥不是中國的?】文化最像_卻沒被併吞?.md)
- [泰國為什麼掉隊了?越南、大馬、印尼都在搶產業,曼谷還能翻身嗎?](./泰國為什麼掉隊了?越南、大馬、印尼都在搶產業,曼谷還能翻身嗎?.md)
- [39年招牌走入歷史!統一砸290億買家樂福又改名、入股Lopia,跟量販店危機有關?【懂商業看商周】Ep.138](./39年招牌走入歷史!統一砸290億買家樂福又改名、入股Lopia,跟量販店危機有關?【懂商業看商周】Ep.138.md)
- [Airbnb 泡沫化危機!各國政府聯手「下架」,房產投資者的惡夢來了?](./Airbnb_泡沫化危機!各國政府聯手「下架」,房產投資者的惡夢來了?.md)
- [曾統治全球99%電腦,為何死於一封公開信?賈伯斯當年手撕 Flash 的背後,隱藏著多恐怖的商戰陰謀!](./曾統治全球99_電腦,為何死於一封公開信?賈伯斯當年手撕_Flash_的背後,隱藏著多恐怖的商戰陰謀!.md)
- [緬甸為何痛恨羅興亞人? 佛教恐怖份子的誕生](./緬甸為何痛恨羅興亞人?_佛教恐怖份子的誕生.md)
- [走进普拉博沃时代的印度尼西亚 Prabowo’s first year: redefining Indonesia’s future | 世界大解说](./走进普拉博沃时代的印度尼西亚_Prabowo’s_first_year:_redefining_Indonesia’s_future_|_世界大解说.md)
- [东非最发达的城市现在怎么样了?揭秘肯尼亚不为人知的一面](./东非最发达的城市现在怎么样了?揭秘肯尼亚不为人知的一面.md)
+1 -1
View File
@@ -1,3 +1,3 @@
*
/*.*
!.gitignore
!README.md
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Loaded 100 of 12482 files, more files were not shown because too many files have changed in this diff. Show more