|
// ==UserScript== |
|
// @name Twitch - Open in IINA |
|
// @match https://www.twitch.tv |
|
// @match https://www.twitch.tv/* |
|
// ==/UserScript== |
|
|
|
;(() => { |
|
const ID = `iina-${Date.now()}-${Math.floor(Math.random() * 1000000)}` |
|
const MENU_VIEWPORT_MARGIN = 5 |
|
const MENU_POINTER_MARGIN = 5 |
|
const VIDEO_URL_PATTERN = /^https:\/\/www\.twitch\.tv\/(?:videos\/)?([^\/]+)$/ |
|
const RESERVED_USERNAMES = [ |
|
'directory', |
|
'settings', |
|
'privacy', |
|
'subscriptions', |
|
'inventory', |
|
'wallet', |
|
'settings', |
|
'downloads', |
|
'jobs', |
|
'store', |
|
'turbo' |
|
] |
|
|
|
const style = document.createElement('style') |
|
style.textContent = ` |
|
.${ID}-menu { |
|
min-width: 150px; |
|
background-color: rgb(50, 50, 57); |
|
border-radius: 6px; |
|
box-shadow: rgba(0, 0, 0, 0.6) 0px 4px 8px 0px, rgba(0, 0, 0, 0.4) 0px 0px 4px 0px; |
|
text-align: center; |
|
position: fixed; |
|
left: -1000px; |
|
top: -1000px; |
|
padding: 8px; |
|
transition: opacity .2s, transform .2s; |
|
z-index: 1000000; |
|
|
|
&[inert] { |
|
opacity: 0; |
|
transform: scale(.95); |
|
pointer-events: none; |
|
} |
|
|
|
button { |
|
width: 100%; |
|
background-color: transparent; |
|
appearance: none; |
|
position: relative; |
|
padding: 6px; |
|
font-size: 14px; |
|
font-weight: 400; |
|
line-height: 19.6px; |
|
color: rgb(239, 239, 241); |
|
border: none; |
|
outline: none; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
display: block; |
|
text-align: start; |
|
|
|
&:hover { |
|
background-color: rgba(83, 83, 95, 0.48); |
|
} |
|
} |
|
} |
|
` |
|
|
|
document.head.appendChild(style) |
|
|
|
const menuElement = document.createElement('div') |
|
menuElement.className = `${ID}-menu` |
|
menuElement.inert = true |
|
|
|
const openInIINAButton = document.createElement('button') |
|
const openInBrowserButton = document.createElement('button') |
|
|
|
openInIINAButton.textContent = 'Open in IINA' |
|
openInBrowserButton.textContent = 'Open in browser' |
|
|
|
menuElement.appendChild(openInIINAButton) |
|
menuElement.appendChild(openInBrowserButton) |
|
document.body.appendChild(menuElement) |
|
|
|
let link |
|
let pointerX = 0 |
|
let pointerY = 0 |
|
let openingInBrowser = false |
|
|
|
menuElement.addEventListener('transitionend', () => { |
|
if (menuElement.inert) { |
|
menuElement.style.top = '' |
|
menuElement.style.left = '' |
|
} |
|
}) |
|
|
|
openInIINAButton.addEventListener('click', () => { |
|
openingInBrowser = false |
|
|
|
const url = new URL('iina://open') |
|
|
|
url.searchParams.set('url', link.href) |
|
url.searchParams.set('new_window', '0') |
|
|
|
location.href = url.href |
|
|
|
closeMenu() |
|
}) |
|
|
|
openInBrowserButton.addEventListener('click', () => { |
|
openingInBrowser = true |
|
link.click() |
|
closeMenu() |
|
}) |
|
|
|
addEventListener('click', (event) => { |
|
if (menuElement.contains(event.target)) { |
|
return |
|
} |
|
|
|
if (openingInBrowser) { |
|
openingInBrowser = false |
|
return |
|
} |
|
|
|
link = event.target.closest('a') |
|
|
|
if (!link || !link.href) { |
|
return |
|
} |
|
|
|
const match = link.href.match(VIDEO_URL_PATTERN) |
|
|
|
if (!match) { |
|
return |
|
} |
|
|
|
if (RESERVED_USERNAMES.includes(match[1])) { |
|
return |
|
} |
|
|
|
if ( |
|
!link.href.startsWith('https://www.twitch.tv/videos/') && |
|
!link.querySelector('.tw-channel-status-text-indicator, .tw-channel-status-indicator') && |
|
!( |
|
link.getAttribute('data-a-target') === 'preview-card-channel-link' && |
|
link.closest('article').querySelector('a[data-a-target="preview-card-image-link"] .tw-channel-status-text-indicator') |
|
) |
|
) { |
|
return |
|
} |
|
|
|
event.preventDefault() |
|
openMenu() |
|
}, { |
|
capture: true |
|
}) |
|
|
|
addEventListener('pointerdown', onPointerdown) |
|
|
|
function openMenu() { |
|
const rect = menuElement.getBoundingClientRect() |
|
|
|
let originX |
|
let originY |
|
|
|
if (pointerX + MENU_POINTER_MARGIN + rect.width + MENU_VIEWPORT_MARGIN > window.innerWidth) { |
|
menuElement.style.left = `${pointerX - MENU_POINTER_MARGIN - rect.width}px` |
|
originX = 'right' |
|
} else { |
|
menuElement.style.left = `${pointerX + MENU_POINTER_MARGIN}px` |
|
originX = 'left' |
|
} |
|
|
|
if (pointerY + MENU_POINTER_MARGIN + rect.height + MENU_POINTER_MARGIN > window.innerHeight) { |
|
menuElement.style.top = `${pointerY - MENU_POINTER_MARGIN - rect.height}px` |
|
originY = 'bottom' |
|
} else { |
|
menuElement.style.top = `${pointerY + MENU_POINTER_MARGIN}px` |
|
originY = 'top' |
|
} |
|
|
|
menuElement.style.transformOrigin = `${originX} ${originY}` |
|
|
|
menuElement.inert = false |
|
addEventListener('keydown', onKeydown) |
|
addEventListener('scroll', onScroll, { once: true, capture: true }) |
|
} |
|
|
|
function closeMenu() { |
|
menuElement.inert = true |
|
removeEventListener('keydown', onKeydown) |
|
removeEventListener('scroll', onScroll) |
|
} |
|
|
|
function onPointerdown(event) { |
|
pointerX = event.clientX |
|
pointerY = event.clientY |
|
|
|
if (!menuElement.inert && !menuElement.contains(event.target)) { |
|
closeMenu() |
|
} |
|
} |
|
|
|
function onKeydown(event) { |
|
if (event.key === 'Escape') { |
|
event.preventDefault() |
|
closeMenu() |
|
} |
|
} |
|
|
|
function onScroll() { |
|
closeMenu() |
|
} |
|
})() |