diff --git a/src/scripts/background/install.ts b/src/scripts/background/install.ts index 4dae5aa..f8357a6 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(reason:chrome.runtime.InstalledDetails): Promise { const donationAllowed = donationReminderAllowed(navigator.userAgent); @@ -13,6 +14,7 @@ export async function handleExtensionInstalled(reason:chrome.runtime.InstalledDe active: false, allowedPlattform: donationAllowed, }, + pointListStyle: DEFAULT_LIST_STYLE }); await checkIfUpdateNeeded(true, reason); diff --git a/src/scripts/constants.ts b/src/scripts/constants.ts index 9cd109e..6c14641 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; diff --git a/src/scripts/views/popup/service.ts b/src/scripts/views/popup/service.ts index 184cd6c..e7036ff 100644 --- a/src/scripts/views/popup/service.ts +++ b/src/scripts/views/popup/service.ts @@ -1,19 +1,27 @@ -import { getApiUrl, getLanguage, isCuratorMode } from './state'; +import { getApiUrl, getLanguage, isCuratorMode, getPointListStyle } from './state'; import { applyHeaderColor } from './theme'; interface ServicePoint { status: string; title: string; - case?: { + case: { classification?: string; localized_title?: string | null; }; + document_id?: number +} + +interface ServiceDocument { + id: number + name: string + url: string } interface ServiceResponse { name: string; rating?: string; points: ServicePoint[]; + documents: ServiceDocument[] } interface SearchResponse { @@ -23,6 +31,13 @@ interface SearchResponse { }>; } +interface FilteredPoints { + blocker: ServicePoint[]; + bad: ServicePoint[]; + good: ServicePoint[]; + neutral: ServicePoint[]; +} + export async function displayServiceDetails( id: string, options: { unverified?: boolean } = {} @@ -52,7 +67,17 @@ export async function displayServiceDetails( updatePointsCount(data.points.length); revealLoadedState(options.unverified === true); - populateList(data.points); + const pointListStyle = getPointListStyle() //check the user preferences to show the list accordingly + + if (pointListStyle === "docCategories") { + populateListDocCategories(data.points, data.documents); + } else if (pointListStyle === "unified") { + populateListUnified(data.points) + } else { + console.error("Unsupported pointListStyle", pointListStyle); + } + + } catch (error) { hideLoadingState(); showErrorOverlay( @@ -164,7 +189,18 @@ function revealLoadedState(unverified: boolean): void { } } -function populateList(points: ServicePoint[]): void { +function populateListUnified(allPoints: ServicePoint[]) { + const documentList = document.getElementById('documentList'); + const doc = document.createElement('div'); + const temp = ` +
+
+
+
` + ; + doc.innerHTML = temp.trim(); + documentList!.appendChild(doc.firstChild!); + const pointsList = document.getElementById('pointList'); if (!pointsList) { return; @@ -173,79 +209,221 @@ function populateList(points: ServicePoint[]): void { pointsList.style.display = 'block'; pointsList.innerHTML = ''; - const filteredPoints = filterPoints(points); + const filteredPoints = filterPoints(allPoints); - appendPointGroup(filteredPoints.blocker, pointsList, false); - appendPointGroup(filteredPoints.bad, pointsList, false); - appendPointGroup(filteredPoints.good, pointsList, false); - appendPointGroup(filteredPoints.neutral, pointsList, true); + createPointList(filteredPoints.blocker, pointsList, false); + createPointList(filteredPoints.bad, pointsList, false); + createPointList(filteredPoints.good, pointsList, false); + createPointList(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; +function populateListDocCategories(allPoints: ServicePoint[], documents: ServiceDocument[]) { + const documentList = document.getElementById('documentList'); + //sort documents alphabetically + try { + documents.sort((a, b) => + a.name.localeCompare(b.name) + ) + } catch (error) { + console.warn(error) + } - 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)} + // prepare the Document Index + const indexElement = document.getElementById("documentIndex") as HTMLElement + indexElement.style = "display:block" + + // Split points by Document and display them seperatly + for (let i of documents) { + const element = i; + + const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id) //only points of the current document + const sortedPoints = filterPoints(docPoints) + + + addDocumentToIndex(element, indexElement, sortedPoints) + + // check if the document has points and either display it with the points else add them to the bottom + if (sortedPoints.blocker.length + sortedPoints.bad.length + sortedPoints.neutral.length + sortedPoints.good.length > 0) { //documents with points + 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}`)! + + createSortedPoints(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" + } + const doc = document.createElement('div'); + const temp = ` +
+

${element.name}

+ Read Original> +
`; + doc.innerHTML = temp.trim(); + docsWithoutPointsWraper!.appendChild(doc.firstChild!); + } + } + //display points not linked to a document + const noDocPoints = allPoints.filter((point: ServicePoint) => point.document_id === undefined) + if (noDocPoints.length > 0) { + 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`)! + createSortedPoints(sortedPoints,pointsList) + + } +} +function filterPoints(points:ServicePoint[]) { + if (isCuratorMode()) { + points = points.filter( + (point) => + point.status === 'approved' || point.status === 'pending' + ); + } else { + points = points.filter((point) => point.status === 'approved'); } - added += 1; + let filteredPoints:FilteredPoints = { + blocker: [], + bad: [], + good: [], + neutral: [] + } + filteredPoints.blocker = points.filter( + (point) => point.case.classification === 'blocker' + ); + filteredPoints.bad = points.filter( + (point) => point.case.classification === 'bad' + ); + filteredPoints.good = points.filter( + (point) => point.case.classification === 'good' + ); + filteredPoints.neutral = points.filter( + (point) => point.case.classification === 'neutral' + ); + return filteredPoints +} + +function createSortedPoints(sortedPoints:FilteredPoints,pointsList:HTMLElement) { + 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: ServicePoint[], pointsList: HTMLElement, last: boolean) { + 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; + + let 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 addDocumentToIndex(serviceDocument:ServiceDocument, indexElement:HTMLElement, points:FilteredPoints) { //creates an index of the documents + const list = document.createElement('li') + const item = document.createElement('a') + + const pointSummary = documentIndexPointSummary(points) + + item.innerText = serviceDocument.name + item.href = `#documents_${serviceDocument.id}` + + + list.appendChild(item) + list.appendChild(pointSummary) + + indexElement.appendChild(list) + + function documentIndexPointSummary(points:FilteredPoints) { //adds the number of points of each classification as a summary below the document index entry + const pointsSummanry = document.createElement("div") + pointsSummanry.classList = "indexSummaryWraper" + + createSummary("good") + createSummary("neutral") + createSummary("bad") + createSummary("blocker") + + return pointsSummanry + + function createSummary(classification:"good" | "bad" | "neutral" | "blocker") { + const wrapper = document.createElement("div") + const count = points[classification].length + + //add the classification icon + const img = document.createElement("img"); + img.src = `icons/${classification}.svg` + + wrapper.appendChild(img) + + + //add number of points of classification + const div = document.createElement("div"); + div.innerText = `${count}`; + div.classList.add("indexSummary", classification); + + wrapper.appendChild(div) + + pointsSummanry.appendChild(wrapper) + } } } + + + + function renderCuratorTag(status: string): string { if (!isCuratorMode() || status === 'approved') { return ''; diff --git a/src/scripts/views/popup/state.ts b/src/scripts/views/popup/state.ts index f1e1ed2..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,11 +8,13 @@ import { let curatorMode = false; let apiUrl = DEFAULT_API_URL; let language: SupportedLanguage = 'en'; +let pointListStyle:"docCategories" | "unified" = "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, }; } 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/popup.html b/src/views/popup.html index a695411..4a97290 100644 --- a/src/views/popup.html +++ b/src/views/popup.html @@ -98,10 +98,16 @@

Points for ...:

- -
+
+
+
+
+ icon displaying Bulletpoints +

Summary Style

+
+

+ How points are displayed in the popup +

+
+ +
+
+

Database Settings

diff --git a/src/views/style/popup.css b/src/views/style/popup.css index 438c439..af17716 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; @@ -149,7 +149,7 @@ html { padding-left: 10px; } -#title { +.title { font-size: 2rem; padding: 0.5rem; margin-bottom: 0.5rem; @@ -270,27 +270,83 @@ 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 +} +.documentIndex { + padding : 1rem +} +.documentIndex > li { + margin-bottom : 0.4rem; +} +.documentIndex > li > a { + color : black +} +.indexSummaryWraper { + display: flex; + justify-content: space-around; +} +.indexSummary { + font-size: small; +} +.indexSummaryWraper > div { + display: flex; + align-items: center; +} +.indexSummaryWraper > div > img { + height: 13px; +} +.indexSummaryWraper > div > .good { + color: #46a546; +} +.indexSummaryWraper > div > .neutral { + color: #969696; +} +.indexSummaryWraper > div > .bad { + color: #f89406; +} +.indexSummaryWraper > div > .blocker { + color: #c43c35; +} + + button { border: none; background-color: transparent; @@ -324,18 +380,25 @@ 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; +} +.dark-mode .documentIndex > li > a { + color : #fbfbfd +} + #toggleButton, #settingsButton, #sourceButton { @@ -351,7 +414,7 @@ button { box-shadow: 0px 0px 5px black; } -.dark-mode #title { +.dark-mode .title { text-shadow: 0px 0px 5px black; } @@ -373,4 +436,4 @@ button { padding: .5rem; margin-bottom: .5rem; line-height: 1; -} \ No newline at end of file +}