├── .DS_Store ├── JS ├── .DS_Store ├── toggle.js ├── inserthtml.js ├── fetch.js ├── favicons.js └── dominant-color.js ├── readme.png ├── apple-icon.png ├── CSS ├── header.css ├── toggle.css ├── style.css ├── colors.css ├── grid.css └── card.css ├── package.json ├── token.js ├── 404.html ├── index.html └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Virgile-fr/Raindrop-HomePage/HEAD/.DS_Store -------------------------------------------------------------------------------- /JS/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Virgile-fr/Raindrop-HomePage/HEAD/JS/.DS_Store -------------------------------------------------------------------------------- /readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Virgile-fr/Raindrop-HomePage/HEAD/readme.png -------------------------------------------------------------------------------- /apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Virgile-fr/Raindrop-HomePage/HEAD/apple-icon.png -------------------------------------------------------------------------------- /CSS/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | width: 100%; 6 | border-bottom: var(--card-border); 7 | margin-bottom: 4vh; 8 | padding: 4vh 0; 9 | } 10 | 11 | header h1{ 12 | margin-bottom: 0px; 13 | margin-top: 0px; 14 | } 15 | 16 | .favorite-priority-toggle { 17 | cursor: pointer; 18 | } 19 | 20 | .favorite-priority-toggle:hover { 21 | color: white; 22 | } 23 | -------------------------------------------------------------------------------- /JS/toggle.js: -------------------------------------------------------------------------------- 1 | const toggle = document.querySelector("input"); 2 | 3 | function deleteGrid() { 4 | grid.innerHTML = ""; 5 | } 6 | 7 | toggle.addEventListener("change", function () { 8 | if (this.checked) { 9 | deleteGrid(); 10 | fetchCardsCovers(); 11 | localStorage.setItem("switch", "on"); 12 | } else { 13 | deleteGrid(); 14 | fetchCardsIcons(); 15 | localStorage.removeItem("switch"); 16 | } 17 | }); 18 | 19 | async function getGrid() { 20 | if (toggle.checked || localStorage.switch == "on") { 21 | fetchCardsCovers(); 22 | toggle.checked = true; 23 | } else { 24 | fetchCardsIcons(); 25 | toggle.checked = false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CSS/toggle.css: -------------------------------------------------------------------------------- 1 | input[type=checkbox] { 2 | height: 0; 3 | width: 0; 4 | visibility: hidden; 5 | } 6 | 7 | label { 8 | cursor: pointer; 9 | text-indent: -999999px; 10 | width: 30px; 11 | height: 20px; 12 | background: rgb(91, 114, 114); 13 | display: block; 14 | border-radius: 100px; 15 | position: relative; 16 | } 17 | 18 | label:after { 19 | content: ''; 20 | position: absolute; 21 | top: 3px; 22 | left: 3px; 23 | width: 14px; 24 | height: 14px; 25 | background: #fff; 26 | border-radius: 100px; 27 | transition: 0.3s; 28 | } 29 | 30 | input:checked+label { 31 | background: #3be696; 32 | } 33 | 34 | input:checked+label:after { 35 | left: calc(100% - 3px); 36 | transform: translateX(-100%); 37 | } 38 | -------------------------------------------------------------------------------- /CSS/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: system-ui, 3 | -apple-system, BlinkMacSystemFont, 4 | 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', 6 | sans-serif; 7 | color: var(--text-color); 8 | } 9 | 10 | a { 11 | text-decoration: none; 12 | } 13 | 14 | body { 15 | background-color: var(--body-background-color); 16 | background-image: var(--body-background-image); 17 | /* background-attachment: fixed; */ 18 | padding: 3vw 10vw 10vw 10vw; 19 | margin:0; 20 | } 21 | 22 | /* No Scrollbar */ 23 | * { 24 | -ms-overflow-style: none !important; 25 | scrollbar-width: none !important; 26 | } 27 | 28 | *::-webkit-scrollbar { 29 | display: none !important; 30 | } 31 | 32 | .element::-webkit-scrollbar { 33 | -webkit-appearance: none; 34 | appearance: none; 35 | width: 0; 36 | height: 0; 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raindrop-homepage", 3 | "version": "1.0.0", 4 | "description": "Raindrop HomePage is a minimalist interface to display your Raindrop.io bookmarks marked as favorites. This application is not intended to replicate the interface of Raindrop.io.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Virgile-fr/Raindrop-HomePage.git" 12 | }, 13 | "keywords": [ 14 | "Raindrop", 15 | "bookmarks", 16 | "favourites", 17 | "homepage" 18 | ], 19 | "author": "Virgile Arlaud", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/Virgile-fr/Raindrop-HomePage/issues" 23 | }, 24 | "homepage": "https://github.com/Virgile-fr/Raindrop-HomePage#readme" 25 | } 26 | -------------------------------------------------------------------------------- /CSS/colors.css: -------------------------------------------------------------------------------- 1 | /* Light Theme */ 2 | 3 | @media (prefers-color-scheme: light) { :root { 4 | --card-backgroud: rgba(255, 255, 255, 0.3); 5 | --card-border: solid 0.5px #c9c9c9; 6 | --card-backgroud-hover: rgba(255, 255, 255, 0.8); 7 | --image-background-color: rgb(41, 56, 74); 8 | --body-background-color: #dfe1e5; 9 | --body-background-image: linear-gradient(0deg, #dfe1e5 0%, #d7ebf0 25%, #f4e8d4 50%, #d7ebf0 75%, #dfe1e5 100%); 10 | --text-color: rgb(62, 64, 73); 11 | --image-border-color:solid 0.5px #c9c9c9; 12 | }} 13 | 14 | @media (prefers-color-scheme: dark) { :root { 15 | --card-backgroud: #222222; 16 | --card-border: solid 1px #363636; 17 | --card-backgroud-hover: #171717; 18 | --image-background-color: rgb(37, 37, 37); 19 | --body-background-color: #26252C; 20 | --body-background-image: linear-gradient(0deg, #26252C 0%, #332416 35%, #12272d 65%, #26252C 100%); 21 | --text-color: rgb(192, 194, 205); 22 | --image-border-color:solid 1px #363636; 23 | }} 24 | -------------------------------------------------------------------------------- /CSS/grid.css: -------------------------------------------------------------------------------- 1 | #grid { 2 | display: grid; 3 | gap: 1rem; 4 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; 5 | } 6 | 7 | @media screen and (max-width: 1800px) { 8 | #grid { 9 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; 10 | } 11 | } 12 | 13 | @media screen and (max-width: 1500px) { 14 | #grid { 15 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; 16 | } 17 | } 18 | 19 | @media screen and (max-width: 1200px) { 20 | #grid { 21 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr; 22 | } 23 | } 24 | 25 | @media screen and (max-width: 900px) { 26 | #grid { 27 | grid-template-columns: 1fr 1fr 1fr 1fr; 28 | } 29 | } 30 | 31 | @media screen and (max-width: 700px) { 32 | #grid { 33 | grid-template-columns: 1fr 1fr 1fr; 34 | } 35 | } 36 | 37 | @media screen and (max-width: 500px) { 38 | #grid { 39 | gap:0.5rem; 40 | } 41 | body { 42 | padding: 0 1rem 1rem 1rem; 43 | } 44 | } 45 | 46 | @media screen and (max-width: 410px) { 47 | #grid { 48 | grid-template-columns: 1fr 1fr; 49 | } 50 | body { 51 | padding: 1rem 1rem; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /token.js: -------------------------------------------------------------------------------- 1 | // 💧 Enter Your Raindrop Token here 👇 // 2 | let raindropToken = "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX"; 3 | 4 | const findTokenInPath = () => { 5 | const pathSegments = window.location.pathname.split("/").filter(Boolean); 6 | if (!pathSegments.length) return null; 7 | 8 | const candidate = decodeURIComponent(pathSegments[pathSegments.length - 1]); 9 | const looksLikeToken = /^[A-Za-z0-9_-]{30,}$/.test(candidate); 10 | 11 | return looksLikeToken ? candidate : null; 12 | }; 13 | 14 | const urlToken = findTokenInPath(); 15 | let token; 16 | 17 | if (urlToken) { 18 | localStorage.setItem("token", urlToken); 19 | token = urlToken; 20 | } else if (raindropToken !== "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX") { 21 | token = raindropToken; 22 | } else if ( 23 | localStorage.token == "null" || 24 | localStorage.token == "undefined" || 25 | localStorage.token == undefined 26 | ) { 27 | const localToken = prompt( 28 | "🏠 Welcome to Raindrop HomePage\n\nplease enter your test-token\nif you dont know how to do it, please visit the url below\n\nℹ️ https://github.com/Virgile-fr/Raindrop-HomePage" 29 | ); 30 | localStorage.setItem("token", localToken); 31 | token = localStorage.token; 32 | } else { 33 | token = localStorage.token; 34 | } 35 | -------------------------------------------------------------------------------- /JS/inserthtml.js: -------------------------------------------------------------------------------- 1 | let grid = document.getElementById("grid"); 2 | 3 | grid.addEventListener("click", handleCardClick); 4 | 5 | function smallerTitle(title) { 6 | if (title.length > 14) { 7 | return title.substring(0, 12) + ".."; 8 | } else return title; 9 | } 10 | 11 | function test(lien, titre) { 12 | const { primary, fallback } = getFaviconPreference(lien); 13 | const initialIcon = primary ?? fallback ?? ""; 14 | const fallbackIcon = fallback ?? ""; 15 | const crossOriginAttr = allowsCrossOriginLoading(initialIcon) 16 | ? ' crossOrigin="anonymous"' 17 | : ""; 18 | let content = ` 19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
${smallerTitle(titre)}
27 |
28 |
`; 29 | return content; 30 | } 31 | 32 | function test2(lien, titre, image) { 33 | let content = ` 34 | 35 |
36 |
37 |
38 |
${smallerTitle(titre)}
39 |
40 |
`; 41 | return content; 42 | } 43 | 44 | function handleCardClick(event) { 45 | const anchor = event.target.closest("a"); 46 | 47 | if (!anchor || !grid.contains(anchor)) { 48 | return; 49 | } 50 | 51 | const link = anchor.getAttribute("href"); 52 | 53 | if (link) { 54 | recordUsage(link); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 15 | HomePage 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 32 |

Favorites

33 |
34 | 35 | 36 | 37 |
38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 15 | HomePage 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 32 |

Favorites

33 |
34 | 35 | 36 | 37 |
38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /CSS/card.css: -------------------------------------------------------------------------------- 1 | * { 2 | --icon-radius: 22px; 3 | --icon-size: 42px; 4 | --radius-cards: 8px; 5 | --radius-images: 6px; 6 | } 7 | 8 | /* Custom properties typées, pour animer le gradient */ 9 | @property --icon-g1 { 10 | syntax: ""; 11 | inherits: true; 12 | initial-value: rgba(255,255,255,0.0); 13 | } 14 | @property --icon-g2 { 15 | syntax: ""; 16 | inherits: true; 17 | initial-value: rgba(255,255,255,0.0); 18 | } 19 | 20 | .card { 21 | background-color: var(--card-backgroud); 22 | border-radius: var(--radius-cards); 23 | border: var(--card-border); 24 | box-sizing: content-box; 25 | transition: background-color 0.2s, box-shadow 0.2s; 26 | transition: box-shadow 0.4s cubic-bezier(0.22, 1, 0.36, 1) 27 | } 28 | 29 | .card:hover { 30 | background-color: var(--card-backgroud-hover); 31 | box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, 32 | rgba(0, 0, 0, 0.07) 0px 2px 4px, 33 | rgba(0, 0, 0, 0.07) 0px 4px 8px, 34 | rgba(0, 0, 0, 0.07) 0px 8px 16px, 35 | rgba(0, 0, 0, 0.07) 0px 16px 32px, 36 | rgba(0, 0, 0, 0.07) 0px 32px 64px; 37 | } 38 | 39 | .image { 40 | background-color: var(--image-background-color); 41 | background-size: cover; 42 | background-repeat: no-repeat; 43 | background-position: center; 44 | border-radius: var(--radius-images) var(--radius-images) 0px 0px; 45 | } 46 | 47 | .title { 48 | padding-left: 6px; 49 | margin: 5px 0px; 50 | display: flex; 51 | align-items: center; 52 | font-size: 10pt; 53 | font-weight: 550; 54 | text-indent: 2px; 55 | } 56 | 57 | .cover-cards .image { 58 | aspect-ratio: 16/9; 59 | border-bottom: var(--image-border-color); 60 | } 61 | .cover-cards .title { 62 | aspect-ratio: 6/1; 63 | } 64 | 65 | .icon-cards .title { 66 | aspect-ratio: 6/1; 67 | } 68 | .icon-cards .image { 69 | aspect-ratio: 6/5; 70 | } 71 | 72 | .card .icon { 73 | height: var(--icon-size); 74 | width: var(--icon-size); 75 | border-radius: var(--icon-radius); 76 | padding: 8px; 77 | box-shadow: 65px 39px 100px 100px rgba(255, 255, 255, 0), 0px 0px 0px 1px rgba(255, 255, 255, 0.4); 78 | 79 | /* Valeurs de départ (optionnel, mais clair) */ 80 | --icon-g1: rgba(255,255,255,0.6); 81 | --icon-g2: rgba(255,255,255,0.2); 82 | 83 | /* Gradient basé sur variables animables */ 84 | background: linear-gradient(var(--icon-g1), var(--icon-g2)); 85 | 86 | /* On transitionne les variables, pas le background */ 87 | transition: --icon-g1 2s, --icon-g2, box-shadow 0.4s ease; 88 | } 89 | 90 | .card:hover .icon { 91 | --icon-g1: rgba(255,255,255,1); 92 | --icon-g2: rgba(255,255,255,.4); 93 | box-shadow: -65px -80px 80px 100px rgba(255, 255, 255, 0.3), 0px 0px 0px 1px rgba(255, 255, 255, .7); 94 | } 95 | 96 | .filter { 97 | background-color: #363636; 98 | display: grid; 99 | place-items: center; 100 | aspect-ratio: 4/3; 101 | border-radius: var(--radius-images) var(--radius-images) 0px 0px; 102 | overflow: hidden; 103 | box-sizing: border-box; 104 | border-bottom: var(--image-border-color); 105 | filter: saturate(1.1); 106 | } 107 | -------------------------------------------------------------------------------- /JS/fetch.js: -------------------------------------------------------------------------------- 1 | const USAGE_STORAGE_KEY = "favoriteUsageCounts"; 2 | const FAVORITES_PER_PAGE = 50; 3 | const MAX_FAVORITE_PAGES = 2; 4 | const FAVORITE_QUERY = encodeURIComponent("❤️"); 5 | 6 | async function fetchJson(url) { 7 | const response = await fetch(url, { 8 | method: "GET", 9 | headers: { Authorization: "Bearer " + token }, 10 | }); 11 | 12 | if (!response.ok) { 13 | throw new Error(`Request failed: ${response.status}`); 14 | } 15 | 16 | return response.json(); 17 | } 18 | 19 | async function fetchcollections() { 20 | const data = await fetchJson("https://api.raindrop.io/rest/v1/collections"); 21 | console.log(data); 22 | return data; 23 | } 24 | 25 | async function fetchcollection(collectionID) { 26 | const data = await fetchJson( 27 | "https://api.raindrop.io/rest/v1/raindrops/" + collectionID 28 | ); 29 | console.log(data); 30 | return data; 31 | } 32 | 33 | function getUsageCount(link) { 34 | const usageData = JSON.parse(localStorage.getItem(USAGE_STORAGE_KEY) || "{}"); 35 | return usageData[link] || 0; 36 | } 37 | 38 | function recordUsage(link) { 39 | const usageData = JSON.parse(localStorage.getItem(USAGE_STORAGE_KEY) || "{}"); 40 | usageData[link] = (usageData[link] || 0) + 1; 41 | localStorage.setItem(USAGE_STORAGE_KEY, JSON.stringify(usageData)); 42 | } 43 | 44 | function sortByUsage(items) { 45 | return [...items].sort((a, b) => { 46 | const usageDifference = getUsageCount(b.link) - getUsageCount(a.link); 47 | 48 | if (usageDifference !== 0) { 49 | return usageDifference; 50 | } 51 | 52 | return new Date(b.created).getTime() - new Date(a.created).getTime(); 53 | }); 54 | } 55 | 56 | function getTotalPages(meta) { 57 | const totalItems = meta?.count ?? meta?.items?.length ?? 0; 58 | const pageCount = Math.ceil(totalItems / FAVORITES_PER_PAGE); 59 | return Math.min(Math.max(pageCount, 1), MAX_FAVORITE_PAGES); 60 | } 61 | 62 | async function fetchFavoritePage(page) { 63 | return fetchJson( 64 | `https://api.raindrop.io/rest/v1/raindrops/0?search=${FAVORITE_QUERY}&perpage=${FAVORITES_PER_PAGE}&page=${page}` 65 | ); 66 | } 67 | 68 | async function fetchAllFavoriteItems() { 69 | const firstPage = await fetchFavoritePage(0); 70 | const totalPages = getTotalPages(firstPage); 71 | 72 | if (totalPages === 1) { 73 | return firstPage.items ?? []; 74 | } 75 | 76 | const additionalPages = await Promise.all( 77 | Array.from({ length: totalPages - 1 }, (_, index) => fetchFavoritePage(index + 1)) 78 | ); 79 | 80 | return [firstPage, ...additionalPages].flatMap((page) => page.items ?? []); 81 | } 82 | 83 | async function renderFavoriteCards(renderer) { 84 | try { 85 | const favoriteItems = await fetchAllFavoriteItems(); 86 | const sortedItems = sortByUsage(favoriteItems); 87 | const content = sortedItems.map(renderer).join(""); 88 | 89 | grid.insertAdjacentHTML("beforeend", content); 90 | refreshIconFilterColors(); 91 | } catch (error) { 92 | console.error("Failed to render favorites", error); 93 | } 94 | } 95 | 96 | function fetchCardsIcons() { 97 | renderFavoriteCards((result) => test(result.link, result.title)); 98 | } 99 | 100 | function fetchCardsCovers() { 101 | renderFavoriteCards((result) => test2(result.link, result.title, result.cover)); 102 | } 103 | 104 | // result.cover = preview in raindrop 105 | // result.link = url of the link 106 | // result.title = name of the link 107 | // ${vemetricfavicon(result.link)} = Vemetric Favicon API icon of the link 108 | // ${googlefavicon(result.link)} = google favicon of the link 109 | // ${favicon(result.link)} = favicon of the link 110 | // ${statvoofavicon(result.link)} = statvoofavicon favicon of the link 111 | -------------------------------------------------------------------------------- /JS/favicons.js: -------------------------------------------------------------------------------- 1 | const GOOGLE_FAVICON_PRIORITY_KEY = "googleFaviconPriority"; 2 | 3 | function extractDomain(address) { 4 | try { 5 | return new URL(address).hostname; 6 | } catch (error) { 7 | try { 8 | return new URL(`https://${address}`).hostname; 9 | } catch { 10 | return null; 11 | } 12 | } 13 | } 14 | 15 | function extractHost(address) { 16 | try { 17 | return new URL(address).host; 18 | } catch (error) { 19 | try { 20 | return new URL(`https://${address}`).host; 21 | } catch { 22 | return null; 23 | } 24 | } 25 | } 26 | 27 | function favicon(adress) { 28 | let splitadress = adress.split("/"); 29 | splitadress = splitadress.slice(0, 3); 30 | splitadress = splitadress.join("/"); 31 | return (newadress = splitadress + "/favicon.ico"); 32 | } 33 | 34 | function googlefavicon(adress) { 35 | let resolution = 256; 36 | const domain = extractHost(adress); 37 | if (!domain) return null; 38 | 39 | return `https://www.google.com/s2/favicons?sz=${resolution}&domain=${domain}`; 40 | } 41 | 42 | function vemetricfavicon(adress) { 43 | const domain = extractDomain(adress); 44 | if (!domain) return null; 45 | 46 | return `https://favicon.vemetric.com/${encodeURIComponent(domain)}`; 47 | } 48 | 49 | function duckduckgofavicon(adress) { 50 | let splitadress = adress.split("/"); 51 | splitadress = splitadress.slice(2, 3); 52 | splitadress = splitadress.join("/"); 53 | return (newadress = 54 | "https://icons.duckduckgo.com/ip2/" + splitadress + ".ico"); 55 | } 56 | 57 | function statvoofavicon(adress) { 58 | let splitadress = adress.split("/"); 59 | splitadress = splitadress.slice(0, 3); 60 | splitadress = splitadress.join("/"); 61 | return (newadress = "https://api.statvoo.com/favicon/?url=" + splitadress); 62 | } 63 | 64 | function isGoogleFaviconPriority() { 65 | return localStorage.getItem(GOOGLE_FAVICON_PRIORITY_KEY) === "true"; 66 | } 67 | 68 | function setGoogleFaviconPriority(isGoogleFirst) { 69 | localStorage.setItem(GOOGLE_FAVICON_PRIORITY_KEY, isGoogleFirst); 70 | } 71 | 72 | function getFaviconPreference(address) { 73 | const vemetricIcon = vemetricfavicon(address); 74 | const googleIcon = googlefavicon(address); 75 | 76 | if (isGoogleFaviconPriority()) { 77 | return { primary: googleIcon, fallback: vemetricIcon }; 78 | } 79 | 80 | return { primary: vemetricIcon, fallback: googleIcon }; 81 | } 82 | 83 | function allowsCrossOriginLoading(url) { 84 | if (!url) return false; 85 | 86 | const blockedHosts = ["www.google.com"]; 87 | try { 88 | const hostname = new URL(url).hostname; 89 | return !blockedHosts.includes(hostname); 90 | } catch { 91 | return false; 92 | } 93 | } 94 | 95 | function updateFaviconPriorityIndicator() { 96 | const toggleIcon = document.querySelector(".favorite-priority-toggle"); 97 | 98 | if (!toggleIcon) { 99 | return; 100 | } 101 | 102 | toggleIcon.classList.toggle("google-priority", isGoogleFaviconPriority()); 103 | toggleIcon.setAttribute( 104 | "title", 105 | isGoogleFaviconPriority() 106 | ? "Icônes Google en priorité (cliquer pour inverser)" 107 | : "Icônes Vemetric en priorité (cliquer pour inverser)" 108 | ); 109 | } 110 | 111 | function toggleFaviconPriority() { 112 | const newPriority = !isGoogleFaviconPriority(); 113 | setGoogleFaviconPriority(newPriority); 114 | updateFaviconPriorityIndicator(); 115 | 116 | if (typeof toggle !== "undefined" && !toggle.checked) { 117 | deleteGrid(); 118 | fetchCardsIcons(); 119 | } 120 | } 121 | 122 | document.addEventListener("DOMContentLoaded", () => { 123 | const toggleIcon = document.querySelector(".favorite-priority-toggle"); 124 | 125 | if (!toggleIcon) { 126 | return; 127 | } 128 | 129 | updateFaviconPriorityIndicator(); 130 | toggleIcon.addEventListener("click", toggleFaviconPriority); 131 | }); 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Raindrop Homepage

3 |

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 | --------------------------------------------------------------------------------