From e21c0bbfc9001f3b4bc3208dadd4eca96deb09af Mon Sep 17 00:00:00 2001 From: jonathan Date: Thu, 20 Aug 2026 11:37:39 +0200 Subject: [PATCH] feat: add picks functionality to service file --- src/lib/data/picks.yml | 51 +++++++++++++++++++++ src/lib/features/awesome-privacy/service.ts | 45 +++++++++++++++++- src/lib/features/awesome-privacy/types.ts | 7 +++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/lib/data/picks.yml diff --git a/src/lib/data/picks.yml b/src/lib/data/picks.yml new file mode 100644 index 0000000..1b7846c --- /dev/null +++ b/src/lib/data/picks.yml @@ -0,0 +1,51 @@ +picks: + - name: LibreWolf + reason: 'Good balance between increased privacy and usability.' + + - name: Tor Browser + reason: 'Provides total anonymity. Use when you need the highest level of privacy.' + + - name: DuckDuckGo + reason: 'Privacy-first search engine. Default for Mullvad, LibreWolf, and Tor browsers.' + + - name: Brave Search + reason: 'Independent search index, unlike DDG which is based on Bing.' + + - name: uBlock Origin + reason: 'Protects from ads, speeds up page load, prevents scripts, trackers, and malware.' + + - name: Cryptomator + reason: 'Encrypts your files so cloud providers only see jibberish.' + + - name: Syncthing + reason: 'Open source peer-2-peer continuous file syncing. Runs on all your devices.' + + - name: Ente Photos + reason: 'End-to-end encrypted photo backup. Fully open source and self-hostable.' + + - name: Obsidian + reason: 'Note-taking with local-first storage and markdown support. Can be combined with Syncthing' + + - name: Bitwarden + reason: 'Free and open source. Liked by many users because it gives a lot for very cheap. Self-hostable.' + + - name: ProtonPass + reason: 'Open source password manager that integrates with SimpleLogin for email aliases.' + + - name: ProtonMail + reason: 'Supports OpenPGP encryption. Easy migration from Gmail with one-click forwarding.' + + - name: Tuta + reason: 'Quantum computer safe encryption.' + + - name: Mullvad + reason: 'Private signup, battle tested, audited. Swedish company with strong privacy stance.' + + - name: ProtonVPN + reason: 'Free tier available, port forwarding, good for streaming. Lots of server options.' + + - name: GrapheneOS + reason: 'Most secure and private mobile OS today.' + + - name: Signal + reason: 'Good fit between privacy, security, and usability. Works without Google notification service.' diff --git a/src/lib/features/awesome-privacy/service.ts b/src/lib/features/awesome-privacy/service.ts index b1a772e..613c19b 100644 --- a/src/lib/features/awesome-privacy/service.ts +++ b/src/lib/features/awesome-privacy/service.ts @@ -1,8 +1,9 @@ import { load } from 'js-yaml'; import rawYaml from '../../data/awesome-privacy.yml?raw'; +import rawPicks from '../../data/picks.yml?raw'; -import type { AwesomePrivacyData, Category, SearchEntry, Section, Service } from './types'; +import type { AwesomePrivacyData, Category, Pick, SearchEntry, Section, Service } from './types'; import { FEATURED_CATEGORIES } from '$lib/configs'; @@ -10,12 +11,14 @@ export class AwesomePrivacy { readonly featuredCatgories = FEATURED_CATEGORIES; private _data: AwesomePrivacyData = this._loadData(); + private _picks: Map = this._loadPicks(); private _searchIndex: SearchEntry[] | null = null; constructor(featuredCategories?: string[]) { if (featuredCategories) { this.featuredCatgories = featuredCategories; } + this._mergePicks(); } /* @@ -25,6 +28,39 @@ export class AwesomePrivacy { return load(rawYaml) as AwesomePrivacyData; } + /* + * Reads the picks YAML and returns a Map of service name to recommendation reason. + */ + private _loadPicks(): Map { + const picksData = load(rawPicks) as { picks: Pick[] }; + const picksMap = new Map(); + + for (const pick of picksData.picks) { + if (pick.reason) { + picksMap.set(pick.name, pick.reason); + } + } + + return picksMap; + } + + /* + * Merges pick data into all services across all categories. + */ + private _mergePicks(): void { + for (const category of this._data.categories) { + for (const section of category.sections) { + for (const service of section.services) { + const reason = this._picks.get(service.name); + if (reason) { + service.recommended = true; + service.recommendationReason = reason; + } + } + } + } + } + /* * Gets the entire Awesome Privacy data object. */ @@ -32,6 +68,13 @@ export class AwesomePrivacy { return this._data; } + /** + * Gets the list of all picks. + */ + public getPicks(): Pick[] { + return Array.from(this._picks.entries()).map(([name, reason]) => ({ name, reason })); + } + /** * Slugifies a name by converting it to lowercase, replacing spaces with hyphens, and removing special characters. * diff --git a/src/lib/features/awesome-privacy/types.ts b/src/lib/features/awesome-privacy/types.ts index a9b4685..49961bd 100644 --- a/src/lib/features/awesome-privacy/types.ts +++ b/src/lib/features/awesome-privacy/types.ts @@ -4,6 +4,11 @@ export type ShortService = { url: string; }; +export type Pick = { + name: string; + reason?: string; +}; + export type Service = { name: string; description: string; @@ -23,6 +28,8 @@ export type Service = { androidApp?: string; discordInvite?: string; subreddit?: string; + recommended?: boolean; + recommendationReason?: string; }; export type Section = {