A lightweight start page that displays your Raindrop.io favorites in a clean grid, fast to open and easy to use.
4 | This is not a Raindrop clone, it’s a focused homepage dashboard.
5 |
6 |
7 | ## 🆕 What’s new
8 |
9 | - Better favicon fetching: **Vemetric Favicon API** by default, **Google** as fallback
10 | - New algorithm to auto-pick an **icon background color** based on the favicon
11 | - Refined **icon style inside cards** for a more cohesive look
12 | - Removed the previous **50 favorites limit**
13 |
14 |
15 |
16 | ## ❓ Why
17 |
18 | Browser bookmark sync is often annoying, especially across mixed ecosystems.
19 | With Raindrop Homepage, you only need:
20 |
21 | 1. A Raindrop account
22 | 2. A test token
23 | 3. One URL set as your start page (or new tab)
24 |
25 |
26 |
27 | ## 🧩 Features
28 |
29 | - Usage-based sorting, your most opened favorites rise to the top
30 | - Two views:
31 | - **Favicon view** (compact, fast scanning)
32 | - **Cover view** (more visual, uses Raindrop covers)
33 | - No backend server, everything runs in your browser
34 |
35 |
36 |
37 | ## 🚀 Try it
38 |
39 | Public instance:
40 |
41 | ```text
42 | https://virgile-fr.github.io/Raindrop-HomePage/
43 | ````
44 |
45 |
46 |
47 | ## 🔑 Add your Raindrop token
48 |
49 | You have two options:
50 |
51 |
52 |
53 |
54 | ### 🅰️ Token in the URL
55 |
56 | Append your token at the end of the URL:
57 |
58 | ```text
59 | https://virgile-fr.github.io/Raindrop-HomePage/YOUR_TEST_TOKEN
60 | ```
61 |
62 | The app will read it and store it in `localStorage`.
63 |
64 | > ⚠️ Do not share that URL: the token will be visible in the address bar, history, and logs.
65 |
66 |
67 |
68 | ### 🅱️ Paste at lauch
69 |
70 | If no token is found, the page prompts you to paste it. It’s then stored in `localStorage` for that browser.
71 | > ⚠️ If you clear your browser data, you’ll be asked again for the token.
72 |
73 |
74 |
75 | ## 🧾 Get a Raindrop test token
76 |
77 | 1. Log in to Raindrop.io
78 | 2. Open:
79 | `https://app.raindrop.io/settings/integrations`
80 | 3. Click **+ Create a new app**
81 | 4. Name it (anything) and accept the terms
82 | 5. Open your new app
83 | 6. Click **Create test token**
84 | 7. Copy it, then use Option A or Option B above
85 |
86 |
87 |
88 | ## 🏠 Set it as start page / new tab
89 |
90 | * **Start page**: set the URL in your browser homepage settings
91 | * **New tab (Chromium browsers)**: use a “new tab redirect” extension, then point it to your Raindrop Homepage URL
92 | Example extension: *New Tab Redirect* : https://chromewebstore.google.com/detail/icpgjfneehieebagbmdbhnlpiopdcmna?utm_source=item-share-cb (or any equivalent)
93 |
94 |
95 |
96 | ## 📱 iOS Safari tip
97 |
98 | iOS Safari cannot set a custom homepage the same way as desktop browsers.
99 | Best workaround:
100 |
101 | 1. Open the page in Safari
102 | 2. Share button → **Add to Home Screen**
103 | 3. Launch it from the home screen icon for an app-like experience
104 |
105 |
106 |
107 | ## 🔒 Token storage and security
108 |
109 | * The token is used **only in your browser** to call the Raindrop API
110 | * Stored locally in **`localStorage`**, never on a remote server
111 | * If you use the URL method, treat it like a password
112 |
113 |
114 |
115 | ## 🏗️ Self-hosting
116 |
117 | 1. Clone the repo
118 | 2. Deploy as static files (GitHub Pages, Netlify, Vercel, etc.)
119 | 3. Optionally set a token in `token.js` if you prefer not to use URL or prompt
120 |
121 |
122 |
123 | ## 🧾 Notes
124 |
125 | * This project is an **unofficial** tool built on top of the **Raindrop.io API**
126 | * Not affiliated with or endorsed by Raindrop.io
127 |
128 |
129 |
130 | ## 🤝 Contributing
131 |
132 | Issues and PRs are welcome.
133 | If you have ideas (layout tweaks, favicon improvements, safer token handling), feel free to open an issue.
134 |
--------------------------------------------------------------------------------
/JS/dominant-color.js:
--------------------------------------------------------------------------------
1 | function clamp(value, min = 0, max = 255) {
2 | return Math.min(max, Math.max(min, Math.round(value)));
3 | }
4 |
5 | function rgbToHsl(r, g, b) {
6 | r /= 255;
7 | g /= 255;
8 | b /= 255;
9 |
10 | const max = Math.max(r, g, b);
11 | const min = Math.min(r, g, b);
12 | const delta = max - min;
13 |
14 | let h = 0;
15 | let s = 0;
16 | const l = (max + min) / 2;
17 |
18 | if (delta !== 0) {
19 | s = delta / (1 - Math.abs(2 * l - 1));
20 |
21 | switch (max) {
22 | case r:
23 | h = ((g - b) / delta) % 6;
24 | break;
25 | case g:
26 | h = (b - r) / delta + 2;
27 | break;
28 | default:
29 | h = (r - g) / delta + 4;
30 | }
31 |
32 | h *= 60;
33 | if (h < 0) h += 360;
34 | }
35 |
36 | return { h, s, l };
37 | }
38 |
39 | function hslToRgb(h, s, l) {
40 | const c = (1 - Math.abs(2 * l - 1)) * s;
41 | const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
42 | const m = l - c / 2;
43 |
44 | let r = 0;
45 | let g = 0;
46 | let b = 0;
47 |
48 | if (h < 60) {
49 | r = c;
50 | g = x;
51 | } else if (h < 120) {
52 | r = x;
53 | g = c;
54 | } else if (h < 180) {
55 | g = c;
56 | b = x;
57 | } else if (h < 240) {
58 | g = x;
59 | b = c;
60 | } else if (h < 300) {
61 | r = x;
62 | b = c;
63 | } else {
64 | r = c;
65 | b = x;
66 | }
67 |
68 | return {
69 | r: clamp((r + m) * 255),
70 | g: clamp((g + m) * 255),
71 | b: clamp((b + m) * 255),
72 | };
73 | }
74 |
75 | function computeDominantColor(image) {
76 | const canvas = document.createElement("canvas");
77 | const sampleSize = 12;
78 | canvas.width = sampleSize;
79 | canvas.height = sampleSize;
80 |
81 | const context = canvas.getContext("2d");
82 | if (!context) return null;
83 |
84 | try {
85 | context.drawImage(image, 0, 0, sampleSize, sampleSize);
86 | const { data } = context.getImageData(0, 0, sampleSize, sampleSize);
87 |
88 | let r = 0;
89 | let g = 0;
90 | let b = 0;
91 | let count = 0;
92 |
93 | for (let i = 0; i < data.length; i += 4) {
94 | const alpha = data[i + 3] / 255;
95 | if (alpha < 0.05) continue;
96 |
97 | r += data[i] * alpha;
98 | g += data[i + 1] * alpha;
99 | b += data[i + 2] * alpha;
100 | count += alpha;
101 | }
102 |
103 | if (count === 0) return null;
104 |
105 | return {
106 | r: r / count,
107 | g: g / count,
108 | b: b / count,
109 | };
110 | } catch (error) {
111 | console.warn("Unable to compute dominant color", error);
112 | return null;
113 | }
114 | }
115 |
116 | function applyFilterBackground(filter, color) {
117 | const { h, s, l } = rgbToHsl(color.r, color.g, color.b);
118 |
119 | const balancedSaturation = Math.min(0.62, Math.max(0.28, s * 0.9 + 0.12));
120 | const contrastBias = (0.5 - l) * 0.35;
121 | const baseLightness = Math.min(
122 | 0.62,
123 | Math.max(0.32, l + contrastBias + 0.08)
124 | );
125 | const accentLightness = Math.min(
126 | 0.68,
127 | Math.max(0.26, baseLightness + (l < 0.5 ? 0.08 : -0.08))
128 | );
129 |
130 | const baseColor = hslToRgb(h, balancedSaturation, baseLightness);
131 | const accentColor = hslToRgb(h, balancedSaturation * 0.92, accentLightness);
132 |
133 | const overlayIsDark = baseLightness > 0.5;
134 | const overlayOpacity = overlayIsDark ? 0.18 : 0.12;
135 | const overlayTone = overlayIsDark ? "0, 0, 0" : "255, 255, 255";
136 |
137 | filter.style.background = `linear-gradient(rgba(${overlayTone}, ${overlayOpacity}), rgba(${overlayTone}, ${overlayOpacity})), linear-gradient(135deg, rgb(${baseColor.r}, ${baseColor.g}, ${baseColor.b}), rgb(${accentColor.r}, ${accentColor.g}, ${accentColor.b}))`;
138 | }
139 |
140 | function colorizeIconBackground(icon) {
141 | if (icon.dataset.colorized) return;
142 |
143 | const filter = icon.closest(".filter");
144 | if (!filter) return;
145 |
146 | const color = computeDominantColor(icon);
147 | if (!color) return;
148 |
149 | applyFilterBackground(filter, color);
150 | icon.dataset.colorized = "true";
151 | }
152 |
153 | function refreshIconFilterColors() {
154 | const icons = document.querySelectorAll("img.icon");
155 |
156 | icons.forEach((icon) => {
157 | const applyColor = () => colorizeIconBackground(icon);
158 |
159 | if (icon.complete && icon.naturalWidth > 0) {
160 | applyColor();
161 | } else {
162 | icon.addEventListener("load", applyColor, { once: true });
163 | }
164 | });
165 | }
166 |
--------------------------------------------------------------------------------