Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions websites/A/AnimeVietSub/iframe.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,60 @@
import type { PrivacyCommandMessage } from './privacy.js'
import {
isPrivacyStateMessage,
PRIVACY_MESSAGE_SOURCE,
} from './privacy.js'
import {
ensurePrivacyButton,
removePrivacyButton,
} from './privacyUi.js'

const iframe = new iFrame()
let privateMode = false
let privacyButtonShown = false
let hasPrivacyState = false
let lastStateRequest = 0

function sendPrivacyCommand(type: PrivacyCommandMessage['type']): void {
window.top?.postMessage({ source: PRIVACY_MESSAGE_SOURCE, type }, '*')
}

function togglePrivacy(): void {
if (!privacyButtonShown)
return

privateMode = !privateMode
ensurePrivacyButton(document, privateMode, togglePrivacy)
sendPrivacyCommand('toggle')
}

function syncPrivacyButton(): void {
if (privacyButtonShown)
ensurePrivacyButton(document, privateMode, togglePrivacy)
else
removePrivacyButton(document)
}

window.addEventListener('message', (event) => {
if (event.source !== window.top || !isPrivacyStateMessage(event.data))
return

privateMode = event.data.privateMode
privacyButtonShown = event.data.privacyButtonShown
hasPrivacyState = true
syncPrivacyButton()
})

iframe.on('UpdateData', async () => {
const video = document.querySelector<HTMLVideoElement>('video')
if (video && !Number.isNaN(video.duration)) {
if (hasPrivacyState) {
syncPrivacyButton()
}
else if (Date.now() - lastStateRequest > 2000) {
lastStateRequest = Date.now()
sendPrivacyCommand('request-state')
}

iframe.send({
duration: video.duration,
currTime: video.currentTime,
Expand Down
8 changes: 4 additions & 4 deletions websites/A/AnimeVietSub/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
"value": true
},
{
"id": "privateMode",
"title": "Chế Độ Riêng Tư",
"icon": "fas fa-user-secret",
"value": false
"id": "privacy-shown",
"title": "Privacy Overwrite Button",
"icon": "fad fa-eye-low-vision",
"value": true
}
]
}
141 changes: 137 additions & 4 deletions websites/A/AnimeVietSub/presence.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,128 @@
import { ActivityType, Assets, getTimestamps } from 'premid'
import {
getEpisodeKey,
getEpisodePrivacy,
isPrivacyCommandMessage,
parsePrivacyOverrides,
PRIVACY_MESSAGE_SOURCE,
toggleEpisodePrivacy,
} from './privacy.js'
import { ensurePrivacyButton, removePrivacyButton } from './privacyUi.js'

const presence = new Presence({
clientId: '1264754447276310599',
})

const privacyStorageKey = 'pmdAnimeVietSubPrivacyOverrides'
const privacyFrames = new Set<Window>()
let currentEpisodeKey = ''
let currentEpisodePrivacy = false
let currentPrivacyButtonShown = true
let lastBroadcastEpisodeKey = ''
let lastBroadcastPrivacy: boolean | null = null
let lastBroadcastPrivacyButtonShown: boolean | null = null

function readPrivacyOverrides() {
return parsePrivacyOverrides(localStorage.getItem(privacyStorageKey))
}

function sendPrivacyState(
target: Window,
privateMode: boolean,
privacyButtonShown: boolean,
): void {
target.postMessage({
source: PRIVACY_MESSAGE_SOURCE,
type: 'state',
privateMode,
privacyButtonShown,
}, '*')
}

function broadcastPrivacyState(
privateMode: boolean,
privacyButtonShown: boolean,
force = false,
): void {
if (!force
&& lastBroadcastEpisodeKey === currentEpisodeKey
&& lastBroadcastPrivacy === privateMode
&& lastBroadcastPrivacyButtonShown === privacyButtonShown) {
return
}

lastBroadcastEpisodeKey = currentEpisodeKey
lastBroadcastPrivacy = privateMode
lastBroadcastPrivacyButtonShown = privacyButtonShown
for (const frame of privacyFrames) {
try {
sendPrivacyState(frame, privateMode, privacyButtonShown)
}
catch {
privacyFrames.delete(frame)
}
}
}

function toggleCurrentEpisodePrivacy(): void {
const overrides = toggleEpisodePrivacy(
false,
currentEpisodeKey,
readPrivacyOverrides(),
)
localStorage.setItem(privacyStorageKey, JSON.stringify(overrides))
currentEpisodePrivacy = getEpisodePrivacy(
false,
currentEpisodeKey,
overrides,
)
if (currentPrivacyButtonShown)
ensurePrivacyButton(document, currentEpisodePrivacy, toggleCurrentEpisodePrivacy)
broadcastPrivacyState(currentEpisodePrivacy, currentPrivacyButtonShown, true)
}

async function sendInitialPrivacyState(target: Window): Promise<void> {
currentPrivacyButtonShown = await presence.getSetting<boolean>('privacy-shown')
currentEpisodeKey = getEpisodeKey(document.location.pathname, document.location.search)
currentEpisodePrivacy = getEpisodePrivacy(
false,
currentEpisodeKey,
readPrivacyOverrides(),
)
sendPrivacyState(target, currentEpisodePrivacy, currentPrivacyButtonShown)
}

window.addEventListener('message', (event) => {
if (!isPrivacyCommandMessage(event.data) || !event.source || !isWatchPage())
return

const target = event.source as Window
privacyFrames.add(target)

if (event.data.type === 'request-state') {
void sendInitialPrivacyState(target)
return
}

currentEpisodeKey = getEpisodeKey(document.location.pathname, document.location.search)

if (currentPrivacyButtonShown) {
const overrides = toggleEpisodePrivacy(
false,
currentEpisodeKey,
readPrivacyOverrides(),
)
localStorage.setItem(privacyStorageKey, JSON.stringify(overrides))
}

currentEpisodePrivacy = getEpisodePrivacy(
false,
currentEpisodeKey,
readPrivacyOverrides(),
)
sendPrivacyState(target, currentEpisodePrivacy, currentPrivacyButtonShown)
})

let browsingTimestamp = Number.parseInt(sessionStorage.getItem('PMD_browsing_time') || '0', 10)
if (!browsingTimestamp || Number.isNaN(browsingTimestamp)) {
browsingTimestamp = Math.floor(Date.now() / 1000)
Expand Down Expand Up @@ -153,9 +272,23 @@ presence.on('UpdateData', async () => {

const { pathname, href } = document.location
const buttons = await presence.getSetting<boolean>('buttons')
const privateMode = await presence.getSetting<boolean>('privateMode')
const privacyButtonShown = await presence.getSetting<boolean>('privacy-shown')
const watchPage = isWatchPage()
const detailPage = isDetailPage()

currentPrivacyButtonShown = privacyButtonShown
currentEpisodeKey = getEpisodeKey(pathname, document.location.search)
currentEpisodePrivacy = watchPage
? getEpisodePrivacy(false, currentEpisodeKey, readPrivacyOverrides())
: false

if (watchPage) {
if (privacyButtonShown)
ensurePrivacyButton(document, currentEpisodePrivacy, toggleCurrentEpisodePrivacy)
else
removePrivacyButton(document)
broadcastPrivacyState(currentEpisodePrivacy, privacyButtonShown)

if (isWatchPage()) {
const title = getAnimeTitle()
const episode = getEpisodeName()
const poster = getAnimePoster()
Expand Down Expand Up @@ -246,7 +379,7 @@ presence.on('UpdateData', async () => {
]
}
}
else if (isDetailPage()) {
else if (detailPage) {
const title = getAnimeTitle()
const poster = getAnimePoster()

Expand Down Expand Up @@ -302,7 +435,7 @@ presence.on('UpdateData', async () => {
}
}

if (privateMode && (isWatchPage() || isDetailPage())) {
if (currentEpisodePrivacy && (watchPage || detailPage)) {
presenceData.details = 'Đang xem Anime'
delete presenceData.state
presenceData.largeImageKey = ActivityAssets.Logo
Expand Down
84 changes: 84 additions & 0 deletions websites/A/AnimeVietSub/privacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
export type PrivacyOverrides = Record<string, boolean>

export const PRIVACY_MESSAGE_SOURCE = 'premid-animevietsub-privacy'

export interface PrivacyCommandMessage {
source: typeof PRIVACY_MESSAGE_SOURCE
type: 'request-state' | 'toggle'
}

export interface PrivacyStateMessage {
source: typeof PRIVACY_MESSAGE_SOURCE
type: 'state'
privateMode: boolean
privacyButtonShown: boolean
}

const unsafeKeys = new Set(['__proto__', 'constructor', 'prototype'])

export function isPrivacyCommandMessage(value: unknown): value is PrivacyCommandMessage {
if (!value || typeof value !== 'object')
return false

const message = value as Partial<PrivacyCommandMessage>
return message.source === PRIVACY_MESSAGE_SOURCE
&& (message.type === 'request-state' || message.type === 'toggle')
}

export function isPrivacyStateMessage(value: unknown): value is PrivacyStateMessage {
if (!value || typeof value !== 'object')
return false

const message = value as Partial<PrivacyStateMessage>
return message.source === PRIVACY_MESSAGE_SOURCE
&& message.type === 'state'
&& typeof message.privateMode === 'boolean'
&& typeof message.privacyButtonShown === 'boolean'
}

export function parsePrivacyOverrides(raw: string | null): PrivacyOverrides {
try {
const parsed: unknown = JSON.parse(raw ?? '{}')
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
return {}

const overrides: PrivacyOverrides = {}
for (const [key, value] of Object.entries(parsed)) {
if (unsafeKeys.has(key) || typeof value !== 'boolean')
return {}
overrides[key] = value
}
return overrides
}
catch {
return {}
}
}

export function getEpisodeKey(pathname: string, search: string): string {
const normalizedPath = pathname.replace(/\/+$/, '') || '/'
const episode = new URLSearchParams(search).get('tap')

return episode
? `${normalizedPath}?tap=${encodeURIComponent(episode)}`
: normalizedPath
}

export function getEpisodePrivacy(
globalPrivacy: boolean,
episodeKey: string,
overrides: PrivacyOverrides,
): boolean {
return overrides[episodeKey] ?? globalPrivacy
}

export function toggleEpisodePrivacy(
globalPrivacy: boolean,
episodeKey: string,
overrides: PrivacyOverrides,
): PrivacyOverrides {
return {
...overrides,
[episodeKey]: !getEpisodePrivacy(globalPrivacy, episodeKey, overrides),
}
}
Loading
Loading