YouTube JS API seek

This commit is contained in:
polv committed 2026-09-23 21:05:12 +07:00
1 parent 218ba874f9
commit d9777c3c82
6 files changed
+99 -12

No files matched your search

+14 -3
View File
@@ -4,15 +4,26 @@ import { execSync } from "node:child_process";
const host = process.env.TAURI_DEV_HOST; const host = process.env.TAURI_DEV_HOST;
const { const {
IMG_ORIGIN = `https://git.polv.cc/polv/yt2press/raw/branch/${ IMG_ORIGIN = `https://git.polv.cc/polv/yt2press/raw/branch/${execSync(
execSync("git branch --show-current").toString().trimEnd() "git branch --show-current",
}/docs/public`, )
.toString()
.trimEnd()}/docs/public`,
} = process.env; } = process.env;
// https://vitepress.dev/reference/site-config // https://vitepress.dev/reference/site-config
export default defineConfig({ export default defineConfig({
title: "yt2press", title: "yt2press",
description: "video to vitepress", description: "video to vitepress",
head: [
[
"script",
{
async: "async",
src: "https://www.youtube.com/iframe_api",
},
],
],
themeConfig: { themeConfig: {
// https://vitepress.dev/reference/default-theme-config // https://vitepress.dev/reference/default-theme-config
nav: [{ text: "Home", link: "/" }], nav: [{ text: "Home", link: "/" }],
+8 -5
View File
@@ -1,7 +1,7 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div :class="$style.container"> <div :class="$style.container">
<YouTube v-if="youtubeID" :id="youtubeID" /> <YouTube ref="yt" v-if="youtubeID" :id="youtubeID" />
<video <video
v-else-if="videoURL" v-else-if="videoURL"
controls controls
@@ -21,6 +21,7 @@ import YouTube from "./YouTube.vue";
const { frontmatter } = useData<{ video?: string }>(); const { frontmatter } = useData<{ video?: string }>();
const videoURL = computed(() => frontmatter.value?.video); const videoURL = computed(() => frontmatter.value?.video);
const videoEl = useTemplateRef("video"); const videoEl = useTemplateRef("video");
const ytEl = useTemplateRef("yt");
const youtubeID = ref(""); const youtubeID = ref("");
@@ -41,14 +42,15 @@ function imgClickListener(ev: PointerEvent) {
if (!(ev.target instanceof HTMLImageElement)) return; if (!(ev.target instanceof HTMLImageElement)) return;
const img = ev.target; const img = ev.target;
if (!videoEl.value) return;
const v = videoEl.value;
const [, min, sec] = /^(\d+)\:(\d+)$/.exec(img.alt) || []; const [, min, sec] = /^(\d+)\:(\d+)$/.exec(img.alt) || [];
const t = min && sec ? parseInt(min) * 60 + parseInt(sec) : null; const t = min && sec ? parseInt(min) * 60 + parseInt(sec) : null;
if (!t) return; if (!t) return;
v.currentTime = t; if (videoEl.value) {
videoEl.value.currentTime = t;
} else if (ytEl.value) {
ytEl.value.seekTo(t);
}
} }
watch(videoURL, () => { watch(videoURL, () => {
@@ -59,6 +61,7 @@ function onVideoNotFound() {
const m = /\[(.+?)\]\.\S+$/.exec(videoURL.value || "") || []; const m = /\[(.+?)\]\.\S+$/.exec(videoURL.value || "") || [];
youtubeID.value = m[1] || ""; youtubeID.value = m[1] || "";
} }
onVideoNotFound();
//@ts-ignore //@ts-ignore
const { SSR } = import.meta.env; const { SSR } = import.meta.env;
+25 -2
View File
@@ -1,10 +1,11 @@
<template> <template>
<div :class="$style.container"> <div :class="$style.container">
<iframe <iframe
id="yt-player"
:class="$style.iframe" :class="$style.iframe"
width="100%" width="100%"
height="100%" height="100%"
:src="'https://www.youtube.com/embed/' + $props.id" :src="url.href"
frameborder="0" frameborder="0"
allow=" allow="
accelerometer; accelerometer;
@@ -20,7 +21,29 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ id: string; title?: string }>(); import { onMounted, ref } from "vue";
const props = defineProps<{ id: string; title?: string }>();
defineExpose({ seekTo });
const url = new URL("https://www.youtube.com/embed/" + props.id);
url.searchParams.set("enablejsapi", "1");
const player = ref();
onMounted(() => {
const { YT } = window as any;
if (YT?.Player) {
player.value = new YT.Player("yt-player");
}
});
function seekTo(t: number) {
if (player.value) {
player.value.seekTo(t, true);
}
}
</script> </script>
<style module> <style module>
+2 -2
View File
@@ -1,6 +1,6 @@
body { /* body {
padding-top: env(safe-area-inset-top); padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left); padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right); padding-right: env(safe-area-inset-right);
} } */
@@ -4,6 +4,12 @@ dedup: 0.05
video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋 [gfx_EnxmOEY].webm' video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋 [gfx_EnxmOEY].webm'
--- ---
你以為 TOTO 只是一家做馬桶的公司?事實上,它花了近 50 年,把最擅長的「燒陶瓷」一路做到半導體產業最深處。
一塊不起眼的靜電吸盤,如今成了晶圓製造不可或缺的關鍵零件。當 AI 浪潮席捲全球,這家百年馬桶公司,也意外站上了科技產業的風口。本期就來看看,TOTO 如何用一門看似無聊的手藝,埋下一顆等待了半個世紀的暗棋。
## AI 報告,為何拉升一支「馬桶股」?
![0:00](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/0_00.webp) ![0:00](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/0_00.webp)
0:00 0:00
@@ -253,6 +259,7 @@ video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋
![1:28](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/1_28.webp) ![1:28](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/1_28.webp)
1:28 1:28
## 馬桶界的愛馬仕:TOTO 的前兩場豪賭
![1:30](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/1_30.webp) ![1:30](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/1_30.webp)
1:30 1:30
@@ -1080,6 +1087,8 @@ video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋
<br/> <br/>
7:04 7:04
## 第三場豪賭:不能只靠一條腿走路
![7:06](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/7_06.webp) ![7:06](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/7_06.webp)
7:06 7:06
@@ -1531,6 +1540,8 @@ video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋
![10:12](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/10_12.webp) ![10:12](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/10_12.webp)
10:12 10:12
## 半導體首戰慘敗,卻留下關鍵技術
![10:14](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/10_14.webp) ![10:14](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/10_14.webp)
10:14 10:14
@@ -2014,6 +2025,7 @@ video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋
![13:36](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/13_36.webp) ![13:36](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/13_36.webp)
13:36 13:36
## 靜電吸盤:一塊陶瓷如何卡住晶圓廠
![13:38](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/13_38.webp) ![13:38](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/13_38.webp)
13:38 13:38
@@ -2489,6 +2501,8 @@ video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋
![16:46](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/16_46.webp) ![16:46](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/16_46.webp)
16:46 16:46
## AI 點火:五十年的暗棋終於爆發
![16:48](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/16_48.webp) ![16:48](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/16_48.webp)
16:48 16:48
@@ -2913,6 +2927,8 @@ video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋
![19:38](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/19_38.webp) ![19:38](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/19_38.webp)
19:38 19:38
## 中國市場失血:TOTO 的冰與火
![19:40](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/19_40.webp) ![19:40](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/19_40.webp)
19:40 19:40
@@ -3330,6 +3346,7 @@ video: '/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋
![22:14](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/22_14.webp) ![22:14](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/22_14.webp)
22:14 22:14
## 不追風口,為什麼反而等到了風口?
![22:16](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/22_16.webp) ![22:16](/images/一家馬桶公司,為什麼成了AI命脈?TOTO埋了50年的暗棋/22_16.webp)
22:16 22:16
@@ -1,11 +1,44 @@
package cc.polv.yt2press package cc.polv.yt2press
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : TauriActivity() { class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val rootView: View = findViewById(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val bottomPadding = if (imeVisible) imeHeight else systemBars.bottom
v.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
bottomPadding
)
insets
}
}
override fun onWebViewCreate(webView: WebView) {
super.onWebViewCreate(webView)
webView.setBackgroundColor(0x00000000)
webView.fitsSystemWindows = true
android.util.Log.i("MainActivity", "WebView created, waiting for Rust hide() call...")
} }
} }