feat: improve logging, add log level

This commit is contained in:
Chen Asraf
2023-04-02 12:28:25 +03:00
parent e162bee758
commit a6e8c17cc3
6 changed files with 192 additions and 76 deletions

View File

@@ -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()

View File

@@ -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'
})
}

View File

@@ -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' ? (
<>

View File

@@ -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]

View File

@@ -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>)
})
})
}

View File

@@ -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'])