├── assets ├── icon.png └── fonts │ ├── Sedan-Italic.ttf │ └── Sedan-Regular.ttf ├── postcss.config.js ├── newtab ├── index.html └── index.tsx ├── utils ├── queryclient.ts └── readwise.ts ├── storage.ts ├── style.css ├── tsconfig.json ├── README.md ├── .gitignore ├── components └── Providers.tsx ├── themes.ts ├── .prettierrc.mjs ├── background └── index.ts ├── tailwind.config.js ├── .github └── workflows │ └── submit.yml ├── package.json └── options └── index.tsx /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djyde/wisetab/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/fonts/Sedan-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djyde/wisetab/HEAD/assets/fonts/Sedan-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Sedan-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djyde/wisetab/HEAD/assets/fonts/Sedan-Regular.ttf -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /newtab/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | __plasmo_static_index_title__ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /utils/queryclient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const queryClient = new QueryClient({ 4 | defaultOptions: { 5 | queries: { 6 | refetchOnWindowFocus: false, 7 | }, 8 | }, 9 | }); -------------------------------------------------------------------------------- /storage.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from "@plasmohq/storage"; 2 | 3 | export const storage = new Storage({ 4 | area: 'local' 5 | }) 6 | 7 | export enum StorageKey { 8 | ReadwiseToken = 'readwise_token', 9 | DailyReview = 'daily_review', 10 | ReviewCursor = 'review_cursor', 11 | } -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | @layer base { 7 | @font-face { 8 | font-family: "Sedan"; 9 | font-style: normal; 10 | font-weight: 400; 11 | font-display: swap; 12 | src: url(data-base64:~assets/fonts/Sedan-Regular.ttf); 13 | } 14 | } 15 | 16 | 17 | body { 18 | font-size: 16px; 19 | @apply antialiased; 20 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plasmo/templates/tsconfig.base", 3 | "exclude": [ 4 | "node_modules" 5 | ], 6 | "include": [ 7 | ".plasmo/index.d.ts", 8 | "./**/*.ts", 9 | "./**/*.tsx" 10 | ], 11 | "compilerOptions": { 12 | "strictNullChecks": true, 13 | "paths": { 14 | "~*": [ 15 | "./*" 16 | ] 17 | }, 18 | "baseUrl": "." 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wisetab 2 | 3 | Readwise daily review on your browser's new tab page. 4 | 5 | ![Frame 12](https://github.com/djyde/wisetab/assets/914329/77841fee-8160-4fc5-9a08-0f26e837142c) 6 | 7 | ## Getting Started 8 | 9 | First, run the development server: 10 | 11 | ```bash 12 | pnpm dev 13 | # or 14 | npm run dev 15 | ``` 16 | 17 | ## Making production build 18 | 19 | Run the following: 20 | 21 | ```bash 22 | pnpm build 23 | # or 24 | npm run build 25 | ``` 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # misc 13 | .DS_Store 14 | *.pem 15 | 16 | # debug 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | .pnpm-debug.log* 21 | 22 | # local env files 23 | .env*.local 24 | 25 | out/ 26 | build/ 27 | dist/ 28 | 29 | # plasmo 30 | .plasmo 31 | 32 | # typescript 33 | .tsbuildinfo 34 | -------------------------------------------------------------------------------- /components/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClientProvider } from "@tanstack/react-query"; 2 | import { queryClient } from "~utils/queryclient"; 3 | import '../style.css' 4 | import { useEffect } from "react"; 5 | import { themeChange } from "theme-change"; 6 | 7 | export function Providers(props: { 8 | children: React.ReactNode 9 | }) { 10 | useEffect(() => { 11 | themeChange(false) 12 | }, []) 13 | 14 | return ( 15 | 16 | {props.children} 17 | 18 | ) 19 | } -------------------------------------------------------------------------------- /themes.ts: -------------------------------------------------------------------------------- 1 | export const themes = [ 2 | "light", 3 | "dark", 4 | "cupcake", 5 | "bumblebee", 6 | "emerald", 7 | "corporate", 8 | "synthwave", 9 | "retro", 10 | "cyberpunk", 11 | "valentine", 12 | "halloween", 13 | "garden", 14 | "forest", 15 | "aqua", 16 | "lofi", 17 | "pastel", 18 | "fantasy", 19 | "wireframe", 20 | "black", 21 | "luxury", 22 | "dracula", 23 | "cmyk", 24 | "autumn", 25 | "business", 26 | "acid", 27 | "lemonade", 28 | "night", 29 | "coffee", 30 | "winter", 31 | "dim", 32 | "nord", 33 | "sunset", 34 | ] -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | export default { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | semi: false, 9 | singleQuote: false, 10 | trailingComma: "none", 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 14 | importOrder: [ 15 | "", // Node.js built-in modules 16 | "", // Imports not matched by other special words or groups. 17 | "", // Empty line 18 | "^@plasmo/(.*)$", 19 | "", 20 | "^@plasmohq/(.*)$", 21 | "", 22 | "^~(.*)$", 23 | "", 24 | "^[./]" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /background/index.ts: -------------------------------------------------------------------------------- 1 | import { StorageKey, storage } from "~storage" 2 | import { getDailyReviewList } from "~utils/readwise" 3 | 4 | async function refreshDailyReview() { 5 | const dailyReviewList = await getDailyReviewList() 6 | // Save the daily review list to storage 7 | await storage.set(StorageKey.DailyReview, dailyReviewList) 8 | } 9 | 10 | const alarmName = 'refreshDailyReview' 11 | 12 | chrome.runtime.onInstalled.addListener(async () => { 13 | console.log('set alarm') 14 | await chrome.alarms.create(alarmName, { periodInMinutes: 60 }) 15 | }) 16 | 17 | 18 | chrome.alarms.onAlarm.addListener(async (alarm) => { 19 | if (alarm.name === alarmName) { 20 | await refreshDailyReview() 21 | } 22 | }) 23 | 24 | storage.watch({ 25 | [StorageKey.ReadwiseToken]: async () => { 26 | await refreshDailyReview() 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /utils/readwise.ts: -------------------------------------------------------------------------------- 1 | import { StorageKey, storage } from "~storage" 2 | 3 | export type DailyReview = { 4 | review_id: number, 5 | review_url: string, 6 | review_completed: boolean, 7 | highlights: { 8 | text: string, 9 | title: string, 10 | author: string, 11 | url: string, 12 | source_type: string, 13 | note: string, 14 | highlighted_at: string, 15 | image_url: string 16 | id: number 17 | }[] 18 | } 19 | 20 | export async function getDailyReviewList() { 21 | const token = await storage.get(StorageKey.ReadwiseToken) 22 | 23 | if (!token) { 24 | return null 25 | } 26 | 27 | const response = await fetch("https://readwise.io/api/v2/review/", { 28 | method: "GET", 29 | headers: { 30 | Authorization: `Token ${token}` 31 | } 32 | }) 33 | 34 | if (response.status === 401) { 35 | return null 36 | } 37 | 38 | const json = await response.json() 39 | return json as DailyReview 40 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./newtab/**/*.tsx", 5 | "./options/**/*.tsx", 6 | ], 7 | daisyui: { 8 | themes: [ 9 | "light", 10 | "dark", 11 | "cupcake", 12 | "bumblebee", 13 | "emerald", 14 | "corporate", 15 | "synthwave", 16 | "retro", 17 | "cyberpunk", 18 | "valentine", 19 | "halloween", 20 | "garden", 21 | "forest", 22 | "aqua", 23 | "lofi", 24 | "pastel", 25 | "fantasy", 26 | "wireframe", 27 | "black", 28 | "luxury", 29 | "dracula", 30 | "cmyk", 31 | "autumn", 32 | "business", 33 | "acid", 34 | "lemonade", 35 | "night", 36 | "coffee", 37 | "winter", 38 | "dim", 39 | "nord", 40 | "sunset", 41 | ] 42 | }, 43 | theme: { 44 | fontFamily: { 45 | "serif-eng": ["Sedan", "serif"], 46 | }, 47 | extend: {}, 48 | }, 49 | plugins: [ 50 | require("daisyui") 51 | ], 52 | } 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Store" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Cache pnpm modules 11 | uses: actions/cache@v3 12 | with: 13 | path: ~/.pnpm-store 14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 15 | restore-keys: | 16 | ${{ runner.os }}- 17 | - uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: latest 20 | run_install: true 21 | - name: Use Node.js 16.x 22 | uses: actions/setup-node@v3.4.1 23 | with: 24 | node-version: 16.x 25 | cache: "pnpm" 26 | - name: Build the extension 27 | run: pnpm build 28 | - name: Package the extension into a zip artifact 29 | run: pnpm package 30 | - name: Browser Platform Publish 31 | uses: PlasmoHQ/bpp@v3 32 | with: 33 | keys: ${{ secrets.SUBMIT_KEYS }} 34 | artifact: build/chrome-mv3-prod.zip 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wisetab", 3 | "displayName": "Wisetab", 4 | "version": "0.0.5", 5 | "description": "Show Readwise highlight in your new tab page", 6 | "author": "Randy Lu", 7 | "scripts": { 8 | "dev": "plasmo dev", 9 | "build": "plasmo build", 10 | "package": "plasmo package" 11 | }, 12 | "dependencies": { 13 | "@plasmohq/storage": "^1.10.0", 14 | "@tanstack/react-query": "^5.29.2", 15 | "autoprefixer": "^10.4.19", 16 | "classnames": "^2.5.1", 17 | "daisyui": "^4.10.2", 18 | "dompurify": "^3.1.0", 19 | "markdown-it": "^14.1.0", 20 | "plasmo": "0.85.2", 21 | "postcss": "^8.4.38", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "react-hook-form": "^7.51.3", 25 | "react-icons": "^5.1.0", 26 | "tailwindcss": "^3.4.3", 27 | "theme-change": "^2.5.0" 28 | }, 29 | "devDependencies": { 30 | "@ianvs/prettier-plugin-sort-imports": "4.1.1", 31 | "@types/chrome": "0.0.258", 32 | "@types/node": "20.11.5", 33 | "@types/react": "18.2.48", 34 | "@types/react-dom": "18.2.18", 35 | "prettier": "3.2.4", 36 | "typescript": "5.3.3" 37 | }, 38 | "manifest": { 39 | "permissions": [ 40 | "alarms" 41 | ], 42 | "host_permissions": [] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /options/index.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from "react-hook-form"; 2 | import { Providers } from "~components/Providers"; 3 | import { StorageKey, storage } from "~storage"; 4 | 5 | export default function Options() { 6 | 7 | 8 | const form = useForm({ 9 | defaultValues: async () => { 10 | return { 11 | readwiseToken: await storage.get(StorageKey.ReadwiseToken) 12 | } 13 | } 14 | }) 15 | 16 | return ( 17 | 18 |
19 |
20 |

Options

21 |
{ 22 | await storage.set(StorageKey.ReadwiseToken, values.readwiseToken) 23 | alert("Saved!") 24 | })}> 25 |
26 | 30 |
31 | Get token 32 |
33 |
34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 |
43 | ) 44 | } -------------------------------------------------------------------------------- /newtab/index.tsx: -------------------------------------------------------------------------------- 1 | import { StorageKey, storage } from '~storage' 2 | import '../style.css' 3 | import { useStorage } from '@plasmohq/storage/hook' 4 | import type { DailyReview } from '~utils/readwise' 5 | import { useEffect } from 'react' 6 | import { QueryClientProvider, useMutation, useQuery } from '@tanstack/react-query' 7 | import { queryClient } from '~utils/queryclient' 8 | import cn from 'classnames' 9 | import { Providers } from '~components/Providers' 10 | import { LuSettings } from 'react-icons/lu' 11 | import { themes } from '~themes' 12 | import { themeChange } from 'theme-change' 13 | import MarkdownIt from 'markdown-it'; 14 | import DOMPurify from 'dompurify'; 15 | 16 | function NewTab() { 17 | const md = new MarkdownIt(); 18 | md.renderer.rules.link_open = function(tokens, idx, options, env, self) { 19 | const aIndex = tokens[idx].attrIndex('class'); 20 | if (aIndex < 0) { 21 | tokens[idx].attrPush(['class', 'link link-primary']); 22 | } else { 23 | tokens[idx].attrs[aIndex][1] += ' link link-primary'; 24 | } 25 | return self.renderToken(tokens, idx, options); 26 | }; 27 | 28 | const review = useQuery({ 29 | queryKey: ['dailyReview'], 30 | queryFn: async () => { 31 | const result = await storage.get(StorageKey.DailyReview) 32 | return result 33 | } 34 | }) 35 | 36 | if (!review.isPending && !review.data) { 37 | return ( 38 | <> 39 |
40 |
41 | Please first 44 |
45 |
46 | 47 | ) 48 | } 49 | 50 | const highlightCount = review.data?.highlights.length || 0 51 | 52 | // random index base on highlight count 53 | const random = Math.floor(Math.random() * highlightCount) 54 | 55 | const currentReview = review.data?.highlights[random] 56 | 57 | const renderTextWithLinks = (text) => { 58 | const renderedText = md.render(text); 59 | const sanitizedText = DOMPurify.sanitize(renderedText); 60 | return sanitizedText; 61 | } 62 | 63 | return ( 64 |
65 | 89 | {currentReview && ( 90 | <> 91 |
200 94 | }) 95 | }> 96 |
97 | 98 |
99 | 100 |
101 |
102 |
103 | {currentReview.note} 104 |
105 |
106 | 107 | 108 |
109 |
110 |
111 | 112 |
113 | {currentReview.author} / {currentReview.title} 114 |
115 |
116 | 117 | )} 118 |
119 | ) 120 | } 121 | 122 | export default function Page() { 123 | 124 | return ( 125 | 126 | 127 | 128 | ) 129 | } --------------------------------------------------------------------------------