├── .github └── workflows │ └── nuxtjs.yml ├── .gitignore ├── LICENSE ├── README.md ├── app.config.ts ├── app.vue ├── components ├── BookCard.vue ├── BookView.vue ├── BooksList.vue ├── Error.vue └── Header.vue ├── middleware └── remotes.ts ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages ├── history.vue ├── index.vue ├── instances.vue ├── open │ ├── [id].vue │ └── dropped.vue ├── search.vue └── settings.vue ├── tsconfig.json └── utils ├── index.ts └── sanitize.ts /.github/workflows/nuxtjs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Nuxt site to GitHub Pages 2 | # 3 | # To get started with Nuxt see: https://nuxtjs.org/docs/get-started/installation 4 | # 5 | name: Deploy Nuxt site to Pages 6 | 7 | env: 8 | NUXT_APP_BASE_URL: /teatime/ 9 | on: 10 | # Runs on pushes targeting the default branch 11 | push: 12 | branches: ["main"] 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 18 | permissions: 19 | contents: read 20 | pages: write 21 | id-token: write 22 | 23 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 24 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 25 | concurrency: 26 | group: "pages" 27 | cancel-in-progress: false 28 | 29 | jobs: 30 | build: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v3 34 | - run: corepack enable 35 | - uses: actions/setup-node@v3 36 | with: 37 | node-version: "20" 38 | # Pick your own package manager and build script 39 | - run: npm install 40 | - run: npx nuxt build --preset github_pages 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | path: ./.output/public 45 | 46 | # Deployment job 47 | deploy: 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | runs-on: ubuntu-latest 52 | needs: build 53 | steps: 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yo'av Moshe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 📚 TeaTime 3 |

4 | 5 |

6 | TeaTime is a fully static distributed library system powered by IPFS, SQLite, and GitHub 7 |

8 |

9 |
10 | Auto-updating instances are hosted on Netlify and GitHub Pages 11 |

12 | 13 | # A Distributed Library 14 | 15 | The TeaTime web application is completely decoupled from its databases and the files it fetches. The databases used in TeaTime are [GitHub repositories tagged with the teatime-database topic](https://github.com/search?q=topic%3Ateatime-database&type=repositories), which are published on GitHub Pages. Each repository contains a [config.json](https://github.com/bjesus/teatime-database/blob/main/config.json) file that points to an SQLite database. Before a user performs a search in TeaTime, they choose which database to use and then TeaTime queries the SQLite database using [sql.js-httpvfs](https://github.com/phiresky/sql.js-httpvfs). Each row in the SQLite database is an item in the library, and a file hash column is used for getting the item from IPFS. 16 | 17 | Since the web application is a static site, and the databases are comprised of static files, both can be easily forked, replicated, and deployed. Frontend instances are [GitHub repositories tagged with the teatime-instance topic](https://github.com/search?q=topic%3Ateatime-instance&type=repositories). With the files being served off IPFS, this distributed architecture contributes to TeaTime's resilience. 18 | 19 | ## Features 20 | 21 | - Search by title, author, year or format 22 | - Maintain reading history, and return to page when re-opening file 23 | - Download files locally 24 | - Cache files in IndexedDB for fast loading 25 | - Drop files on TeaTime to render them 26 | - Dark mode and full screen mode 27 | - No cookies, no login 28 | - **...Completely distributed** 29 | 30 | ## Developing the Frontend 31 | 32 | TeaTime is Nuxt.js application. You can easily run it locally by cloning the repository and following these steps: 33 | 34 | 1. Install the dependencies: `npm install` 35 | 2. Run the server: `npm run dev` 36 | 3. Navigate to `http://localhost:3000` 37 | 38 | Check out the [Nuxt documentation](https://nuxt.com/docs/getting-started) for more information. 39 | 40 | ## Creating a Database 41 | 42 | > [!TIP] 43 | > The easiest way to create your own database is by forking the [JSON-based database repository](https://github.com/bjesus/teatime-json-database/) and adjusting the JSON files according to your needs. GitHub Actions will then generate an SQLite file and upload it to GitHub Pages. 44 | 45 | To manually generate an SQLite database that TeaTime can work with, follow the example on [the database repository](https://github.com/bjesus/teatime-database/). 46 | 47 | Each SQLite database contains a table with the below schema. Note that column names can be adjusted in the `config.json` file. 48 | 49 | ```sql 50 | CREATE TABLE "books" ( 51 | "id" INTEGER, 52 | "title" TEXT, 53 | "author" TEXT, 54 | "year" INTEGER, 55 | "lang" TEXT, 56 | "size" INTEGER, 57 | "ext" TEXT, 58 | "ipfs_cid" TEXT, 59 | PRIMARY KEY("id" AUTOINCREMENT) 60 | ); 61 | ``` 62 | 63 | The `dbConfig` section of `config.json` is identical to the output of the [sql.js-httpvfs create_db.sh](https://github.com/phiresky/sql.js-httpvfs/blob/master/create_db.sh) script. 64 | 65 | If the SQLite file is too big, you can [split it](https://github.com/phiresky/sql.js-httpvfs?tab=readme-ov-file#usage). Note the information about optimizing your database. You will also want to [use FTS](https://github.com/bjesus/teatime-database/blob/main/create_indexes.sql). Then, publish your repository to GitHub Pages and assign the `teatime-database` topic to your repository. 66 | 67 | ## Contributing 68 | 69 | Even if you cannot code, a great way to contribute is to simply fork this repository, as well as your favorite database repositories. If you fork the repository, it could be better to do it manually (`git clone` && `git remote add your-origin ...` && `git push your-origin main`) so that the repositories won't be directly linked. 70 | 71 | It's also a good practice to star the database repositories you find useful, as this determines their order in the TeaTime user interface, making it easier for other users to find the best databases. 72 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | title: "TeaTime", 3 | icon: "🫖", 4 | }); 5 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | 34 | 81 | 82 | 117 | -------------------------------------------------------------------------------- /components/BookCard.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 101 | 102 | 123 | -------------------------------------------------------------------------------- /components/BookView.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 50 | 51 | 110 | -------------------------------------------------------------------------------- /components/BooksList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 56 | 57 | 73 | -------------------------------------------------------------------------------- /components/Error.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | 43 | 55 | -------------------------------------------------------------------------------- /components/Header.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 139 | 178 | -------------------------------------------------------------------------------- /middleware/remotes.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from "@vueuse/core"; 2 | export default defineNuxtRouteMiddleware(async (to, from) => { 3 | const availableRemotes = useLocalStorage("availableRemotes", []); 4 | const enabledRemotes = useLocalStorage("enabledRemotes", []); 5 | 6 | if (!availableRemotes.value.length) { 7 | // Fetch remotes 8 | const response = await fetch( 9 | "https://api.github.com/search/repositories?q=topic:teatime-database&sort=stars&order=desc", 10 | // "/repos.json", // localdev 11 | ); 12 | let { items } = await response.json(); 13 | // if (!items.length) { 14 | // items = [ 15 | // { 16 | // full_name: "yourmargin/libgen-db", 17 | // description: 18 | // "Library Genesis Non-Fiction, metadata snapshot from archive.org", 19 | // stargazers_count: 10, 20 | // }, 21 | // { 22 | // full_name: "bjesus/teatime-datase", 23 | // description: "Public domain library", 24 | // stargazers_count: 5, 25 | // }, 26 | // { 27 | // full_name: "bjesus/teatime-json-datase", 28 | // description: "An example database with The Communist Manifesto", 29 | // stargazers_count: 1, 30 | // }, 31 | // ]; 32 | // } 33 | availableRemotes.value = await Promise.all( 34 | items.map(async (r) => { 35 | const [owner, repo] = r.full_name.split("/"); 36 | const response = await fetch( 37 | `https://${owner}.github.io/${repo}/config.json`, 38 | ); 39 | const config = await response.json(); 40 | 41 | return { 42 | full_name: r.full_name, 43 | description: r.description, 44 | stargazers_count: r.stargazers_count, 45 | config, 46 | }; 47 | }), 48 | ); 49 | // Enable all 50 | enabledRemotes.value = items.map((r) => r.full_name); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: "2024-04-03", 4 | devtools: { enabled: process.env.ENV === "production" ? false : true }, 5 | modules: ["nuxt-lucide-icons"], 6 | ssr: false, 7 | build: { 8 | transpile: ["vue-book-reader", ({ isDev }) => !isDev && "sql.js-httpvfs"], 9 | }, 10 | vite: { 11 | build: { 12 | target: "es2022", 13 | }, 14 | }, 15 | app: { 16 | head: { 17 | charset: "utf-8", 18 | viewport: "width=device-width, initial-scale=1", 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@vueuse/core": "^11.1.0", 14 | "liquidjs": "^10.19.0", 15 | "lucide-vue-next": "^0.439.0", 16 | "nuxt": "^3.13.0", 17 | "nuxt-lucide-icons": "^1.0.5", 18 | "pretty-bytes": "^6.1.1", 19 | "sanitize-filename": "^1.6.3", 20 | "sql.js-httpvfs": "^0.8.12", 21 | "vue": "latest", 22 | "vue-book-reader": "^1.0.7", 23 | "vue-router": "latest" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/history.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 230 | -------------------------------------------------------------------------------- /pages/instances.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 80 | 81 | 108 | -------------------------------------------------------------------------------- /pages/open/[id].vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 165 | -------------------------------------------------------------------------------- /pages/open/dropped.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /pages/search.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 117 | 118 | 141 | -------------------------------------------------------------------------------- /pages/settings.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 120 | 121 | 142 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | const dbName = "TeaTimeStorage"; 2 | const dbVersion = 1; 3 | const storeName = "files"; 4 | 5 | function openDB(): Promise { 6 | return new Promise((resolve, reject) => { 7 | const request = indexedDB.open(dbName, dbVersion); 8 | 9 | request.onerror = () => reject(request.error); 10 | request.onsuccess = () => resolve(request.result); 11 | 12 | request.onupgradeneeded = (event) => { 13 | const db = (event.target as IDBOpenDBRequest).result; 14 | db.createObjectStore(storeName, { keyPath: "cid" }); 15 | }; 16 | }); 17 | } 18 | 19 | // Save a File object and its hash 20 | export async function saveFile(file: File, cid: string): Promise { 21 | const db = await openDB(); 22 | const transaction = db.transaction(storeName, "readwrite"); 23 | const store = transaction.objectStore(storeName); 24 | 25 | return new Promise((resolve, reject) => { 26 | const request = store.put({ cid, file }); 27 | request.onerror = () => reject(request.error); 28 | request.onsuccess = () => resolve(); 29 | }); 30 | } 31 | 32 | // Lookup a File object by its hash 33 | export async function getFile(cid: string): Promise { 34 | const db = await openDB(); 35 | const transaction = db.transaction(storeName, "readonly"); 36 | const store = transaction.objectStore(storeName); 37 | 38 | return new Promise((resolve, reject) => { 39 | const request = store.get(cid); 40 | request.onerror = () => reject(request.error); 41 | request.onsuccess = () => resolve(request.result?.file); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /utils/sanitize.ts: -------------------------------------------------------------------------------- 1 | import sanitize from "sanitize-filename"; 2 | 3 | export default function (s: string) { 4 | return sanitize(s); 5 | } 6 | --------------------------------------------------------------------------------