Skip to content

Commit

Permalink
Showing 39 changed files with 1,228 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

.vercel

# TODO: Generated files
# ./public/Packages
# ./public/Packages.bz2
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": false,
"trailingComma": "all",
"tabWidth": 4,
"printWidth": 120,
"singleQuote": true,
"useTabs": true
}
Binary file added .vscode/Button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .vscode/VercelComment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode", "andrewrazumovsky.vscode-styled-jsx-languageserver"]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"editor.tabSize": 4,
"editor.insertSpaces": false
}
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Dimitar Nestorov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
98 changes: 98 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Repo on Vercel

This is a Cydia/APT repository template built for the [Vercel platform](https://vercel.com/). This is a [Next.js](https://nextjs.org/) project.

## Why Repo on Vercel?

Repo on Vercel is aimed to make possible downloading packages directly from GitHub Releases which has a download counter unlike GitHub Pages.

## Getting Started

<p align="center"><a href="https://github.com/dimitarnestorov/RepoOnVercel/generate"><img width="200" src=".vscode/Button.png" alt="Use this template"></a></p>

### Run the development server

#### Install Node.js

To build and run this repo locally you need Node.js: https://nodejs.org/

You can install it using [Homebrew](https://brew.sh/) on macOS: `brew install node`

You can install it using [Chocolatey](https://chocolatey.org/) on Windows: `choco install nodejs`

#### Install the dependencies

Run the following in the root folder of your repo:

```sh
npm install
```

#### Run the server

```bash
npm run dev
```

### Edit the repository metadata

Edit the `exports.name` and `exports.description` strings in the `repo.js` file.

### Replace `CydiaIcon.png` and `favicon.ico` with your own

Icons are located in the `public` folder. They are visible in package managers and on the web.

### Add your packages

Insert your GitHub Releases package URLs in the `exports.packages` array in the `repo.js` file.

### Add your depictions

Depictions live in the `pages` folder. The format for the file name is `name.js` where `name` is the name of your package. To view your depictions before deploying run the development server and visit `http://localhost:3000/name` where `name` is the name of your package.

If your package name has spaces in it replace those with `-` for the depiction file.

### Add your package icons

Put your icons in the public folder and then edit the `exports.icons` object in the `repo.js` file. The key is the name of your package, the value is the path of the icon without `public`.

### Modify the home page

Modify `index.js` in the `pages` folder, [run the development server](#run-the-development-server), and open [http://localhost:3000](http://localhost:3000) in your browser to see the result.

### Deploy on Vercel

After you've created your repo click [here to import](https://vercel.com/import) your repo on Vercel. Choose "Import Git Repository" and enter the URL of your Git repository.

## Increase rate limit (fix "Error: rate limit exceeded")

This template uses the GitHub API to cache requests. The GitHub API is limited to 60 unauthenticated requests per IP address per hour. To increase this limit you need to specify a GitHub token as an environment variable (`GITHUB_TOKEN`) when running `npm run dev`. To get a token go to [Settings -> Developer settings -> Personal access tokens](https://github.com/settings/tokens) and click "Generate new token". If you experience `rate limit exceeded` when deploying to Vercel you need to specify a GitHub token as an environment variable in your project settings.

```sh
GITHUB_TOKEN=d107d6aaf3a6b550ebeead351a3974cb8b262b74 npm run dev
```

## Disable Vercel comments

<p align="center"><img src=".vscode/VercelComment.png" width="836" alt="Vercel comment example"></p>

By default Vercel for GitHub will comment on commits and pull requests when it successfully deploys your repo. This can be disabled by setting `github.silent` to `true` in your Vercel configuration (add `vercel.json` in the root of your repository). [Reference](https://vercel.com/docs/configuration#git-integrations/github-silent).

## Notes

- If you reupload a package with the same name and tag (resulting with a URL that is already in `repo.js`) you will need to redeploy your repo or restart your development server

## Join [The Community Repo](https://repo.community/)

To become a part of The Community Repo add a file in the `public` folder called `repo_community_validation.txt` with the following contents:

```
community.repo.access: allow_forwarding_all
```

Visit the [Add Your Repo](https://repo.community/add) page, fill out the form at the bottom of the page, and click Submit.

## Repositories using this template

- [https://dimitarnestorov.com/](https://dimitarnestorov.com/) ([Source code](https://github.com/dimitarnestorov/website))
- [https://yulkytulky.com/](https://yulkytulky.com/) ([Source code](https://github.com/YulkyTulky/Repo))
59 changes: 59 additions & 0 deletions components/Depiction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Links from './Links'
import Page from './Page'
import Section, { SectionPadding } from './Section'

import { icons } from '../loader!../repo'

export default function Depiction({ children, name, subtitle, github }) {
return (
<Page title={name}>
<style jsx>{`
.title {
margin: 16px 0;
display: flex;
align-items: center;
justify-content: center;
}
.title > img {
width: 48px;
height: 48px;
}
.titles {
margin: 0 0 0 8px;
text-align: center;
}
.titles > h1 {
font-size: 24px;
margin: 0;
}
.titles > h2 {
font-size: 12px;
margin: 0;
font-weight: 400;
}
`}</style>
<div className="title">
<img src={icons[name]} alt={`${name} icon`} />
<div className="titles">
<h1>{name}</h1>
{subtitle && <h2>{subtitle}</h2>}
</div>
</div>
<Section>
<SectionPadding>{children}</SectionPadding>
</Section>
<Links>
{[
{ icon: '/assets/link-icons/PayPal.png', label: 'PayPal', href: 'https://www.paypal.me/' },
{ icon: '/assets/link-icons/Discord.png', label: 'Discord', href: 'https://discord.gg/' },
{ icon: '/assets/link-icons/Twitter.png', label: 'Twitter', href: 'https://twitter.com/' },
{ icon: '/assets/link-icons/GitHub.png', label: 'GitHub', href: github },
]}
</Links>
</Page>
)
}
40 changes: 40 additions & 0 deletions components/Links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Section from './Section'

export default function Links({ children }) {
return (
<Section>
<div className="links">
<style jsx>{`
.links > a {
height: 44px;
padding: 0 12px;
display: flex;
align-items: center;
border-bottom: 1px solid #333;
font-size: 16px;
font-weight: 300;
}
.links > a:last-child {
border-bottom: 0 none;
}
.links > a > img {
width: 29px;
height: 29px;
margin-right: 8px;
}
`}</style>
{children.map(
(link, i) =>
link.href && (
<a href={link.href} key={i} target="_blank">
<img src={link.icon} alt={`${link.label} icon`} />
<span>{link.label}</span>
</a>
),
)}
</div>
</Section>
)
}
41 changes: 41 additions & 0 deletions components/Page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Head from 'next/head'

import { name as repoName } from '../loader!../repo'

export default function Page({ children, title }) {
return (
<>
<Head>
<title>{title ? `${title} - ${repoName}` : repoName}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<style jsx global>{`
* {
box-sizing: border-box;
}
html {
background-color: black;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
margin: 0;
}
a {
color: #1687e9;
}
.container {
max-width: 400px;
width: 90%;
margin: 0 auto;
}
`}</style>
<div className="container">{children}</div>
</>
)
}
27 changes: 27 additions & 0 deletions components/Section.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default function Section({ children }) {
return (
<div className="section">
<style jsx>{`
.section {
background-color: #222222;
margin: 16px 0;
border-radius: 8px;
}
`}</style>
{children}
</div>
)
}

export function SectionPadding({ children }) {
return (
<div className="padding">
<style jsx>{`
.padding {
padding: 12px;
}
`}</style>
{children}
</div>
)
}
179 changes: 179 additions & 0 deletions loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const stream = require('stream')

const ar = require('ar')
const axios = require('axios')
const gunzipMaybe = require('gunzip-maybe')
const tar = require('tar-stream')

const { urlRegexp } = require('./utils')

// Create node_modules/.cache folder
const cacheFolder = path.join(__dirname, 'node_modules', '.cache')
if (!fs.existsSync(cacheFolder)) {
fs.mkdirSync(cacheFolder)
}

// Load node_modules/.cache/urlsLoader.json
const cacheVersion = 2
const cacheFile = path.join(__dirname, 'node_modules', '.cache', 'urlsLoader.json')
function getCacheFileContents() {
const emptyCache = {
version: cacheVersion,
cache: {},
}

if (fs.existsSync(cacheFile)) {
try {
const cacheFileContents = require(cacheFile)
if (cacheFileContents.version !== cacheVersion) return emptyCache
if (typeof cacheFileContents.cache !== 'object') return emptyCache

return cacheFileContents
} catch (error) {
return emptyCache
}
} else {
return emptyCache
}
}
const cache = getCacheFileContents().cache
let lastWrittenCache = JSON.stringify(cache)

const urlsToAssetId = {}

function getAssetIdForURL(url) {
const result = url.match(urlRegexp)
if (!result) throw new Error(`Bad URL: ${url}`)
const [, owner, repo, tag] = result
const options = {}
if (process.env.GITHUB_TOKEN) options.headers = { Authorization: `token ${process.env.GITHUB_TOKEN}` }
return axios
.get(`https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`, options)
.then(({ data }) => (data.assets.filter((asset) => asset.browser_download_url === url)[0] || {}).id)
.catch(({ response }) => {
throw new Error(response.statusText)
})
}

function extractControlTarGunzipMaybe(data) {
return new Promise((resolve, reject) => {
const readableStream = new stream.Readable({
read() {
this.push(data)
this.push(null)
},
})

const extract = tar.extract()

extract.on('entry', function (header, stream, next) {
if (header.name === './control') {
const data = []
stream.on('data', (chunk) => {
data.push(chunk)
})
stream.on('end', function () {
resolve(Buffer.concat(data).toString())
})
stream.resume()
} else {
next()
}
})

extract.on('finish', function () {
reject(new Error('control file missing'))
})

readableStream.pipe(gunzipMaybe()).pipe(extract)
})
}

function convertControlToObject(control) {
const controlRegExp = /^([A-Za-z-]+): (.*)$/gm
const meta = {}

let result
while ((result = controlRegExp.exec(control))) {
meta[result[1]] = result[2]
}

delete meta.Icon
return meta
}

async function getMetaForURL(url) {
const { data } = await axios.get(url, { responseType: 'arraybuffer' })

const archive = new ar.Archive(data)

const meta = {}

for (const file of archive.getFiles()) {
const fileName = file.name()
if (fileName.startsWith('control.tar')) {
Object.assign(meta, convertControlToObject(await extractControlTarGunzipMaybe(file.fileData())))
} else if (fileName.startsWith('data.tar') || fileName === 'debian-binary') {
// Skip
} else {
console.warn('File', fileName, 'not supported; skipping')
}
}

delete meta.Depiction
delete meta.Icon

// Calculate Size [size of package]
meta.Size = data.length

// Calculate MD5sum of package
meta.MD5sum = crypto.createHash('md5').update(data).digest('hex')

meta.Filename = `api/deb/${meta.MD5sum}.deb`

// Calculate SHA1 of package
meta.SHA1 = crypto.createHash('sha1').update(data).digest('hex')

// Calculate SHA256 of package
meta.SHA256 = crypto.createHash('sha256').update(data).digest('hex')

return meta
}

module.exports = async function () {
const repo = require(this.resourcePath)

const packages = await Promise.all(
repo.packages.map(async (url) => {
const assetId = urlsToAssetId[url] || (urlsToAssetId[url] = await getAssetIdForURL(url))
if (!assetId) throw new Error(`Asset with URL ${url} not found`)
const meta = cache[assetId] || (cache[assetId] = await getMetaForURL(url))
return { meta, url }
}),
)

const currentCacheSerialized = JSON.stringify({ version: cacheVersion, cache })
if (lastWrittenCache !== currentCacheSerialized) {
fs.writeFileSync(cacheFile, currentCacheSerialized)
lastWrittenCache = currentCacheSerialized
}

const restructuredPackages = {}
const md5Table = {}

for (const p of packages) {
// package is a reserved variable name
if (!restructuredPackages[p.meta.Name]) restructuredPackages[p.meta.Name] = {}
restructuredPackages[p.meta.Name][p.meta.Version] = p
md5Table[p.meta.MD5sum] = p.url
}

return `export const packages = ${JSON.stringify(restructuredPackages)};
export const md5Table = ${JSON.stringify(md5Table)};
export const name = ${JSON.stringify(repo.name)};
export const description = ${JSON.stringify(repo.description)};
export const icons = ${JSON.stringify(repo.icons)};`
}
14 changes: 14 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
async rewrites() {
return [
{ source: '/Release', destination: '/api/Release' },
{ source: '/./Release', destination: '/api/Release' },
{ source: '/./Packages', destination: '/api/Packages' },
{ source: '/./Packages.gz', destination: '/api/Packages.gz' },
{ source: '/Packages', destination: '/api/Packages' },
{ source: '/Packages.gz', destination: '/api/Packages.gz' },
{ source: '/./CydiaIcon.png', destination: '/CydiaIcon.png' },
{ source: '/./api/(.*)', destination: '/api/$1' },
]
},
}
30 changes: 30 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "repo",
"version": "0.0.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@apollo/react-hooks": "^3.1.5",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link-http": "^1.5.17",
"graphql": "^15.3.0",
"graphql-tag": "^2.10.4",
"next": "^9.5.1",
"nookies": "^2.3.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"timeago-react": "^3.0.0"
},
"devDependencies": {
"ar": "^0.0.1",
"axios": "^0.19.2",
"gunzip-maybe": "^1.4.2",
"tar-stream": "^2.1.2"
}
}
9 changes: 9 additions & 0 deletions pages/Blue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Depiction from '../components/Depiction'

export default function Blue() {
return (
<Depiction name="Blue" github="https://github.com/dimitarnestorov/RepoOnVercel/tree/example-tweak-blue">
Blue is a <span style={{ color: 'blue' }}>blue</span> package.
</Depiction>
)
}
9 changes: 9 additions & 0 deletions pages/Green.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Depiction from '../components/Depiction'

export default function Green() {
return (
<Depiction name="Green" subtitle="Green like lettuce">
Green is a <span style={{ color: 'green' }}>green</span> package.
</Depiction>
)
}
13 changes: 13 additions & 0 deletions pages/Red.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Depiction from '../components/Depiction'

export default function Red() {
return (
<Depiction
name="Red"
github="https://github.com/dimitarnestorov/RepoOnVercel/tree/example-tweak-red"
subtitle="Reddest of them all"
>
Red is a <span style={{ color: 'red' }}>red</span> package.
</Depiction>
)
}
15 changes: 15 additions & 0 deletions pages/api/Packages.gz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { gzipSync } from 'zlib'

import Packages from './Packages'

export default (req, res) => {
const fakeRes = {
setHeader() {},
end(end) {
this.end = end
},
}
Packages(req, fakeRes)
res.setHeader('Cache-Control', 's-maxage=31536000')
res.end(gzipSync(Buffer.from(fakeRes.end)))
}
27 changes: 27 additions & 0 deletions pages/api/Packages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { packages, icons } from '../../loader!../../repo'
import { getRepoUrl } from '../../utils'

const spaceRegExp = /\s/g

export default (req, res) => {
const url = getRepoUrl(req)
const result = []
for (const name in packages) {
const versions = packages[name]
for (const version in versions) {
const p = versions[version] // package is a reserved variable name
const strings = []
for (const entry in p.meta) {
strings.push(`${entry}: ${p.meta[entry]}`)
}

icons[name] && strings.push(`Icon: ${url}${icons[name]}`)
strings.push(`Depiction: ${url}${name.replace(spaceRegExp, '-')}`)

result.push(strings.join('\n'))
}
}

res.setHeader('Cache-Control', 's-maxage=31536000')
res.end(result.join('\n\n'))
}
14 changes: 14 additions & 0 deletions pages/api/Release.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { name, description } from '../../loader!../../repo'

export default (req, res) => {
res.setHeader('Cache-Control', 's-maxage=31536000')
res.end(`Origin: ${name}
Label: ${name}
Suite: stable
Version: 1.0
Codename: ios
Architectures: iphoneos-arm
Components: main
Description: ${description}
`)
}
20 changes: 20 additions & 0 deletions pages/api/deb/[md5.deb].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { md5Table } from '../../../loader!../../../repo'

export default (req, res) => {
if (req.method === 'HEAD') {
res.status(200)
res.end()
return
}

const query = req.query['md5.deb']
const md5 = query.substr(0, query.length - 4)
const url = md5Table[md5]
if (!url) {
res.status(404)
} else {
res.setHeader('Location', url)
res.status(302)
}
res.end()
}
135 changes: 135 additions & 0 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useRef } from 'react'

import { name as repoName } from '../loader!../repo'
import Page from '../components/Page'
import { getRepoUrl } from '../utils'

export default function Home({ repoURL }) {
const inputRef = useRef()
return (
<Page>
<style jsx>{`
h1 {
text-align: center;
margin-bottom: 100px;
}
.package-manager {
display: flex;
align-items: center;
color: white;
text-align: center;
background-color: #000000;
border: 1px solid rgba(255, 255, 255, 0.5);
margin: 10px auto;
width: 200px;
border-radius: 14px;
font-size: 20px;
text-decoration: none;
}
.package-manager > span {
flex-grow: 1;
margin-right: 8px;
}
.package-manager::before {
content: '';
background-repeat: no-repeat;
width: 36px;
height: 36px;
margin: 6px 0 6px 6px;
background-size: contain;
}
.package-manager.cydia::before {
background-image: url('/assets/package-managers/Cydia.png');
}
.package-manager.zebra::before {
background-image: url('/assets/package-managers/Zebra.png');
}
.package-manager.sileo::before {
background-image: url('/assets/package-managers/Sileo.png');
}
.input-container {
width: 320px;
max-width: 90%;
position: relative;
margin: 32px auto;
}
.input-container > input {
border: 0 none;
padding: 0 68px 0 12px;
font-size: 18px;
height: 50px;
width: 100%;
border-radius: 12px;
flex-grow: 1;
background-color: #333333;
color: #ffffff;
}
.input-container > button {
font-size: 16px;
position: absolute;
height: 30px;
right: 10px;
top: 10px;
padding: 6px 8px;
border-radius: 6px;
background-color: #1e90ff;
color: #ffffff;
border: 0 none;
line-height: 16px;
}
`}</style>

<h1>{repoName}</h1>

<a
href={`cydia://url/https://cydia.saurik.com/api/share#?source=${repoURL}`}
className="package-manager cydia"
>
<span>Add to Cydia</span>
</a>
<a href={`zbra://sources/add/${repoURL}`} className="package-manager zebra">
<span>Add to Zebra</span>
</a>
<a href={`sileo://source/${repoURL}`} className="package-manager sileo">
<span>Add to Sileo</span>
</a>
{/* <a href={`installer://add/repo=${repoURL}`}>Add to Installer</a> */}

<div className="input-container">
<input
value={repoURL}
readOnly
ref={inputRef}
onClick={() => {
inputRef.current.select()
inputRef.current.setSelectionRange(0, repoURL.length)
}}
/>
<button
onClick={() => {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(repoURL)
} else {
inputRef.current.select()
inputRef.current.setSelectionRange(0, repoURL.length)
document.execCommand('copy')
}
}}
>
Copy
</button>
</div>
</Page>
)
}

Home.getInitialProps = ({ req }) => ({
repoURL: getRepoUrl(req),
})
394 changes: 394 additions & 0 deletions pages/stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,394 @@
import { ApolloProvider, useQuery } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import gql from 'graphql-tag'
import { setCookie, parseCookies, destroyCookie } from 'nookies'
import { useMemo, useRef, useState, createContext, useContext, useEffect } from 'react'
import TimeAgo from 'timeago-react'

import Page from '../components/Page'

import { packages, icons } from '../loader!../repo'
import { urlRegexp } from '../utils'

let apolloClient

function createApolloClient(token) {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: new HttpLink({
uri: 'https://api.github.com/graphql', // Server URL (must be absolute)
// credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
headers: {
authorization: `bearer ${token}`,
},
}),
cache: new InMemoryCache(),
shouldBatch: true,
})
}

function initializeApollo(token, initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient(token)

if (initialState) {
_apolloClient.cache.restore(initialState)
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient

return _apolloClient
}

function useApollo(token, initialState) {
return useMemo(() => initializeApollo(token, initialState), [token, initialState])
}

function hashCode(string) {
var hash = 0,
i,
chr
for (i = 0; i < string.length; i++) {
chr = string.charCodeAt(i)
hash = (hash << 5) - hash + chr
hash |= 0 // Convert to 32bit integer
}
return ('a' + hash).replace('a-', 'az')
}

function setObjectIfNotSet(object, key) {
const currentValue = object[key]
if (currentValue) return currentValue
return (object[key] = {})
}

// owner -> repo -> tag -> asset
const resources = {}
const nodes = []
for (const i in packages) {
const p = packages[i]

for (const version in p) {
const url = p[version].url
const matches = url.match(urlRegexp)
if (!matches) throw new Error(`Bad URL: ${url}`)

const [, owner, repoName, tagName, assetName] = matches

const ownerObject = setObjectIfNotSet(resources, owner)
const repoObject = setObjectIfNotSet(ownerObject, repoName)
const tagObject = setObjectIfNotSet(repoObject, tagName)
tagObject[assetName] = hashCode(url)
}

nodes.push(<Package key={i} name={i} package={p} />)
}

const buildReleaseQuery = (hash, tagName, assetName) =>
`${hash}: release(tagName: "${tagName}") {
releaseAssets(name: "${assetName}", first: 1) {
nodes {
downloadCount
}
}
}`

const queryParts = []
for (const owner in resources) {
const ownerObject = resources[owner]
for (const repoName in ownerObject) {
const repoObject = ownerObject[repoName]
queryParts.push(`${hashCode(`${owner}/${repoName}`)}: repository(owner: "${owner}", name: "${repoName}") {`)
for (const tagName in repoObject) {
const tagObject = repoObject[tagName]
for (const assetName in tagObject) {
const hash = tagObject[assetName]
queryParts.push(buildReleaseQuery(hash, tagName, assetName))
}
}
queryParts.push(`}`)
}
}

const query = gql`
query GetDownloads {
${queryParts.join('')}
rateLimit {
limit
cost
remaining
resetAt
}
}
`

const DataContext = createContext()

function Version({ version, url }) {
const data = useContext(DataContext)

return (
<li>
Version {version} has {data[hashCode(url)].releaseAssets.nodes[0].downloadCount} downloads
</li>
)
}

function Package({ package: p, name }) {
const versions = []
for (const i in p) {
versions.push(<Version key={i} version={i} url={p[i].url} />)
}

return (
<div className="tweak">
<style jsx>{`
.tweak {
background-color: #272727;
padding: 12px;
margin: 10px 0;
border-radius: 10px;
}
.tweakName {
display: flex;
align-items: center;
}
.tweakName > img {
margin-right: 8px;
}
.tweakName > h3 {
margin: 0;
}
ul {
margin: 8px 0 0;
padding-left: 20px;
font-size: 18px;
}
`}</style>
<div className="tweakName">
{icons[name] && <img src={icons[name]} alt={`${name} icon`} width="40" height="40" />}
<h3>{name}</h3>
</div>
<ul>{versions}</ul>
</div>
)
}

function Stats({ setState }) {
const { loading, error, data } = useQuery(query)

const flatData = useMemo(() => {
const object = {}
for (const i in data) {
if (i === 'rateLimit') continue
Object.assign(object, data[i])
}
return object
}, [data])

return (
<>
<style jsx>{`
h1 {
text-align: center;
}
details {
opacity: 0.8;
}
details > ul {
margin-top: 6px;
}
details > summary {
cursor: pointer;
padding: 4px;
user-select: none;
}
`}</style>
<h1>Statistics</h1>

{error ? (
<>
<p style={{ color: 'red' }}>Error: {error.message}</p>
<button
onClick={() => {
setState({ token: '' })
destroyCookie(undefined, 'token')
}}
>
Delete saved token
</button>
</>
) : loading ? (
'Loading…'
) : (
<DataContext.Provider value={flatData}>
{nodes}
<details>
<summary>Rate limit</summary>
<ul>
<li>Limit: {data.rateLimit.limit}</li>
<li>Cost: {data.rateLimit.cost}</li>
<li>Remaining: {data.rateLimit.remaining}</li>
<li>
Remaining resets <TimeAgo datetime={data.rateLimit.resetAt} />
</li>
</ul>
</details>
</DataContext.Provider>
)}
</>
)
}

function Provider({ state, children }) {
const apolloClient = useApollo(state.token, state.initialState)
return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>
}

export default function GetToken({ token, initialState }) {
const [state, setState] = useState({ token, initialState })
const inputRef = useRef()

const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
}, [setIsMounted])

return (
<Page title="Statistics">
{state.token ? (
<Provider state={state}>
<Stats setState={setState} />
</Provider>
) : (
<div>
<style jsx>{`
div {
text-align: center;
font-size: 18px;
}
div > * {
margin: 8px 0;
}
a {
display: block;
margin: 32px 0 !important;
}
form {
display: flex;
align-items: center;
justify-content: center;
}
input {
border-radius: 16px;
border: 0;
padding: 0
${typeof window !== 'undefined' && isMounted && navigator.clipboard ? '68px' : '16px'} 0
12px;
font-size: 16px;
height: 32px;
background-color: #222222;
color: #ffffff;
width: 248px;
font-family: monospace;
}
button[type='button'] {
height: 26px;
border-radius: 13px;
border: 0;
padding: 0;
width: 60px;
font-size: 15px;
margin-left: -63px;
text-align: center;
background-color: #000000;
border: 1px solid #666666;
color: #ffffff;
}
button[type='submit'] {
height: 32px;
border-radius: 16px;
border: 0;
padding: 0 16px;
font-size: 16px;
margin-left: 8px;
background-color: #1e90ff;
color: #ffffff;
}
`}</style>
<a href="https://github.com/settings/tokens" target="_blank">
Get a GitHub personal access token
</a>
<div>Enter your GitHub personal access token:</div>
<form
onSubmit={(event) => {
event.preventDefault()
const token = inputRef.current.value
setCookie(undefined, 'token', token, { maxAge: 500000000 })
setState({ token, initialState: state.initialState })
}}
>
<input
type="text"
ref={inputRef}
placeholder="76d80224611fc919a5d54f0ff9fba446cdec93ec"
autoCorrect="off"
autoComplete="off"
autoCapitalize="off"
/>
{typeof window !== 'undefined' && isMounted && navigator.clipboard && (
<button
type="button"
onClick={() =>
navigator.clipboard
.readText()
.then((text) => (inputRef.current.value = text))
.catch(() =>
alert(
'Please allow access to your clipboard in order for the paste button to work',
),
)
}
>
Paste
</button>
)}
<button type="submit">Save</button>
</form>
</div>
)}
</Page>
)
}

GetToken.getInitialProps = async (ctx) => {
const token = parseCookies(ctx).token
const apolloClient = initializeApollo(token)

try {
if (token) {
await apolloClient.query({
query,
})
}
} catch (error) {
console.error(error)
}

return { token, initialState: apolloClient.cache.extract() }
}
Binary file added public/CydiaIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/blue/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/green/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/link-icons/Discord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/link-icons/GitHub.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/link-icons/PayPal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/link-icons/Twitter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/package-managers/Cydia.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/package-managers/Sileo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/package-managers/Zebra.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/red/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/favicon.ico
Binary file not shown.
2 changes: 2 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
15 changes: 15 additions & 0 deletions repo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
exports.name = 'Repo on Vercel'
exports.description = "Repo on Vercel's example repository"

exports.packages = [
'https://github.com/dimitarnestorov/RepoOnVercel/releases/download/red-1.0.1/example.red_1.0.1_iphoneos-arm.deb',
'https://github.com/dimitarnestorov/RepoOnVercel/releases/download/red-1.0.0/example.red_1.0.0_iphoneos-arm.deb',
'https://github.com/dimitarnestorov/RepoOnVercel/releases/download/green-1.0.0/example.green_1.0.0_iphoneos-arm.deb',
'https://github.com/dimitarnestorov/RepoOnVercel/releases/download/blue-1.0.0/example.blue_1.0.0_iphoneos-arm.deb',
]

exports.icons = {
Red: 'assets/red/icon.png',
Blue: 'assets/blue/icon.png',
Green: 'assets/green/icon.png',
}
15 changes: 15 additions & 0 deletions utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
exports.urlRegexp = /^https:\/\/github.com\/([^\/]+)\/([^\/]+)\/releases\/download\/([^\/]+)\/([^\/]+)$/

exports.getRepoUrl = function getRepoUrl(req) {
if (req) {
return `${
req.headers['x-forwarded-proto']
? req.headers['x-forwarded-proto']
: process.env.NODE_ENV === 'production'
? 'https'
: 'http'
}://${req.headers['x-forwarded-host'] ? req.headers['x-forwarded-host'] : req.headers.host}/`
}

return `${window.location.protocol}//${window.locations.host}/`
}

0 comments on commit ed6b5ec

Please sign in to comment.