This commit is contained in:
asimov
2025-05-27 15:23:51 +12:00
parent 037edfa7b5
commit 0de2d786f0
20 changed files with 6948 additions and 0 deletions

40
entrypoints/background.ts Normal file
View 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
}
})
}
})

View 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';
}
}

View 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); /* 轻微放大 */
}

View 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
View 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);
});

View 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%;
}