From 4a28e2218703a531d9fd74f63ab76b7cfba64af9 Mon Sep 17 00:00:00 2001 From: ptgms Date: Mon, 22 Sep 2025 15:45:17 +0200 Subject: [PATCH 01/32] refactor: split background script into modular components with TypeScript types --- src/manifest.json | 3 +- src/scripts/background.ts | 328 +--------------- src/scripts/background/database.ts | 90 +++++ src/scripts/background/donation.ts | 48 +++ src/scripts/background/install.ts | 32 ++ src/scripts/background/pageAction.ts | 93 +++++ src/scripts/background/serviceDetection.ts | 54 +++ src/scripts/background/tabUi.ts | 65 ++++ src/scripts/background/types.ts | 15 + src/scripts/constants.ts | 13 + src/scripts/lib/chromeStorage.ts | 17 + src/scripts/views/popup.ts | 428 ++------------------- src/scripts/views/popup/donation.ts | 52 +++ src/scripts/views/popup/events.ts | 61 +++ src/scripts/views/popup/layout.ts | 9 + src/scripts/views/popup/navigation.ts | 132 +++++++ src/scripts/views/popup/service.ts | 245 ++++++++++++ src/scripts/views/popup/state.ts | 46 +++ src/scripts/views/popup/theme.ts | 41 ++ src/scripts/views/settings.ts | 127 +----- src/scripts/views/settings/handlers.ts | 43 +++ src/scripts/views/settings/state.ts | 70 ++++ src/views/popup.html | 2 +- src/views/settings/settings.html | 2 +- 24 files changed, 1191 insertions(+), 825 deletions(-) create mode 100644 src/scripts/background/database.ts create mode 100644 src/scripts/background/donation.ts create mode 100644 src/scripts/background/install.ts create mode 100644 src/scripts/background/pageAction.ts create mode 100644 src/scripts/background/serviceDetection.ts create mode 100644 src/scripts/background/tabUi.ts create mode 100644 src/scripts/background/types.ts create mode 100644 src/scripts/constants.ts create mode 100644 src/scripts/lib/chromeStorage.ts create mode 100644 src/scripts/views/popup/donation.ts create mode 100644 src/scripts/views/popup/events.ts create mode 100644 src/scripts/views/popup/layout.ts create mode 100644 src/scripts/views/popup/navigation.ts create mode 100644 src/scripts/views/popup/service.ts create mode 100644 src/scripts/views/popup/state.ts create mode 100644 src/scripts/views/popup/theme.ts create mode 100644 src/scripts/views/settings/handlers.ts create mode 100644 src/scripts/views/settings/state.ts diff --git a/src/manifest.json b/src/manifest.json index f363f3f..aecec94 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -6,7 +6,8 @@ "homepage_url": "https://tosdr.org", "permissions": ["tabs", "storage"], "background": { - "service_worker": "scripts/background.js" + "service_worker": "scripts/background.js", + "type": "module" }, "action": { "default_popup": "views/popup.html", diff --git a/src/scripts/background.ts b/src/scripts/background.ts index 32d3956..ba84d52 100644 --- a/src/scripts/background.ts +++ b/src/scripts/background.ts @@ -1,326 +1,32 @@ -// import * as Sentry from '@sentry/browser'; - -interface DatabaseEntry { - id: string; - url: string; - rating: string; -} - -interface Service { - id: string; - rating: string; -} - -const ALLOWED_PROTOCOLS = ['http:', 'https:']; - -var apiUrl = 'api.tosdr.org'; - -// let sentry = false; - -function setPopup(tabId: number | null, popup: string): void { - if (!tabId) { - console.log('tabid is undefined, goodbye'); - // Sentry.captureException(`tabid is undefined! - ${popup}`); - return; - } - chrome.action.setPopup({ - tabId, - popup, - }); -} - -function serviceDetected(tab: chrome.tabs.Tab, service: Service): void { - setTabIcon(tab, service.rating.toLowerCase()); - - setPopup(tab?.id ?? null, `/views/popup.html?service-id=${service.id}`); - setTabBadgeNotification(false, tab); -} - -function initializePageAction(tab: chrome.tabs.Tab): void { - if (!tab || !tab.url) { - console.log('tab is undefined'); - setPopup(null, '/views/popup.html'); - setTabIcon(tab, 'logo'); - setTabBadgeNotification(true, tab); - return; - } - const url = new URL(tab.url); - if (!ALLOWED_PROTOCOLS.includes(url.protocol)) { - // we only want to check http and https - setPopup(tab?.id ?? null, '/views/popup.html'); - setTabIcon(tab, 'logo'); - setTabBadgeNotification(true, tab); - return; - } - - if (tab.url == '') { - setPopup(tab?.id ?? null, '/views/popup.html'); - setTabIcon(tab, 'logo'); - setTabBadgeNotification(true, tab); - return; - } - - // change icon to icons/loading.png - setTabIcon(tab, 'loading'); - setTabBadgeNotification(false, tab); - - // get database from chrome.storage - chrome.storage.local.get(['db'], function (result) { - if (result["db"]) { - // parse the database - const db = result["db"] as DatabaseEntry[]; - - var domain = url.hostname; - - if (domain.startsWith('www.')) { - domain = domain.substring(4); - } - - console.log(domain); - - var domainEntry = db.filter((entry) => - entry.url.split(',').includes(domain) - ); - if (domainEntry.length === 1 && domainEntry[0]) { - console.log('exact match!'); - serviceDetected(tab, domainEntry[0] as Service); - return; - } else { - const maxTries = 4; - var current = 0; - - while (current < maxTries) { - const domainParts = domain.split('.'); - if (domainParts.length > 2) { - domain = domainParts.slice(1).join('.'); - console.log(`try ${current}: ${domain}`); - domainEntry = db.filter((entry) => - entry.url.split(',').includes(domain) - ); - if (domainEntry.length === 1 && domainEntry[0]) { - console.log('exact match!'); - current = maxTries + 1; - serviceDetected(tab, domainEntry[0] as Service); - return; - } - } else { - break; - } - current++; - } - } - - // we didnt find the domain in the database try parent else show notfound.png - setPopup(tab?.id ?? null, `/views/popup.html?url=${domain}`); - setTabIcon(tab, 'notfound'); - } else { - // database is not in chrome.storage, download it - console.log('Database is not in chrome.storage'); - downloadDatabase().then(() => { - initializePageAction(tab); - }); - } - }); -} - -// Removed unused function handleRuntimeError - -function setTabIcon(tab: chrome.tabs.Tab | null, icon: string): void { - const iconDetails: chrome.action.TabIconDetails = { - path: { - 32: `/icons/${icon}/${icon}32.png`, - 48: `/icons/${icon}/${icon}48.png`, - 128: `/icons/${icon}/${icon}128.png`, - }, - }; - - if (tab) { - iconDetails.tabId = tab.id; - } - - chrome.action.setIcon(iconDetails); -} - -async function setTabBadgeNotification(on: boolean, tab: chrome.tabs.Tab): Promise { - // Retrieve the value from storage and ensure it's a boolean - const data = await chrome.storage.local.get('displayDonationReminder'); - const dDR = Boolean(data["displayDonationReminder"]?.active); - - if (on && dDR) { - chrome.action.setBadgeText({ text: '!', tabId: tab.id }); - chrome.action.setBadgeBackgroundColor({ color: 'red' }); - } else { - chrome.action.setBadgeText({ text: '', tabId: tab.id }); - } -} - -async function downloadDatabase() { - // get the database directly from the new endpoint - const db_url = `https://${apiUrl}/appdb/version/v2`; - const response = await fetch(db_url, { - headers: { - apikey: atob('Y29uZ3JhdHMgb24gZ2V0dGluZyB0aGUga2V5IDpQ'), - }, - }); - - if (response.status >= 300) { - chrome.action.setBadgeText({ text: 'err ' + response.status }); - return; - } - - const data = await response.json(); - - chrome.storage.local.set( - { - db: data, - lastModified: new Date().toISOString(), - }, - function () { - console.log('Database downloaded and saved to chrome.storage'); - } - ); -} +import { checkIfUpdateNeeded } from './background/database'; +import { checkDonationReminder } from './background/donation'; +import { handleExtensionInstalled } from './background/install'; +import { initializePageAction } from './background/pageAction'; chrome.action.setBadgeText({ text: '' }); -//check if its time to show a donation reminder -async function checkDonationReminder() { - // Retrieve the value from storage and ensure it's a boolean - const data = await chrome.storage.local.get('displayDonationReminder'); - const displayDonationReminder = data["displayDonationReminder"]; - const dDR = Boolean(displayDonationReminder?.active); - - if ( - dDR !== true && - displayDonationReminder?.allowedPlattform === true - ) { - const currentDate = new Date(); - const currentYear = currentDate.getFullYear(); - - try { - const result: any = await chrome.storage.local.get( - 'lastDismissedReminder' - ); - const lastDismissedReminder = result.lastDismissedReminder; - const lastDismissedYear = lastDismissedReminder?.year; - console.log(lastDismissedYear); - - if ( - currentYear > lastDismissedYear || - lastDismissedYear === undefined - ) { - chrome.action.setBadgeText({ text: '!' }); - chrome.storage.local.set({ - displayDonationReminder: { - active: true, - allowedPlattform: - displayDonationReminder?.allowedPlattform, - }, - }); - } - } catch (error) { - console.error('Error in checkDonationReminder:', error); - } - } else { - chrome.action.setBadgeText({ text: '!' }); - } -} -function checkIfUpdateNeeded(firstStart = false) { - chrome.storage.local.get( - ['db', 'lastModified', 'interval', 'api', 'sentry'], - function (result) { - // if (result.sentry) { - // sentry = result.sentry; - // Sentry.init({ - // dsn: 'https://07c0ebcab5894cff990fd0d3871590f0@sentry.internal.jrbit.de/38', - // }); - // } - if (result["api"]) { - if (result["api"].length !== 0) apiUrl = result["api"]; - } - - if (result["db"] && result["lastModified"]) { - var interval = 8; - if (result["interval"]) { - interval = result["interval"]; - interval++; - } - // check if the database is less than 7 days old - const lastModified = new Date(result["lastModified"]); - const today = new Date(); - const diffTime = Math.abs( - today.getTime() - lastModified.getTime() - ); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - if (diffDays < interval) { - console.log( - `Database is less than ${interval - 1 - } days old, skipping download` - ); - return; - } - } - downloadDatabase().then(() => { - if (firstStart) { - chrome.runtime.openOptionsPage(); - } - }); - } - ); -} - -chrome.tabs.onUpdated.addListener(function (_tabId, changeInfo, tab) { +chrome.tabs.onUpdated.addListener((_, changeInfo, tab) => { if (changeInfo.status === 'complete') { - initializePageAction(tab); + void initializePageAction(tab); } }); -chrome.tabs.onCreated.addListener(function (tab) { - initializePageAction(tab); +chrome.tabs.onCreated.addListener((tab) => { + void initializePageAction(tab); }); -chrome.tabs.onActivated.addListener(function (activeInfo) { - chrome.tabs.get(activeInfo.tabId, function (tab) { - initializePageAction(tab); +chrome.tabs.onActivated.addListener((activeInfo) => { + chrome.tabs.get(activeInfo.tabId, (tab) => { + void initializePageAction(tab); }); }); -chrome.runtime.onInstalled.addListener(function () { - const userAgent = navigator.userAgent; - let donationReminderAllowed: boolean; - if (userAgent.indexOf('Mac') != -1 && userAgent.indexOf('Safari') != -1) { - console.log('MacOS and Safari detected' + userAgent); - donationReminderAllowed = false; - } else { - console.log('MacOS and Safari NOT detected' + userAgent); - donationReminderAllowed = true; - } - - chrome.storage.local.set( - { - themeHeader: true, - sentry: false, - displayDonationReminder: { - active: false, - allowedPlattform: donationReminderAllowed, - }, - }, - function () { - console.log('enabled theme header by default'); - checkIfUpdateNeeded(true); - chrome.tabs.query( - { active: true, currentWindow: true }, - function (tabs) { - if (tabs[0]) { - initializePageAction(tabs[0]); - } - } - ); - } - ); +chrome.runtime.onInstalled.addListener(() => { + void handleExtensionInstalled(); }); -chrome.runtime.onStartup.addListener(function () { - checkIfUpdateNeeded(); +chrome.runtime.onStartup.addListener(() => { + void checkIfUpdateNeeded(); }); -checkDonationReminder(); + +void checkDonationReminder(); diff --git a/src/scripts/background/database.ts b/src/scripts/background/database.ts new file mode 100644 index 0000000..9782402 --- /dev/null +++ b/src/scripts/background/database.ts @@ -0,0 +1,90 @@ +import { API_HEADERS, DEFAULT_API_URL } from '../constants'; +import { getLocal, setLocal } from '../lib/chromeStorage'; +import type { DatabaseEntry } from './types'; + +export async function resolveApiUrl(): Promise { + const data = await getLocal('api'); + const api = data['api']; + if (typeof api === 'string' && api.length > 0) { + return api; + } + return DEFAULT_API_URL; +} + +export async function downloadDatabase(apiUrl?: string): Promise { + const targetApi = apiUrl ?? (await resolveApiUrl()); + + try { + const response = await fetch(`https://${targetApi}/appdb/version/v2`, { + headers: API_HEADERS, + }); + + if (response.status >= 300) { + chrome.action.setBadgeText({ text: `err ${response.status}` }); + return; + } + + const data = (await response.json()) as DatabaseEntry[]; + + await setLocal({ + db: data, + lastModified: new Date().toISOString(), + }); + } catch (error) { + console.error('Failed to download database', error); + } +} + +export async function checkIfUpdateNeeded(firstStart = false): Promise { + const data = await getLocal([ + 'db', + 'lastModified', + 'interval', + 'api', + ]); + + const api = await resolveApiUrlFromData(data); + + const db = data['db'] as DatabaseEntry[] | undefined; + const lastModifiedRaw = data['lastModified']; + + if (db && typeof lastModifiedRaw === 'string') { + const intervalDays = computeIntervalDays(data['interval']); + const lastModified = new Date(lastModifiedRaw); + const today = new Date(); + const diffTime = Math.abs(today.getTime() - lastModified.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays < intervalDays) { + return; + } + } + + await downloadDatabase(api); + + if (firstStart) { + chrome.runtime.openOptionsPage(); + } +} + +function computeIntervalDays(rawValue: unknown): number { + const DEFAULT_INTERVAL = 8; + if (typeof rawValue === 'number') { + return rawValue + 1; + } + if (typeof rawValue === 'string') { + const parsed = Number(rawValue); + if (!Number.isNaN(parsed)) { + return parsed + 1; + } + } + return DEFAULT_INTERVAL; +} + +async function resolveApiUrlFromData(data: Record): Promise { + const api = data['api']; + if (typeof api === 'string' && api.length > 0) { + return api; + } + return resolveApiUrl(); +} diff --git a/src/scripts/background/donation.ts b/src/scripts/background/donation.ts new file mode 100644 index 0000000..d5f7e0c --- /dev/null +++ b/src/scripts/background/donation.ts @@ -0,0 +1,48 @@ +import { DONATION_BADGE_TEXT } from '../constants'; +import { getLocal, setLocal } from '../lib/chromeStorage'; +import type { DonationReminderState } from './types'; + +export async function checkDonationReminder(): Promise { + const data = await getLocal('displayDonationReminder'); + const displayDonationReminder = data['displayDonationReminder'] as + | DonationReminderState + | undefined; + + const isActive = displayDonationReminder?.active === true; + const allowed = displayDonationReminder?.allowedPlattform === true; + + if (!isActive && allowed) { + const currentYear = new Date().getFullYear(); + + try { + const reminderData = await getLocal('lastDismissedReminder'); + const lastDismissedReminder = reminderData['lastDismissedReminder'] as + | { year?: number } + | undefined; + const lastDismissedYear = lastDismissedReminder?.year; + + if (lastDismissedYear === undefined || currentYear > lastDismissedYear) { + chrome.action.setBadgeText({ text: DONATION_BADGE_TEXT }); + await setLocal({ + displayDonationReminder: { + active: true, + allowedPlattform: + displayDonationReminder?.allowedPlattform ?? true, + }, + }); + } + } catch (error) { + console.error('Error in checkDonationReminder:', error); + } + + return; + } + + chrome.action.setBadgeText({ text: DONATION_BADGE_TEXT }); +} + +export function donationReminderAllowed(userAgent: string): boolean { + const isMac = userAgent.includes('Mac'); + const isSafari = userAgent.includes('Safari'); + return !(isMac && isSafari); +} diff --git a/src/scripts/background/install.ts b/src/scripts/background/install.ts new file mode 100644 index 0000000..293eb5c --- /dev/null +++ b/src/scripts/background/install.ts @@ -0,0 +1,32 @@ +import { setLocal } from '../lib/chromeStorage'; +import { checkIfUpdateNeeded } from './database'; +import { donationReminderAllowed } from './donation'; +import { initializePageAction } from './pageAction'; + +export async function handleExtensionInstalled(): Promise { + const donationAllowed = donationReminderAllowed(navigator.userAgent); + + await setLocal({ + themeHeader: true, + sentry: false, + displayDonationReminder: { + active: false, + allowedPlattform: donationAllowed, + }, + }); + + await checkIfUpdateNeeded(true); + + const [activeTab] = await queryActiveTab(); + if (activeTab) { + await initializePageAction(activeTab); + } +} + +async function queryActiveTab(): Promise { + return new Promise((resolve) => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + resolve(tabs); + }); + }); +} diff --git a/src/scripts/background/pageAction.ts b/src/scripts/background/pageAction.ts new file mode 100644 index 0000000..026c27b --- /dev/null +++ b/src/scripts/background/pageAction.ts @@ -0,0 +1,93 @@ +import { + ALLOWED_PROTOCOLS, + DEFAULT_POPUP_PATH, +} from '../constants'; +import { getLocal } from '../lib/chromeStorage'; +import type { DatabaseEntry } from './types'; +import { findServiceMatch } from './serviceDetection'; +import { + serviceDetected, + setPopup, + setTabBadgeNotification, + setTabIcon, +} from './tabUi'; +import { downloadDatabase, resolveApiUrl } from './database'; + +export async function initializePageAction( + tab?: chrome.tabs.Tab | null +): Promise { + if (!tab || !tab.url) { + setPopup(null, DEFAULT_POPUP_PATH); + setTabIcon(tab, 'logo'); + await setTabBadgeNotification(true, tab); + return; + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(tab.url); + } catch (error) { + console.error('Invalid URL for tab', error); + setPopup(tab.id, DEFAULT_POPUP_PATH); + setTabIcon(tab, 'logo'); + await setTabBadgeNotification(true, tab); + return; + } + + if (!isAllowedProtocol(parsedUrl.protocol)) { + setPopup(tab.id, DEFAULT_POPUP_PATH); + setTabIcon(tab, 'logo'); + await setTabBadgeNotification(true, tab); + return; + } + + if (tab.url.trim() === '') { + setPopup(tab.id, DEFAULT_POPUP_PATH); + setTabIcon(tab, 'logo'); + await setTabBadgeNotification(true, tab); + return; + } + + setTabIcon(tab, 'loading'); + await setTabBadgeNotification(false, tab); + + const db = await getDatabase(); + if (!db) { + setPopup(tab.id, DEFAULT_POPUP_PATH); + setTabIcon(tab, 'logo'); + await setTabBadgeNotification(true, tab); + return; + } + + const { service, normalizedDomain } = findServiceMatch( + parsedUrl.hostname, + db + ); + + if (service) { + await serviceDetected(tab, service); + return; + } + + setPopup(tab.id, `${DEFAULT_POPUP_PATH}?url=${normalizedDomain}`); + setTabIcon(tab, 'notfound'); +} + +async function getDatabase(): Promise { + const stored = await getLocal('db'); + let db = stored['db'] as DatabaseEntry[] | undefined; + + if (db) { + return db; + } + + await downloadDatabase(await resolveApiUrl()); + + const refreshed = await getLocal('db'); + db = refreshed['db'] as DatabaseEntry[] | undefined; + return db; +} + +function isAllowedProtocol(protocol: string): boolean { + return (ALLOWED_PROTOCOLS as readonly string[]).includes(protocol); +} diff --git a/src/scripts/background/serviceDetection.ts b/src/scripts/background/serviceDetection.ts new file mode 100644 index 0000000..bab2ab5 --- /dev/null +++ b/src/scripts/background/serviceDetection.ts @@ -0,0 +1,54 @@ +import { MAX_DOMAIN_REDUCTIONS } from '../constants'; +import type { DatabaseEntry, Service } from './types'; + +export interface ServiceMatchResult { + service: Service | null; + normalizedDomain: string; +} + +export function findServiceMatch( + hostname: string, + db: DatabaseEntry[] +): ServiceMatchResult { + let domain = stripWwwPrefix(hostname); + + const directMatch = lookupDomain(domain, db); + if (directMatch) { + return { service: directMatch, normalizedDomain: domain }; + } + + let attempts = 0; + while (attempts < MAX_DOMAIN_REDUCTIONS) { + const reduced = reduceDomain(domain); + if (!reduced) { + break; + } + + domain = reduced; + const match = lookupDomain(domain, db); + if (match) { + return { service: match, normalizedDomain: domain }; + } + + attempts += 1; + } + + return { service: null, normalizedDomain: domain }; +} + +function stripWwwPrefix(domain: string): string { + return domain.startsWith('www.') ? domain.substring(4) : domain; +} + +function reduceDomain(domain: string): string | null { + const parts = domain.split('.'); + if (parts.length <= 2) { + return null; + } + return parts.slice(1).join('.'); +} + +function lookupDomain(domain: string, db: DatabaseEntry[]): Service | null { + const match = db.find((entry) => entry.url.split(',').includes(domain)); + return match ? { id: match.id, rating: match.rating } : null; +} diff --git a/src/scripts/background/tabUi.ts b/src/scripts/background/tabUi.ts new file mode 100644 index 0000000..d1484f3 --- /dev/null +++ b/src/scripts/background/tabUi.ts @@ -0,0 +1,65 @@ +import { DEFAULT_POPUP_PATH, DONATION_BADGE_TEXT } from '../constants'; +import { getLocal } from '../lib/chromeStorage'; +import type { DonationReminderState, Service } from './types'; + +export function setPopup( + tabId: number | undefined | null, + popup: string = DEFAULT_POPUP_PATH +): void { + if (typeof tabId !== 'number') { + chrome.action.setPopup({ popup }); + return; + } + + chrome.action.setPopup({ tabId, popup }); +} + +export function setTabIcon( + tab: chrome.tabs.Tab | null | undefined, + icon: string +): void { + const iconDetails: chrome.action.TabIconDetails = { + path: { + 32: `/icons/${icon}/${icon}32.png`, + 48: `/icons/${icon}/${icon}48.png`, + 128: `/icons/${icon}/${icon}128.png`, + }, + }; + + if (tab?.id) { + iconDetails.tabId = tab.id; + } + + chrome.action.setIcon(iconDetails); +} + +export async function setTabBadgeNotification( + on: boolean, + tab?: chrome.tabs.Tab | null +): Promise { + if (!tab?.id) { + return; + } + + const data = await getLocal('displayDonationReminder'); + const reminder = data['displayDonationReminder'] as + | DonationReminderState + | undefined; + + if (on && reminder?.active) { + chrome.action.setBadgeText({ text: DONATION_BADGE_TEXT, tabId: tab.id }); + chrome.action.setBadgeBackgroundColor({ color: 'red' }); + return; + } + + chrome.action.setBadgeText({ text: '', tabId: tab.id }); +} + +export async function serviceDetected( + tab: chrome.tabs.Tab, + service: Service +): Promise { + setTabIcon(tab, service.rating.toLowerCase()); + setPopup(tab.id, `${DEFAULT_POPUP_PATH}?service-id=${service.id}`); + await setTabBadgeNotification(false, tab); +} diff --git a/src/scripts/background/types.ts b/src/scripts/background/types.ts new file mode 100644 index 0000000..66644f5 --- /dev/null +++ b/src/scripts/background/types.ts @@ -0,0 +1,15 @@ +export interface DatabaseEntry { + id: string; + url: string; + rating: string; +} + +export interface Service { + id: string; + rating: string; +} + +export interface DonationReminderState { + active?: boolean; + allowedPlattform?: boolean; +} diff --git a/src/scripts/constants.ts b/src/scripts/constants.ts new file mode 100644 index 0000000..c93e512 --- /dev/null +++ b/src/scripts/constants.ts @@ -0,0 +1,13 @@ +export const DEFAULT_API_URL = 'api.tosdr.org'; + +export const ALLOWED_PROTOCOLS = ['http:', 'https:'] as const; + +export const MAX_DOMAIN_REDUCTIONS = 4; + +export const DONATION_BADGE_TEXT = '!'; + +export const DEFAULT_POPUP_PATH = '/views/popup.html'; + +export const API_HEADERS = { + apikey: atob('Y29uZ3JhdHMgb24gZ2V0dGluZyB0aGUga2V5IDpQ'), +}; diff --git a/src/scripts/lib/chromeStorage.ts b/src/scripts/lib/chromeStorage.ts new file mode 100644 index 0000000..ac6fae6 --- /dev/null +++ b/src/scripts/lib/chromeStorage.ts @@ -0,0 +1,17 @@ +export async function getLocal( + keys: T +): Promise> { + return new Promise((resolve) => { + chrome.storage.local.get(keys, (result) => { + resolve(result as Record); + }); + }); +} + +export async function setLocal(items: Record): Promise { + return new Promise((resolve) => { + chrome.storage.local.set(items, () => { + resolve(); + }); + }); +} diff --git a/src/scripts/views/popup.ts b/src/scripts/views/popup.ts index fd74394..deb270a 100644 --- a/src/scripts/views/popup.ts +++ b/src/scripts/views/popup.ts @@ -1,417 +1,43 @@ -let curatorMode = false; -var apiUrl = 'api.tosdr.org'; +import { hydrateState } from './popup/state'; +import { registerUiEventHandlers } from './popup/events'; +import { initializePopupFromLocation } from './popup/navigation'; +import { adjustLayoutForFirefoxDesktop } from './popup/layout'; -chrome.storage.local.get(['api'], function (result) { - if (result["api"] && result["api"].length !== 0) { - apiUrl = result["api"]; - } -}); +void (async function initPopup(): Promise { + await waitForDomReady(); -async function donationReminderLogic(): Promise { - const result = await chrome.storage.local.get('displayDonationReminder'); - console.log('displayDonationReminder:', result["displayDonationReminder"]); - - if (result["displayDonationReminder"]?.active) { - try { - const currentDate = new Date(); - const currentMonth = currentDate.getMonth(); - const currentYear = currentDate.getFullYear(); - - // Reset the badge text for all tabs - const tabs = await chrome.tabs.query({}); - for (const tab of tabs) { - if (tab.id) { - await chrome.action.setBadgeText({ text: '', tabId: tab.id }); - } - } + const preferences = await hydrateState(); + applyPreferences(preferences); - await chrome.storage.local.set({ - lastDismissedReminder: { - month: currentMonth, - year: currentYear, - }, - displayDonationReminder: { - active: false, - allowedPlattform: result["displayDonationReminder"].allowedPlattform, - }, - }); + registerUiEventHandlers(); + await initializePopupFromLocation(window.location.href); + adjustLayoutForFirefoxDesktop(); +})(); - const donationReminder = document.getElementById('donationReminder'); - if (donationReminder) { - donationReminder.style.display = 'block'; - } - } catch (error) { - console.error('Error in donationReminderLogic:', error); - } +function applyPreferences(preferences: { + darkmode: boolean; + curatorMode: boolean; +}): void { + if (preferences.darkmode) { + document.body.classList.add('dark-mode'); } -} - -async function handleUrlInURLIfExists(urlOriginal: string): Promise { - const url = urlOriginal.split('?url=')[1]; - if (!url) { - await donationReminderLogic(); - const idElement = document.getElementById('id'); - const loadingElement = document.getElementById('loading'); - const loadedElement = document.getElementById('loaded'); - const nourlElement = document.getElementById('nourl'); - const pointListElement = document.getElementById('pointList'); - if (idElement) idElement.innerHTML = 'Error: no service-id in url'; - if (loadingElement) loadingElement.style.display = 'none'; - if (loadedElement) loadedElement.style.display = 'none'; - if (nourlElement) nourlElement.style.display = 'block'; - if (pointListElement) pointListElement.style.display = 'none'; + const curatorElement = document.getElementById('curator'); + if (!curatorElement) { return; } - const result = await searchToSDR(url); - - if (result) { - const phoenixButton = document.getElementById('phoenixButton'); - if (phoenixButton) { - phoenixButton.onclick = () => { - window.open(`https://edit.tosdr.org/services/${result}`); - }; - } - - themeHeaderIfEnabled(result); - - const logo = document.getElementById('logo') as HTMLImageElement; - if (logo) { - logo.src = `https://s3.tosdr.org/logos/${result}.png`; - } - - const idElement = document.getElementById('id'); - if (idElement) { - idElement.innerText = result; - } - - await getServiceDetails(result, true); - } else { - await donationReminderLogic(); - const idElement = document.getElementById('id'); - const loadingElement = document.getElementById('loading'); - const loadedElement = document.getElementById('loaded'); - const nourlElement = document.getElementById('nourl'); - const pointListElement = document.getElementById('pointList'); - - if (idElement) idElement.innerText = 'Error: no service-id in url'; - if (loadingElement) loadingElement.style.display = 'none'; - if (loadedElement) loadedElement.style.display = 'none'; - if (nourlElement) nourlElement.style.display = 'block'; - if (pointListElement) pointListElement.style.display = 'none'; - } + curatorElement.style.display = preferences.curatorMode ? 'block' : 'none'; } -function getServiceIDFromURL(url: string): void { - const serviceID = url.split('?service-id=')[1]?.replace('#', ''); - - if (!serviceID) { - handleUrlInURLIfExists(url); +async function waitForDomReady(): Promise { + if (document.readyState !== 'loading') { return; } - if (serviceID === '-1') { - donationReminderLogic(); - const idElement = document.getElementById('id'); - const loadingElement = document.getElementById('loading'); - const loadedElement = document.getElementById('loaded'); - const nourlElement = document.getElementById('nourl'); - const notreviewedElement = document.getElementById('notreviewed'); - const pointListElement = document.getElementById('pointList'); - const edittextElement = document.getElementById('edittext'); - - if (idElement) idElement.innerHTML = 'Error: no service-id in url'; - if (loadingElement) loadingElement.style.display = 'none'; - if (loadedElement) loadedElement.style.display = 'none'; - if (nourlElement) nourlElement.style.display = 'block'; - if (notreviewedElement) notreviewedElement.style.display = 'block'; - if (pointListElement) pointListElement.style.display = 'none'; - if (edittextElement) { - edittextElement.onclick = () => { - window.open('https://edit.tosdr.org'); - }; - } - return; - } - - const phoenixButton = document.getElementById('phoenixButton'); - const webbutton = document.getElementById('webbutton'); - const logo = document.getElementById('logo') as HTMLImageElement; - const idElement = document.getElementById('id'); - - if (phoenixButton) { - phoenixButton.onclick = () => { - window.open(`https://edit.tosdr.org/services/${serviceID}`); - }; - } - - if (webbutton) { - webbutton.onclick = () => { - window.open(`https://tosdr.org/en/service/${serviceID}`); - }; - } - - themeHeaderIfEnabled(serviceID); - - if (logo) { - logo.src = `https://s3.tosdr.org/logos/${serviceID}.png`; - } - - if (idElement) { - idElement.innerHTML = serviceID; - } - - getServiceDetails(serviceID); -} - -function themeHeaderIfEnabled(serviceID: string): void { - chrome.storage.local.get(['themeHeader'], function (result) { - if (result["themeHeader"]) { - const blurredTemplate = `.header::before { - content: ''; - position: absolute; - background-image: url('https://s3.tosdr.org/logos/${serviceID}.png'); - top: 0; - left: 0; - width: 100%; - height: 90%; - background-repeat: no-repeat; - background-position: center; - background-size: cover; - filter: blur(30px); - z-index: -2; - }`; - - const styleElement = document.createElement('style'); - document.head.appendChild(styleElement); - styleElement.sheet?.insertRule(blurredTemplate); - } + await new Promise((resolve) => { + document.addEventListener('DOMContentLoaded', () => resolve(), { + once: true, + }); }); } - -function themeHeaderColorIfEnabled(rating: string): void { - chrome.storage.local.get(['themeHeaderRating'], function (result) { - if (result["themeHeaderRating"]) { - const header = document.getElementById('headerPopup'); - if (header) { - header.classList.add(rating); - } - } - }); -} - -async function getServiceDetails(id: string, unverified = false) { - const service_url = `https://${apiUrl}/service/v3?id=${id}`; - const response = await fetch(service_url); - - // check if we got a 200 response - if (response.status >= 300) { - document.getElementById('loading')!.style.display = 'none'; - document.getElementById('loaded')!.style.display = 'none'; - document.getElementById('error')!.style.display = 'flex'; - return; - } - - const data = await response.json(); - - const name = data.name; - const rating = data.rating; - const points = data.points; - - const serviceNames = document.getElementsByClassName('serviceName'); - - for (let i = 0; i < serviceNames.length; i++) { - (serviceNames[i] as HTMLElement).innerText = name; - } - - document.getElementById('title')!.innerText = name; - if (rating) { - document - .getElementById('gradelabel')! - .classList.add(rating.toLowerCase()); - themeHeaderColorIfEnabled(rating.toLowerCase()); - document.getElementById('grade')!.innerText = rating; - } else { - document.getElementById('grade')!.innerText = 'N/A'; - } - document.getElementById('pointsCount')!.innerText = - points.length.toString(); - - document.getElementById('loading')!.style.opacity = '0'; - document.getElementById('loaded')!.style.filter = 'none'; - setTimeout(function () { - document.getElementById('loading')!.style.display = 'none'; - }, 200); - - if (unverified) { - document.getElementById('notreviewedShown')!.style.display = 'block'; - } - - populateList(points); -} - -function populateList(points: any) { - const pointsList = document.getElementById('pointList'); - - if (!curatorMode) { - points = points.filter((point: any) => point.status === 'approved'); - } else { - points = points.filter( - (point: any) => - point.status === 'approved' || point.status === 'pending' - ); - } - - const blockerPoints = points.filter( - (point: any) => point.case.classification === 'blocker' - ); - const badPoints = points.filter( - (point: any) => point.case.classification === 'bad' - ); - const goodPoints = points.filter( - (point: any) => point.case.classification === 'good' - ); - const neutralPoints = points.filter( - (point: any) => point.case.classification === 'neutral' - ); - - createPointList(blockerPoints, pointsList, false); - createPointList(badPoints, pointsList, false); - createPointList(goodPoints, pointsList, false); - createPointList(neutralPoints, pointsList, true); -} - -function curatorTag(pointStatus: string) { - if (!curatorMode || pointStatus === 'approved') { - return ''; - } - return ""; -} - -function createPointList(pointsFiltered: any, pointsList: any, last: boolean) { - var added = 0; - for (let i = 0; i < pointsFiltered.length; i++) { - const point = document.createElement('div'); - var temp = ` -
- -

${pointsFiltered[i].title}

- ${curatorTag(pointsFiltered[i].status)} -
`; - point.innerHTML = temp.trim(); - pointsList.appendChild(point.firstChild); - added++; - if (i !== pointsFiltered.length - 1) { - const divider = document.createElement('hr'); - pointsList.appendChild(divider); - } - } - if (added !== 0 && !last) { - const divider = document.createElement('hr'); - divider.classList.add('group'); - pointsList.appendChild(divider); - } -} - -async function searchToSDR(term: string) { - const service_url = `https://${apiUrl}/search/v5/?query=${term}`; - const response = await fetch(service_url); - - if (response.status !== 200) { - document.getElementById('loading')!.style.display = 'none'; - document.getElementById('loaded')!.style.display = 'none'; - document.getElementById('error')!.style.display = 'flex'; - return; - } - - const data = await response.json(); - - if (data.services.length !== 0) { - const urls = data.services[0].urls as string[]; - for (let i = 0; i < urls.length; i++) { - if (urls[i] === term) { - return data.services[0].id; - } - } - } -} - -getServiceIDFromURL(window.location.href); - -// Get settings -chrome.storage.local.get(['darkmode', 'curatorMode', 'api'], function (result) { - if (result["darkmode"]) { - const body = document.querySelector('body')!; - body.classList.toggle('dark-mode'); - } - - if (result["curatorMode"]) { - document.getElementById('curator')!.style.display = 'block'; - curatorMode = true; - } else { - document.getElementById('curator')!.style.display = 'none'; - } -}); - -// Event listeners -document.addEventListener('DOMContentLoaded', () => { - const toggleButton = document.getElementById('toggleButton'); - const settingsButton = document.getElementById('settingsButton'); - const sourceButton = document.getElementById('sourceButton'); - const donationButton = document.getElementById('donationButton'); - const source = document.getElementById('source'); - const opentosdr = document.getElementById('opentosdr'); - - if (toggleButton) { - toggleButton.onclick = () => { - const body = document.querySelector('body'); - if (body) { - body.classList.toggle('dark-mode'); - const darkmode = body.classList.contains('dark-mode'); - chrome.storage.local.set({ darkmode }); - } - }; - } - - if (settingsButton) { - settingsButton.onclick = () => { - chrome.runtime.openOptionsPage(); - }; - } - - if (sourceButton) { - sourceButton.onclick = () => { - window.open('https://github.com/tosdr/browser-extensions'); - }; - } - - if (donationButton) { - donationButton.onclick = () => { - window.open('https://tosdr.org/en/sites/donate'); - }; - } - - if (source) { - source.onclick = () => { - window.open('https://github.com/tosdr'); - }; - } - - if (opentosdr) { - opentosdr.onclick = () => { - window.open('https://tosdr.org/'); - }; - } -}); - -function ifFirefoxDesktopResize(): void { - if ( - navigator.userAgent.includes('Firefox') && - !navigator.userAgent.includes('Mobile') - ) { - document.body.style.width = '350px'; - } -} - -ifFirefoxDesktopResize(); diff --git a/src/scripts/views/popup/donation.ts b/src/scripts/views/popup/donation.ts new file mode 100644 index 0000000..7b29002 --- /dev/null +++ b/src/scripts/views/popup/donation.ts @@ -0,0 +1,52 @@ +import { getLocal, setLocal } from '../../lib/chromeStorage'; + +export async function showDonationReminderIfNeeded(): Promise { + const result = await getLocal('displayDonationReminder'); + const state = result['displayDonationReminder'] as + | { + active?: boolean; + allowedPlattform?: boolean; + } + | undefined; + + if (!state?.active) { + return; + } + + try { + const currentDate = new Date(); + const currentMonth = currentDate.getMonth(); + const currentYear = currentDate.getFullYear(); + + const tabs = await queryAllTabs(); + for (const tab of tabs) { + if (tab.id) { + await chrome.action.setBadgeText({ text: '', tabId: tab.id }); + } + } + + await setLocal({ + lastDismissedReminder: { + month: currentMonth, + year: currentYear, + }, + displayDonationReminder: { + active: false, + allowedPlattform: state.allowedPlattform, + }, + }); + + const donationReminder = document.getElementById('donationReminder'); + if (donationReminder) { + donationReminder.style.display = 'block'; + } + } catch (error) { + console.error('Error in donation reminder logic:', error); + } +} + +async function queryAllTabs(): Promise { + return new Promise((resolve) => { + chrome.tabs.query({}, (tabs) => resolve(tabs)); + }); +} diff --git a/src/scripts/views/popup/events.ts b/src/scripts/views/popup/events.ts new file mode 100644 index 0000000..5c54ea0 --- /dev/null +++ b/src/scripts/views/popup/events.ts @@ -0,0 +1,61 @@ +import { setLocal } from '../../lib/chromeStorage'; + +export function registerUiEventHandlers(): void { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupHandlers, { once: true }); + } else { + setupHandlers(); + } +} + +function setupHandlers(): void { + const toggleButton = document.getElementById('toggleButton'); + const settingsButton = document.getElementById('settingsButton'); + const sourceButton = document.getElementById('sourceButton'); + const donationButton = document.getElementById('donationButton'); + const source = document.getElementById('source'); + const opentosdr = document.getElementById('opentosdr'); + + if (toggleButton) { + toggleButton.addEventListener('click', () => { + const body = document.querySelector('body'); + if (!body) { + return; + } + + body.classList.toggle('dark-mode'); + const darkmode = body.classList.contains('dark-mode'); + void setLocal({ darkmode }); + }); + } + + if (settingsButton) { + settingsButton.addEventListener('click', () => { + chrome.runtime.openOptionsPage(); + }); + } + + if (sourceButton) { + sourceButton.addEventListener('click', () => { + window.open('https://github.com/tosdr/browser-extensions'); + }); + } + + if (donationButton) { + donationButton.addEventListener('click', () => { + window.open('https://tosdr.org/en/sites/donate'); + }); + } + + if (source) { + source.addEventListener('click', () => { + window.open('https://github.com/tosdr'); + }); + } + + if (opentosdr) { + opentosdr.addEventListener('click', () => { + window.open('https://tosdr.org/'); + }); + } +} diff --git a/src/scripts/views/popup/layout.ts b/src/scripts/views/popup/layout.ts new file mode 100644 index 0000000..566d784 --- /dev/null +++ b/src/scripts/views/popup/layout.ts @@ -0,0 +1,9 @@ +export function adjustLayoutForFirefoxDesktop(): void { + const userAgent = navigator.userAgent; + const isFirefox = userAgent.includes('Firefox'); + const isMobile = userAgent.includes('Mobile'); + + if (isFirefox && !isMobile) { + document.body.style.width = '350px'; + } +} diff --git a/src/scripts/views/popup/navigation.ts b/src/scripts/views/popup/navigation.ts new file mode 100644 index 0000000..ad3be89 --- /dev/null +++ b/src/scripts/views/popup/navigation.ts @@ -0,0 +1,132 @@ +import { showDonationReminderIfNeeded } from './donation'; +import { displayServiceDetails, searchService } from './service'; +import { applyHeaderTheme } from './theme'; + +export async function initializePopupFromLocation(locationHref: string): Promise { + const serviceId = extractServiceId(locationHref); + + if (!serviceId) { + await handleUrlParameter(locationHref); + return; + } + + if (serviceId === '-1') { + await handleMissingServiceId(); + return; + } + + configureServiceButtons(serviceId); + updateLogo(serviceId); + updateServiceIdentifier(serviceId); + void applyHeaderTheme(serviceId); + + await displayServiceDetails(serviceId); +} + +async function handleUrlParameter(locationHref: string): Promise { + const url = extractUrlParameter(locationHref); + if (!url) { + await showMissingServiceUi(); + return; + } + + const result = await searchService(url); + + if (result) { + configureServiceButtons(result); + updateLogo(result); + updateServiceIdentifier(result); + void applyHeaderTheme(result); + await displayServiceDetails(result, { unverified: true }); + return; + } + + await showMissingServiceUi(); +} + +async function handleMissingServiceId(): Promise { + await showDonationReminderIfNeeded(); + updateServiceIdentifier('Error: no service-id in url'); + hideElement('loading'); + hideElement('loaded'); + showElement('nourl', 'block'); + showElement('notreviewed', 'block'); + hideElement('pointList'); + + const editTextElement = document.getElementById('edittext'); + if (editTextElement) { + editTextElement.onclick = () => { + window.open('https://edit.tosdr.org'); + }; + } +} + +async function showMissingServiceUi(): Promise { + await showDonationReminderIfNeeded(); + + updateServiceIdentifier('Error: no service-id in url'); + hideElement('loading'); + hideElement('loaded'); + showElement('nourl', 'block'); + hideElement('pointList'); +} + +function configureServiceButtons(serviceId: string): void { + const phoenixButton = document.getElementById('phoenixButton'); + if (phoenixButton) { + phoenixButton.onclick = () => { + window.open(`https://edit.tosdr.org/services/${serviceId}`); + }; + } + + const webbutton = document.getElementById('webbutton'); + if (webbutton) { + webbutton.onclick = () => { + window.open(`https://tosdr.org/en/service/${serviceId}`); + }; + } +} + +function updateLogo(serviceId: string): void { + const logo = document.getElementById('logo') as HTMLImageElement | null; + if (logo) { + logo.src = `https://s3.tosdr.org/logos/${serviceId}.png`; + } +} + +function updateServiceIdentifier(message: string): void { + const idElement = document.getElementById('id'); + if (idElement) { + idElement.innerText = message; + } +} + +function extractServiceId(locationHref: string): string | undefined { + const match = locationHref.split('?service-id=')[1]; + if (!match) { + return undefined; + } + return match.replace('#', ''); +} + +function extractUrlParameter(locationHref: string): string | undefined { + const match = locationHref.split('?url=')[1]; + if (!match) { + return undefined; + } + return match; +} + +function hideElement(elementId: string): void { + const element = document.getElementById(elementId); + if (element) { + element.style.display = 'none'; + } +} + +function showElement(elementId: string, display: string): void { + const element = document.getElementById(elementId); + if (element) { + element.style.display = display; + } +} diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts new file mode 100644 index 0000000..a4efcfa --- /dev/null +++ b/src/scripts/views/popup/service.ts @@ -0,0 +1,245 @@ +import { getApiUrl, isCuratorMode } from './state'; +import { applyHeaderColor } from './theme'; + +interface ServicePoint { + status: string; + title: string; + case?: { + classification?: string; + }; +} + +interface ServiceResponse { + name: string; + rating?: string; + points: ServicePoint[]; +} + +interface SearchResponse { + services: Array<{ + id: string; + urls: string[]; + }>; +} + +export async function displayServiceDetails( + id: string, + options: { unverified?: boolean } = {} +): Promise { + const response = await fetch( + `https://${getApiUrl()}/service/v3?id=${encodeURIComponent(id)}` + ); + + if (response.status >= 300) { + hideLoadingState(); + showElement('error'); + return; + } + + const data = (await response.json()) as ServiceResponse; + const rating = data.rating; + + updateServiceName(data.name); + updateTitle(data.name); + updateGrade(rating); + updatePointsCount(data.points.length); + revealLoadedState(options.unverified === true); + + populateList(data.points); +} + +export async function searchService(term: string): Promise { + const response = await fetch( + `https://${getApiUrl()}/search/v5/?query=${encodeURIComponent(term)}` + ); + + if (response.status !== 200) { + hideLoadingState(); + showElement('error'); + return undefined; + } + + const data = (await response.json()) as SearchResponse; + + if (data.services.length === 0) { + return undefined; + } + + const [firstService] = data.services; + if (firstService) { + for (const url of firstService.urls) { + if (url === term) { + return firstService.id; + } + } + } + + return undefined; +} + +function updateServiceName(name: string): void { + const serviceNames = document.getElementsByClassName('serviceName'); + for (const element of Array.from(serviceNames)) { + (element as HTMLElement).innerText = name; + } +} + +function updateTitle(name: string): void { + const titleElement = document.getElementById('title'); + if (titleElement) { + titleElement.innerText = name; + } +} + +function updateGrade(rating?: string): void { + const gradeLabel = document.getElementById('gradelabel'); + const gradeElement = document.getElementById('grade'); + + if (!gradeElement) { + return; + } + + if (rating) { + if (gradeLabel) { + gradeLabel.classList.add(rating.toLowerCase()); + } + void applyHeaderColor(rating.toLowerCase()); + gradeElement.innerText = rating; + } else { + gradeElement.innerText = 'N/A'; + } +} + +function updatePointsCount(count: number): void { + const pointsCount = document.getElementById('pointsCount'); + if (pointsCount) { + pointsCount.innerText = count.toString(); + } +} + +function revealLoadedState(unverified: boolean): void { + const loadingElement = document.getElementById('loading'); + const loadedElement = document.getElementById('loaded'); + + if (loadingElement) { + loadingElement.style.opacity = '0'; + setTimeout(() => { + loadingElement.style.display = 'none'; + }, 200); + } + + if (loadedElement) { + loadedElement.style.filter = 'none'; + } + + if (unverified) { + showElement('notreviewedShown'); + } +} + +function populateList(points: ServicePoint[]): void { + const pointsList = document.getElementById('pointList'); + if (!pointsList) { + return; + } + + pointsList.style.display = 'block'; + pointsList.innerHTML = ''; + + const filteredPoints = filterPoints(points); + + appendPointGroup(filteredPoints.blocker, pointsList, false); + appendPointGroup(filteredPoints.bad, pointsList, false); + appendPointGroup(filteredPoints.good, pointsList, false); + appendPointGroup(filteredPoints.neutral, pointsList, true); +} + +function filterPoints(points: ServicePoint[]): { + blocker: ServicePoint[]; + bad: ServicePoint[]; + good: ServicePoint[]; + neutral: ServicePoint[]; +} { + const curatedPoints = points.filter((point) => { + if (!isCuratorMode()) { + return point.status === 'approved'; + } + return point.status === 'approved' || point.status === 'pending'; + }); + + return { + blocker: curatedPoints.filter( + (point) => point.case?.classification === 'blocker' + ), + bad: curatedPoints.filter( + (point) => point.case?.classification === 'bad' + ), + good: curatedPoints.filter( + (point) => point.case?.classification === 'good' + ), + neutral: curatedPoints.filter( + (point) => point.case?.classification === 'neutral' + ), + }; +} + +function appendPointGroup( + points: ServicePoint[], + container: HTMLElement, + isLastGroup: boolean +): void { + let added = 0; + + points.forEach((point, index) => { + const wrapper = document.createElement('div'); + const classification = point.case?.classification ?? 'neutral'; + wrapper.innerHTML = ` +
+ +

${point.title}

+ ${renderCuratorTag(point.status)} +
+ `.trim(); + if (wrapper.firstChild) { + container.appendChild(wrapper.firstChild as HTMLElement); + } + added += 1; + + if (index !== points.length - 1) { + const divider = document.createElement('hr'); + container.appendChild(divider); + } + }); + + if (added > 0 && !isLastGroup) { + const divider = document.createElement('hr'); + divider.classList.add('group'); + container.appendChild(divider); + } +} + +function renderCuratorTag(status: string): string { + if (!isCuratorMode() || status === 'approved') { + return ''; + } + return ""; +} + +function hideLoadingState(): void { + const loadingElement = document.getElementById('loading'); + const loadedElement = document.getElementById('loaded'); + + if (loadingElement) { + loadingElement.style.display = 'none'; + } + if (loadedElement) { + loadedElement.style.display = 'none'; + } +} + +function showElement(elementId: string): void { + const element = document.getElementById(elementId); + if (element) { + element.style.display = 'block'; + } +} diff --git a/src/scripts/views/popup/state.ts b/src/scripts/views/popup/state.ts new file mode 100644 index 0000000..2a60649 --- /dev/null +++ b/src/scripts/views/popup/state.ts @@ -0,0 +1,46 @@ +import { DEFAULT_API_URL } from '../../constants'; +import { getLocal } from '../../lib/chromeStorage'; + +let curatorMode = false; +let apiUrl = DEFAULT_API_URL; + +export interface PopupPreferences { + darkmode: boolean; + curatorMode: boolean; +} + +export function isCuratorMode(): boolean { + return curatorMode; +} + +export function setCuratorMode(value: boolean): void { + curatorMode = value; +} + +export function getApiUrl(): string { + return apiUrl; +} + +export function setApiUrl(url: string): void { + apiUrl = url; +} + +export async function hydrateState(): Promise { + const result = await getLocal(['darkmode', 'curatorMode', 'api']); + + const darkmode = Boolean(result['darkmode']); + const storedCuratorMode = Boolean(result['curatorMode']); + setCuratorMode(storedCuratorMode); + + const api = result['api']; + if (typeof api === 'string' && api.length > 0) { + setApiUrl(api); + } else { + setApiUrl(DEFAULT_API_URL); + } + + return { + darkmode, + curatorMode: storedCuratorMode, + }; +} diff --git a/src/scripts/views/popup/theme.ts b/src/scripts/views/popup/theme.ts new file mode 100644 index 0000000..eabc069 --- /dev/null +++ b/src/scripts/views/popup/theme.ts @@ -0,0 +1,41 @@ +import { getLocal } from '../../lib/chromeStorage'; + +export async function applyHeaderTheme(serviceId: string): Promise { + const result = await getLocal('themeHeader'); + if (!result['themeHeader']) { + return; + } + + const blurredTemplate = `.header::before { + content: ''; + position: absolute; + background-image: url('https://s3.tosdr.org/logos/${serviceId}.png'); + top: 0; + left: 0; + width: 100%; + height: 90%; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + filter: blur(30px); + z-index: -2; + }`; + + const styleElement = document.createElement('style'); + document.head.appendChild(styleElement); + styleElement.sheet?.insertRule(blurredTemplate); +} + +export async function applyHeaderColor(rating: string): Promise { + const result = await getLocal('themeHeaderRating'); + if (!result['themeHeaderRating']) { + return; + } + + const header = document.getElementById('headerPopup'); + if (!header) { + return; + } + + header.classList.add(rating); +} diff --git a/src/scripts/views/settings.ts b/src/scripts/views/settings.ts index c4684ef..10cd8cf 100644 --- a/src/scripts/views/settings.ts +++ b/src/scripts/views/settings.ts @@ -1,113 +1,20 @@ -const updateInput = document.getElementById('update') as HTMLInputElement; -const themeInput = document.getElementById('theme') as HTMLInputElement; -const themeRatingInput = document.getElementById('themeRating') as HTMLInputElement; -const curatorModeInput = document.getElementById('curatorMode') as HTMLInputElement; -// const telemetryInput = document.getElementById('telemetry') as HTMLInputElement; -const apiInput = document.getElementById('api') as HTMLInputElement; - - -chrome.storage.local.get( - [ - 'db', - 'lastModified', - 'interval', - 'themeHeader', - 'themeHeaderRating', - 'curatorMode', - 'sentry', - 'api' - ], - function (result) { - if (result["db"]) { - const db = result["db"]; - const lastModified = new Date(result["lastModified"]); - - document.getElementById('date')!.innerText = - lastModified.toLocaleDateString('en-US'); - document.getElementById('indexed')!.innerText = db.length; - } else { - const elements = document.getElementsByClassName('dbavailable'); - for (let i = 0; i < elements.length; i++) { - elements[i]?.remove(); - } - } - - if (result["interval"]) { - document.getElementById('days')!.innerText = result["interval"]; - updateInput.value = result["interval"]; - } - - if (result["themeHeader"]) { - themeInput.checked = result["themeHeader"]; - } - - if (result["themeHeaderRating"]) { - themeRatingInput.checked = result["themeHeaderRating"]; - } - - if (result["curatorMode"]) { - curatorModeInput.checked = result["curatorMode"]; - } - - if (result["api"]) { - if (result["api"].length !== 0) apiInput.value = result["api"]; - } - - // if (result.sentry) { - // telemetryInput.checked = result.sentry; - // } +import { populateSettingsForm } from './settings/state'; +import { registerSettingsHandlers } from './settings/handlers'; + +void (async function initSettings(): Promise { + await waitForDomReady(); + await populateSettingsForm(); + registerSettingsHandlers(); +})(); + +async function waitForDomReady(): Promise { + if (document.readyState !== 'loading') { + return; } -); - -updateInput.onchange = function () { - document.getElementById('days')!.innerText = updateInput.value; - chrome.storage.local.set({ interval: updateInput.value }, function () { - console.log('database update interval changed'); + await new Promise((resolve) => { + document.addEventListener('DOMContentLoaded', () => resolve(), { + once: true, + }); }); -}; - -themeInput.addEventListener('change', function () { - chrome.storage.local.set( - { themeHeader: themeInput.checked }, - function () { - console.log('theme header value changed'); - } - ); -}); - -themeRatingInput.addEventListener('change', function () { - chrome.storage.local.set( - { themeHeaderRating: themeRatingInput.checked }, - function () { - console.log('theme header (rating) value changed'); - } - ); -}); - -curatorModeInput.addEventListener('change', function () { - chrome.storage.local.set( - { curatorMode: curatorModeInput.checked }, - function () { - console.log('curatormode has been toggled.'); - } - ); -}); - -apiInput.addEventListener('change', function () { - chrome.storage.local.set( - { api: apiInput.value }, - function () { - console.log('api url has been changed.'); - } - ); -}); - -// telemetryInput.addEventListener('change', function () { -// chrome.storage.local.set( -// { sentry: telemetryInput.checked }, -// function () { -// console.log('telemetry has been toggled.'); -// } -// ); -// }); \ No newline at end of file +} diff --git a/src/scripts/views/settings/handlers.ts b/src/scripts/views/settings/handlers.ts new file mode 100644 index 0000000..895f866 --- /dev/null +++ b/src/scripts/views/settings/handlers.ts @@ -0,0 +1,43 @@ +import { setLocal } from '../../lib/chromeStorage'; + +export function registerSettingsHandlers(): void { + const updateInput = document.getElementById('update') as HTMLInputElement | null; + const themeInput = document.getElementById('theme') as HTMLInputElement | null; + const themeRatingInput = document.getElementById('themeRating') as HTMLInputElement | null; + const curatorModeInput = document.getElementById('curatorMode') as HTMLInputElement | null; + const apiInput = document.getElementById('api') as HTMLInputElement | null; + + if (updateInput) { + updateInput.addEventListener('change', () => { + const daysElement = document.getElementById('days'); + if (daysElement) { + daysElement.textContent = updateInput.value; + } + void setLocal({ interval: updateInput.value }); + }); + } + + if (themeInput) { + themeInput.addEventListener('change', () => { + void setLocal({ themeHeader: themeInput.checked }); + }); + } + + if (themeRatingInput) { + themeRatingInput.addEventListener('change', () => { + void setLocal({ themeHeaderRating: themeRatingInput.checked }); + }); + } + + if (curatorModeInput) { + curatorModeInput.addEventListener('change', () => { + void setLocal({ curatorMode: curatorModeInput.checked }); + }); + } + + if (apiInput) { + apiInput.addEventListener('change', () => { + void setLocal({ api: apiInput.value }); + }); + } +} diff --git a/src/scripts/views/settings/state.ts b/src/scripts/views/settings/state.ts new file mode 100644 index 0000000..155a176 --- /dev/null +++ b/src/scripts/views/settings/state.ts @@ -0,0 +1,70 @@ +import { getLocal } from '../../lib/chromeStorage'; + +export async function populateSettingsForm(): Promise { + const elements = collectElements(); + const result = await getLocal([ + 'db', + 'lastModified', + 'interval', + 'themeHeader', + 'themeHeaderRating', + 'curatorMode', + 'sentry', + 'api', + ]); + + if (Array.isArray(result['db'])) { + const lastModified = new Date(String(result['lastModified'])); + if (!Number.isNaN(lastModified.getTime()) && elements.date) { + elements.date.textContent = lastModified.toLocaleDateString('en-US'); + } + if (elements.indexed) { + elements.indexed.textContent = String(result['db'].length); + } + } else { + removeDatabaseIndicators(); + } + + if (typeof result['interval'] === 'number' || typeof result['interval'] === 'string') { + if (elements.days) { + elements.days.textContent = String(result['interval']); + } + if (elements.updateInput) { + elements.updateInput.value = String(result['interval']); + } + } + + if (elements.themeInput) { + elements.themeInput.checked = Boolean(result['themeHeader']); + } + + if (elements.themeRatingInput) { + elements.themeRatingInput.checked = Boolean(result['themeHeaderRating']); + } + + if (elements.curatorModeInput) { + elements.curatorModeInput.checked = Boolean(result['curatorMode']); + } + + if (elements.apiInput && typeof result['api'] === 'string') { + elements.apiInput.value = result['api']; + } +} + +function collectElements() { + return { + updateInput: document.getElementById('update') as HTMLInputElement | null, + themeInput: document.getElementById('theme') as HTMLInputElement | null, + themeRatingInput: document.getElementById('themeRating') as HTMLInputElement | null, + curatorModeInput: document.getElementById('curatorMode') as HTMLInputElement | null, + apiInput: document.getElementById('api') as HTMLInputElement | null, + date: document.getElementById('date') as HTMLElement | null, + indexed: document.getElementById('indexed') as HTMLElement | null, + days: document.getElementById('days') as HTMLElement | null, + }; +} + +function removeDatabaseIndicators(): void { + const availableElements = document.getElementsByClassName('dbavailable'); + Array.from(availableElements).forEach((element) => element.remove()); +} diff --git a/src/views/popup.html b/src/views/popup.html index 2cfd36b..d0017af 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -110,5 +110,5 @@

- v6.0.0 - + diff --git a/src/views/settings/settings.html b/src/views/settings/settings.html index 3447074..f9149cb 100644 --- a/src/views/settings/settings.html +++ b/src/views/settings/settings.html @@ -153,6 +153,6 @@

Advanced Debugging Settings

- + From 145eb119e32707ce773eeafdb529aa59299bae1f Mon Sep 17 00:00:00 2001 From: ptgms Date: Mon, 22 Sep 2025 15:53:53 +0200 Subject: [PATCH 02/32] fix: make firefox background script work --- src/manifest-ff.json | 2 +- src/views/background.html | 10 ++++++++++ vite.config.ts | 5 +++-- vite.firefox.config.ts | 11 +++++++++-- 4 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/views/background.html diff --git a/src/manifest-ff.json b/src/manifest-ff.json index 6ee61c2..31c4e88 100644 --- a/src/manifest-ff.json +++ b/src/manifest-ff.json @@ -6,7 +6,7 @@ "homepage_url": "https://tosdr.org", "permissions": ["tabs", "storage"], "background": { - "scripts": ["scripts/background.js"] + "page": "views/background.html" }, "browser_specific_settings": { "gecko": { diff --git a/src/views/background.html b/src/views/background.html new file mode 100644 index 0000000..ee00358 --- /dev/null +++ b/src/views/background.html @@ -0,0 +1,10 @@ + + + + + ToS;DR + + + + + diff --git a/vite.config.ts b/vite.config.ts index 4502a5d..b08206d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,7 +23,8 @@ export default defineConfig({ popup: resolve(__dirname, 'src/scripts/views/popup.ts'), settings: resolve(__dirname, 'src/scripts/views/settings.ts'), 'popup-html': resolve(__dirname, 'src/views/popup.html'), - 'settings-html': resolve(__dirname, 'src/views/settings/settings.html') + 'settings-html': resolve(__dirname, 'src/views/settings/settings.html'), + 'background-html': resolve(__dirname, 'src/views/background.html') }, output: { entryFileNames: (chunkInfo) => { @@ -62,4 +63,4 @@ export default defineConfig({ define: { 'process.env.NODE_ENV': JSON.stringify(process.env['NODE_ENV'] || 'development') } -}); \ No newline at end of file +}); diff --git a/vite.firefox.config.ts b/vite.firefox.config.ts index 825b398..57aff1a 100644 --- a/vite.firefox.config.ts +++ b/vite.firefox.config.ts @@ -53,13 +53,20 @@ function copyFirefoxAssetsPlugin() { } copyFileSync(`${outDir}/src/views/popup.html`, `${outDir}/views/popup.html`); } - + if (existsSync(`${outDir}/src/views/settings/settings.html`)) { if (!existsSync(`${outDir}/views/settings`)) { mkdirSync(`${outDir}/views/settings`, { recursive: true }); } copyFileSync(`${outDir}/src/views/settings/settings.html`, `${outDir}/views/settings/settings.html`); } + + if (existsSync(`${outDir}/src/views/background.html`)) { + if (!existsSync(`${outDir}/views`)) { + mkdirSync(`${outDir}/views`, { recursive: true }); + } + copyFileSync(`${outDir}/src/views/background.html`, `${outDir}/views/background.html`); + } console.log('Firefox assets copied successfully'); } @@ -79,4 +86,4 @@ export default defineConfig( minify: true } }) -); \ No newline at end of file +); From 1412bb1596e7f84633314b0eeb691271852ee4e9 Mon Sep 17 00:00:00 2001 From: ptgms Date: Tue, 30 Sep 2025 13:09:08 +0200 Subject: [PATCH 03/32] feat: improve error handling with detailed error messages and UI updates --- src/scripts/views/popup/service.ts | 181 +++++++++++++++++++++++------ src/views/popup.html | 5 +- src/views/style/popup.css | 18 ++- 3 files changed, 165 insertions(+), 39 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index a4efcfa..653fe63 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -26,55 +26,80 @@ export async function displayServiceDetails( id: string, options: { unverified?: boolean } = {} ): Promise { - const response = await fetch( - `https://${getApiUrl()}/service/v3?id=${encodeURIComponent(id)}` - ); - - if (response.status >= 300) { - hideLoadingState(); - showElement('error'); - return; - } + try { + const response = await fetch( + `https://${getApiUrl()}/service/v3?id=${encodeURIComponent(id)}` + ); + + if (!response.ok) { + hideLoadingState(); + const errorDescription = await formatHttpError(response); + showErrorOverlay( + 'Unable to load service details.', + errorDescription + ); + return; + } - const data = (await response.json()) as ServiceResponse; - const rating = data.rating; + const data = (await response.json()) as ServiceResponse; + const rating = data.rating; - updateServiceName(data.name); - updateTitle(data.name); - updateGrade(rating); - updatePointsCount(data.points.length); - revealLoadedState(options.unverified === true); + updateServiceName(data.name); + updateTitle(data.name); + updateGrade(rating); + updatePointsCount(data.points.length); + revealLoadedState(options.unverified === true); - populateList(data.points); + populateList(data.points); + } catch (error) { + hideLoadingState(); + showErrorOverlay( + 'Unable to load service details.', + formatUnknownError(error) + ); + } } export async function searchService(term: string): Promise { - const response = await fetch( - `https://${getApiUrl()}/search/v5/?query=${encodeURIComponent(term)}` - ); - - if (response.status !== 200) { - hideLoadingState(); - showElement('error'); - return undefined; - } + try { + const response = await fetch( + `https://${getApiUrl()}/search/v5/?query=${encodeURIComponent(term)}` + ); + + if (response.status !== 200) { + hideLoadingState(); + const errorDescription = await formatHttpError(response); + showErrorOverlay( + 'Unable to search for matching services.', + errorDescription + ); + return undefined; + } - const data = (await response.json()) as SearchResponse; + const data = (await response.json()) as SearchResponse; - if (data.services.length === 0) { - return undefined; - } + if (data.services.length === 0) { + return undefined; + } - const [firstService] = data.services; - if (firstService) { - for (const url of firstService.urls) { - if (url === term) { - return firstService.id; + const [firstService] = data.services; + if (firstService) { + for (const url of firstService.urls) { + if (url === term) { + return firstService.id; + } } } - } - return undefined; + return undefined; + } catch (error) { + hideLoadingState(); + showErrorOverlay( + 'Unable to search for matching services.', + formatUnknownError(error) + ); + return undefined; + } } function updateServiceName(name: string): void { @@ -243,3 +268,85 @@ function showElement(elementId: string): void { element.style.display = 'block'; } } + +async function formatHttpError(response: Response): Promise { + const statusSummary = `${response.status} ${response.statusText}`.trim(); + + try { + const contentType = response.headers.get('content-type') ?? ''; + const bodyText = await response.text(); + + if (!bodyText) { + return statusSummary || 'Request failed.'; + } + + if (contentType.includes('application/json')) { + try { + const parsed = JSON.parse(bodyText) as { + error?: string; + message?: string; + } | null; + + const jsonMessage = + typeof parsed?.message === 'string' + ? parsed.message + : typeof parsed?.error === 'string' + ? parsed.error + : undefined; + + if (jsonMessage) { + return statusSummary + ? `${statusSummary} – ${jsonMessage}` + : jsonMessage; + } + } catch { + // Fall back to using the raw body text below. + } + } + + const trimmedBody = bodyText.trim(); + if (!trimmedBody) { + return statusSummary || 'Request failed.'; + } + + return statusSummary + ? `${statusSummary} – ${trimmedBody}` + : trimmedBody; + } catch { + return statusSummary || 'Request failed.'; + } +} + +function showErrorOverlay(title: string, description: string): void { + const errorContainer = document.getElementById('error'); + const titleElement = document.getElementById('errorTitle'); + const descriptionElement = document.getElementById('errorDescription'); + + if (titleElement) { + titleElement.innerText = title; + } + + if (descriptionElement) { + descriptionElement.innerText = description; + } + + if (errorContainer) { + errorContainer.style.display = 'flex'; + } +} + +function formatUnknownError(error: unknown): string { + if (error instanceof Error) { + return error.message || error.name; + } + + if (typeof error === 'string') { + return error; + } + + try { + return JSON.stringify(error); + } catch { + return 'An unexpected error occurred.'; + } +} diff --git a/src/views/popup.html b/src/views/popup.html index d0017af..9681ada 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -50,7 +50,10 @@

Please wait, loading...

diff --git a/src/views/style/popup.css b/src/views/style/popup.css index 438c439..c69926d 100644 --- a/src/views/style/popup.css +++ b/src/views/style/popup.css @@ -221,6 +221,22 @@ html { align-items: center; position: absolute; color: white; + flex-direction: row; + gap: 1.5rem; + padding: 1.5rem; + text-align: left; +} + +.errorText { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-width: 260px; +} + +.errorText > h2, +.errorText > p { + margin: 0; } #loading { @@ -373,4 +389,4 @@ button { padding: .5rem; margin-bottom: .5rem; line-height: 1; -} \ No newline at end of file +} From c2cc935c6588a3c58ef9477a94af756742e02629 Mon Sep 17 00:00:00 2001 From: ptgms Date: Tue, 30 Sep 2025 13:26:49 +0200 Subject: [PATCH 04/32] feat: add multi-language support --- src/scripts/lib/language.ts | 52 ++++++++++++++++++++++++++ src/scripts/views/popup.ts | 21 +++++++---- src/scripts/views/popup/service.ts | 9 +++-- src/scripts/views/popup/state.ts | 20 +++++++++- src/scripts/views/settings/handlers.ts | 12 ++++++ src/scripts/views/settings/state.ts | 8 ++++ src/views/popup.html | 3 ++ src/views/settings/settings.html | 21 +++++++++++ src/views/settings/style/settings.css | 12 +++++- src/views/style/popup.css | 7 ++++ 10 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 src/scripts/lib/language.ts diff --git a/src/scripts/lib/language.ts b/src/scripts/lib/language.ts new file mode 100644 index 0000000..2721fe3 --- /dev/null +++ b/src/scripts/lib/language.ts @@ -0,0 +1,52 @@ +export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const; +export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]; + +export function normalizeLanguage( + value: string +): SupportedLanguage | undefined { + const trimmed = value.trim().toLowerCase(); + if (!trimmed) { + return undefined; + } + + const baseCode = trimmed.split('-')[0]; + if (SUPPORTED_LANGUAGES.includes(baseCode as SupportedLanguage)) { + return baseCode as SupportedLanguage; + } + + return undefined; +} + +export function detectBrowserLanguage(): SupportedLanguage { + const candidateLanguages: string[] = []; + + if (typeof navigator !== 'undefined') { + if (Array.isArray(navigator.languages)) { + candidateLanguages.push(...navigator.languages); + } + + if (typeof navigator.language === 'string') { + candidateLanguages.push(navigator.language); + } + } + + for (const candidate of candidateLanguages) { + const normalized = normalizeLanguage(candidate); + if (normalized) { + return normalized; + } + } + + return 'en'; +} + +export function resolveLanguage(candidate: unknown): SupportedLanguage { + if (typeof candidate === 'string') { + const normalized = normalizeLanguage(candidate); + if (normalized) { + return normalized; + } + } + + return detectBrowserLanguage(); +} diff --git a/src/scripts/views/popup.ts b/src/scripts/views/popup.ts index deb270a..4b716c7 100644 --- a/src/scripts/views/popup.ts +++ b/src/scripts/views/popup.ts @@ -1,4 +1,4 @@ -import { hydrateState } from './popup/state'; +import { hydrateState, PopupPreferences } from './popup/state'; import { registerUiEventHandlers } from './popup/events'; import { initializePopupFromLocation } from './popup/navigation'; import { adjustLayoutForFirefoxDesktop } from './popup/layout'; @@ -14,20 +14,25 @@ void (async function initPopup(): Promise { adjustLayoutForFirefoxDesktop(); })(); -function applyPreferences(preferences: { - darkmode: boolean; - curatorMode: boolean; -}): void { +function applyPreferences(preferences: PopupPreferences): void { if (preferences.darkmode) { document.body.classList.add('dark-mode'); } const curatorElement = document.getElementById('curator'); - if (!curatorElement) { - return; + if (curatorElement) { + curatorElement.style.display = preferences.curatorMode + ? 'block' + : 'none'; } - curatorElement.style.display = preferences.curatorMode ? 'block' : 'none'; + const translationWarningElement = document.getElementById( + 'translationWarning' + ); + if (translationWarningElement) { + translationWarningElement.style.display = + preferences.language === 'en' ? 'none' : 'block'; + } } async function waitForDomReady(): Promise { diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 653fe63..0f1ff71 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -1,4 +1,4 @@ -import { getApiUrl, isCuratorMode } from './state'; +import { getApiUrl, getLanguage, isCuratorMode } from './state'; import { applyHeaderColor } from './theme'; interface ServicePoint { @@ -6,6 +6,7 @@ interface ServicePoint { title: string; case?: { classification?: string; + localized_title?: string | null; }; } @@ -27,8 +28,9 @@ export async function displayServiceDetails( options: { unverified?: boolean } = {} ): Promise { try { + const language = getLanguage(); const response = await fetch( - `https://${getApiUrl()}/service/v3?id=${encodeURIComponent(id)}` + `https://${getApiUrl()}/service/v3?id=${encodeURIComponent(id)}&lang=${encodeURIComponent(language)}` ); if (!response.ok) { @@ -218,10 +220,11 @@ function appendPointGroup( points.forEach((point, index) => { const wrapper = document.createElement('div'); const classification = point.case?.classification ?? 'neutral'; + const pointTitle = point.case?.localized_title ?? point.title; wrapper.innerHTML = `
-

${point.title}

+

${pointTitle}

${renderCuratorTag(point.status)}
`.trim(); diff --git a/src/scripts/views/popup/state.ts b/src/scripts/views/popup/state.ts index 2a60649..f1e1ed2 100644 --- a/src/scripts/views/popup/state.ts +++ b/src/scripts/views/popup/state.ts @@ -1,12 +1,18 @@ import { DEFAULT_API_URL } from '../../constants'; import { getLocal } from '../../lib/chromeStorage'; +import { + SupportedLanguage, + resolveLanguage, +} from '../../lib/language'; let curatorMode = false; let apiUrl = DEFAULT_API_URL; +let language: SupportedLanguage = 'en'; export interface PopupPreferences { darkmode: boolean; curatorMode: boolean; + language: SupportedLanguage; } export function isCuratorMode(): boolean { @@ -26,7 +32,7 @@ export function setApiUrl(url: string): void { } export async function hydrateState(): Promise { - const result = await getLocal(['darkmode', 'curatorMode', 'api']); + const result = await getLocal(['darkmode', 'curatorMode', 'api', 'language']); const darkmode = Boolean(result['darkmode']); const storedCuratorMode = Boolean(result['curatorMode']); @@ -39,8 +45,20 @@ export async function hydrateState(): Promise { setApiUrl(DEFAULT_API_URL); } + const resolvedLanguage = resolveLanguage(result['language']); + setLanguage(resolvedLanguage); + return { darkmode, curatorMode: storedCuratorMode, + language: resolvedLanguage, }; } + +export function getLanguage(): SupportedLanguage { + return language; +} + +export function setLanguage(value: SupportedLanguage): void { + language = value; +} diff --git a/src/scripts/views/settings/handlers.ts b/src/scripts/views/settings/handlers.ts index 895f866..a32ddc0 100644 --- a/src/scripts/views/settings/handlers.ts +++ b/src/scripts/views/settings/handlers.ts @@ -1,4 +1,5 @@ import { setLocal } from '../../lib/chromeStorage'; +import { normalizeLanguage } from '../../lib/language'; export function registerSettingsHandlers(): void { const updateInput = document.getElementById('update') as HTMLInputElement | null; @@ -6,6 +7,7 @@ export function registerSettingsHandlers(): void { const themeRatingInput = document.getElementById('themeRating') as HTMLInputElement | null; const curatorModeInput = document.getElementById('curatorMode') as HTMLInputElement | null; const apiInput = document.getElementById('api') as HTMLInputElement | null; + const languageSelect = document.getElementById('language') as HTMLSelectElement | null; if (updateInput) { updateInput.addEventListener('change', () => { @@ -40,4 +42,14 @@ export function registerSettingsHandlers(): void { void setLocal({ api: apiInput.value }); }); } + + if (languageSelect) { + languageSelect.addEventListener('change', () => { + const normalized = normalizeLanguage(languageSelect.value) ?? 'en'; + if (languageSelect.value !== normalized) { + languageSelect.value = normalized; + } + void setLocal({ language: normalized }); + }); + } } diff --git a/src/scripts/views/settings/state.ts b/src/scripts/views/settings/state.ts index 155a176..a86d711 100644 --- a/src/scripts/views/settings/state.ts +++ b/src/scripts/views/settings/state.ts @@ -1,4 +1,5 @@ import { getLocal } from '../../lib/chromeStorage'; +import { resolveLanguage } from '../../lib/language'; export async function populateSettingsForm(): Promise { const elements = collectElements(); @@ -11,6 +12,7 @@ export async function populateSettingsForm(): Promise { 'curatorMode', 'sentry', 'api', + 'language', ]); if (Array.isArray(result['db'])) { @@ -49,6 +51,11 @@ export async function populateSettingsForm(): Promise { if (elements.apiInput && typeof result['api'] === 'string') { elements.apiInput.value = result['api']; } + + if (elements.languageSelect) { + const language = resolveLanguage(result['language']); + elements.languageSelect.value = language; + } } function collectElements() { @@ -58,6 +65,7 @@ function collectElements() { themeRatingInput: document.getElementById('themeRating') as HTMLInputElement | null, curatorModeInput: document.getElementById('curatorMode') as HTMLInputElement | null, apiInput: document.getElementById('api') as HTMLInputElement | null, + languageSelect: document.getElementById('language') as HTMLSelectElement | null, date: document.getElementById('date') as HTMLElement | null, indexed: document.getElementById('indexed') as HTMLElement | null, days: document.getElementById('days') as HTMLElement | null, diff --git a/src/views/popup.html b/src/views/popup.html index 9681ada..a2ef6a3 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -112,6 +112,9 @@

- - v6.0.0 + diff --git a/src/views/settings/settings.html b/src/views/settings/settings.html index f9149cb..df1b3a9 100644 --- a/src/views/settings/settings.html +++ b/src/views/settings/settings.html @@ -54,6 +54,27 @@

Appearance Settings

+
+
+
+
+ +

Language

+
+

+ Controls the language used for service details. +

+
+ +
+
+

Database Settings

diff --git a/src/views/settings/style/settings.css b/src/views/settings/style/settings.css index 3cc523f..d7d5c18 100644 --- a/src/views/settings/style/settings.css +++ b/src/views/settings/style/settings.css @@ -159,4 +159,14 @@ input[type='text'] { padding: 0.2rem; font-size: 1rem; font-weight: normal; -} \ No newline at end of file +} + +.languageSelect { + width: 100%; + border: none; + border-bottom: 1px solid #ccc; + background-color: transparent; + padding: 0.2rem; + font-size: 1rem; + font-weight: normal; +} diff --git a/src/views/style/popup.css b/src/views/style/popup.css index c69926d..ab8f89f 100644 --- a/src/views/style/popup.css +++ b/src/views/style/popup.css @@ -201,6 +201,13 @@ html { cursor: pointer; } +.translationWarning { + margin: 0.5rem 1rem 1.5rem; + font-size: 0.8rem; + color: #555; + text-align: center; +} + .labeltext { font-size: 1rem; font-weight: normal; From 6f297e114f68644d88a7d75e50053bfde8ca57da Mon Sep 17 00:00:00 2001 From: ptgms Date: Tue, 30 Sep 2025 13:37:15 +0200 Subject: [PATCH 05/32] remove src folder if exists --- vite.chrome.config.ts | 19 +++++++++++++++---- vite.firefox.config.ts | 8 ++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/vite.chrome.config.ts b/vite.chrome.config.ts index 13d4d46..8290d56 100644 --- a/vite.chrome.config.ts +++ b/vite.chrome.config.ts @@ -1,6 +1,6 @@ import { defineConfig, mergeConfig } from 'vite'; import baseConfig from './vite.config'; -import { copyFileSync, mkdirSync, existsSync, cpSync } from 'fs'; +import { copyFileSync, mkdirSync, existsSync, cpSync, rmSync } from 'fs'; // Custom plugin to copy Chrome-specific assets function copyChromeAssetsPlugin() { @@ -53,14 +53,25 @@ function copyChromeAssetsPlugin() { } copyFileSync(`${outDir}/src/views/popup.html`, `${outDir}/views/popup.html`); } - + if (existsSync(`${outDir}/src/views/settings/settings.html`)) { if (!existsSync(`${outDir}/views/settings`)) { mkdirSync(`${outDir}/views/settings`, { recursive: true }); } copyFileSync(`${outDir}/src/views/settings/settings.html`, `${outDir}/views/settings/settings.html`); } - + + if (existsSync(`${outDir}/src/views/background.html`)) { + if (!existsSync(`${outDir}/views`)) { + mkdirSync(`${outDir}/views`, { recursive: true }); + } + copyFileSync(`${outDir}/src/views/background.html`, `${outDir}/views/background.html`); + } + + if (existsSync(`${outDir}/src`)) { + rmSync(`${outDir}/src`, { recursive: true, force: true }); + } + console.log('Chrome assets copied successfully'); } }; @@ -79,4 +90,4 @@ export default defineConfig( minify: true } }) -); \ No newline at end of file +); diff --git a/vite.firefox.config.ts b/vite.firefox.config.ts index 57aff1a..e977266 100644 --- a/vite.firefox.config.ts +++ b/vite.firefox.config.ts @@ -1,6 +1,6 @@ import { defineConfig, mergeConfig } from 'vite'; import baseConfig from './vite.config'; -import { copyFileSync, mkdirSync, existsSync, cpSync } from 'fs'; +import { copyFileSync, mkdirSync, existsSync, cpSync, rmSync } from 'fs'; // Custom plugin to copy Firefox-specific assets function copyFirefoxAssetsPlugin() { @@ -67,7 +67,11 @@ function copyFirefoxAssetsPlugin() { } copyFileSync(`${outDir}/src/views/background.html`, `${outDir}/views/background.html`); } - + + if (existsSync(`${outDir}/src`)) { + rmSync(`${outDir}/src`, { recursive: true, force: true }); + } + console.log('Firefox assets copied successfully'); } }; From ba0834766ffc0b8d36b14f832d0f7dbe76876454 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 19:16:38 +0200 Subject: [PATCH 06/32] defer script, rename variable --- src/views/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/popup.html b/src/views/popup.html index a2ef6a3..b1bfedd 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -104,7 +104,7 @@

Points for ...:

-
+
...
From 8aeb951f4fb7b23bda3b21398d0e98e79a3f51ff Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 19:18:19 +0200 Subject: [PATCH 07/32] add document interface --- src/scripts/views/popup/service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 0f1ff71..0621fb0 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -10,10 +10,17 @@ interface ServicePoint { }; } +interface ServiceDocument { + id: string + name: string + url: string +} + interface ServiceResponse { name: string; rating?: string; points: ServicePoint[]; + documents: ServiceDocument[] } interface SearchResponse { From 78a8b5383ee3f7b98e0f3fffc260df769572b5b1 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 22:08:56 +0200 Subject: [PATCH 08/32] html --- src/views/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/popup.html b/src/views/popup.html index b1bfedd..e0cac92 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -116,5 +116,5 @@

Translations are machine translated. You can turn them off in Settings by choosing English.

- + From d95fc5ad50b799b8904943e638d4479311cbefa5 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 22:09:09 +0200 Subject: [PATCH 09/32] tmp --- src/scripts/views/popup/service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 0621fb0..a1b228c 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -59,7 +59,7 @@ export async function displayServiceDetails( updatePointsCount(data.points.length); revealLoadedState(options.unverified === true); - populateList(data.points); + populateList(data.points, data.documents); } catch (error) { hideLoadingState(); showErrorOverlay( @@ -171,14 +171,14 @@ function revealLoadedState(unverified: boolean): void { } } -function populateList(points: ServicePoint[]): void { - const pointsList = document.getElementById('pointList'); - if (!pointsList) { +function populateList(points: ServicePoint[], documents: ServiceDocuments[]): void { + const documentList = document.getElementById('documentList'); + if (!documentList) { return; } - pointsList.style.display = 'block'; - pointsList.innerHTML = ''; + documentList.style.display = 'block'; + documentList.innerHTML = ''; const filteredPoints = filterPoints(points); From 60091f4006e631d5fea7a3a11a02a7522356d2c4 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 22:16:42 +0200 Subject: [PATCH 10/32] merge code --- src/scripts/views/popup/service.ts | 170 ++++++++++++++++++----------- src/views/style/popup.css | 40 +++++-- 2 files changed, 136 insertions(+), 74 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index a1b228c..eab82b5 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -171,88 +171,126 @@ function revealLoadedState(unverified: boolean): void { } } -function populateList(points: ServicePoint[], documents: ServiceDocuments[]): void { +function populateList(allPoints: any, documents: any) { const documentList = document.getElementById('documentList'); - if (!documentList) { - return; - } + // Split points by Document and display them seperatly + for (let i = 0; i < documents.length; i++) { + let added = 0; + const element = documents[i]; + console.log(element) + const doc = document.createElement('div'); + const temp = ` +
`; + doc.innerHTML = temp.trim(); + documentList!.appendChild(doc.firstChild!); + added++; - documentList.style.display = 'block'; - documentList.innerHTML = ''; + const docPoints = allPoints.filter((point:any) => point.document_id === element.id) - const filteredPoints = filterPoints(points); + const sortedPoints = filterPoints(docPoints) - appendPointGroup(filteredPoints.blocker, pointsList, false); - appendPointGroup(filteredPoints.bad, pointsList, false); - appendPointGroup(filteredPoints.good, pointsList, false); - appendPointGroup(filteredPoints.neutral, pointsList, true); -} + const pointsList = document.getElementById(`pointList_${element.id}`) -function filterPoints(points: ServicePoint[]): { - blocker: ServicePoint[]; - bad: ServicePoint[]; - good: ServicePoint[]; - neutral: ServicePoint[]; -} { - const curatedPoints = points.filter((point) => { - if (!isCuratorMode()) { - return point.status === 'approved'; - } - return point.status === 'approved' || point.status === 'pending'; - }); - - return { - blocker: curatedPoints.filter( - (point) => point.case?.classification === 'blocker' - ), - bad: curatedPoints.filter( - (point) => point.case?.classification === 'bad' - ), - good: curatedPoints.filter( - (point) => point.case?.classification === 'good' - ), - neutral: curatedPoints.filter( - (point) => point.case?.classification === 'neutral' - ), - }; -} + createSortetPoints(sortedPoints,pointsList) -function appendPointGroup( - points: ServicePoint[], - container: HTMLElement, - isLastGroup: boolean -): void { - let added = 0; - - points.forEach((point, index) => { - const wrapper = document.createElement('div'); - const classification = point.case?.classification ?? 'neutral'; - const pointTitle = point.case?.localized_title ?? point.title; - wrapper.innerHTML = ` -
- -

${pointTitle}

- ${renderCuratorTag(point.status)} + } + //display points not liked to a document + const noDocPoints = allPoints.filter((point:any) => point.document_id === null) + if (noDocPoints !== null) { + const doc = document.createElement('div'); + const temp = ` +
+
+

Points not linked to a Document

- `.trim(); - if (wrapper.firstChild) { - container.appendChild(wrapper.firstChild as HTMLElement); +
+ ... +
+
`; + doc.innerHTML = temp.trim(); + documentList!.appendChild(doc.firstChild!); + const sortedPoints = filterPoints(noDocPoints) + const pointsList = document.getElementById(`pointList_unlinkedPoints`) + createSortetPoints(sortedPoints,pointsList) + + } +} + function filterPoints(points:any) { + if (!isCuratorMode) { + points = points.filter((point: any) => point.status === 'approved'); + } else { + points = points.filter( + (point: any) => + point.status === 'approved' || point.status === 'pending' + ); } - added += 1; + let filteredPoints: any = {} + filteredPoints.blocker = points.filter( + (point: any) => point.case.classification === 'blocker' + ); + filteredPoints.bad = points.filter( + (point: any) => point.case.classification === 'bad' + ); + filteredPoints.good = points.filter( + (point: any) => point.case.classification === 'good' + ); + filteredPoints.neutral = points.filter( + (point: any) => point.case.classification === 'neutral' + ); + return filteredPoints + } + +function createSortetPoints(sortedPoints:any,pointsList:any) { + if (sortedPoints.blocker) { + createPointList(sortedPoints.blocker, pointsList, false); + } + if (sortedPoints.bad) { + createPointList(sortedPoints.bad, pointsList, false); + } + if (sortedPoints.good) { + createPointList(sortedPoints.good, pointsList, false); + } + if (sortedPoints.neutral) { + createPointList(sortedPoints.neutral, pointsList, true); + } +} - if (index !== points.length - 1) { +function createPointList(pointsFiltered: any, pointsList: any, last: boolean) { + var added = 0; + for (let i = 0; i < pointsFiltered.length; i++) { + const point = document.createElement('div'); + const pointTitle = pointsFiltered[i].case?.localized_title ?? point.title; + + var temp = ` +
+ +

${pointTitle}

+ ${renderCuratorTag(pointsFiltered[i].status)} +
`; + point.innerHTML = temp.trim(); + pointsList.appendChild(point.firstChild); + added++; + if (i !== pointsFiltered.length - 1) { const divider = document.createElement('hr'); - container.appendChild(divider); + pointsList.appendChild(divider); } - }); - - if (added > 0 && !isLastGroup) { + } + if (added !== 0 && !last) { const divider = document.createElement('hr'); divider.classList.add('group'); - container.appendChild(divider); + pointsList.appendChild(divider); } } + function renderCuratorTag(status: string): string { if (!isCuratorMode() || status === 'approved') { return ''; diff --git a/src/views/style/popup.css b/src/views/style/popup.css index ab8f89f..e695628 100644 --- a/src/views/style/popup.css +++ b/src/views/style/popup.css @@ -1,8 +1,8 @@ @font-face { font-family: 'Open Sans'; src: - url('fonts/OpenSans-Regular.woff2') format('woff2'), - url('fonts/OpenSans-Regular.woff') format('woff'); + url('../fonts/OpenSans-Regular.woff2') format('woff2'), + url('../fonts/OpenSans-Regular.woff') format('woff'); font-weight: normal; font-style: normal; font-display: swap; @@ -293,27 +293,47 @@ h3 { margin: 0; } -#pointList > hr { +.pointList > hr { border: none; height: 1px; width: 90%; background-color: #e7e7e7; } -#pointList > hr.group { +.pointList > hr.group { border: none; height: 1px; width: 90%; background-color: #cccbcb; } -#pointList { +.pointList { margin: 0.5rem; padding: 0.4rem; background-color: #fefefe; border-radius: 1rem; } +.documentHeader { + display: flex; + justify-content: space-around; + margin-bottom: -1.1rem; +} +.documentHeader > a { + display: flex; + align-items: center; + text-decoration: none; + color: #04040461; + font-size: small; +} +.documentHeader > a:hover{ + text-decoration: underline; + +} +.documentHeader > h3 { + all:revert +} + button { border: none; background-color: transparent; @@ -347,18 +367,22 @@ button { ); } -.dark-mode #pointList { +.dark-mode .pointList { background-color: #1c1c1e; } -.dark-mode #pointList > hr.group { +.dark-mode .pointList > hr.group { background-color: #232323; } -.dark-mode #pointList > hr { +.dark-mode .pointList > hr { background-color: #1b1b1d; } +.dark-mode .documentHeader a { + color: #cccbcb; +} + #toggleButton, #settingsButton, #sourceButton { From 65f02a88fe714f7253d6794b8f323517310b29cd Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 22:30:30 +0200 Subject: [PATCH 11/32] fix types, remove any --- src/scripts/views/popup/service.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index eab82b5..6269a85 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -8,10 +8,11 @@ interface ServicePoint { classification?: string; localized_title?: string | null; }; + document_id?: number } interface ServiceDocument { - id: string + id: number name: string url: string } @@ -25,7 +26,7 @@ interface ServiceResponse { interface SearchResponse { services: Array<{ - id: string; + id: number; urls: string[]; }>; } @@ -171,12 +172,12 @@ function revealLoadedState(unverified: boolean): void { } } -function populateList(allPoints: any, documents: any) { +function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { const documentList = document.getElementById('documentList'); // Split points by Document and display them seperatly for (let i = 0; i < documents.length; i++) { let added = 0; - const element = documents[i]; + const element = documents[i]!; console.log(element) const doc = document.createElement('div'); const temp = ` @@ -193,7 +194,7 @@ function populateList(allPoints: any, documents: any) { documentList!.appendChild(doc.firstChild!); added++; - const docPoints = allPoints.filter((point:any) => point.document_id === element.id) + const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id) const sortedPoints = filterPoints(docPoints) From f7efcf9180c4abd91837c7b01b453840d62f058c Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 23:02:15 +0200 Subject: [PATCH 12/32] add more types --- src/scripts/views/popup/service.ts | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 6269a85..e3816cc 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -4,7 +4,7 @@ import { applyHeaderColor } from './theme'; interface ServicePoint { status: string; title: string; - case?: { + case: { classification?: string; localized_title?: string | null; }; @@ -70,7 +70,7 @@ export async function displayServiceDetails( } } -export async function searchService(term: string): Promise { +export async function searchService(term: string): Promise { try { const response = await fetch( `https://${getApiUrl()}/search/v5/?query=${encodeURIComponent(term)}` @@ -198,13 +198,13 @@ function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { const sortedPoints = filterPoints(docPoints) - const pointsList = document.getElementById(`pointList_${element.id}`) + const pointsList = document.getElementById(`pointList_${element.id}`)! createSortetPoints(sortedPoints,pointsList) } //display points not liked to a document - const noDocPoints = allPoints.filter((point:any) => point.document_id === null) + const noDocPoints = allPoints.filter((point: ServicePoint) => point.document_id === null) if (noDocPoints !== null) { const doc = document.createElement('div'); const temp = ` @@ -219,37 +219,37 @@ function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { doc.innerHTML = temp.trim(); documentList!.appendChild(doc.firstChild!); const sortedPoints = filterPoints(noDocPoints) - const pointsList = document.getElementById(`pointList_unlinkedPoints`) + const pointsList = document.getElementById(`pointList_unlinkedPoints`)! createSortetPoints(sortedPoints,pointsList) } } - function filterPoints(points:any) { + function filterPoints(points:ServicePoint[]) { if (!isCuratorMode) { - points = points.filter((point: any) => point.status === 'approved'); + points = points.filter((point) => point.status === 'approved'); } else { points = points.filter( - (point: any) => + (point) => point.status === 'approved' || point.status === 'pending' ); } - let filteredPoints: any = {} + let filteredPoints:any = {} filteredPoints.blocker = points.filter( - (point: any) => point.case.classification === 'blocker' + (point) => point.case.classification === 'blocker' ); filteredPoints.bad = points.filter( - (point: any) => point.case.classification === 'bad' + (point) => point.case.classification === 'bad' ); filteredPoints.good = points.filter( - (point: any) => point.case.classification === 'good' + (point) => point.case.classification === 'good' ); filteredPoints.neutral = points.filter( - (point: any) => point.case.classification === 'neutral' + (point) => point.case.classification === 'neutral' ); return filteredPoints } -function createSortetPoints(sortedPoints:any,pointsList:any) { +function createSortetPoints(sortedPoints:any,pointsList:HTMLElement) { if (sortedPoints.blocker) { createPointList(sortedPoints.blocker, pointsList, false); } @@ -264,20 +264,20 @@ function createSortetPoints(sortedPoints:any,pointsList:any) { } } -function createPointList(pointsFiltered: any, pointsList: any, last: boolean) { +function createPointList(pointsFiltered: ServicePoint[], pointsList: HTMLElement, last: boolean) { var added = 0; for (let i = 0; i < pointsFiltered.length; i++) { const point = document.createElement('div'); - const pointTitle = pointsFiltered[i].case?.localized_title ?? point.title; + const pointTitle = pointsFiltered[i]!.case?.localized_title ?? point.title; var temp = ` -
- +
+

${pointTitle}

- ${renderCuratorTag(pointsFiltered[i].status)} + ${renderCuratorTag(pointsFiltered[i]!.status)}
`; point.innerHTML = temp.trim(); - pointsList.appendChild(point.firstChild); + pointsList.appendChild(point.firstChild!); added++; if (i !== pointsFiltered.length - 1) { const divider = document.createElement('hr'); From e534beffff5438539bae90fe3360a11671a05f34 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 23:10:01 +0200 Subject: [PATCH 13/32] revert serviceresponse id number to string --- src/scripts/views/popup/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index e3816cc..6b2af9a 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -26,7 +26,7 @@ interface ServiceResponse { interface SearchResponse { services: Array<{ - id: number; + id: string; urls: string[]; }>; } From 4381737a38902efef815d657d6b2ae71058e7d54 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 23:25:25 +0200 Subject: [PATCH 14/32] clean --- src/scripts/views/popup/service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 6b2af9a..d9320b4 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -176,9 +176,7 @@ function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { const documentList = document.getElementById('documentList'); // Split points by Document and display them seperatly for (let i = 0; i < documents.length; i++) { - let added = 0; const element = documents[i]!; - console.log(element) const doc = document.createElement('div'); const temp = `
@@ -192,7 +190,6 @@ function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) {
`; doc.innerHTML = temp.trim(); documentList!.appendChild(doc.firstChild!); - added++; const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id) From de4ffcae49e7a1bd272c79f21a860331b9b98f05 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 23:51:50 +0200 Subject: [PATCH 15/32] Group documents wihtout points --- src/scripts/views/popup/service.ts | 50 +++++++++++++++++++----------- src/views/popup.html | 6 ++++ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index d9320b4..434b6cc 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -177,28 +177,42 @@ function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { // Split points by Document and display them seperatly for (let i = 0; i < documents.length; i++) { const element = documents[i]!; - const doc = document.createElement('div'); - const temp = ` -
-
-

${element.name}

- Read Original> -
-
- ... -
-
`; - doc.innerHTML = temp.trim(); - documentList!.appendChild(doc.firstChild!); const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id) - const sortedPoints = filterPoints(docPoints) - const pointsList = document.getElementById(`pointList_${element.id}`)! - - createSortetPoints(sortedPoints,pointsList) - + if (sortedPoints.blocker.length + sortedPoints.bad.length + sortedPoints.neutral.length + sortedPoints.good.length > 0) { + const doc = document.createElement('div'); + const temp = ` +
+
+

${element.name}

+ Read Original> +
+
+ ... +
+
`; + doc.innerHTML = temp.trim(); + documentList!.appendChild(doc.firstChild!); + + const pointsList = document.getElementById(`pointList_${element.id}`)! + + createSortetPoints(sortedPoints,pointsList) + } else { //documents without points + const docsWithoutPoints = document.getElementById('docsWithoutPoints') + if (docsWithoutPoints?.style.display === "none") { + docsWithoutPoints.style.display = "block" + } + const doc = document.createElement('div'); + const temp = ` +
+

${element.name}

+ Read Original> +
`; + doc.innerHTML = temp.trim(); + docsWithoutPoints!.appendChild(doc.firstChild!); + } } //display points not liked to a document const noDocPoints = allPoints.filter((point: ServicePoint) => point.document_id === null) diff --git a/src/views/popup.html b/src/views/popup.html index e0cac92..6a1b98f 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -107,6 +107,12 @@

...
+
+
+

Documents without Points

+
+ +
- - From 6547ea4d70da2c6386f381a7e19001c8d13b9d88 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 21 Oct 2025 23:56:30 +0200 Subject: [PATCH 16/32] fix points without docs and docs without points always showing --- src/scripts/views/popup/service.ts | 6 ++++-- src/views/popup.html | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 434b6cc..54283fb 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -200,7 +200,9 @@ function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { createSortetPoints(sortedPoints,pointsList) } else { //documents without points + const docsWithoutPointsWraper = document.getElementById('docsWithoutPointsWraper') const docsWithoutPoints = document.getElementById('docsWithoutPoints') + if (docsWithoutPoints?.style.display === "none") { docsWithoutPoints.style.display = "block" } @@ -211,12 +213,12 @@ function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { Read Original>

`; doc.innerHTML = temp.trim(); - docsWithoutPoints!.appendChild(doc.firstChild!); + docsWithoutPointsWraper!.appendChild(doc.firstChild!); } } //display points not liked to a document const noDocPoints = allPoints.filter((point: ServicePoint) => point.document_id === null) - if (noDocPoints !== null) { + if (noDocPoints.length > 0) { const doc = document.createElement('div'); const temp = `
diff --git a/src/views/popup.html b/src/views/popup.html index 6a1b98f..4b6e73e 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -107,11 +107,11 @@

...
-
+ - From 1c8688270df0a6de198b1dd6e552cbf0149839a5 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Wed, 22 Oct 2025 00:12:14 +0200 Subject: [PATCH 17/32] fix type error --- src/scripts/views/popup/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 54283fb..5f5dd8f 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -70,7 +70,7 @@ export async function displayServiceDetails( } } -export async function searchService(term: string): Promise { +export async function searchService(term: string): Promise { try { const response = await fetch( `https://${getApiUrl()}/search/v5/?query=${encodeURIComponent(term)}` From 39b074ae140aff85de2b8ecfb8b29eaa1d511926 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Wed, 22 Oct 2025 00:14:36 +0200 Subject: [PATCH 18/32] fix typo --- src/scripts/views/popup/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 5f5dd8f..d4d8f98 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -281,7 +281,7 @@ function createPointList(pointsFiltered: ServicePoint[], pointsList: HTMLElement var added = 0; for (let i = 0; i < pointsFiltered.length; i++) { const point = document.createElement('div'); - const pointTitle = pointsFiltered[i]!.case?.localized_title ?? point.title; + const pointTitle = pointsFiltered[i]!.case?.localized_title ?? pointsFiltered[i]!.title; var temp = `
From e12cb1a59eb89eefc52b6c416490a4d3ce0be227 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Wed, 22 Oct 2025 11:52:54 +0200 Subject: [PATCH 19/32] add pointListStyle options to settings page --- src/scripts/views/settings/handlers.ts | 12 ++++++++++++ src/scripts/views/settings/state.ts | 5 +++++ .../settings/icons/FormatListBulleted.svg | 1 + src/views/settings/settings.html | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 src/views/settings/icons/FormatListBulleted.svg diff --git a/src/scripts/views/settings/handlers.ts b/src/scripts/views/settings/handlers.ts index a32ddc0..63e7c83 100644 --- a/src/scripts/views/settings/handlers.ts +++ b/src/scripts/views/settings/handlers.ts @@ -8,6 +8,8 @@ export function registerSettingsHandlers(): void { const curatorModeInput = document.getElementById('curatorMode') as HTMLInputElement | null; const apiInput = document.getElementById('api') as HTMLInputElement | null; const languageSelect = document.getElementById('language') as HTMLSelectElement | null; + const pointListStyleSelect = document.getElementById('pointListStyle') as HTMLSelectElement | null; + if (updateInput) { updateInput.addEventListener('change', () => { @@ -52,4 +54,14 @@ export function registerSettingsHandlers(): void { void setLocal({ language: normalized }); }); } + + if (pointListStyleSelect) { + pointListStyleSelect.addEventListener('change', () => { + const normalized = pointListStyleSelect.value ?? 'docCategories'; + if (pointListStyleSelect.value !== normalized) { + pointListStyleSelect.value = normalized; + } + void setLocal({ pointListStyle: normalized }); + }); + } } diff --git a/src/scripts/views/settings/state.ts b/src/scripts/views/settings/state.ts index a86d711..8547a88 100644 --- a/src/scripts/views/settings/state.ts +++ b/src/scripts/views/settings/state.ts @@ -13,6 +13,7 @@ export async function populateSettingsForm(): Promise { 'sentry', 'api', 'language', + 'pointListStyle' ]); if (Array.isArray(result['db'])) { @@ -56,6 +57,9 @@ export async function populateSettingsForm(): Promise { const language = resolveLanguage(result['language']); elements.languageSelect.value = language; } + if (elements.pointListStyle) { + elements.pointListStyle.value = String(result['pointListStyle']); + } } function collectElements() { @@ -69,6 +73,7 @@ function collectElements() { date: document.getElementById('date') as HTMLElement | null, indexed: document.getElementById('indexed') as HTMLElement | null, days: document.getElementById('days') as HTMLElement | null, + pointListStyle: document.getElementById('pointListStyle') as HTMLSelectElement | null }; } diff --git a/src/views/settings/icons/FormatListBulleted.svg b/src/views/settings/icons/FormatListBulleted.svg new file mode 100644 index 0000000..b4b53fa --- /dev/null +++ b/src/views/settings/icons/FormatListBulleted.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/views/settings/settings.html b/src/views/settings/settings.html index df1b3a9..49065e3 100644 --- a/src/views/settings/settings.html +++ b/src/views/settings/settings.html @@ -75,6 +75,24 @@

Appearance Settings

+
+
+
+
+ +

Summary Style

+
+

+ How points are displayed in the popup +

+
+ +
+
+

Database Settings

From 14e8670f78209dc80a73c6ad63e4cabb9e19f693 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Wed, 22 Oct 2025 14:35:53 +0200 Subject: [PATCH 20/32] add default docStyle --- src/scripts/background/install.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/background/install.ts b/src/scripts/background/install.ts index 293eb5c..de59904 100644 --- a/src/scripts/background/install.ts +++ b/src/scripts/background/install.ts @@ -13,6 +13,7 @@ export async function handleExtensionInstalled(): Promise { active: false, allowedPlattform: donationAllowed, }, + pointListStyle: "docCategories" }); await checkIfUpdateNeeded(true); From 0219c4d7f15d11342126e37601fbd1655a61eb9d Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Wed, 22 Oct 2025 15:23:21 +0200 Subject: [PATCH 21/32] check pointListStyle before rendering --- src/scripts/views/popup/service.ts | 22 ++++++++++++++++++---- src/scripts/views/popup/state.ts | 10 +++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index d4d8f98..88c7b8f 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -1,5 +1,6 @@ import { getApiUrl, getLanguage, isCuratorMode } from './state'; import { applyHeaderColor } from './theme'; +import { getpointListStyle } from './state' interface ServicePoint { status: string; @@ -60,7 +61,15 @@ export async function displayServiceDetails( updatePointsCount(data.points.length); revealLoadedState(options.unverified === true); - populateList(data.points, data.documents); + if (getpointListStyle() === "docCategories") { + populateListDocCategories(data.points, data.documents); + } else if (getpointListStyle() === "unified") { + populateListUnified(data.points) + } else { + console.error("Unsupported pointListStyle", getpointListStyle()); + } + + } catch (error) { hideLoadingState(); showErrorOverlay( @@ -172,7 +181,12 @@ function revealLoadedState(unverified: boolean): void { } } -function populateList(allPoints: ServicePoint[], documents: ServiceDocument[]) { +function populateListUnified(allPoints: ServicePoint[]) { + +} + + +function populateListDocCategories(allPoints: ServicePoint[], documents: ServiceDocument[]) { const documentList = document.getElementById('documentList'); // Split points by Document and display them seperatly for (let i = 0; i < documents.length; i++) { @@ -278,12 +292,12 @@ function createSortetPoints(sortedPoints:any,pointsList:HTMLElement) { } function createPointList(pointsFiltered: ServicePoint[], pointsList: HTMLElement, last: boolean) { - var added = 0; + let added = 0; for (let i = 0; i < pointsFiltered.length; i++) { const point = document.createElement('div'); const pointTitle = pointsFiltered[i]!.case?.localized_title ?? pointsFiltered[i]!.title; - var temp = ` + let temp = `

${pointTitle}

diff --git a/src/scripts/views/popup/state.ts b/src/scripts/views/popup/state.ts index f1e1ed2..9d34e04 100644 --- a/src/scripts/views/popup/state.ts +++ b/src/scripts/views/popup/state.ts @@ -8,11 +8,13 @@ import { let curatorMode = false; let apiUrl = DEFAULT_API_URL; let language: SupportedLanguage = 'en'; +let pointListStyle:"docCategories" | "unified" export interface PopupPreferences { darkmode: boolean; curatorMode: boolean; language: SupportedLanguage; + pointListStyle:"docCategories" | "unified" } export function isCuratorMode(): boolean { @@ -31,11 +33,16 @@ export function setApiUrl(url: string): void { apiUrl = url; } +export function getpointListStyle() { + return pointListStyle +} + export async function hydrateState(): Promise { - const result = await getLocal(['darkmode', 'curatorMode', 'api', 'language']); + const result = await getLocal(['darkmode', 'curatorMode', 'api', 'language', 'pointListStyle']); const darkmode = Boolean(result['darkmode']); const storedCuratorMode = Boolean(result['curatorMode']); + pointListStyle = result['pointListStyle'] as "docCategories" | "unified" setCuratorMode(storedCuratorMode); const api = result['api']; @@ -52,6 +59,7 @@ export async function hydrateState(): Promise { darkmode, curatorMode: storedCuratorMode, language: resolvedLanguage, + pointListStyle, }; } From 405e0672ffd0f28c6149cc26e1a06cd49591b1fb Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Wed, 22 Oct 2025 15:30:23 +0200 Subject: [PATCH 22/32] re-add the unified pointsList --- src/scripts/views/popup/service.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 88c7b8f..527c883 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -182,7 +182,32 @@ function revealLoadedState(unverified: boolean): void { } function populateListUnified(allPoints: ServicePoint[]) { - + const documentList = document.getElementById('documentList'); + const doc = document.createElement('div'); + const temp = ` +
+
+ ... + /div> +
` + ; + doc.innerHTML = temp.trim(); + documentList!.appendChild(doc.firstChild!); + + const pointsList = document.getElementById('pointList'); + if (!pointsList) { + return; + } + + pointsList.style.display = 'block'; + pointsList.innerHTML = ''; + + const filteredPoints = filterPoints(allPoints); + + createPointList(filteredPoints.blocker, pointsList, false); + createPointList(filteredPoints.bad, pointsList, false); + createPointList(filteredPoints.good, pointsList, false); + createPointList(filteredPoints.neutral, pointsList, true); } From b55d34eed53456ba74d149511a313848c45c160f Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:53:18 +0200 Subject: [PATCH 23/32] clean up --- src/scripts/views/popup/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 527c883..2c1f9dd 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -1,6 +1,5 @@ import { getApiUrl, getLanguage, isCuratorMode } from './state'; import { applyHeaderColor } from './theme'; -import { getpointListStyle } from './state' interface ServicePoint { status: string; From 15a01fddd9429aa6eaf1b246e2ea098ad424b797 Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:53:46 +0200 Subject: [PATCH 24/32] fix Unexpected negated condition. --- src/scripts/views/popup/service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 2c1f9dd..47d935b 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -1,4 +1,4 @@ -import { getApiUrl, getLanguage, isCuratorMode } from './state'; +import { getApiUrl, getLanguage, isCuratorMode, getpointListStyle } from './state'; import { applyHeaderColor } from './theme'; interface ServicePoint { @@ -276,13 +276,13 @@ function populateListDocCategories(allPoints: ServicePoint[], documents: Service } } function filterPoints(points:ServicePoint[]) { - if (!isCuratorMode) { - points = points.filter((point) => point.status === 'approved'); - } else { + if (isCuratorMode()) { points = points.filter( (point) => point.status === 'approved' || point.status === 'pending' ); + } else { + points = points.filter((point) => point.status === 'approved'); } let filteredPoints:any = {} filteredPoints.blocker = points.filter( From 54aa075964378da1042ac72fd257e050eb600eca Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:23:54 +0200 Subject: [PATCH 25/32] use for of --- src/scripts/views/popup/service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 47d935b..32d5a8e 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -213,8 +213,8 @@ function populateListUnified(allPoints: ServicePoint[]) { function populateListDocCategories(allPoints: ServicePoint[], documents: ServiceDocument[]) { const documentList = document.getElementById('documentList'); // Split points by Document and display them seperatly - for (let i = 0; i < documents.length; i++) { - const element = documents[i]!; + for (let i of documents) { + const element = i!; const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id) const sortedPoints = filterPoints(docPoints) From 132a165240796af56e3870fbc3d10440ba7e95ea Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:26:40 +0200 Subject: [PATCH 26/32] add alt text to icon --- src/views/settings/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/settings/settings.html b/src/views/settings/settings.html index 49065e3..123c4df 100644 --- a/src/views/settings/settings.html +++ b/src/views/settings/settings.html @@ -79,7 +79,7 @@

Appearance Settings

- + icon displaying Bulletpoints

Summary Style

From 5fd853ccfc02de19f629b5a386fce8380397c544 Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:31:11 +0200 Subject: [PATCH 27/32] clean --- src/scripts/views/popup/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 32d5a8e..8d411f9 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -214,7 +214,7 @@ function populateListDocCategories(allPoints: ServicePoint[], documents: Service const documentList = document.getElementById('documentList'); // Split points by Document and display them seperatly for (let i of documents) { - const element = i!; + const element = i; const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id) const sortedPoints = filterPoints(docPoints) From 775d3b10c0eca02e3b8441307e83b7d035b37a02 Mon Sep 17 00:00:00 2001 From: ptgms Date: Thu, 30 Oct 2025 00:26:33 +0100 Subject: [PATCH 28/32] move a constant to constants --- src/scripts/constants.ts | 2 ++ src/scripts/lib/language.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scripts/constants.ts b/src/scripts/constants.ts index c93e512..9cd109e 100644 --- a/src/scripts/constants.ts +++ b/src/scripts/constants.ts @@ -11,3 +11,5 @@ export const DEFAULT_POPUP_PATH = '/views/popup.html'; export const API_HEADERS = { apikey: atob('Y29uZ3JhdHMgb24gZ2V0dGluZyB0aGUga2V5IDpQ'), }; + +export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const; \ No newline at end of file diff --git a/src/scripts/lib/language.ts b/src/scripts/lib/language.ts index 2721fe3..8aad9bb 100644 --- a/src/scripts/lib/language.ts +++ b/src/scripts/lib/language.ts @@ -1,4 +1,5 @@ -export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const; +import { SUPPORTED_LANGUAGES } from "../constants"; + export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]; export function normalizeLanguage( From b944b9883857cccb4162071dd892a3f67a55b777 Mon Sep 17 00:00:00 2001 From: shadowwwind Date: Tue, 4 Nov 2025 22:39:26 +0100 Subject: [PATCH 29/32] fix: remove dupped id's and unused elements, change style selector to classes --- src/scripts/views/popup/service.ts | 2 +- src/views/popup.html | 7 +------ src/views/style/popup.css | 25 +------------------------ 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 0f1ff71..184cd6c 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -112,7 +112,7 @@ function updateServiceName(name: string): void { } function updateTitle(name: string): void { - const titleElement = document.getElementById('title'); + const titleElement = document.getElementById('serviceTitle'); if (titleElement) { titleElement.innerText = name; } diff --git a/src/views/popup.html b/src/views/popup.html index a2ef6a3..779974c 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -32,11 +32,6 @@

-
@@ -62,7 +57,7 @@

An error has occurred.

-
Loading
+
Loading
diff --git a/src/views/style/popup.css b/src/views/style/popup.css index ab8f89f..438c439 100644 --- a/src/views/style/popup.css +++ b/src/views/style/popup.css @@ -201,13 +201,6 @@ html { cursor: pointer; } -.translationWarning { - margin: 0.5rem 1rem 1.5rem; - font-size: 0.8rem; - color: #555; - text-align: center; -} - .labeltext { font-size: 1rem; font-weight: normal; @@ -228,22 +221,6 @@ html { align-items: center; position: absolute; color: white; - flex-direction: row; - gap: 1.5rem; - padding: 1.5rem; - text-align: left; -} - -.errorText { - display: flex; - flex-direction: column; - gap: 0.5rem; - max-width: 260px; -} - -.errorText > h2, -.errorText > p { - margin: 0; } #loading { @@ -396,4 +373,4 @@ button { padding: .5rem; margin-bottom: .5rem; line-height: 1; -} +} \ No newline at end of file From 1c327fb3db58e2722f8037371e62d3ab8153b8eb Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:46:52 +0100 Subject: [PATCH 30/32] [feat] sort docuements alphabetically --- src/scripts/views/popup/service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 7597c75..ba15ae1 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -212,6 +212,15 @@ function populateListUnified(allPoints: ServicePoint[]) { function populateListDocCategories(allPoints: ServicePoint[], documents: ServiceDocument[]) { const documentList = document.getElementById('documentList'); + //sort docuements alphabetically + try { + documents.sort((a, b) => + a.name.localeCompare(b.name) + ) + } catch (error) { + console.warn(error) + } + console.log(documents) // Split points by Document and display them seperatly for (let i of documents) { const element = i; From 31557cc1541ee87c06ff946e8b60f918180d895b Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:32:05 +0100 Subject: [PATCH 31/32] [fix] move default listStyle to constants, improve types, fix typo --- src/scripts/background/install.ts | 3 ++- src/scripts/constants.ts | 4 +++- src/scripts/views/popup/service.ts | 32 +++++++++++++++++++++--------- src/scripts/views/popup/state.ts | 6 +++--- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/scripts/background/install.ts b/src/scripts/background/install.ts index de59904..8642c34 100644 --- a/src/scripts/background/install.ts +++ b/src/scripts/background/install.ts @@ -2,6 +2,7 @@ import { setLocal } from '../lib/chromeStorage'; import { checkIfUpdateNeeded } from './database'; import { donationReminderAllowed } from './donation'; import { initializePageAction } from './pageAction'; +import { DEFAULT_LIST_STYLE } from "../constants"; export async function handleExtensionInstalled(): Promise { const donationAllowed = donationReminderAllowed(navigator.userAgent); @@ -13,7 +14,7 @@ export async function handleExtensionInstalled(): Promise { active: false, allowedPlattform: donationAllowed, }, - pointListStyle: "docCategories" + pointListStyle: DEFAULT_LIST_STYLE }); await checkIfUpdateNeeded(true); diff --git a/src/scripts/constants.ts b/src/scripts/constants.ts index 9cd109e..62b6c12 100644 --- a/src/scripts/constants.ts +++ b/src/scripts/constants.ts @@ -12,4 +12,6 @@ export const API_HEADERS = { apikey: atob('Y29uZ3JhdHMgb24gZ2V0dGluZyB0aGUga2V5IDpQ'), }; -export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const; \ No newline at end of file +export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const; + +export const DEFAULT_LIST_STYLE :"docCategories" | "unified" = "docCategories" as const; \ No newline at end of file diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index ba15ae1..1b3fb79 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -1,4 +1,4 @@ -import { getApiUrl, getLanguage, isCuratorMode, getpointListStyle } from './state'; +import { getApiUrl, getLanguage, isCuratorMode, getPointListStyle } from './state'; import { applyHeaderColor } from './theme'; interface ServicePoint { @@ -31,6 +31,13 @@ interface SearchResponse { }>; } +interface FilteredPoints { + blocker: ServicePoint[]; + bad: ServicePoint[]; + good: ServicePoint[]; + neutral: ServicePoint[]; +} + export async function displayServiceDetails( id: string, options: { unverified?: boolean } = {} @@ -60,12 +67,14 @@ export async function displayServiceDetails( updatePointsCount(data.points.length); revealLoadedState(options.unverified === true); - if (getpointListStyle() === "docCategories") { + const pointListStyle = getPointListStyle() + + if (pointListStyle === "docCategories") { populateListDocCategories(data.points, data.documents); - } else if (getpointListStyle() === "unified") { + } else if (pointListStyle === "unified") { populateListUnified(data.points) } else { - console.error("Unsupported pointListStyle", getpointListStyle()); + console.error("Unsupported pointListStyle", pointListStyle); } @@ -187,7 +196,7 @@ function populateListUnified(allPoints: ServicePoint[]) {
... - /div> +
` ; doc.innerHTML = temp.trim(); @@ -284,7 +293,7 @@ function populateListDocCategories(allPoints: ServicePoint[], documents: Service } } - function filterPoints(points:ServicePoint[]) { +function filterPoints(points:ServicePoint[]) { if (isCuratorMode()) { points = points.filter( (point) => @@ -293,7 +302,12 @@ function populateListDocCategories(allPoints: ServicePoint[], documents: Service } else { points = points.filter((point) => point.status === 'approved'); } - let filteredPoints:any = {} + let filteredPoints:FilteredPoints = { + blocker: [], + bad: [], + good: [], + neutral: [] + } filteredPoints.blocker = points.filter( (point) => point.case.classification === 'blocker' ); @@ -307,9 +321,9 @@ function populateListDocCategories(allPoints: ServicePoint[], documents: Service (point) => point.case.classification === 'neutral' ); return filteredPoints - } +} -function createSortetPoints(sortedPoints:any,pointsList:HTMLElement) { +function createSortetPoints(sortedPoints:FilteredPoints,pointsList:HTMLElement) { if (sortedPoints.blocker) { createPointList(sortedPoints.blocker, pointsList, false); } diff --git a/src/scripts/views/popup/state.ts b/src/scripts/views/popup/state.ts index 9d34e04..6cf6df0 100644 --- a/src/scripts/views/popup/state.ts +++ b/src/scripts/views/popup/state.ts @@ -1,4 +1,4 @@ -import { DEFAULT_API_URL } from '../../constants'; +import { DEFAULT_API_URL} from '../../constants'; import { getLocal } from '../../lib/chromeStorage'; import { SupportedLanguage, @@ -8,7 +8,7 @@ import { let curatorMode = false; let apiUrl = DEFAULT_API_URL; let language: SupportedLanguage = 'en'; -let pointListStyle:"docCategories" | "unified" +let pointListStyle:"docCategories" | "unified" = "unified" export interface PopupPreferences { darkmode: boolean; @@ -33,7 +33,7 @@ export function setApiUrl(url: string): void { apiUrl = url; } -export function getpointListStyle() { +export function getPointListStyle() { return pointListStyle } From 81f92edbcaa8730f74c3e4c5b5e01f0157b0a9f5 Mon Sep 17 00:00:00 2001 From: shadowwwind <76408524+shadowwwind@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:09:17 +0100 Subject: [PATCH 32/32] merge --- src/scripts/background/install.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/background/install.ts b/src/scripts/background/install.ts index 6418acb..f8357a6 100644 --- a/src/scripts/background/install.ts +++ b/src/scripts/background/install.ts @@ -14,6 +14,7 @@ export async function handleExtensionInstalled(reason:chrome.runtime.InstalledDe active: false, allowedPlattform: donationAllowed, }, + pointListStyle: DEFAULT_LIST_STYLE }); await checkIfUpdateNeeded(true, reason);