diff --git a/src/content_script.ts b/src/content_script.ts index a9ba3b2..21858ab 100644 --- a/src/content_script.ts +++ b/src/content_script.ts @@ -1,40 +1,43 @@ import { lastPlayedFeature, showCompletionFeature, timestampsFeature } from './fp_features' -import { Settings } from './settings' -import { waitUntil } from './utils' +import { Settings, defaultSettings } from './settings' +import { debugLog, getSettings, infoLog, pick, setLogLevel, waitUntil } from './utils' -export default function main() { - console.log('FP Max Loaded') - chrome.storage.sync.get( - { - saveInterval: 5000, - lastPlayedMap: {}, - useTimestamps: true, - returnToLastTime: true, - showCompletion: true, - } as Settings, - ({ useTimestamps, showCompletion, returnToLastTime }: Settings) => { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (returnToLastTime || showCompletion) { - lastPlayedFeature(mutation.target as HTMLElement) - } - if (useTimestamps) { - timestampsFeature(mutation.target as HTMLElement) - } - if (showCompletion) { - showCompletionFeature(mutation.target as HTMLElement) - } - }) - }) - if (returnToLastTime) { - waitUntil( - () => document.querySelector('.video-js video') !== null, - () => lastPlayedFeature(document.body), - ) +export default async function main() { + const settings = pick(defaultSettings, [ + 'useTimestamps', + 'showCompletion', + 'returnToLastTime', + 'logLevel', + ]) + const { useTimestamps, showCompletion, returnToLastTime, logLevel } = await getSettings(settings) + setLogLevel(logLevel) + infoLog('FP Max Loaded', { useTimestamps, showCompletion, returnToLastTime, logLevel }) + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (returnToLastTime || showCompletion) { + lastPlayedFeature(mutation.target as HTMLElement) } - observer.observe(document.body, { attributes: true, childList: true, subtree: true }) - }, - ) + if (useTimestamps) { + timestampsFeature(mutation.target as HTMLElement) + } + if (showCompletion) { + showCompletionFeature(mutation.target as HTMLElement) + } + }) + if (returnToLastTime) { + waitUntil( + () => document.querySelector('.video-js video') !== null, + () => lastPlayedFeature(document.body), + ) + } + if (showCompletion) { + waitUntil( + () => document.querySelector('.PostTileWrapper') !== null, + () => showCompletionFeature(document.body), + ) + } + }) + observer.observe(document.body, { attributes: true, childList: true, subtree: true }) } main() diff --git a/src/fp_features.ts b/src/fp_features.ts index 0690f05..7c9b42b 100644 --- a/src/fp_features.ts +++ b/src/fp_features.ts @@ -1,3 +1,5 @@ +import { debugLog, infoLog } from './utils' + let handled = '' export function timestampsFeature(target: HTMLElement) { if (target.matches('.comment-body')) { @@ -20,6 +22,7 @@ export function lastPlayedFeature(target: HTMLElement) { return const vidCont = document.querySelector('.video-js') const vid = vidCont?.querySelector('video') + debugLog('lastPlayedFeature video:', vid, vid?.duration, vid?.currentTime) if (vid && vid.duration && vid.currentTime !== undefined) { handled = document.location.pathname @@ -29,9 +32,11 @@ export function lastPlayedFeature(target: HTMLElement) { [vidKey, 'saveInterval', 'returnToLastTime'], ({ returnToLastTime, saveInterval = 5000, ...result }) => { const lastPlayed = result[vidKey] + infoLog('lastPlayed', vidId, lastPlayed) if (returnToLastTime && lastPlayed) { vid.currentTime = lastPlayed + infoLog('Loading saved time:', vidId, lastPlayed) // vid.play() // FIXME doesn't work } @@ -65,10 +70,12 @@ function lastPlayedUpdateCallback(vid: HTMLVideoElement, vidId: string): () => v ({ completedPercent = 95, ...result }) => { if (Math.floor(result[vidKey]) === Math.floor(vid.currentTime)) return if (vid.currentTime / vid.duration >= completedPercent / 100) { + debugLog('Video completed:', vidId) chrome.storage.sync.remove([vidKey]) chrome.storage.sync.set({ [vidCompletionKey]: 1 }) return } + debugLog('Saving last played:', vidId, vid.currentTime) chrome.storage.sync.set({ [vidKey]: vid.currentTime, [vidCompletionKey]: vid.currentTime / vid.duration, @@ -88,7 +95,12 @@ function handleCommentTimestamps(commentBody: HTMLElement) { const content = body.textContent! const TIME_REGEX = /(\d+):(\d+)(:\d+)?/g const matchesTime = content.match(TIME_REGEX) - if (!matchesTime) return + if (!matchesTime) { + debugLog('No timestamps found', content) + return + } + + debugLog('Found timestamps:', matchesTime, content) const newContent = content.replace(TIME_REGEX, (match, hrs, mns, scs) => { if (scs) { @@ -124,6 +136,7 @@ function handleShowCompletion(target: HTMLElement) { const vidCompletionKey = `completionPercentMap.${vidId}` chrome.storage.sync.get([vidCompletionKey], (result) => { if (target.querySelector('.progress') || target.dataset.fpMax) return + debugLog('Checking completion', vidId, result[vidCompletionKey]) if (!result[vidCompletionKey]) return const progress = document.createElement('div') progress.classList.add('progress') @@ -137,6 +150,7 @@ function handleShowCompletion(target: HTMLElement) { const thumb = target.querySelector('.PostTileThumbnail') as HTMLDivElement thumb.appendChild(progress) + debugLog('Added progress bar', vidId, target, progress) target.dataset.fpMax = 'true' }) } diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index f0b0b2e..25cb554 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx' -import { ComponentChild, createContext, render } from 'preact' +import { ComponentChild, createContext } from 'preact' import { useEffect, useState } from 'preact/hooks' -import { Settings } from '../settings' +import { LogLevel, defaultSettings } from '../settings' export interface SettingsProps { mode: 'popup' | 'options' @@ -11,29 +11,24 @@ export const ModeContext = createContext<'popup' | 'options'>('popup') export function SettingsPage(props: SettingsProps) { const { mode } = props + const [loading, setLoading] = useState(true) const [returnToLastTime, setReturnToLastTime] = useState(true) const [saveInterval, setSaveInterval] = useState(5) const [useTimestamps, setUseTimestamps] = useState(true) const [completedPercent, setCompletedPercent] = useState(95) const [showCompletion, setShowCompletion] = useState(true) + const [logLevel, setLogLevel] = useState('info') useEffect(() => { - chrome.storage.sync.get( - { - returnToLastTime: false, - saveInterval: 5, - useTimestamps: true, - completedPercent: 95, - showCompletion: true, - } as Settings, - (items) => { - setReturnToLastTime(items.returnToLastTime) - setSaveInterval(items.saveInterval) - setUseTimestamps(items.useTimestamps) - setCompletedPercent(items.completedPercent) - setShowCompletion(items.showCompletion) - }, - ) + chrome.storage.sync.get(defaultSettings, (items) => { + setReturnToLastTime(items.returnToLastTime) + setSaveInterval(items.saveInterval) + setUseTimestamps(items.useTimestamps) + setCompletedPercent(items.completedPercent) + setShowCompletion(items.showCompletion) + setLogLevel(items.logLevel) + setLoading(false) + }) }, []) useEffect(() => { @@ -51,6 +46,9 @@ export function SettingsPage(props: SettingsProps) { useEffect(() => { chrome.storage.sync.set({ showCompletion }) }, [showCompletion]) + useEffect(() => { + chrome.storage.sync.set({ logLevel }) + }, [logLevel]) return ( @@ -132,6 +130,26 @@ export function SettingsPage(props: SettingsProps) { +
+ + + {mode === 'options' ? ( <> diff --git a/src/settings.ts b/src/settings.ts index 6d69a6c..cf026d5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -6,4 +6,25 @@ export interface Settings { useTimestamps?: boolean completedPercent?: number showCompletion?: boolean + logLevel?: LogLevel } + +export const defaultSettings: Required = { + saveInterval: 5000, + lastPlayedMap: {}, + completionPercentMap: {}, + useTimestamps: true, + returnToLastTime: true, + completedPercent: 95, + showCompletion: true, + logLevel: 'info', +} + +export const LogLevel = { + debug: 'debug', + info: 'info', + warn: 'warn', + error: 'error', +} as const + +export type LogLevel = typeof LogLevel[keyof typeof LogLevel] diff --git a/src/utils.ts b/src/utils.ts index c9ecb2c..9f41c1d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,12 @@ -export function waitUntil(cond: () => boolean, cb: () => void) { +import { LogLevel, Settings } from './settings' + +export function waitUntil(cond: () => boolean, cb: () => void, timeout: number = 10000) { + let startTime = Date.now() if (cond()) { cb() return } - setTimeout(() => waitUntil(cond, cb), 100) + setTimeout(() => waitUntil(cond, cb, timeout - (Date.now() - startTime)), 100) } export function objectKeys(obj: T): (keyof T)[] { @@ -15,3 +18,69 @@ type Entry = [keyof T, T[keyof T]] export function objectEntries(obj: T): Entry[] { return Object.entries(obj as object) as Entry[] } + +export function pick(obj: T, keys: K[]): Pick { + const out: Partial> = {} + for (const key of keys) { + out[key] = obj[key] + } + return out as Pick +} + +export function omit(obj: T, keys: K[]): Omit { + const out: Partial = {} + for (const key of objectKeys(obj) as K[]) { + if (keys.includes(key)) continue + out[key] = obj[key] + } + return out as Omit +} + +let visibleLogLevel: LogLevel | null = LogLevel.info + +export function setLogLevel(level: LogLevel | null | undefined) { + visibleLogLevel = level || null +} + +export function log(level: LogLevel, ...args: any[]) { + const logLevelOrder = [LogLevel.debug, LogLevel.info, LogLevel.warn, LogLevel.error] as const + if ( + visibleLogLevel === null || + logLevelOrder.indexOf(level) < logLevelOrder.indexOf(visibleLogLevel) + ) + return + console[level](`[${formatDate(new Date())}]`, '[fp_max]', ...args) +} + +export function formatDate(date: Date) { + return date.toISOString().replace(/T/, ' ').replace(/Z/, '') +} + +export function debugLog(...args: any[]) { + log(LogLevel.debug, ...args) +} + +export function infoLog(...args: any[]) { + log(LogLevel.info, ...args) +} + +export function warnLog(...args: any[]) { + log(LogLevel.warn, ...args) +} + +export function errorLog(...args: any[]) { + log(LogLevel.error, ...args) +} + +export function getSettings, K extends keyof T>( + keysOrObject: T, +): Promise +export function getSettings, K extends keyof T>( + keysOrObject: T | K[], +): Promise> { + return new Promise((resolve) => { + chrome.storage.sync.get(keysOrObject, (results) => { + resolve(results as Pick) + }) + }) +} diff --git a/src/worker.ts b/src/worker.ts index 763c3d9..de5e72e 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,28 +1,19 @@ -import { Settings } from './settings' -import { objectEntries, objectKeys } from './utils' +import { Settings, defaultSettings } from './settings' +import { getSettings, infoLog, objectEntries, objectKeys } from './utils' -const defaultSettings = { - saveInterval: 5000, - lastPlayedMap: {}, - completionPercentMap: {}, - useTimestamps: true, - returnToLastTime: true, - completedPercent: 95, -} as Required chrome.runtime.onInstalled.addListener(async () => { - console.log('Extension installed!') + infoLog('Extension installed!') - chrome.storage.sync.get(objectKeys(defaultSettings), (settings) => { - const out: Partial = {} - for (const entry of objectEntries(defaultSettings)) { - const [key, value] = entry - if (settings[key] === undefined) { - out[key] = value as never - } + const settings = await getSettings(defaultSettings) + const out: Partial = {} + for (const entry of objectEntries(defaultSettings)) { + const [key, value] = entry + if (settings[key] === undefined) { + out[key] = value as never } - console.log('Filling missing settings:', out) - chrome.storage.sync.set(settings) - }) + } + infoLog('Filling missing settings:', out) + chrome.storage.sync.set(settings) // remove old keys - 31 Mar 2023 chrome.storage.sync.remove(['lastPlayed'])