├── .nvmrc ├── .dockerignore ├── public ├── robots.txt ├── og-image.png ├── sitemap.xml └── logo.svg ├── containers ├── index.js ├── submit-new-rom-form.js └── devices-list.js ├── db ├── index.js ├── db.sqlite3 ├── status_enum.js ├── db.js └── options.js ├── jsconfig.json ├── lib ├── search-utils.js ├── analytical-utils.js ├── mods-to-style.js ├── date-utils.js ├── filter-devices.js └── sdk.js ├── components ├── input.js ├── index.js ├── note.js ├── link.js ├── button.js ├── box.js ├── footer.js ├── header.js ├── head.js └── icons.js ├── lefthook.yml ├── postcss.config.cjs ├── pages ├── _app.js ├── submit-rom.js ├── devices.js └── index.js ├── scripts ├── generate-missing-releases.js ├── sync-search-index.js ├── sync-aospa.js ├── sync-lineage-os.js ├── sync-legionos.js ├── sync-potatorom.js ├── sync-aospextended.js ├── sync-pixys-os.js ├── sync-pixel-experience.js ├── sync-dotos.js ├── fill-release-dates.js ├── sync-coltos.js ├── sync.js ├── sync-havocos.js ├── import-from-devices.js ├── sync-crdroid.js ├── sync-arrowos.js └── sync-manual-devices.js ├── knexfile.js ├── migrations ├── 20220723112117_add-search-index.js └── 20220718185631_init.js ├── .gitignore ├── .github └── workflows │ ├── fly-deploy.yml │ └── cron.yml ├── fly.toml ├── seeds └── options.js ├── tailwind.config.js ├── Dockerfile ├── LICENSE ├── package.json ├── .husky ├── pre-commit └── prepare-commit-msg ├── README.md ├── server.mjs ├── styles └── styles.css └── biome.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.17.0 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /containers/index.js: -------------------------------------------------------------------------------- 1 | export * from './devices-list' 2 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | export * from './db' 2 | export * from './status_enum' 3 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /db/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barelyhuman/custom-rom-index/HEAD/db/db.sqlite3 -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barelyhuman/custom-rom-index/HEAD/public/og-image.png -------------------------------------------------------------------------------- /db/status_enum.js: -------------------------------------------------------------------------------- 1 | export const STATUS_ENUM = { 2 | active: 'active', 3 | discontinued: 'discontinued', 4 | unknown: 'unknown', 5 | } 6 | -------------------------------------------------------------------------------- /lib/search-utils.js: -------------------------------------------------------------------------------- 1 | export function normaliseSearchableString(str) { 2 | return str 3 | .trim() 4 | .toLowerCase() 5 | .replace(/[-_.()[\] ]/g, '') 6 | } 7 | -------------------------------------------------------------------------------- /components/input.js: -------------------------------------------------------------------------------- 1 | import Box from './box' 2 | 3 | export function Input({ label, ...props }) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | export * from './input' 2 | export * from './button' 3 | export * from './header' 4 | export * from './head' 5 | export * from './footer' 6 | export * from './icons' 7 | export * from './note' 8 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | linter: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx biome check --write {staged_files} 8 | -------------------------------------------------------------------------------- /components/note.js: -------------------------------------------------------------------------------- 1 | export function Note({ children, ...props }) { 2 | return ( 3 | <> 4 |

5 | Note: 6 | {children} 7 |

8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /components/link.js: -------------------------------------------------------------------------------- 1 | import Box from './box.js' 2 | 3 | export function Link({ children, primary, ...props }) { 4 | return ( 5 | <> 6 | 7 | {children} 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /components/button.js: -------------------------------------------------------------------------------- 1 | import Box from './box' 2 | 3 | export function Button({ children, primary, ...props }) { 4 | return ( 5 | <> 6 | 7 | {children} 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import 'styles/styles.css' 2 | import { Head } from 'components' 3 | 4 | function MyApp({ Component, pageProps }) { 5 | return ( 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | ) 15 | } 16 | 17 | export default MyApp 18 | -------------------------------------------------------------------------------- /db/db.js: -------------------------------------------------------------------------------- 1 | import knex from 'knex' 2 | import kconfig from '../knexfile.js' 3 | 4 | /** 5 | * @type { import("knex").Knex } 6 | */ 7 | let connection 8 | 9 | const createConnection = () => { 10 | if (connection) return connection 11 | 12 | connection = knex({ 13 | ...kconfig[process.env.NODE_ENV || 'development'], 14 | useNullAsDefault: true, 15 | }) 16 | 17 | return connection 18 | } 19 | 20 | export const db = createConnection() 21 | -------------------------------------------------------------------------------- /components/box.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import { modsToStyle } from '../lib/mods-to-style' 3 | 4 | const Box = function ({ elm = 'div', children, ...props }) { 5 | const { style, sanitizedProps } = modsToStyle(props, '') // pass dimension as an empty string so it used the actual numbers 6 | 7 | return h( 8 | elm, 9 | { 10 | style, 11 | ...sanitizedProps, 12 | }, 13 | children 14 | ) 15 | } 16 | 17 | export default Box 18 | -------------------------------------------------------------------------------- /scripts/generate-missing-releases.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { writeFileSync } from 'fs' 4 | import path from 'path' 5 | import devicesJSON from '../db/devices.json' 6 | 7 | const missing = {} 8 | 9 | devicesJSON.devices.forEach(item => { 10 | if (missing[item.codename]) return 11 | 12 | missing[item.codename] = '' 13 | }) 14 | 15 | const filePath = path.join(__dirname, '../db/missing-releases.json') 16 | writeFileSync(filePath, JSON.stringify(missing, null, 2)) 17 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | // Update with your config settings. 3 | 4 | /** 5 | * @type { Object. } 6 | */ 7 | 8 | const dbPath = path.resolve(path.join(process.cwd(), 'db', 'db.sqlite3')) 9 | 10 | export default { 11 | development: { 12 | client: 'sqlite3', 13 | connection: { 14 | filename: dbPath, 15 | }, 16 | }, 17 | production: { 18 | client: 'sqlite3', 19 | connection: { 20 | filename: dbPath, 21 | }, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /migrations/20220723112117_add-search-index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = function (knex) { 6 | return knex.schema.raw( 7 | `CREATE VIRTUAL TABLE roms_search_index USING fts4(rom_mapping_id,keywords)` 8 | ) 9 | } 10 | 11 | /** 12 | * @param { import("knex").Knex } knex 13 | * @returns { Promise } 14 | */ 15 | export const down = function (knex) { 16 | return knex.schema.raw(`DROP TABLE IF EXISTS roms_search_index`) 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.github/workflows/fly-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "5 0 * * *" 9 | 10 | 11 | jobs: 12 | deploy: 13 | name: Deploy app 14 | runs-on: ubuntu-latest 15 | concurrency: deploy-group 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: superfly/flyctl-actions/setup-flyctl@master 19 | - run: flyctl deploy --remote-only --ha=false 20 | env: 21 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 22 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for custom-rom-index on 2025-01-02T16:17:50+05:30 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'custom-rom-index' 7 | primary_region = 'sin' 8 | 9 | [build] 10 | 11 | [http_service] 12 | internal_port = 3000 13 | force_https = true 14 | auto_stop_machines = 'stop' 15 | auto_start_machines = true 16 | min_machines_running = 0 17 | processes = ['app'] 18 | 19 | [[vm]] 20 | memory = '256mb' 21 | cpu_kind = 'shared' 22 | cpus = 1 23 | -------------------------------------------------------------------------------- /seeds/options.js: -------------------------------------------------------------------------------- 1 | const { options } = require('../db/options') 2 | 3 | /** 4 | * @param { import("knex").Knex } knex 5 | * @returns { Promise } 6 | */ 7 | exports.seed = async function (knex) { 8 | // Deletes ALL existing entries 9 | await knex('options').del() 10 | const rows = [] 11 | Object.entries(options).forEach(([identifier, optionDef]) => { 12 | Object.entries(optionDef).forEach(([_, optValue]) => { 13 | rows.push({ 14 | identifier, 15 | value: optValue.value, 16 | sequence: optValue.sequence, 17 | label: optValue.label, 18 | }) 19 | }) 20 | }) 21 | 22 | await knex('options').insert(rows) 23 | } 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx}', 5 | './components/**/*.{js,ts,jsx,tsx}', 6 | './containers/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | darkMode: 'media', // or 'media' or 'class' 9 | theme: { 10 | extend: { 11 | colors: { 12 | base: 'var(--base)', 13 | surface: 'var(--surface)', 14 | overlay: 'var(--overlay)', 15 | dimmer: 'var(--dimmer)', 16 | dim: 'var(--dim)', 17 | text: 'var(--text)', 18 | success: 'var(--success)', 19 | error: 'var(--error)', 20 | warn: 'var(--warn)', 21 | }, 22 | }, 23 | }, 24 | variants: { 25 | extend: {}, 26 | }, 27 | plugins: [], 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS base 2 | RUN apk update && apk upgrade && apk add --no-cache sqlite 3 | 4 | FROM base AS deps 5 | WORKDIR /app 6 | COPY package.json . 7 | COPY pnpm-lock.yaml . 8 | RUN corepack enable && \ 9 | pnpm i --frozen-lockfile 10 | 11 | FROM base AS builder 12 | WORKDIR /app 13 | COPY --from=deps /app/node_modules ./node_modules 14 | COPY . . 15 | RUN corepack enable && \ 16 | pnpm build 17 | 18 | FROM base AS dist 19 | WORKDIR /app 20 | COPY --from=builder /app/out ./out 21 | COPY --from=deps /app/node_modules ./node_modules 22 | COPY server.mjs . 23 | COPY package.json . 24 | COPY ./public ./public 25 | COPY ./db ./db 26 | COPY ./knexfile.js . 27 | RUN corepack enable 28 | 29 | EXPOSE 3000 30 | 31 | CMD ["pnpm","start"] -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | contents: write 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: pnpm/action-setup@v4 20 | with: 21 | version: 9.9.0 22 | - run: pnpm i --frozen-lockfile 23 | - run: export GH_TOKEN=${{ secrets.GITHUB_TOKEN }}; npm run sync; node scripts/fill-release-dates.js; 24 | - name: Commit changes 25 | uses: stefanzweifel/git-auto-commit-action@v5 26 | with: 27 | commit_message: ":robot: Sync Services!" 28 | branch: ${{ github.head_ref }} 29 | -------------------------------------------------------------------------------- /db/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | STATUS: { 3 | unknown: { 4 | value: 0, 5 | label: 'Unknown', 6 | sequence: 0, 7 | }, 8 | active: { 9 | value: 1, 10 | label: 'Active', 11 | sequence: 1, 12 | }, 13 | discontinued: { 14 | value: 2, 15 | label: 'Discontinued', 16 | sequence: 2, 17 | }, 18 | }, 19 | } 20 | 21 | const findInOptions = (identifier, value) => { 22 | let opt 23 | Object.entries(options).forEach(([k, v]) => { 24 | if (k !== identifier) return 25 | Object.entries(v).forEach(([k1, v1]) => { 26 | if (v1.value !== value) return 27 | opt = v[k1] 28 | }) 29 | }) 30 | 31 | if (!opt) return null 32 | 33 | return opt 34 | } 35 | 36 | export { findInOptions, options } 37 | -------------------------------------------------------------------------------- /containers/submit-new-rom-form.js: -------------------------------------------------------------------------------- 1 | import { Button, Input } from 'components' 2 | 3 | export function SubmitNewRomForm() { 4 | return ( 5 | <> 6 |
7 |
8 |
9 |

10 | Submit a new rom ? 11 |

12 | 13 | 14 | {/* TODO: Replace with spacer */} 15 |
16 | 17 |

Thank you for the contribution!

18 |
19 |
20 |
21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/footer.js: -------------------------------------------------------------------------------- 1 | import { GithubIcon, TwitterIcon } from 'components' 2 | 3 | export function Footer({ ...props }) { 4 | return ( 5 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | https://cri.barelyhuman.dev/ 12 | 2021-08-22T08:01:37+00:00 13 | 1.00 14 | 15 | 16 | https://cri.barelyhuman.dev/devices 17 | 2021-08-22T08:01:37+00:00 18 | 0.80 19 | 20 | 21 | https://cri.barelyhuman.dev/submit-rom 22 | 2021-08-22T08:01:37+00:00 23 | 0.80 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /scripts/sync-search-index.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { conch } from '@barelyreaper/conch' 3 | import { db } from '../db/db.js' 4 | 5 | async function main() { 6 | await db('roms_search_index').del() 7 | const romData = await db('roms_devices_mapping') 8 | .leftJoin('roms', 'roms_devices_mapping.rom_id', 'roms.id') 9 | .leftJoin('devices', 'roms_devices_mapping.device_id', 'devices.id') 10 | .select( 11 | 'roms_devices_mapping.*', 12 | 'roms.name as rom_name', 13 | 'devices.basename as device_name', 14 | 'devices.codename as device_codename' 15 | ) 16 | 17 | await conch( 18 | romData, 19 | async row => { 20 | const execChain = await db('roms_search_index').insert({ 21 | rom_mapping_id: row.id, 22 | keywords: [row.rom_name, row.device_name, row.device_codename].join( 23 | ', ' 24 | ), 25 | }) 26 | }, 27 | { limit: 20 } 28 | ) 29 | } 30 | 31 | export const syncSearchIndex = main 32 | 33 | if (fileURLToPath(import.meta.url) === process.argv[1]) 34 | main().then(() => process.exit(0)) 35 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present Reaper (https://reaper.is) 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. -------------------------------------------------------------------------------- /components/header.js: -------------------------------------------------------------------------------- 1 | export function Header() { 2 | return ( 3 | <> 4 |
5 |
6 |
7 |
8 | 9 | 35 |
36 |
37 |
38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node server.mjs", 6 | "build": "node build.mjs", 7 | "fix": "biome check --write", 8 | "sync": "node scripts/sync.js" 9 | }, 10 | "dependencies": { 11 | "@barelyreaper/conch": "^1.2.0", 12 | "@fastify/static": "^8.0.3", 13 | "fastify": "^5.3.2", 14 | "got": "^11.8.6", 15 | "kleur": "^4.1.5", 16 | "knex": "^2.5.1", 17 | "logcons": "^0.0.4", 18 | "luxon": "^3.5.0", 19 | "preact": "^10.25.3", 20 | "sqlite3": "^5.1.7", 21 | "tiny-glob": "^0.2.9", 22 | "yaml": "^1.10.2" 23 | }, 24 | "devDependencies": { 25 | "@babel/generator": "^7.26.3", 26 | "@babel/parser": "^7.26.3", 27 | "@babel/traverse": "^7.26.4", 28 | "@barelyhuman/prettier-config": "^1.1.0", 29 | "@biomejs/biome": "^1.9.4", 30 | "@evilmartians/lefthook": "^0.8.0", 31 | "autoprefixer": "^10.4.20", 32 | "esbuild": "^0.24.0", 33 | "esbuild-plugin-node-externals": "^1.0.1", 34 | "esbuild-plugin-postcss": "^0.2.1", 35 | "esbuild-postcss": "^0.0.4", 36 | "postcss": "^8.4.49", 37 | "preact-render-to-string": "^6.5.12", 38 | "shx": "^0.3.4", 39 | "tailwindcss": "^3.4.17" 40 | }, 41 | "packageManager": "pnpm@9.9.0" 42 | } 43 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK" = "0" ]; then 4 | exit 0 5 | fi 6 | 7 | if [ -t 1 ] ; then 8 | exec < /dev/tty ; # <- enables interactive shell 9 | fi 10 | 11 | dir="$(git rev-parse --show-toplevel)" 12 | osArch=$(echo "$(uname)" | tr '[:upper:]' '[:lower:]') 13 | cpuArch=$(echo "$(uname -m)" | sed 's/aarch64/arm64/') 14 | 15 | call_lefthook() 16 | { 17 | if lefthook -h >/dev/null 2>&1 18 | then 19 | eval lefthook $@ 20 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" 21 | then 22 | eval "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook $@" 23 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" 24 | then 25 | eval "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook $@" 26 | elif bundle exec lefthook -h >/dev/null 2>&1 27 | then 28 | bundle exec lefthook $@ 29 | elif yarn lefthook -h >/dev/null 2>&1 30 | then 31 | yarn lefthook $@ 32 | elif npx @evilmartians/lefthook -h >/dev/null 2>&1 33 | then 34 | npx @evilmartians/lefthook $@ 35 | else 36 | echo "Can't find lefthook in PATH" 37 | fi 38 | } 39 | 40 | 41 | 42 | call_lefthook "run pre-commit $@" 43 | -------------------------------------------------------------------------------- /scripts/sync-aospa.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fileURLToPath } from 'node:url' 4 | import got from 'got' 5 | import kluer from 'kleur' 6 | import { logcons } from 'logcons' 7 | import { STATUS_ENUM } from '../db/status_enum.js' 8 | import { upsertDevice } from '../lib/sdk' 9 | 10 | const success = kluer.green().bold 11 | 12 | const URL = 'https://api.aospa.co/devices' 13 | 14 | async function main() { 15 | const response = await got(URL) 16 | const deviceList = JSON.parse(response.body) 17 | 18 | const promises = deviceList.devices.map(async deviceItem => { 19 | await upsertDevice({ 20 | deviceName: deviceItem.manufacturer + deviceItem.description, 21 | codename: deviceItem.name, 22 | rom: { 23 | name: 'Paranoid Android', 24 | status: STATUS_ENUM.unknown, 25 | links: [`https://aospa.co/downloads/${deviceItem.name}`], 26 | }, 27 | }) 28 | }) 29 | 30 | await Promise.all(promises) 31 | 32 | console.log( 33 | success(`${logcons.tick()} Done, Syncing AOSPA - Paranoid Android...`) 34 | ) 35 | } 36 | 37 | export const syncParanoidAndroid = main 38 | 39 | if (fileURLToPath(import.meta.url) === process.argv[1]) 40 | main() 41 | .then(() => process.exit(0)) 42 | .catch(err => { 43 | console.error(err) 44 | process.exit(1) 45 | }) 46 | -------------------------------------------------------------------------------- /lib/analytical-utils.js: -------------------------------------------------------------------------------- 1 | import { db } from 'db' 2 | import { options } from 'db/options' 3 | 4 | export async function totalDevices() { 5 | const groupedByCodename = await db('devices').count('id').first() 6 | return groupedByCodename['count(`id`)'] 7 | } 8 | 9 | export async function totalRoms() { 10 | const allRoms = await db('roms_devices_mapping').count('id').first() 11 | return allRoms['count(`id`)'] 12 | } 13 | 14 | export async function totalActiveRoms() { 15 | const activeRoms = await db('roms_devices_mapping') 16 | .where({ 17 | status: options.STATUS.active.value, 18 | }) 19 | .count('id') 20 | .first() 21 | return activeRoms['count(`id`)'] 22 | } 23 | 24 | export function topDevicesInROMCount() { 25 | const groupedByCodename = devices.reduce((acc, deviceItem) => { 26 | ;(acc[deviceItem.codename] || (acc[deviceItem.codename] = [])).push( 27 | deviceItem 28 | ) 29 | return acc 30 | }, {}) 31 | 32 | const sortByHighestFirst = Object.keys(groupedByCodename).sort( 33 | (x, y) => groupedByCodename[y].length - groupedByCodename[x].length 34 | ) 35 | 36 | return sortByHighestFirst.slice(0, 10).map(item => { 37 | return { 38 | name: groupedByCodename[item][0].deviceName, 39 | count: groupedByCodename[item].length, 40 | releasedOn: groupedByCodename[item][0].releasedOn, 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /scripts/sync-lineage-os.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { fileURLToPath } from 'node:url' 3 | import got from 'got' 4 | import kluer from 'kleur' 5 | import { logcons } from 'logcons' 6 | import { STATUS_ENUM } from '../db/status_enum.js' 7 | import { upsertDevice } from '../lib/sdk.js' 8 | 9 | const success = kluer.green().bold 10 | 11 | const URL = 12 | 'https://raw.githubusercontent.com/LineageOS/hudson/main/updater/devices.json' 13 | 14 | async function main() { 15 | const response = await got(URL) 16 | 17 | const promises = (JSON.parse(response.body) || []).map(async deviceItem => { 18 | const codename = deviceItem.model 19 | const deviceName = `${deviceItem.oem} ${deviceItem.name}` 20 | 21 | await upsertDevice({ 22 | deviceName, 23 | codename, 24 | rom: { 25 | status: STATUS_ENUM.unknown, 26 | androidVersion: ['N/A'], 27 | links: [`https://download.lineageos.org/${codename}`], 28 | name: 'LineageOS', 29 | }, 30 | }) 31 | }) 32 | 33 | await Promise.all(promises) 34 | 35 | console.log(success(`${logcons.tick()} Done, Syncing Lineage OS`)) 36 | } 37 | 38 | export const syncLineageOS = main 39 | 40 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 41 | main() 42 | .then(() => process.exit(0)) 43 | .catch(err => { 44 | console.error(err) 45 | process.exit(1) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK" = "0" ]; then 4 | exit 0 5 | fi 6 | 7 | if [ -t 1 ] ; then 8 | exec < /dev/tty ; # <- enables interactive shell 9 | fi 10 | 11 | dir="$(git rev-parse --show-toplevel)" 12 | osArch=$(echo "$(uname)" | tr '[:upper:]' '[:lower:]') 13 | cpuArch=$(echo "$(uname -m)" | sed 's/aarch64/arm64/') 14 | 15 | call_lefthook() 16 | { 17 | if lefthook -h >/dev/null 2>&1 18 | then 19 | eval lefthook $@ 20 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" 21 | then 22 | eval "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook $@" 23 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" 24 | then 25 | eval "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook $@" 26 | elif bundle exec lefthook -h >/dev/null 2>&1 27 | then 28 | bundle exec lefthook $@ 29 | elif yarn lefthook -h >/dev/null 2>&1 30 | then 31 | yarn lefthook $@ 32 | elif npx @evilmartians/lefthook -h >/dev/null 2>&1 33 | then 34 | npx @evilmartians/lefthook $@ 35 | else 36 | echo "Can't find lefthook in PATH" 37 | fi 38 | } 39 | 40 | # lefthook_version: fe323e3692fa170cc6cd9236e2fb5d81 41 | 42 | call_lefthook "install" 43 | 44 | call_lefthook "run prepare-commit-msg $@" 45 | -------------------------------------------------------------------------------- /scripts/sync-legionos.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import got from 'got' 3 | import kluer from 'kleur' 4 | import { logcons } from 'logcons' 5 | import { STATUS_ENUM } from '../db/status_enum.js' 6 | import { upsertDevice } from '../lib/sdk.js' 7 | 8 | import { fileURLToPath } from 'node:url' 9 | 10 | const success = kluer.green().bold 11 | 12 | const URL = 13 | 'https://raw.githubusercontent.com/legionos-devices/OTA/11/devices.json' 14 | 15 | async function main() { 16 | const response = await got(URL) 17 | 18 | const promises = (JSON.parse(response.body) || []).map(async deviceItem => { 19 | const codename = deviceItem.codename 20 | const deviceName = `${deviceItem.brand} ${deviceItem.name}` 21 | 22 | await upsertDevice({ 23 | deviceName, 24 | codename, 25 | rom: { 26 | status: deviceItem.active ? STATUS_ENUM.active : STATUS_ENUM.unknown, 27 | androidVersion: ['11'], 28 | links: [deviceItem.xda_thread], 29 | name: 'LegionOS', 30 | }, 31 | }) 32 | }) 33 | 34 | await Promise.all(promises) 35 | 36 | console.log(success(`${logcons.tick()} Done, Syncing Legion OS`)) 37 | } 38 | 39 | export const syncLegionOS = main 40 | 41 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 42 | main() 43 | .then(() => process.exit(0)) 44 | .catch(err => { 45 | console.error(err) 46 | process.exit(1) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /scripts/sync-potatorom.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { fileURLToPath } from 'node:url' 3 | import got from 'got' 4 | import kluer from 'kleur' 5 | import { logcons } from 'logcons' 6 | import { STATUS_ENUM } from '../db/status_enum.js' 7 | import { upsertDevice } from '../lib/sdk.js' 8 | 9 | const success = kluer.green().bold 10 | 11 | const URL = 12 | 'https://raw.githubusercontent.com/PotatoProject/vendor_potato/92e407b1696ac962dbd3ef2acaff8dc560268010/devices.json' 13 | 14 | async function main() { 15 | const response = await got(URL) 16 | const deviceList = JSON.parse(response.body) || {} 17 | const promises = Object.keys(deviceList).map(async deviceCodeName => { 18 | const codename = deviceCodeName 19 | const device = deviceList[deviceCodeName] 20 | 21 | await upsertDevice({ 22 | deviceName: '', 23 | codename, 24 | rom: { 25 | status: STATUS_ENUM.active, 26 | androidVersion: ['N/A'], 27 | links: [`${device.repo}/releases/latest`], 28 | name: 'Potato Open Source Project | POSP', 29 | }, 30 | }) 31 | }) 32 | 33 | await Promise.all(promises) 34 | 35 | console.log(success(`${logcons.tick()} Done, Syncing Potato Project`)) 36 | } 37 | 38 | export const syncPotatoProject = main 39 | 40 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 41 | main() 42 | .then(() => process.exit(0)) 43 | .catch(err => { 44 | console.error(err) 45 | process.exit(1) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /pages/submit-rom.js: -------------------------------------------------------------------------------- 1 | import { Button, Header } from 'components' 2 | 3 | function SubmitRom({ ...props }) { 4 | return ( 5 | <> 6 |
7 |
8 |
9 |

Want to add a new rom to the list?

10 |

11 | If you wish to add a rom to the list for whatever reason, follow the 12 | below steps 13 |

14 |
    15 |
  1. 16 | Visit 17 | 21 | {' '} 22 | https://github.com/barelyhuman/custom-rom-index 23 | {' '} 24 | or click on Submit ROM below 25 |
  2. 26 |
  3. Fork the repository
  4. 27 |
  5. 28 | Modify scripts/sync-manual-devices.js to add your rom 29 | with the needed data 30 |
  6. 31 |
  7. Raise a new Pull Request
  8. 32 |
  9. Done!
  10. 33 |
34 |

35 | Your ROM will be added as soon as we're done 36 | reviewing the addition 37 |

38 | 45 |
46 |
47 | 48 | ) 49 | } 50 | 51 | export default SubmitRom 52 | -------------------------------------------------------------------------------- /components/head.js: -------------------------------------------------------------------------------- 1 | export function Head({ ...props }) { 2 | return ( 3 | 4 | Custom Rom Index | CRI 5 | 6 | 7 | 8 | 9 | {/* Primary Meta Tags */} 10 | 11 | 15 | {/* Open Graph / Facebook */} 16 | 17 | 18 | 19 | 23 | 27 | {/* Twitter */} 28 | 29 | 30 | 31 | 35 | 39 | {props.children} 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /scripts/sync-aospextended.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fileURLToPath } from 'node:url' 4 | import got from 'got' 5 | import kluer from 'kleur' 6 | import { logcons } from 'logcons' 7 | import { STATUS_ENUM } from '../db/status_enum.js' 8 | import { upsertDevice } from '../lib/sdk.js' 9 | const success = kluer.green().bold 10 | 11 | const URL = 12 | 'https://raw.githubusercontent.com/AospExtended/official_devices/main/devices.json' 13 | 14 | async function main() { 15 | const response = await got(URL) 16 | 17 | const promises = (JSON.parse(response.body) || []).map(async deviceItem => { 18 | const codename = deviceItem.codename 19 | const deviceName = `${deviceItem.brand} ${deviceItem.name}` 20 | 21 | const _internalPromises = (deviceItem.supported_versions || []).map( 22 | async device => { 23 | if (device.version_code !== 'q' && device.version_code !== 'r') return 24 | 25 | let version 26 | if (device.version_code === 'q') version = 10 27 | 28 | if (device.version_code === 'r') version = 11 29 | 30 | await upsertDevice({ 31 | deviceName, 32 | codename, 33 | rom: { 34 | status: STATUS_ENUM.active, 35 | androidVersion: [version], 36 | links: [device.xda_thread], 37 | name: 'AospExtended', 38 | }, 39 | }) 40 | } 41 | ) 42 | 43 | await Promise.all(_internalPromises) 44 | }) 45 | 46 | await Promise.all(promises) 47 | 48 | console.log(success(`${logcons.tick()} Done, Syncing AOSPExtended`)) 49 | } 50 | 51 | export const syncAospExtended = main 52 | 53 | if (fileURLToPath(import.meta.url) === process.argv[1]) 54 | main() 55 | .then(() => process.exit(0)) 56 | .catch(err => { 57 | console.error(err) 58 | process.exit(1) 59 | }) 60 | -------------------------------------------------------------------------------- /scripts/sync-pixys-os.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { fileURLToPath } from 'node:url' 3 | import got from 'got' 4 | import kluer from 'kleur' 5 | import { logcons } from 'logcons' 6 | import { STATUS_ENUM } from '../db/status_enum.js' 7 | import { upsertDevice } from '../lib/sdk.js' 8 | 9 | const success = kluer.green().bold 10 | 11 | const URL = 12 | 'https://raw.githubusercontent.com/PixysOS/official_devices/master/devices.json' 13 | 14 | async function main() { 15 | const response = await got(URL) 16 | 17 | const promises = (JSON.parse(response.body) || []).map(async deviceItem => { 18 | const codename = deviceItem.codename 19 | const deviceName = `${deviceItem.brand} ${deviceItem.name}` 20 | 21 | const _internalPromises = (deviceItem.supported_bases || []).map( 22 | async versionDef => { 23 | const version = 24 | (versionDef.name === 'ten' && 10) || 25 | (versionDef.name === 'eleven' && 11) || 26 | (versionDef.name === 'twelve' && 12) || 27 | (versionDef.name === 'thirteen' && 13) || 28 | (versionDef.name === 'fourteen' && 14) 29 | await upsertDevice({ 30 | deviceName, 31 | codename, 32 | rom: { 33 | status: STATUS_ENUM.active, 34 | androidVersion: [version], 35 | links: [versionDef.xda_thread], 36 | name: 'Pixys OS', 37 | }, 38 | }) 39 | } 40 | ) 41 | 42 | await Promise.all(_internalPromises) 43 | }) 44 | 45 | await Promise.all(promises) 46 | 47 | console.log(success(`${logcons.tick()} Done, Syncing PixysOS`)) 48 | } 49 | 50 | export const syncPixysOS = main 51 | 52 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 53 | main() 54 | .then(() => process.exit(0)) 55 | .catch(err => { 56 | console.error(err) 57 | process.exit(1) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /scripts/sync-pixel-experience.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import got from 'got' 3 | import kluer from 'kleur' 4 | import { logcons } from 'logcons' 5 | import { STATUS_ENUM } from '../db/status_enum.js' 6 | import { upsertDevice } from '../lib/sdk.js' 7 | 8 | import { fileURLToPath } from 'node:url' 9 | 10 | const success = kluer.green().bold 11 | 12 | const URL = 13 | 'https://raw.githubusercontent.com/PixelExperience/official_devices/master/devices.json' 14 | 15 | const ignoreVersionKeys = '_plus' 16 | 17 | async function main() { 18 | const response = await got(URL) 19 | ;(JSON.parse(response.body) || []).forEach(deviceItem => { 20 | const codename = deviceItem.codename 21 | const deviceName = `${deviceItem.brand} ${deviceItem.name}` 22 | 23 | deviceItem.supported_versions 24 | .filter(x => !x.version_code.includes(ignoreVersionKeys)) 25 | .forEach(deviceVersionItem => { 26 | const versions = [] 27 | 28 | if (deviceVersionItem.version_code === 'eleven') versions.push(11) 29 | 30 | if (deviceVersionItem.version_code === 'ten') versions.push(10) 31 | 32 | upsertDevice({ 33 | deviceName, 34 | codename, 35 | rom: { 36 | status: deviceVersionItem.deprecated 37 | ? STATUS_ENUM.discontinued 38 | : STATUS_ENUM.active, 39 | androidVersion: versions, 40 | links: [`https://download.pixelexperience.org/${codename}`], 41 | name: 'Pixel Experience', 42 | }, 43 | }) 44 | }) 45 | }) 46 | 47 | console.log(success(`${logcons.tick()} Done, Syncing Pixel Experience`)) 48 | } 49 | 50 | export const syncPixelExperience = main 51 | 52 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 53 | main() 54 | .then(() => process.exit(0)) 55 | .catch(err => { 56 | console.error(err) 57 | process.exit(1) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /pages/devices.js: -------------------------------------------------------------------------------- 1 | import { Header } from 'components' 2 | import { DevicesListTable } from 'containers' 3 | 4 | import { getDevices } from '../lib/sdk.js' 5 | 6 | function Devices({ 7 | deviceList, 8 | searchTerm, 9 | sort, 10 | status, 11 | limit, 12 | currPage, 13 | maxPage, 14 | }) { 15 | return ( 16 | <> 17 |
18 |
19 | 25 |
26 | { 27 | 36 | } 37 | 38 | ) 39 | } 40 | 41 | export default Devices 42 | 43 | export async function getServerSideProps({ query }) { 44 | const defaultLimit = 15 45 | const order = { 46 | release: 'desc', 47 | } 48 | const limit = query.limit || defaultLimit 49 | 50 | switch (query.sort) { 51 | case 'releasedOn:asc': { 52 | order.release = 'asc' 53 | break 54 | } 55 | case 'releasedOn:desc': { 56 | order.release = 'desc' 57 | break 58 | } 59 | default: { 60 | order.release = 'desc' 61 | break 62 | } 63 | } 64 | 65 | const { deviceList, count } = await getDevices({ 66 | page: query.page || 0, 67 | limit, 68 | status: query.status || 'all', 69 | searchTerm: query.q || '', 70 | order, 71 | }) 72 | 73 | return { 74 | props: { 75 | deviceList, 76 | searchTerm: query.q || '', 77 | sort: query.sort || 'releasedOn:desc', 78 | status: query.status || 'all', 79 | limit, 80 | currPage: query.page || 0, 81 | maxPage: Math.floor(count / limit), 82 | }, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | CRI 3 |

4 | 5 |

6 | Custom Rom Index 7 |

8 | 9 |

10 | GitHub 11 | JavaScript Style Guide 12 | GitHub Workflow Status 13 |

14 | 15 | As an enthusiast, I’ve wasted hours together to find a working ROM 16 | for my devices and also, sometimes I feel like it’s better to 17 | spend on devices that already have a great collection of roms. 18 | Which is why this website was created so you don’t have to waste 19 | time like I did. 20 | 21 | ## Link 22 | 23 | You can access all the devices at [https://cri.barelyhuman.dev](https://cri.barelyhuman.dev). 24 | 25 | ## Contribute 26 | 27 | If you wish to add more devices/ROMs to the list you can follow the steps [here](https://cri.barelyhuman.dev/submit-rom). 28 | 29 | ## Data Credits 30 | 31 | While most of them are being added manually right now, we are using a lot of data from the open source repositories listed here, 32 | they make it easier to handle syncing of data 33 | 34 | - [PixelExperience](https://github.com/PixelExperience) 35 | - [DotOS](https://github.com/DotOS) 36 | - [LineageOS](https://github.com/LineageOS) 37 | - [Potato Project](https://github.com/PotatoProject) 38 | - [Dot OS](https://github.com/DotOS) 39 | - [Havoc OS](https://github.com/Havoc-OS) 40 | - [Colt OS](https://github.com/ColtOS-Devices) 41 | - [AospExtended](https://github.com/AospExtended) 42 | - [crDroid](https://github.com/crdroidandroid/) 43 | - [ArrowOS](https://github.com/ArrowOS) 44 | 45 | ## Appreciations 46 | 47 | Every ROM developer that's out there doing amazing work for the community! THANKS! 48 | -------------------------------------------------------------------------------- /scripts/sync-dotos.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import _got from 'got' 4 | 5 | import { fileURLToPath } from 'node:url' 6 | import { conch } from '@barelyreaper/conch' 7 | import kluer from 'kleur' 8 | import { logcons } from 'logcons' 9 | import { STATUS_ENUM } from '../db/status_enum.js' 10 | import { upsertDevice } from '../lib/sdk.js' 11 | 12 | const success = kluer.green().bold 13 | 14 | const URL = 15 | 'https://api.github.com/repos/DotOS/official_devices/contents/devices' 16 | 17 | async function main() { 18 | const { parse } = JSON 19 | const response = await got(URL) 20 | const devices = parse(response.body) 21 | 22 | await conch(devices, item => addDotOSToDevices(item), { 23 | limit: 1, 24 | }) 25 | 26 | console.log(success(`${logcons.tick()} Done, Syncing DotOS`)) 27 | } 28 | 29 | async function addDotOSToDevices(item) { 30 | const { parse } = JSON 31 | const deviceBlob = await got(item.url) 32 | const fileContent = Buffer.from( 33 | parse(deviceBlob.body).content, 34 | 'base64' 35 | ).toString('utf8') 36 | 37 | if (!fileContent) return true 38 | 39 | let parsedFileData 40 | try { 41 | parsedFileData = parse(fileContent) 42 | } catch (_) { 43 | parsedFileData = false 44 | } 45 | 46 | if (!parsedFileData) return true 47 | 48 | const deviceData = parsedFileData || false 49 | 50 | if (!deviceData) return true 51 | 52 | const codename = deviceData.codename 53 | const deviceName = `${deviceData.brandName} ${deviceData.deviceName}` 54 | 55 | await upsertDevice({ 56 | deviceName, 57 | codename, 58 | rom: { 59 | status: STATUS_ENUM.active, 60 | androidVersion: ['11'], 61 | links: [deviceData.links.xda], 62 | name: 'Dot OS', 63 | }, 64 | }) 65 | } 66 | 67 | function got(url) { 68 | return _got(url, { 69 | headers: { 70 | Authorization: `token ${process.env.GH_TOKEN}`, 71 | }, 72 | }) 73 | } 74 | 75 | export const syncDotOS = main 76 | 77 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 78 | main() 79 | .then(() => process.exit(0)) 80 | .catch(err => { 81 | console.error(err) 82 | process.exit(1) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /scripts/fill-release-dates.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import got from 'got' 4 | import kluer from 'kleur' 5 | import YAML from 'yaml' 6 | const info = kluer.cyan().bold 7 | import { fileURLToPath } from 'url' 8 | import { conch } from '@barelyreaper/conch' 9 | import { db } from '../db/db.js' 10 | import { getReleasedOn } from '../lib/date-utils.js' 11 | 12 | const URL_TEMPLATE = deviceCodeName => 13 | `https://raw.githubusercontent.com/PixelExperience/wiki/master/_data/devices/${deviceCodeName}.yml` 14 | 15 | const URL_TEMPLATE_TWO = deviceCodeName => 16 | `https://raw.githubusercontent.com/LineageOS/lineage_wiki/master/_data/devices/${deviceCodeName}.yml` 17 | 18 | function parseYAML(text) { 19 | try { 20 | return text && YAML.parse(text) 21 | } catch (err) { 22 | return null 23 | } 24 | } 25 | 26 | async function deviceInfoAPI(codename) { 27 | try { 28 | const response = await got(URL_TEMPLATE(codename)).catch(err => err) 29 | const responseTwo = await got(URL_TEMPLATE_TWO(codename)).catch(err => err) 30 | const text = response.body 31 | const textTwo = responseTwo.body 32 | const parsed = parseYAML(text) 33 | const parsedTwo = parseYAML(textTwo) 34 | return { 35 | dataSourceOne: parsed, 36 | dataSourceTwo: parsedTwo, 37 | } 38 | } catch (err) { 39 | console.log(`Failed: ${codename}`) 40 | } 41 | } 42 | 43 | async function main() { 44 | const devices = await db('devices').where({ released_on: null }) 45 | const mapper = async device => { 46 | const { dataSourceOne, dataSourceTwo } = await deviceInfoAPI( 47 | device.codename 48 | ) 49 | const releaseDate = 50 | (dataSourceOne && dataSourceOne.release) || 51 | (dataSourceTwo && dataSourceTwo.release) 52 | 53 | if (!releaseDate) return 54 | 55 | const dateData = getReleasedOn(releaseDate) 56 | 57 | if (dateData) { 58 | const _date = dateData 59 | await db('devices') 60 | .update({ 61 | released_on: _date, 62 | }) 63 | .where({ 64 | id: device.id, 65 | }) 66 | } 67 | } 68 | 69 | await conch(devices, mapper, { limit: 20 }) 70 | } 71 | 72 | if (fileURLToPath(import.meta.url) === process.argv[1]) 73 | main().then(() => process.exit(0)) 74 | -------------------------------------------------------------------------------- /scripts/sync-coltos.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { fileURLToPath } from 'node:url' 3 | import { conch } from '@barelyreaper/conch' 4 | import _got from 'got' 5 | import kluer from 'kleur' 6 | import { logcons } from 'logcons' 7 | import { STATUS_ENUM } from '../db/status_enum.js' 8 | import { upsertDevice } from '../lib/sdk.js' 9 | 10 | const info = kluer.cyan().bold 11 | const success = kluer.green().bold 12 | 13 | const URL = 14 | 'https://api.github.com/repos/ColtOS-Devices/official_devices/contents/builds' 15 | 16 | async function main() { 17 | await syncColtOSDevices() 18 | console.log(success(`${logcons.tick()} Done, Syncing ColtOS`)) 19 | } 20 | 21 | async function syncColtOSDevices() { 22 | const { parse } = JSON 23 | const response = await got(URL) 24 | const devices = parse(response.body) 25 | 26 | await conch(devices, item => addColtOSToDevices(item), { 27 | limit: 1, 28 | }) 29 | console.log(info(`${logcons.info()} Synced: Colt OS`)) 30 | } 31 | 32 | async function addColtOSToDevices(item) { 33 | const { parse } = JSON 34 | const deviceBlob = await got(item.url) 35 | const fileContent = Buffer.from( 36 | parse(deviceBlob.body).content, 37 | 'base64' 38 | ).toString('utf8') 39 | 40 | if (!fileContent) return true 41 | 42 | let parsedFileData 43 | try { 44 | parsedFileData = parse(fileContent) 45 | } catch (_) { 46 | parsedFileData = false 47 | } 48 | 49 | if (!parsedFileData) return true 50 | 51 | const deviceData = parsedFileData || false 52 | 53 | if (!deviceData) return true 54 | 55 | const codename = item.name.replace('.json', '') 56 | 57 | await upsertDevice({ 58 | deviceName: deviceData.devicename, 59 | codename, 60 | rom: { 61 | status: STATUS_ENUM.active, 62 | androidVersion: [parseInt(deviceData.version, 10)], 63 | links: [deviceData.url], 64 | name: 'ColtOS', 65 | }, 66 | }) 67 | } 68 | 69 | function got(url) { 70 | return _got(url, { 71 | headers: { 72 | Authorization: `token ${process.env.GH_TOKEN}`, 73 | }, 74 | }) 75 | } 76 | 77 | export const syncColtOS = main 78 | 79 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 80 | main() 81 | .then(() => process.exit(0)) 82 | .catch(err => { 83 | console.error(err) 84 | process.exit(1) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /lib/mods-to-style.js: -------------------------------------------------------------------------------- 1 | const mgnPattern = /(margin)[TBRLXY]*-/ 2 | const padPattern = /(padding)[TBRLXY]*-/ 3 | const XPattern = /X$/ 4 | const YPattern = /Y$/ 5 | 6 | const toDims = (value, unit) => { 7 | if (isNaN(value)) return value 8 | 9 | if (unit) return value + unit 10 | 11 | return +value 12 | } 13 | 14 | const normalizeModifer = prop => { 15 | let x = prop 16 | return ( 17 | (x.endsWith('T') && (x += 'op')) || 18 | (x.endsWith('L') && (x += 'eft')) || 19 | (x.endsWith('B') && (x += 'ottom')) || 20 | (x.endsWith('R') && (x += 'ight')) || 21 | prop 22 | ) 23 | } 24 | 25 | /** 26 | * @name modsToStyle 27 | * @description convert given modifiers object of the form 28 | * {margin-10:true} into a style object => {margin:10} with an appended dimensionUnit (eg: "px") 29 | * @param {Object} mods Object of spacing modifiers 30 | * @param {string} dimUnit Dimension to append to the style value `(default: "px") 31 | * @returns {Object} result 32 | */ 33 | export function modsToStyle(mods, dimUnit = 'px') { 34 | const style = {} 35 | 36 | // collection of props that do not have the above modifiers 37 | const sanitizedProps = { ...mods } 38 | 39 | for (const key of Object.keys(mods)) { 40 | if (!(mgnPattern.test(key) || padPattern.test(key))) continue 41 | 42 | // delete from the sanitize as it's a valid modifier 43 | delete sanitizedProps[key] 44 | 45 | const propSplits = key.split('-') 46 | 47 | // Change the accessor to it's style equivalent eg: marginR => marginRight 48 | const modifier = normalizeModifer(propSplits[0]) 49 | 50 | propSplits[1] = propSplits[1] || 0 51 | 52 | // If not one of the above then check for accessor as X or Y , eg: marginX or marginY 53 | if (XPattern.test(modifier)) { 54 | const xProp = modifier.replace(XPattern, '') 55 | style[xProp + 'Left'] = toDims(propSplits[1], dimUnit) 56 | style[xProp + 'Right'] = toDims(propSplits[1], dimUnit) 57 | } else if (YPattern.test(modifier)) { 58 | const yProp = modifier.replace(YPattern, '') 59 | style[yProp + 'Top'] = toDims(propSplits[1], dimUnit) 60 | style[yProp + 'Bottom'] = toDims(propSplits[1], dimUnit) 61 | } else { 62 | style[modifier] = toDims(propSplits[1], dimUnit) 63 | } 64 | } 65 | 66 | return { style, sanitizedProps } 67 | } 68 | -------------------------------------------------------------------------------- /server.mjs: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import fastifyStatic from '@fastify/static' 4 | import fastify from 'fastify' 5 | import { h } from 'preact' 6 | import { renderToString } from 'preact-render-to-string' 7 | 8 | const app = fastify({ logger: true }) 9 | 10 | const __dirname = dirname(fileURLToPath(import.meta.url)) 11 | 12 | app.register(fastifyStatic, { 13 | root: path.join(__dirname, 'out/client'), 14 | prefix: '/client', 15 | }) 16 | 17 | app.register(fastifyStatic, { 18 | decorateReply: false, 19 | root: path.join(__dirname, 'public'), 20 | prefix: '/', 21 | }) 22 | 23 | async function mapComponent(path, req, res) { 24 | const appFile = await import('./out/_app.js') 25 | const Component = await import(path) 26 | let pageProps = { 27 | props: {}, 28 | } 29 | if ('getServerSideProps' in Component) { 30 | Object.assign( 31 | pageProps, 32 | await Component.getServerSideProps({ 33 | request: req, 34 | response: res, 35 | query: req.query, 36 | }) 37 | ) 38 | } 39 | const mod = 'default' in appFile ? appFile.default : appFile 40 | const FinalComponent = () => 41 | h(mod, { 42 | Component: Component.default, 43 | pageProps: pageProps.props, 44 | }) 45 | const toString = renderToString(h(FinalComponent)) 46 | return toString 47 | .replace( 48 | /<\/head\>/, 49 | '' 50 | ) 51 | .replace( 52 | /<\/body\>/, 53 | ` 57 | 60 | ` 61 | ) 62 | } 63 | 64 | app.get('/', async (req, res) => { 65 | res.header('content-type', 'text/html') 66 | return mapComponent('./out/index.js', req, res) 67 | }) 68 | 69 | app.get('/devices', async (req, res) => { 70 | res.header('content-type', 'text/html') 71 | return mapComponent('./out/devices.js', req, res) 72 | }) 73 | 74 | app.get('/submit-rom', async (req, res) => { 75 | res.header('content-type', 'text/html') 76 | return mapComponent('./out/submit-rom.js', req, res) 77 | }) 78 | 79 | await app.listen({ host: '0.0.0.0', port: 3000 }) 80 | -------------------------------------------------------------------------------- /scripts/sync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import kluer from 'kleur' 3 | import { logcons } from 'logcons' 4 | import { syncAospExtended } from './sync-aospextended.js' 5 | import { syncArrowOS } from './sync-arrowos.js' 6 | import { syncColtOS } from './sync-coltos.js' 7 | import { syncCRAndroid } from './sync-crdroid.js' 8 | import { syncDotOS } from './sync-dotos.js' 9 | // import { syncParanoidAndroid } from "./sync-aospa".js; 10 | import { syncHavocOS } from './sync-havocos.js' 11 | import { syncLegionOS } from './sync-legionos.js' 12 | import { syncLineageOS } from './sync-lineage-os.js' 13 | import { syncManualDevices } from './sync-manual-devices.js' 14 | import { syncPixelExperience } from './sync-pixel-experience.js' 15 | import { syncPixysOS } from './sync-pixys-os.js' 16 | import { syncPotatoProject } from './sync-potatorom.js' 17 | import { syncSearchIndex } from './sync-search-index.js' 18 | 19 | const bullet = kluer.white().bold 20 | const success = kluer.green().bold 21 | 22 | async function main() { 23 | console.log(bullet('Syncing, Manual Devices...')) 24 | await syncManualDevices() 25 | console.log(bullet('Syncing, Pixel Experience...')) 26 | await syncPixelExperience() 27 | console.log(bullet('Syncing, Lineage OS...')) 28 | await syncLineageOS() 29 | console.log(bullet('Syncing, Pixys OS...')) 30 | await syncPixysOS() 31 | console.log(bullet('Syncing, Potato Project...')) 32 | await syncPotatoProject() 33 | console.log(bullet('Syncing, Dot OS...')) 34 | await syncDotOS() 35 | console.log(bullet('Syncing, ArrowOS...')) 36 | await syncArrowOS() 37 | console.log(bullet('Syncing, AOSPExtended...')) 38 | await syncAospExtended() 39 | 40 | // Disabled cause their API is down. 41 | // console.log(bullet('Syncing, AOSPA - Paranoid Android...')) 42 | // await syncParanoidAndroid() 43 | 44 | console.log(bullet('Syncing, LegionOS...')) 45 | await syncLegionOS() 46 | console.log(bullet('Syncing, CRDroid...')) 47 | await syncCRAndroid() 48 | console.log(bullet('Syncing, HavocOS...')) 49 | await syncHavocOS() 50 | console.log(bullet('Syncing, ColtOS...')) 51 | await syncColtOS() 52 | console.log(bullet('Syncinc,Search Index...')) 53 | await syncSearchIndex() 54 | console.log(success(`${logcons.tick()} Done Syncing everything`)) 55 | } 56 | 57 | main() 58 | .then(() => { 59 | process.exit(0) 60 | }) 61 | .catch(() => { 62 | process.exit(1) 63 | }) 64 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Box from '../components/box.js' 2 | import { Button, Footer, Header, Note } from '../components/index.js' 3 | import { Link } from '../components/link.js' 4 | import { totalActiveRoms, totalDevices } from '../lib/analytical-utils' 5 | 6 | function Home({ totalDevicesCount, totalActiveRomsCount }) { 7 | return ( 8 | <> 9 |
10 | 11 |
12 |
13 |

The easiest way to find a rom for your phone.

14 |

Custom Rom Index

15 |

16 | If you've wasted hours looking on XDA for android phone's that 17 | have a custom rom? Well, here's the solution to it all. 18 |

19 |
20 | With about

{totalDevicesCount}

{' '} 21 | devices and{' '} 22 |

{totalActiveRomsCount}

ROM's 23 | syncing daily the Custom Rom Index has it all 24 | listed in a friendly and easy to use table. 25 |
26 | 27 | It is community sourced so if a device is missing, consider 28 | submitting a request for the same. 29 | 30 | 31 | 32 | Go To Index 33 | 34 | 35 | 36 | Submit ROM 37 | 38 | 39 |
40 |
41 |
42 | 43 |

Support

44 |

45 | This project is run by an indie developer and it would be really 46 | appreciated if you were to support / sponsor the project. You can get 47 | started by doing so on{' '} 48 | github sponsors 49 |

50 |
51 |