mirror of
https://github.com/chenasraf/fp-max-extension.git
synced 2026-05-17 17:48:07 +00:00
feat: improve logging, add log level
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<LogLevel | null>('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 (
|
||||
<ModeContext.Provider value={mode}>
|
||||
@@ -132,6 +130,26 @@ export function SettingsPage(props: SettingsProps) {
|
||||
<option value={60000}>1 Minute</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
<hr className="my-2" />
|
||||
<SettingRow
|
||||
label="Log level"
|
||||
disabled={!returnToLastTime && !showCompletion}
|
||||
helpText="Determines how much information is logged to the developer console. Default is 'Info'."
|
||||
>
|
||||
<select
|
||||
className="border border-gray-300 rounded-md px-2 py-1"
|
||||
value={logLevel || ''}
|
||||
onChange={(e) =>
|
||||
setLogLevel(((e.target as HTMLSelectElement).value || null) as LogLevel | null)
|
||||
}
|
||||
>
|
||||
<option value="">None (disabled)</option>
|
||||
<option value="debug">Debug</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warn">Warn</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
|
||||
{mode === 'options' ? (
|
||||
<>
|
||||
|
||||
@@ -6,4 +6,25 @@ export interface Settings {
|
||||
useTimestamps?: boolean
|
||||
completedPercent?: number
|
||||
showCompletion?: boolean
|
||||
logLevel?: LogLevel
|
||||
}
|
||||
|
||||
export const defaultSettings: Required<Settings> = {
|
||||
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]
|
||||
|
||||
73
src/utils.ts
73
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<T>(obj: T): (keyof T)[] {
|
||||
@@ -15,3 +18,69 @@ type Entry<T> = [keyof T, T[keyof T]]
|
||||
export function objectEntries<T>(obj: T): Entry<T>[] {
|
||||
return Object.entries(obj as object) as Entry<T>[]
|
||||
}
|
||||
|
||||
export function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
||||
const out: Partial<Pick<T, K>> = {}
|
||||
for (const key of keys) {
|
||||
out[key] = obj[key]
|
||||
}
|
||||
return out as Pick<T, K>
|
||||
}
|
||||
|
||||
export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
||||
const out: Partial<T> = {}
|
||||
for (const key of objectKeys(obj) as K[]) {
|
||||
if (keys.includes(key)) continue
|
||||
out[key] = obj[key]
|
||||
}
|
||||
return out as Omit<T, K>
|
||||
}
|
||||
|
||||
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<T extends Partial<Settings>, K extends keyof T>(
|
||||
keysOrObject: T,
|
||||
): Promise<T>
|
||||
export function getSettings<T extends Partial<Settings>, K extends keyof T>(
|
||||
keysOrObject: T | K[],
|
||||
): Promise<Pick<T, K>> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.sync.get(keysOrObject, (results) => {
|
||||
resolve(results as Pick<T, K>)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<Settings>
|
||||
chrome.runtime.onInstalled.addListener(async () => {
|
||||
console.log('Extension installed!')
|
||||
infoLog('Extension installed!')
|
||||
|
||||
chrome.storage.sync.get(objectKeys(defaultSettings), (settings) => {
|
||||
const out: Partial<Settings> = {}
|
||||
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<Settings> = {}
|
||||
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'])
|
||||
|
||||
Reference in New Issue
Block a user