diff --git a/pages/apps/details/[appDetails].tsx b/pages/apps/details/[appDetails].tsx index d38cc473..02891feb 100644 --- a/pages/apps/details/[appDetails].tsx +++ b/pages/apps/details/[appDetails].tsx @@ -6,11 +6,13 @@ import { fetchAppstream, fetchAppStats, fetchSummary, + fetchAddons, } from '../../../src/fetchers' import { APPSTREAM_URL } from '../../../src/env' import { NextSeo } from 'next-seo' import { - Appstream, + AddonAppstream, + DesktopAppstream, pickScreenshot, Screenshot, } from '../../../src/types/Appstream' @@ -21,15 +23,17 @@ export default function Details({ data, summary, stats, + addons, }: { - data: Appstream + data: DesktopAppstream summary: Summary stats: AppStats + addons: AddonAppstream[] }) { const screenshots = data.screenshots ? data.screenshots.filter(pickScreenshot).map((screenshot: Screenshot) => ({ - url: pickScreenshot(screenshot).url, - })) + url: pickScreenshot(screenshot).url, + })) : [] return ( @@ -46,7 +50,7 @@ export default function Details({ ], }} /> - + ) } @@ -58,12 +62,14 @@ export const getStaticProps: GetStaticProps = async ({ const data = await fetchAppstream(appDetails as string) const summary = await fetchSummary(appDetails as string) const stats = await fetchAppStats(appDetails as string) + const addons = await fetchAddons(appDetails as string) return { props: { data, summary, stats, + addons }, } } diff --git a/src/components/application/Addons.module.scss b/src/components/application/Addons.module.scss new file mode 100644 index 00000000..7d458885 --- /dev/null +++ b/src/components/application/Addons.module.scss @@ -0,0 +1,64 @@ +.addons { + background-color: var(--bg-color-secondary); + border-radius: var(--border-radius); + + .addon { + width: 100%; + padding: 0px 24px; + border: 1px solid var(--border); + border-bottom-width: 0; + + display: flex; + flex-direction: column; + + &:first-child { + border-top-right-radius: 8px; + border-top-left-radius: 8px; + } + + &:last-child { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + border-bottom-width: 1px; + } + + .time { + align-self: center; + text-align: right; + font-size: 0.9rem; + } + + .externalLink { + filter: opacity(0.6); + align-self: center; + + &:hover { + filter: opacity(1); + } + } + } +} + +.tooltip { + position: relative; + align-self: center; +} + +.tooltip .tooltiptext { + visibility: hidden; + width: 200px; + background-color: var(--bg-color-secondary); + color: var(--text-primary); + text-align: center; + border-radius: var(--border-radius); + padding: 4px; + position: absolute; + z-index: 1; + top: -40px; + right: 110%; + border: 1px solid var(--border); +} + +.tooltip:hover .tooltiptext { + visibility: visible; +} diff --git a/src/components/application/Addons.tsx b/src/components/application/Addons.tsx new file mode 100644 index 00000000..983a22e4 --- /dev/null +++ b/src/components/application/Addons.tsx @@ -0,0 +1,91 @@ +import { useMatomo } from '@datapunt/matomo-tracker-react' +import { formatDistance } from 'date-fns' +import { FunctionComponent, useState } from 'react' +import { MdInfo, MdOpenInNew } from 'react-icons/md' + +import { AddonAppstream } from '../../types/Appstream' +import styles from './Addons.module.scss' + +interface Props { + addons: AddonAppstream[] +} + +const Addons: FunctionComponent = ({ addons }) => { + const { trackEvent } = useMatomo() + + const [isInfoVisible, setInfoVisible] = useState(false) + + function toggleInfoVisible() { + setInfoVisible(!isInfoVisible) + } + + return ( + <> + {addons && addons.length > 0 && ( + <> +
+

Available add-ons

+ <> + + +
+ Please use the flatpak manager supplied by your distribution to install add-ons. +
+
+ +
+
+ {addons.map(addon => { + const latestRelease = addon.releases ? addon.releases[0] : null + const linkClicked = () => { + trackEvent({ + category: 'App', + action: 'AddonHomepage', + name: addon.id ?? 'unknownAddon', + }) + } + return ( +
+
+

{addon.name}

+ {latestRelease && latestRelease.timestamp && ( +
+ updated {formatDistance( + new Date(latestRelease.timestamp * 1000), + new Date(), + { addSuffix: true } + )} +
) + } +
+
+

{addon.summary}

+ {addon.urls.homepage && ( +
+ + + +
+ )} +
+
+ ) + })} +
+ + ) + } + + ) +} + +export default Addons diff --git a/src/components/application/Details.tsx b/src/components/application/Details.tsx index 45cafc0a..ad058c40 100644 --- a/src/components/application/Details.tsx +++ b/src/components/application/Details.tsx @@ -1,7 +1,11 @@ import { useMatomo } from '@datapunt/matomo-tracker-react' import { FunctionComponent, useState } from 'react' import { Carousel } from 'react-responsive-carousel' -import { Appstream, pickScreenshot } from '../../types/Appstream' +import { + AddonAppstream, + DesktopAppstream, + pickScreenshot, +} from '../../types/Appstream' import { Summary } from '../../types/Summary' @@ -18,11 +22,13 @@ import { SoftwareAppJsonLd, VideoGameJsonLd } from 'next-seo' import Lightbox from 'react-image-lightbox' import 'react-image-lightbox/style.css' // This only needs to be imported once in your app +import Addons from './Addons' interface Props { - data: Appstream + data: DesktopAppstream summary: Summary stats: AppStats + addons: AddonAppstream[] } function categoryToSeoCategories(categories: string[]) { @@ -58,7 +64,12 @@ function categoryToSeoCategory(category) { } } -const Details: FunctionComponent = ({ data, summary, stats }) => { +const Details: FunctionComponent = ({ + data, + summary, + stats, + addons, +}) => { const [showLightbox, setShowLightbox] = useState(false) const [currentScreenshot, setCurrentScreenshot] = useState(0) @@ -206,6 +217,12 @@ const Details: FunctionComponent = ({ data, summary, stats }) => { + {addons.length > 0 && ( +
+ +
+ )} + = ({ releases }) => {

Changes in version {latestRelease.version}

- {latestRelease.timestamp && + updated {latestRelease.timestamp && formatDistance( new Date(latestRelease.timestamp * 1000), new Date(), diff --git a/src/env.ts b/src/env.ts index 378c93c4..e6853fb7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -16,6 +16,8 @@ export const EDITORS_PICKS_APPS_URL: string = `${BASE_URI}/picks/apps` export const RECENTLY_UPDATED_URL: string = `${BASE_URI}/collection/recently-updated` export const CATEGORY_URL = (category: keyof typeof Category): string => `${BASE_URI}/category/${category}` +export const ADDONS_URL = (appid: string): string => + `${BASE_URI}/addon/${appid}` export const FEED_RECENTLY_UPDATED_URL: string = `${BASE_URI}/feed/recently-updated` export const FEED_NEW_URL: string = `${BASE_URI}/feed/new` diff --git a/src/fetchers.ts b/src/fetchers.ts index 3e607569..592b637b 100644 --- a/src/fetchers.ts +++ b/src/fetchers.ts @@ -1,4 +1,4 @@ -import { Appstream } from './types/Appstream' +import { AddonAppstream, Appstream, DesktopAppstream } from './types/Appstream' import { Collection, Collections } from './types/Collection' import { Category } from './types/Category' @@ -13,13 +13,14 @@ import { SUMMARY_DETAILS, STATS_DETAILS, STATS, + ADDONS_URL, } from './env' import { Summary } from './types/Summary' import { AppStats } from './types/AppStats' import { Stats } from './types/Stats' -export async function fetchAppstream(appId: string): Promise { - let entryJson: Appstream +export async function fetchAppstream(appId: string): Promise { + let entryJson: DesktopAppstream try { const entryData = await fetch(`${APP_DETAILS(appId)}`) entryJson = await entryData.json() @@ -135,6 +136,17 @@ export async function fetchCategory(category: keyof typeof Category) { return items.filter((item) => Boolean(item)) } +export async function fetchAddons(appid: string) { + const appListRes = await fetch(ADDONS_URL(appid)) + const appList = await appListRes.json() + + const items: AddonAppstream[] = await Promise.all(appList.map(fetchAppstream)) + + console.log('\nAddons for ', appid, ' fetched') + + return items.filter((item) => Boolean(item)) +} + export async function fetchSearchQuery(query: string) { const appListRes = await fetch(SEARCH_APP(query)) const appList = await appListRes.json() diff --git a/src/types/Appstream.ts b/src/types/Appstream.ts index 23efa239..8c90a10b 100644 --- a/src/types/Appstream.ts +++ b/src/types/Appstream.ts @@ -1,4 +1,7 @@ -export interface Appstream { +export type Appstream = DesktopAppstream | AddonAppstream + +export interface DesktopAppstream { + type: "desktop" description: string screenshots?: Screenshot[] releases: Release[] @@ -18,6 +21,19 @@ export interface Appstream { bundle: Bundle } +export interface AddonAppstream { + type: "addon" + releases: Release[]; + urls: Urls; + icon?: any; + id: string; + name: string; + summary: string; + project_license?: string; + extends: string; + bundle: Bundle; +} + interface ContentRating { type: string 'violence-cartoon': ContentRatingLevel