init
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
assets/typescript.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
9
components/counter.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function setupCounter(element: HTMLButtonElement) {
|
||||
let counter = 0;
|
||||
const setCounter = (count: number) => {
|
||||
counter = count;
|
||||
element.innerHTML = `count is ${counter}`;
|
||||
};
|
||||
element.addEventListener('click', () => setCounter(counter + 1));
|
||||
setCounter(0);
|
||||
}
|
||||
40
entrypoints/background.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
interface FetchMissavRequest {
|
||||
type: 'fetchMissav'
|
||||
url: string
|
||||
}
|
||||
|
||||
export default defineBackground({
|
||||
main() {
|
||||
chrome.runtime.onMessage.addListener((
|
||||
request: FetchMissavRequest,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response: any) => void
|
||||
) => {
|
||||
if (request.type === 'fetchMissav') {
|
||||
console.log('fetchMissav', request.url)
|
||||
fetch(request.url, {
|
||||
redirect: 'follow',
|
||||
// Add headers to mimic a browser request
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
console.log('response', response)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
return response.text()
|
||||
})
|
||||
.then(html => {
|
||||
console.log('html', html)
|
||||
sendResponse({ success: true, html })
|
||||
})
|
||||
.catch(error => {
|
||||
sendResponse({ success: false, error: error.message })
|
||||
})
|
||||
return true // Keep the message channel open for the async response
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
169
entrypoints/content/index.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import './style.css'
|
||||
|
||||
// same storage key as popup
|
||||
const STORAGE_KEY = 'feature_enabled';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.javdb.com/v/*'],
|
||||
async main() {
|
||||
// check if the feature is enabled
|
||||
const isEnabled = await storage.getItem(`sync:${STORAGE_KEY}`) ?? true;
|
||||
|
||||
if (!isEnabled) {
|
||||
console.log('❌ [JavDB Helper] Feature is disabled.');
|
||||
hideFloatingButton();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🚀 [JavDB Helper] Feature is enabled, running script...');
|
||||
|
||||
// core logic, now includes the control of button visibility
|
||||
const processPage = async () => {
|
||||
// check if the path is the one we care about
|
||||
if (window.location.pathname.startsWith('/v/')) {
|
||||
const videoNumber = getVideoNumber();
|
||||
if (videoNumber) {
|
||||
const missavUUID = await getMissavUUID(videoNumber);
|
||||
if (missavUUID) {
|
||||
// if the UUID is successfully fetched, create or update the button
|
||||
addOrUpdateFloatingButton(missavUUID);
|
||||
} else {
|
||||
// if the UUID is not fetched, hide the button
|
||||
hideFloatingButton();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if not in the video detail page, hide the button
|
||||
hideFloatingButton();
|
||||
}
|
||||
};
|
||||
|
||||
// listen for URL path change
|
||||
let lastPathname = window.location.pathname;
|
||||
new MutationObserver(() => {
|
||||
const currentPathname = window.location.pathname;
|
||||
if (currentPathname !== lastPathname) {
|
||||
lastPathname = currentPathname;
|
||||
processPage();
|
||||
}
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// execute once when page is loaded
|
||||
processPage();
|
||||
}
|
||||
});
|
||||
|
||||
// get target video number
|
||||
function getVideoNumber(): string {
|
||||
// get target video number
|
||||
const targetElement = document.querySelector('a.button.is-white.copy-to-clipboard')
|
||||
if (!targetElement) {
|
||||
console.log('not found target element')
|
||||
return ''
|
||||
}
|
||||
|
||||
const targetNumber = targetElement.getAttribute('data-clipboard-text')
|
||||
if (!targetNumber) {
|
||||
console.log('no target number')
|
||||
return ''
|
||||
}
|
||||
|
||||
console.log('targetNumber', targetNumber)
|
||||
return targetNumber
|
||||
}
|
||||
|
||||
// get missav UUID
|
||||
async function getMissavUUID(videoNumber: string): Promise<string> {
|
||||
const lowerTargetNumber = videoNumber.toLowerCase()
|
||||
const targetUrl = `https://missav.ws/dm1/en/${lowerTargetNumber}`
|
||||
|
||||
try {
|
||||
// 'response' here is the object { success: boolean, html?: string, error?: string }
|
||||
// sent from your background script. It is NOT a Fetch API Response object.
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'fetchMissav',
|
||||
url: targetUrl
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error)
|
||||
}
|
||||
|
||||
// Directly use the 'html' property which is already a string.
|
||||
// DO NOT try to call response.text().
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(response.html, 'text/html')
|
||||
|
||||
const scripts = doc.getElementsByTagName('script')
|
||||
// console.log('scripts', scripts)
|
||||
|
||||
for (const script of scripts) {
|
||||
const content = script.textContent || ''
|
||||
if (content.includes('thumbnail')) {
|
||||
const urlsMatch = content.match(/urls:\s*\[(.*?)\]/s)
|
||||
if (urlsMatch) {
|
||||
const firstUrl = urlsMatch[1].split(',')[0].trim().replace(/"/g, '').replace(/\\/g, '')
|
||||
const uuidMatch = firstUrl.match(/\/([0-9a-f-]+)\/seek\//i)
|
||||
if (uuidMatch) {
|
||||
console.log('uuidMatch', uuidMatch[1])
|
||||
return uuidMatch[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.warn('uuid not found')
|
||||
return ''
|
||||
} catch (error) {
|
||||
// This is where your error was being caught.
|
||||
console.error('fetch or parse document error:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create a new floating button, or update the existing button's link
|
||||
* @param missavUUID - the UUID used to generate the playback link
|
||||
*/
|
||||
function addOrUpdateFloatingButton(missavUUID: string) {
|
||||
const BUTTON_ID = 'wxt-iina-floating-button';
|
||||
const iinaUrl = `iina://weblink?url=https://surrit.com/${missavUUID}/playlist.m3u8`;
|
||||
console.log('Playlist URL:', iinaUrl);
|
||||
|
||||
// check if the button already exists
|
||||
let iinaButton = document.getElementById(BUTTON_ID) as HTMLAnchorElement | null;
|
||||
|
||||
if (!iinaButton) {
|
||||
// if the button does not exist, create it
|
||||
iinaButton = document.createElement('a');
|
||||
iinaButton.id = BUTTON_ID;
|
||||
iinaButton.className = 'wxt-iina-button'; // use class name to apply style
|
||||
iinaButton.innerHTML = `
|
||||
<i class="icon-play"></i>
|
||||
<span>IINA</span>
|
||||
`;
|
||||
|
||||
// add the button to the body
|
||||
document.body.appendChild(iinaButton);
|
||||
|
||||
// click event (since we use the a tag's href directly, we can omit this, but it's better to add it to prevent default behavior)
|
||||
iinaButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = iinaButton!.href;
|
||||
});
|
||||
}
|
||||
|
||||
// update the href property of the button and ensure it is visible
|
||||
iinaButton.href = iinaUrl;
|
||||
iinaButton.style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* hide the floating button
|
||||
*/
|
||||
function hideFloatingButton() {
|
||||
const BUTTON_ID = 'wxt-iina-floating-button';
|
||||
const iinaButton = document.getElementById(BUTTON_ID);
|
||||
if (iinaButton) {
|
||||
iinaButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
29
entrypoints/content/style.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.wxt-iina-button {
|
||||
/* 定位和层级 */
|
||||
position: fixed;
|
||||
top: 100px; /* 距离顶部 150px,可以按喜好调整 */
|
||||
right: 0px; /* 距离右侧 20px */
|
||||
z-index: 9999; /* 确保在页面最上层 */
|
||||
|
||||
/* 外观和布局 */
|
||||
display: flex; /* 使用 flexbox 让图标和文字居中 */
|
||||
align-items: center;
|
||||
gap: 5px; /* 图标和文字的间距 */
|
||||
background-color: #3173dc; /* 一个不错的蓝色 */
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
/* border-radius: 5px; 圆角 */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); /* 添加阴影增加立体感 */
|
||||
|
||||
/* 交互效果 */
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease-in-out, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.wxt-iina-button:hover {
|
||||
transform: scale(1.05); /* 轻微放大 */
|
||||
}
|
||||
20
entrypoints/popup/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Settings</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="setting-row">
|
||||
<label for="feature-toggle">Feature Enabled</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="feature-toggle">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
entrypoints/popup/main.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import './style.css';
|
||||
|
||||
const featureToggle = document.querySelector<HTMLInputElement>('#feature-toggle');
|
||||
|
||||
// storage key, define as constant, for easy use in multiple places
|
||||
const STORAGE_KEY = 'feature_enabled';
|
||||
|
||||
// 1. when popup is opened, load and set the initial state of the switch
|
||||
// WXT provided storage API is a wrapper of chrome.storage, usage is basically the same
|
||||
storage.getItem(`sync:${STORAGE_KEY}`).then((result) => {
|
||||
// if there is no value in the storage, we default to true
|
||||
// '??' is the nullish coalescing operator
|
||||
const isEnabled = (result as boolean | null) ?? true;
|
||||
if (featureToggle) {
|
||||
featureToggle.checked = isEnabled;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. listen for the change of the switch state, and save the new setting
|
||||
featureToggle?.addEventListener('change', () => {
|
||||
const isEnabled = featureToggle.checked;
|
||||
storage.setItem(`sync:${STORAGE_KEY}`, isEnabled);
|
||||
});
|
||||
75
entrypoints/popup/style.css
Normal file
@@ -0,0 +1,75 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
width: 180px; /* 给 popup 一个合适的宽度 */
|
||||
padding: 0 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#app {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 开关的样式 */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #007bff;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
6475
package-lock.json
generated
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "jav-play",
|
||||
"description": "manifest.json description",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"zip": "wxt zip",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.323",
|
||||
"typescript": "^5.8.3",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
}
|
||||
BIN
public/JavPlayer.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/icon/16.png
Normal file
|
After Width: | Height: | Size: 362 B |
BIN
public/icon/32.png
Normal file
|
After Width: | Height: | Size: 571 B |
BIN
public/icon/48.png
Normal file
|
After Width: | Height: | Size: 725 B |
BIN
public/icon/64.png
Normal file
|
After Width: | Height: | Size: 910 B |
BIN
public/icon/96.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
38
structure.md
Normal file
@@ -0,0 +1,38 @@
|
||||
Here's a brief summary of each of these files and directories:
|
||||
|
||||
📂 {rootDir}/
|
||||
📁 .output/
|
||||
📁 .wxt/
|
||||
📁 assets/
|
||||
📁 components/
|
||||
📁 composables/
|
||||
📁 entrypoints/
|
||||
📁 hooks/
|
||||
📁 modules/
|
||||
📁 public/
|
||||
📁 utils/
|
||||
📄 .env
|
||||
📄 .env.publish
|
||||
📄 app.config.ts
|
||||
📄 package.json
|
||||
📄 tsconfig.json
|
||||
📄 web-ext.config.ts
|
||||
📄 wxt.config.ts
|
||||
|
||||
.output/: All build artifacts will go here
|
||||
.wxt/: Generated by WXT, it contains TS config
|
||||
assets/: Contains all CSS, images, and other assets that should be processed by WXT
|
||||
components/: Auto-imported by default, contains UI components
|
||||
composables/: Auto-imported by default, contains source code for your project's composable functions for Vue
|
||||
entrypoints/: Contains all the entrypoints that get bundled into your extension
|
||||
hooks/: Auto-imported by default, contains source code for your project's hooks for React and Solid
|
||||
modules/: Contains local WXT Modules for your project
|
||||
public/: Contains any files you want to copy into the output folder as-is, without being processed by WXT
|
||||
utils/: Auto-imported by default, contains generic utilities used throughout your project
|
||||
.env: Contains Environment Variables
|
||||
.env.publish: Contains Environment Variables for publishing
|
||||
app.config.ts: Contains Runtime Config
|
||||
package.json: The standard file used by your package manager
|
||||
tsconfig.json: Config telling TypeScript how to behave
|
||||
web-ext.config.ts: Configure Browser Startup
|
||||
wxt.config.ts: The main config file for WXT projects
|
||||
3
tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json"
|
||||
}
|
||||
18
wxt.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
name: 'Jav Play',
|
||||
description: 'Play video directly in JAVDB',
|
||||
permissions: [
|
||||
'storage',
|
||||
'scripting'
|
||||
],
|
||||
// Explicitly grant permission for the background script to access this host.
|
||||
host_permissions: [
|
||||
'*://*.javdb.com/*',
|
||||
'*://*.missav.ws/*'
|
||||
],
|
||||
},
|
||||
});
|
||||