├── .eslintignore ├── .eslintrc ├── .github ├── actions │ └── build │ │ └── action.yml └── workflows │ ├── build.yml │ └── tag.yml ├── .gitignore ├── .yarn └── releases │ └── yarn-3.1.1.cjs ├── .yarnrc.yml ├── README.md ├── bin └── cmark-gfm ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── gh-pages-cache-restore.js ├── gh-pages-cache.js ├── github-source.js ├── package.json ├── src ├── components │ ├── 404.tsx │ ├── module.scss │ ├── module.tsx │ ├── repo-card.tsx │ ├── search-result-card.tsx │ └── seo.tsx ├── debounce.ts ├── flexsearch-config.d.ts ├── flexsearch-config.js ├── images │ └── favicon.png ├── layout.tsx ├── pages │ ├── 404.tsx │ └── submission.tsx ├── shims.d.ts ├── styles.styl └── templates │ ├── index.tsx │ └── module.tsx ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | .cache 2 | public 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard-with-typescript", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "rules": { 7 | "@typescript-eslint/strict-boolean-expressions": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | description: 'Build page' 3 | inputs: 4 | repo: 5 | description: 'full name of repo' 6 | required: false 7 | token: 8 | description: 'token for graphql' 9 | required: true 10 | runs: 11 | using: "composite" 12 | steps: 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '18' 16 | - name: Yarn cache directory 17 | id: yarn-cache-dir 18 | shell: bash 19 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 20 | - name: cache yarn modules 21 | uses: actions/cache@v4 22 | with: 23 | path: ${{ steps.yarn-cache-dir.outputs.dir }} 24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 25 | restore-keys: ${{ runner.os }}-yarn- 26 | - name: cache gatsby cache 27 | uses: actions/cache@v4 28 | with: 29 | path: .cache 30 | key: ${{ runner.os }}-gatsby-cache-${{ github.run_id }} 31 | restore-keys: ${{ runner.os }}-gatsby-cache- 32 | save-always: true 33 | - name: cache gatsby public 34 | uses: actions/cache@v4 35 | with: 36 | path: public 37 | key: ${{ runner.os }}-gatsby-public-${{ github.run_id }} 38 | restore-keys: ${{ runner.os }}-gatsby-public- 39 | save-always: true 40 | - name: cache gatsby public-cache 41 | uses: actions/cache@v4 42 | with: 43 | path: public-cache 44 | key: ${{ runner.os }}-gatsby-public-cache-${{ github.run_id }} 45 | restore-keys: ${{ runner.os }}-gatsby-public-cache- 46 | save-always: true 47 | - name: cache graphql response 48 | uses: actions/cache@v4 49 | with: 50 | path: cached_graphql.json 51 | key: ${{ runner.os }}-github-graphql-response-${{ github.run_id }} 52 | restore-keys: ${{ runner.os }}-github-graphql-response- 53 | save-always: true 54 | - name: Restore cache 55 | shell: bash 56 | run: node gh-pages-cache-restore.js 57 | - name: Install and Build 🔧 # This example project is built using yarn and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 58 | shell: bash 59 | run: | 60 | yarn install --immutable 61 | yarn build 62 | echo "modules.lsposed.org" > ./public/CNAME 63 | env: 64 | GRAPHQL_TOKEN: ${{ inputs.token }} 65 | REPO: ${{ inputs.repo }} 66 | GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES: true 67 | - name: clean up caches on failure 68 | if: ${{ failure() || cancelled() }} 69 | shell: bash 70 | run: | 71 | rm -rf public/* 72 | rm -rf public-cache/* 73 | rm -rf .cache/* 74 | rm -f cached_graphql.json 75 | - name: Refresh cache 76 | shell: bash 77 | run: node gh-pages-cache.js 78 | - name: Upload artifact 79 | uses: actions/upload-pages-artifact@v3 80 | with: 81 | # Upload entire repository 82 | path: 'public' 83 | - name: Deploy to GitHub Pages 84 | id: deployment 85 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths-ignore: 6 | - '.github/workflows/tag.yml' 7 | workflow_dispatch: 8 | inputs: 9 | repo: 10 | description: 'repo name' 11 | required: false 12 | schedule: 13 | - cron: "0 0 * * *" 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | run-name: Build ${{ github.event.inputs.repo }} 22 | 23 | jobs: 24 | deploy: 25 | concurrency: 26 | group: ${{ github.sha }} 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | name: Build Website 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 1 37 | - name: Build 38 | uses: ./.github/actions/build 39 | with: 40 | token: ${{ secrets.GRAPHQL_TOKEN }} 41 | repo: ${{ github.event.inputs.repo }} 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | repo: 6 | description: 'repo name' 7 | required: true 8 | release: 9 | description: 'release id' 10 | required: true 11 | apk: 12 | description: 'apk url' 13 | required: true 14 | tag: 15 | description: 'tag name' 16 | required: true 17 | 18 | run-name: Tag ${{ github.event.inputs.repo }}@${{ github.event.inputs.tag }} 19 | 20 | jobs: 21 | update_tag: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - run: | 25 | wget -O release.apk '${{ github.event.inputs.apk }}' 26 | AAPT=$(ls $ANDROID_SDK_ROOT/build-tools/**/aapt2 | tail -1) 27 | VERCODE=$($AAPT dump badging release.apk | head -1 | sed -n "/^package:/ s/^.*versionCode='\([0-9]*\)'.*/\1/p") 28 | VERNAME=$($AAPT dump badging release.apk | head -1 | sed -n "/^package:/ s/^.*versionName='\([^']*\)'.*/\1/p") 29 | VERNAME="${VERNAME// /_}" 30 | echo '${{ github.event.inputs.tag }}' "-> $VERCODE-$VERNAME" 31 | if [[ '${{ github.event.inputs.tag }}' == "$VERCODE-$VERNAME" ]]; then 32 | exit 33 | fi 34 | mkdir test 35 | cd test 36 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 37 | git config --global user.name "github-actions[bot]" 38 | git init . 39 | git commit -m "$VERCODE-$VERNAME" --allow-empty 40 | git tag "$VERCODE-$VERNAME" -m "$VERCODE-$VERNAME" -f 41 | git remote add origin "https://${{ secrets.TAG_TOKEN }}@github.com/${{ github.event.inputs.repo }}.git" 42 | git push origin "$VERCODE-$VERNAME" -f 43 | curl -X PATCH -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.TAG_TOKEN }}" https://api.github.com/repos/${{ github.event.inputs.repo }}/releases/${{ github.event.inputs.release }} -d "{\"tag_name\":\"${VERCODE}-${VERNAME}\"}" 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # IDE 72 | .idea 73 | .vs 74 | .vscode 75 | .yarn/* 76 | !.yarn/patches 77 | !.yarn/plugins 78 | !.yarn/releases 79 | !.yarn/sdks 80 | !.yarn/versions 81 | 82 | cached_graphql.json 83 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xposed Module Repository [[1]](#refer-1) 2 | 3 | https://modules.lsposed.org 4 | 5 | ## Submit Modules 6 | 7 | https://modules.lsposed.org/submission 8 | 9 | ## API Reference 10 | 11 | ### All Modules 12 | 13 | GET https://modules.lsposed.org/modules.json 14 | 15 | ### Module 16 | 17 | GET https://modules.lsposed.org/module/[:name].json 18 | 19 | ## Reference 20 | 21 | [1] https://github.com/LSPosed/LSPosed/issues/7 22 | -------------------------------------------------------------------------------- /bin/cmark-gfm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xposed-Modules-Repo/modules/9830351868b10ddd9a66082b2e14a23e6728fd76/bin/cmark-gfm -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const flexsearchConfig = require('./src/flexsearch-config') 2 | 3 | module.exports = { 4 | siteMetadata: { 5 | title: 'Xposed Module Repository', 6 | siteUrl: 'https://modules.lsposed.org', 7 | description: 'New Xposed Module Repository', 8 | author: 'https://github.com/Xposed-Modules-Repo/modules/graphs/contributors' 9 | }, 10 | plugins: [ 11 | 'gatsby-plugin-postcss', 12 | 'gatsby-plugin-stylus', 13 | 'gatsby-plugin-sitemap', 14 | 'gatsby-plugin-sass', 15 | { 16 | resolve: 'gatsby-plugin-manifest', 17 | options: { 18 | name: 'Xposed Module Repository', 19 | short_name: 'Xposed Module Repo', 20 | start_url: '/', 21 | background_color: '#1e88e5', 22 | theme_color: '#1e88e5', 23 | display: 'minimal-ui', 24 | icon: 'src/images/favicon.png' // This path is relative to the root of the site. 25 | } 26 | }, 27 | { 28 | resolve: 'gatsby-transformer-remark', 29 | options: { 30 | plugins: [ 31 | 'gatsby-remark-external-links' 32 | ] 33 | } 34 | }, 35 | { 36 | resolve: 'gatsby-plugin-local-search', 37 | options: { 38 | name: 'repositories', 39 | engine: 'flexsearch', 40 | engineOptions: flexsearchConfig, 41 | query: ` 42 | { 43 | allGithubRepository(filter: {isModule: {eq: true}, hide: {eq: false}}) { 44 | edges { 45 | node { 46 | name 47 | description 48 | collaborators { 49 | edges { 50 | node { 51 | login 52 | name 53 | } 54 | } 55 | } 56 | releases { 57 | edges { 58 | node { 59 | description 60 | } 61 | } 62 | } 63 | readme 64 | readmeHTML 65 | childGitHubReadme { 66 | childMarkdownRemark { 67 | excerpt(pruneLength: 250, truncate: true) 68 | } 69 | } 70 | summary 71 | additionalAuthors { 72 | name 73 | } 74 | } 75 | } 76 | } 77 | } 78 | `, 79 | ref: 'name', 80 | index: ['name', 'description', 'summary', 'readme', 'collaborators', 'additionalAuthors', 'release'], 81 | store: ['name', 'description', 'summary', 'readmeExcerpt'], 82 | normalizer: ({ data }) => 83 | data.allGithubRepository.edges.map(({ node: repo }) => ({ 84 | name: repo.name, 85 | description: repo.description, 86 | summary: repo.summary, 87 | readme: repo.readme, 88 | readmeExcerpt: repo.childGitHubReadme && repo.childGitHubReadme.childMarkdownRemark.excerpt, 89 | release: repo.releases && repo.releases.edges.length && 90 | repo.releases.edges[0].node.description, 91 | collaborators: repo.collaborators && 92 | repo.collaborators.edges.map(({ node: collaborator }) => `${collaborator.name} (@${collaborator.login})`) 93 | .join(', '), 94 | additionalAuthors: repo.additionalAuthors && 95 | repo.additionalAuthors.map((author) => author.name).join(', ') 96 | })) 97 | } 98 | }, 99 | { 100 | resolve: 'gatsby-plugin-nprogress', 101 | options: { 102 | color: '#eee', 103 | showSpinner: false 104 | } 105 | }, 106 | { 107 | resolve: 'gatsby-source-filesystem', 108 | options: { 109 | name: 'pages', 110 | path: './src/pages/' 111 | }, 112 | __key: 'pages' 113 | } 114 | ] 115 | } 116 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { v4: uuid } = require('uuid') 3 | const crypto = require('crypto') 4 | const path = require('path') 5 | const ellipsize = require('ellipsize') 6 | const { gql } = require('@apollo/client') 7 | const { execFileSync } = require('child_process') 8 | 9 | const { fetchFromGithub, replacePrivateImage } = require('./github-source') 10 | 11 | function makeRepositoryQuery (name) { 12 | return gql` 13 | { 14 | repository(owner: "Xposed-Modules-Repo", name: "${name}") { 15 | name 16 | description 17 | url 18 | homepageUrl 19 | collaborators(affiliation: DIRECT, first: 100) { 20 | edges { 21 | node { 22 | login 23 | name 24 | } 25 | } 26 | } 27 | readme: object(expression: "HEAD:README.md") { 28 | ... on Blob { 29 | text 30 | } 31 | } 32 | summary: object(expression: "HEAD:SUMMARY") { 33 | ... on Blob { 34 | text 35 | } 36 | } 37 | scope: object(expression: "HEAD:SCOPE") { 38 | ... on Blob { 39 | text 40 | } 41 | } 42 | sourceUrl: object(expression: "HEAD:SOURCE_URL") { 43 | ... on Blob { 44 | text 45 | } 46 | } 47 | hide: object(expression: "HEAD:HIDE") { 48 | ... on Blob { 49 | text 50 | } 51 | } 52 | additionalAuthors: object(expression: "HEAD:ADDITIONAL_AUTHORS") { 53 | ... on Blob { 54 | text 55 | } 56 | } 57 | latestRelease { 58 | name 59 | url 60 | isDraft 61 | description 62 | descriptionHTML 63 | createdAt 64 | publishedAt 65 | updatedAt 66 | tagName 67 | isPrerelease 68 | releaseAssets(first: 50) { 69 | edges { 70 | node { 71 | name 72 | contentType 73 | downloadUrl 74 | downloadCount 75 | size 76 | } 77 | } 78 | } 79 | } 80 | releases(first: 20) { 81 | edges { 82 | node { 83 | name 84 | url 85 | isDraft 86 | description 87 | descriptionHTML 88 | createdAt 89 | publishedAt 90 | updatedAt 91 | tagName 92 | isPrerelease 93 | isLatest 94 | releaseAssets(first: 50) { 95 | edges { 96 | node { 97 | name 98 | contentType 99 | downloadUrl 100 | downloadCount 101 | size 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | updatedAt 109 | createdAt 110 | stargazerCount 111 | } 112 | } 113 | ` 114 | } 115 | 116 | 117 | const PAGINATION = 10 118 | function makeRepositoriesQuery (cursor) { 119 | const arg = cursor ? `, after: "${cursor}"` : '' 120 | return gql` 121 | { 122 | organization(login: "Xposed-Modules-Repo") { 123 | repositories(first: ${PAGINATION}${arg}, orderBy: {field: UPDATED_AT, direction: DESC}, privacy: PUBLIC) { 124 | edges { 125 | node { 126 | name 127 | description 128 | url 129 | homepageUrl 130 | collaborators(affiliation: DIRECT, first: 100) { 131 | edges { 132 | node { 133 | login 134 | name 135 | } 136 | } 137 | } 138 | readme: object(expression: "HEAD:README.md") { 139 | ... on Blob { 140 | text 141 | } 142 | } 143 | summary: object(expression: "HEAD:SUMMARY") { 144 | ... on Blob { 145 | text 146 | } 147 | } 148 | scope: object(expression: "HEAD:SCOPE") { 149 | ... on Blob { 150 | text 151 | } 152 | } 153 | sourceUrl: object(expression: "HEAD:SOURCE_URL") { 154 | ... on Blob { 155 | text 156 | } 157 | } 158 | hide: object(expression: "HEAD:HIDE") { 159 | ... on Blob { 160 | text 161 | } 162 | } 163 | additionalAuthors: object(expression: "HEAD:ADDITIONAL_AUTHORS") { 164 | ... on Blob { 165 | text 166 | } 167 | } 168 | latestRelease { 169 | name 170 | url 171 | isDraft 172 | description 173 | descriptionHTML 174 | createdAt 175 | publishedAt 176 | updatedAt 177 | tagName 178 | isPrerelease 179 | releaseAssets(first: 50) { 180 | edges { 181 | node { 182 | name 183 | contentType 184 | downloadUrl 185 | downloadCount 186 | size 187 | } 188 | } 189 | } 190 | } 191 | releases(first: 20) { 192 | edges { 193 | node { 194 | name 195 | url 196 | isDraft 197 | description 198 | descriptionHTML 199 | createdAt 200 | publishedAt 201 | updatedAt 202 | tagName 203 | isPrerelease 204 | isLatest 205 | releaseAssets(first: 50) { 206 | edges { 207 | node { 208 | name 209 | contentType 210 | downloadUrl 211 | downloadCount 212 | size 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | updatedAt 220 | createdAt 221 | stargazerCount 222 | } 223 | cursor 224 | } 225 | pageInfo { 226 | hasNextPage 227 | endCursor 228 | } 229 | totalCount 230 | } 231 | } 232 | }` 233 | } 234 | 235 | const generateGatsbyNode = (result, createNode) => { 236 | createNode({ 237 | data: result.data, 238 | id: result.id || uuid(), 239 | // see https://github.com/ldd/gatsby-source-github-api/issues/19 240 | // provide the raw result to see errors, or other information 241 | rawResult: result, 242 | parent: null, 243 | children: [], 244 | internal: { 245 | type: 'GithubData', 246 | contentDigest: crypto 247 | .createHash('md5') 248 | .update(JSON.stringify(result)) 249 | .digest('hex'), 250 | // see https://github.com/ldd/gatsby-source-github-api/issues/10 251 | // our node should have an 'application/json' MIME type, but we wish 252 | // transformers to ignore it, so we set its mediaType to text/plain for now 253 | mediaType: 'text/plain' 254 | } 255 | }) 256 | } 257 | 258 | function parseRepositoryObject (repo) { 259 | if (repo.summary) { 260 | repo.summary = ellipsize(repo.summary.text.trim(), 512).trim() 261 | } 262 | if (repo.readme) { 263 | repo.readme = repo.readme.text 264 | } 265 | if (repo.sourceUrl) { 266 | repo.sourceUrl = repo.sourceUrl.text.replace(/[\r\n]/g, '').trim() 267 | } 268 | if (repo.additionalAuthors) { 269 | try { 270 | const additionalAuthors = JSON.parse(repo.additionalAuthors.text) 271 | const validAuthors = [] 272 | if (additionalAuthors instanceof Array) { 273 | for (const author of additionalAuthors) { 274 | if (author && typeof author === 'object') { 275 | const validAuthor = {} 276 | for (const key of Object.keys(author)) { 277 | if (['type', 'name', 'link'].includes(key)) { 278 | validAuthor[key] = author[key] 279 | } 280 | } 281 | validAuthors.push(validAuthor) 282 | } 283 | } 284 | } 285 | repo.additionalAuthors = validAuthors 286 | } catch (e) { 287 | repo.additionalAuthors = null 288 | } 289 | } 290 | if (repo.scope) { 291 | try { 292 | repo.scope = JSON.parse(repo.scope.text) 293 | } catch (e) { 294 | repo.scope = null 295 | } 296 | } 297 | repo.hide = !!repo.hide 298 | if (repo.releases) { 299 | if (repo.latestRelease) { 300 | repo.releases.edges = [{ node: repo.latestRelease }, ...repo.releases.edges] 301 | } 302 | repo.releases.edges = repo.releases.edges 303 | .filter(({ node: { releaseAssets, isDraft, isLatest, tagName } }) => 304 | !isLatest && !isDraft && releaseAssets && tagName.match(/^\d+-.+$/) && releaseAssets.edges 305 | .some(({ node: { contentType } }) => contentType === 'application/vnd.android.package-archive')) 306 | } 307 | repo.isModule = !!(repo.name.match(/\./) && 308 | repo.description && 309 | repo.releases && 310 | repo.releases.edges.length && 311 | repo.name !== 'org.meowcat.example' && repo.name !== '.github') 312 | if (repo.isModule) { 313 | for (const release of repo.releases.edges) { 314 | release.node.descriptionHTML = replacePrivateImage(release.node.description, release.node.descriptionHTML) 315 | } 316 | repo.latestRelease = repo.releases.edges.find(({ node: { isPrerelease } }) => !isPrerelease) 317 | repo.latestReleaseTime = '1970-01-01T00:00:00Z' 318 | if (repo.latestRelease) { 319 | repo.latestRelease = repo.latestRelease.node 320 | repo.latestReleaseTime = repo.latestRelease.publishedAt 321 | repo.latestRelease.isLatest = true 322 | } 323 | repo.latestBetaRelease = repo.releases.edges.find(({ node: { isPrerelease, name } }) => isPrerelease && !name.match(/^(snapshot|nightly).*/i)) || { node: repo.latestRelease } 324 | repo.latestBetaReleaseTime = '1970-01-01T00:00:00Z' 325 | if (repo.latestBetaRelease) { 326 | repo.latestBetaRelease = repo.latestBetaRelease.node 327 | repo.latestBetaReleaseTime = repo.latestBetaRelease.publishedAt 328 | repo.latestBetaRelease.isLatestBeta = true 329 | } 330 | repo.latestSnapshotRelease = repo.releases.edges.find(({ node: { isPrerelease, name } }) => isPrerelease && name.match(/^(snapshot|nightly).*/i)) || { node: repo.latestBetaRelease } 331 | repo.latestSnapshotReleaseTime = '1970-01-01T00:00:00Z' 332 | if (repo.latestSnapshotRelease) { 333 | repo.latestSnapshotRelease = repo.latestSnapshotRelease.node 334 | repo.latestSnapshotReleaseTime = repo.latestSnapshotRelease.publishedAt 335 | repo.latestSnapshotRelease.isLatestSnapshot = true 336 | } 337 | } 338 | console.log(`Got repo: ${repo.name}, is module: ${repo.isModule}`) 339 | return repo 340 | } 341 | 342 | exports.onCreateNode = async ({ 343 | node, 344 | actions, 345 | createContentDigest, 346 | cache 347 | }) => { 348 | const { createNode } = actions 349 | if (node.internal.type === 'GithubData' && node.data) { 350 | for (let { node: repo } of node.data.organization.repositories.edges) { 351 | repo = JSON.parse(JSON.stringify(repo)) 352 | repo = parseRepositoryObject(repo) 353 | try { 354 | repo.readmeHTML = execFileSync( 355 | './bin/cmark-gfm', 356 | ['--smart', '--validate-utf8', '--github-pre-lang', '-e', 'footnotes', '-e', 'table', '-e', 'strikethrough', '-e', 'autolink', '-e', 'tagfilter', '-e', 'tasklist', '--unsafe', '--strikethrough-double-tilde', '-t', 'html'], 357 | { input: repo.readme }, 358 | ).toString() 359 | } catch (err) { 360 | if (err.code) { 361 | console.error(err.code); 362 | } else { 363 | const { stdout, stderr } = err; 364 | console.error({ stdout, stderr }); 365 | } 366 | } 367 | await createNode({ 368 | ...repo, 369 | id: repo.name, 370 | parent: null, 371 | children: repo.readme ? [repo.name + '-readme'] : [], 372 | internal: { 373 | type: 'GithubRepository', 374 | contentDigest: crypto 375 | .createHash('md5') 376 | .update(JSON.stringify(repo)) 377 | .digest('hex'), 378 | mediaType: 'application/json' 379 | } 380 | }) 381 | } 382 | } 383 | if (node.internal.type === 'GithubRepository' && node.readme) { 384 | createNode({ 385 | id: node.id + '-readme', 386 | parent: node.id, 387 | internal: { 388 | type: 'GitHubReadme', 389 | mediaType: 'text/markdown', 390 | content: node.readme, 391 | contentDigest: createContentDigest(node.readme) 392 | } 393 | }) 394 | } 395 | } 396 | 397 | exports.sourceNodes = async ( 398 | { actions } 399 | ) => { 400 | const { createNode } = actions 401 | let cursor = null 402 | let page = 1 403 | let total 404 | let mergedResult = { 405 | data: { 406 | organization: { 407 | repositories: { 408 | edges: [] 409 | } 410 | } 411 | } 412 | } 413 | const repo_name = process.env.REPO ? process.env.REPO.split('/')[1] : null 414 | if (repo_name && fs.existsSync('./cached_graphql.json')) { 415 | const data = fs.readFileSync('./cached_graphql.json') 416 | mergedResult = JSON.parse(data) 417 | mergedResult.data.organization.repositories.edges.forEach((value, index, array) => { 418 | if (value.node.name === repo_name) { 419 | array.splice(index, 1) 420 | } 421 | }) 422 | console.log(`Fetching ${repo_name} from GitHub API`) 423 | const result = await fetchFromGithub(makeRepositoryQuery(repo_name)) 424 | if (result.errors || !result.data) { 425 | const errMsg = result.errors || 'result.data is null' 426 | console.error(errMsg) 427 | throw errMsg 428 | } 429 | mergedResult.data.organization.repositories.edges.unshift({'node': result.data.repository}) 430 | } else { 431 | while (true) { 432 | console.log(`Querying GitHub API, page ${page}, total ${Math.ceil(total / PAGINATION) || 'unknown'}, cursor: ${cursor}`) 433 | const result = await fetchFromGithub(makeRepositoriesQuery(cursor)) 434 | if (result.errors || !result.data) { 435 | const errMsg = result.errors || 'result.data is null' 436 | console.error(errMsg) 437 | throw errMsg 438 | } 439 | mergedResult.data.organization.repositories.edges = 440 | mergedResult.data.organization.repositories.edges.concat(result.data.organization.repositories.edges) 441 | if (!result.data.organization.repositories.pageInfo.hasNextPage) { 442 | break 443 | } 444 | cursor = result.data.organization.repositories.pageInfo.endCursor 445 | total = result.data.organization.repositories.totalCount 446 | page++ 447 | } 448 | } 449 | fs.writeFileSync('./cached_graphql.json', JSON.stringify(mergedResult)) 450 | generateGatsbyNode(mergedResult, createNode) 451 | } 452 | 453 | exports.createPages = async ({ graphql, actions }) => { 454 | const { createPage } = actions 455 | const indexPageResult = await graphql(` 456 | { 457 | allGithubRepository(limit: 30, filter: {isModule: {eq: true}, hide: {eq: false}}) { 458 | pageInfo { 459 | pageCount 460 | perPage 461 | } 462 | } 463 | }`) 464 | for (let i = 1; i <= indexPageResult.data.allGithubRepository.pageInfo.pageCount; i++) { 465 | createPage({ 466 | path: `page/${i}`, 467 | component: path.resolve('./src/templates/index.tsx'), 468 | context: { 469 | skip: (i - 1) * indexPageResult.data.allGithubRepository.pageInfo.perPage, 470 | limit: indexPageResult.data.allGithubRepository.pageInfo.perPage 471 | } 472 | }) 473 | } 474 | createPage({ 475 | path: '/', 476 | component: path.resolve('./src/templates/index.tsx'), 477 | context: { 478 | skip: 0, 479 | limit: indexPageResult.data.allGithubRepository.pageInfo.perPage 480 | } 481 | }) 482 | const modulePageResult = await graphql(` 483 | { 484 | allGithubRepository(filter: {isModule: {eq: true}}) { 485 | edges { 486 | node { 487 | name 488 | } 489 | } 490 | } 491 | }`) 492 | for (const { node: repo } of modulePageResult.data.allGithubRepository.edges) { 493 | createPage({ 494 | path: `module/${repo.name}`, 495 | component: path.resolve('./src/templates/module.tsx'), 496 | context: { 497 | name: repo.name 498 | } 499 | }) 500 | } 501 | } 502 | 503 | function flatten (object) { 504 | for (const key of Object.keys(object)) { 505 | if (object[key] !== null && object[key] !== undefined && typeof object[key] === 'object') { 506 | if (object[key].edges) { 507 | object[key] = object[key].edges.map(edge => edge.node) 508 | } 509 | } 510 | if (object[key] !== null && object[key] !== undefined && typeof object[key] === 'object') { 511 | flatten(object[key]) 512 | } 513 | } 514 | } 515 | 516 | exports.onPostBuild = async ({ graphql }) => { 517 | const result = await graphql(` 518 | { 519 | allGithubRepository(filter: {isModule: {eq: true}, hide: {eq: false}}) { 520 | edges { 521 | node { 522 | name 523 | description 524 | url 525 | homepageUrl 526 | collaborators { 527 | edges { 528 | node { 529 | login 530 | name 531 | } 532 | } 533 | } 534 | latestRelease { 535 | name 536 | url 537 | isDraft 538 | descriptionHTML 539 | createdAt 540 | publishedAt 541 | updatedAt 542 | tagName 543 | isPrerelease 544 | releaseAssets { 545 | edges { 546 | node { 547 | name 548 | contentType 549 | downloadUrl 550 | downloadCount 551 | size 552 | } 553 | } 554 | } 555 | } 556 | latestBetaRelease { 557 | name 558 | url 559 | isDraft 560 | descriptionHTML 561 | createdAt 562 | publishedAt 563 | updatedAt 564 | tagName 565 | isPrerelease 566 | releaseAssets { 567 | edges { 568 | node { 569 | name 570 | contentType 571 | downloadUrl 572 | downloadCount 573 | size 574 | } 575 | } 576 | } 577 | } 578 | latestSnapshotRelease { 579 | name 580 | url 581 | isDraft 582 | descriptionHTML 583 | createdAt 584 | publishedAt 585 | updatedAt 586 | tagName 587 | isPrerelease 588 | releaseAssets { 589 | edges { 590 | node { 591 | name 592 | contentType 593 | downloadUrl 594 | downloadCount 595 | size 596 | } 597 | } 598 | } 599 | } 600 | latestReleaseTime 601 | latestBetaReleaseTime 602 | latestSnapshotReleaseTime 603 | releases { 604 | edges { 605 | node { 606 | name 607 | url 608 | isDraft 609 | descriptionHTML 610 | createdAt 611 | publishedAt 612 | updatedAt 613 | tagName 614 | isPrerelease 615 | releaseAssets { 616 | edges { 617 | node { 618 | name 619 | contentType 620 | downloadUrl 621 | downloadCount 622 | size 623 | } 624 | } 625 | } 626 | } 627 | } 628 | } 629 | readme 630 | readmeHTML 631 | childGitHubReadme { 632 | childMarkdownRemark { 633 | html 634 | } 635 | } 636 | summary 637 | scope 638 | sourceUrl 639 | hide 640 | additionalAuthors { 641 | type 642 | name 643 | link 644 | } 645 | updatedAt 646 | createdAt 647 | stargazerCount 648 | } 649 | } 650 | } 651 | }`) 652 | const rootPath = './public' 653 | if (!fs.existsSync(rootPath)) fs.mkdirSync(rootPath, { recursive: true }) 654 | flatten(result) 655 | const modules = result.data.allGithubRepository 656 | for (const repo of modules) { 657 | const modulePath = path.join(rootPath, 'module') 658 | if (!fs.existsSync(modulePath)) fs.mkdirSync(modulePath, { recursive: true }) 659 | const latestRelease = repo.latestRelease 660 | const latestBetaRelease = repo.latestBetaRelease 661 | const latestSnapshotRelease = repo.latestSnapshotRelease 662 | repo.latestRelease = latestRelease ? latestRelease.tagName : undefined 663 | repo.latestBetaRelease = latestBetaRelease && repo.latestRelease !== latestBetaRelease.tagName ? latestBetaRelease.tagName : undefined 664 | repo.latestSnapshotRelease = latestSnapshotRelease && repo.latestBetaRelease !== latestSnapshotRelease.tagName && repo.latestRelease !== latestSnapshotRelease.tagName ? latestSnapshotRelease.tagName : undefined 665 | fs.writeFileSync(`${modulePath}/${repo.name}.json`, JSON.stringify(repo)) 666 | repo.releases = latestRelease ? [latestRelease] : [] 667 | if (repo.latestBetaRelease) { 668 | repo.betaReleases = [latestBetaRelease] 669 | } 670 | if (repo.latestSnapshotRelease) { 671 | repo.snapshotReleases = [latestSnapshotRelease] 672 | } 673 | if (!repo.readmeHTML && repo.readme) repo.readmeHTML = repo.childGitHubReadme.childMarkdownRemark.html 674 | delete repo.readme 675 | delete repo.childGitHubReadme 676 | } 677 | fs.writeFileSync(`${rootPath}/modules.json`, JSON.stringify(modules)) 678 | } 679 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // eslint-disable-next-line no-unused-vars 8 | const React = require('react') 9 | 10 | export const onPreRenderHTML = ({ 11 | getHeadComponents, 12 | replaceHeadComponents 13 | }) => { 14 | const headComponents = getHeadComponents() 15 | headComponents.push( 16 | 17 | ) 18 | headComponents.push( 19 | 20 | ) 21 | headComponents.push( 22 | 23 | ) 24 | headComponents.push( 25 | 26 | ) 27 | headComponents.push( 28 | 29 | ) 30 | replaceHeadComponents(headComponents) 31 | } 32 | -------------------------------------------------------------------------------- /gh-pages-cache-restore.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // Work around for caches on gh-pages 5 | // https://github.com/gatsbyjs/gatsby/issues/15080#issuecomment-765338035 6 | const publicPath = path.join(__dirname, 'public') 7 | const publicCachePath = path.join(__dirname, 'public-cache') 8 | if (fs.existsSync(publicCachePath)) { 9 | console.log(`[onPreBuild] Cache exists, renaming ${publicCachePath} to ${publicPath}`) 10 | if (fs.existsSync(publicPath)) { 11 | fs.rmdirSync(publicPath, { recursive: true }) 12 | } 13 | fs.renameSync(publicCachePath, publicPath) 14 | } 15 | -------------------------------------------------------------------------------- /gh-pages-cache.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const glob = require('glob') 4 | const { v4: md5 } = require('uuid') 5 | 6 | // Work around for caches on gh-pages 7 | // https://github.com/gatsbyjs/gatsby/issues/15080#issuecomment-765338035 8 | const publicPath = path.join(__dirname, 'public') 9 | const publicCachePath = path.join(__dirname, 'public-cache') 10 | if (fs.existsSync(publicCachePath)) { 11 | fs.rmdirSync(publicCachePath, { recursive: true }) 12 | } 13 | fs.cpSync(publicPath, publicCachePath, { recursive: true }) 14 | console.log(`[onPostBuild] Copied ${publicPath} to ${publicCachePath}`) 15 | const hash = md5(Math.random().toString(36).substring(7)) 16 | const jsonFiles = glob.sync(`${publicPath}/page-data/**/page-data.json`) 17 | console.log(`[onPostBuild] Renaming the following files to page-data.${hash}.json:`) 18 | for (const file of jsonFiles) { 19 | console.log(file) 20 | const newFilename = file.replace('page-data.json', `page-data.${hash}.json`) 21 | fs.renameSync(file, newFilename) 22 | } 23 | const appShaFiles = glob.sync(`${publicPath}/**/app-+([^-]).js`) 24 | const [appShaFile] = appShaFiles 25 | const [appShaFilename] = appShaFile.split('/').slice(-1) 26 | const appShaFilenameReg = new RegExp(appShaFilename, 'g') 27 | const newAppShaFilename = `app-${hash}.js` 28 | const newFilePath = appShaFile.replace(appShaFilename, newAppShaFilename) 29 | console.log(`[onPostBuild] Renaming: ${appShaFilename} to ${newAppShaFilename}`) 30 | fs.renameSync(appShaFile, newFilePath) 31 | if (fs.existsSync(`${appShaFile}.map`)) { 32 | fs.renameSync(`${appShaFile}.map`, `${newFilePath}.map`) 33 | } 34 | if (fs.existsSync(`${appShaFile}.LICENSE.txt`)) { 35 | fs.renameSync(`${appShaFile}.LICENSE.txt`, `${newFilePath}.LICENSE.txt`) 36 | } 37 | const htmlJSAndJSONFiles = [ 38 | `${newFilePath}.map`, 39 | ...glob.sync(`${publicPath}/**/*.{html,js,json}`) 40 | ] 41 | console.log( 42 | `[onPostBuild] Replacing page-data.json, ${appShaFilename}, and ${appShaFilename}.map references in the following files:` 43 | ) 44 | for (const file of htmlJSAndJSONFiles) { 45 | const stats = fs.statSync(file, 'utf8') 46 | if (!stats.isFile()) { 47 | continue 48 | } 49 | const content = fs.readFileSync(file, 'utf8') 50 | const result = content 51 | .replace(appShaFilenameReg, newAppShaFilename) 52 | .replace(/page-data.json/g, `page-data.${hash}.json`) 53 | if (result !== content) { 54 | console.log(file) 55 | fs.writeFileSync(file, result, 'utf8') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /github-source.js: -------------------------------------------------------------------------------- 1 | const { ApolloClient, InMemoryCache, createHttpLink, from } = require('@apollo/client') 2 | const { RetryLink } = require('@apollo/client/link/retry') 3 | const { ApolloError } = require('@apollo/client/errors') 4 | 5 | const httpLink = createHttpLink({ 6 | uri: 'https://api.github.com/graphql', 7 | headers: { 8 | authorization: `Bearer ${process.env.GRAPHQL_TOKEN}`, 9 | } 10 | }) 11 | 12 | const retryLink = new RetryLink({ 13 | attempts: (count, _operation, /** @type {ApolloError} */ error) => { 14 | return count < 3 15 | }, 16 | delay: (_count, operation, _error) => { 17 | const context = operation.getContext() 18 | /** @type {Response} */ 19 | const response = context.response 20 | const xRatelimitRemaining = parseInt(response.headers.get('x-ratelimit-remaining')) 21 | if (!isNaN(xRatelimitRemaining) && xRatelimitRemaining > 0) { 22 | console.log('[NetworkError] retry after 1 second') 23 | return 1000 24 | } 25 | let retryAfter = parseInt(response.headers.get('retry-after')) 26 | const xRateLimitReset = parseInt(response.headers.get('x-ratelimit-reset')) 27 | if (isNaN(retryAfter) && isNaN(xRateLimitReset)) { 28 | console.log('[NetworkError] response header missing...') 29 | console.log('[NetworkError] retry after 1 min') 30 | return 60 * 1000 31 | } 32 | if (isNaN(retryAfter)) { 33 | const retryAfter = (xRateLimitReset * 1000) - Date.now() 34 | console.log(`[NetworkError] retry after ${retryAfter} ms`) 35 | } 36 | return retryAfter * 1000 37 | }, 38 | }) 39 | 40 | /** @type {import('@apollo/client').DefaultOptions} */ 41 | const defaultOptions = { 42 | watchQuery: { 43 | fetchPolicy: 'no-cache', 44 | }, 45 | query: { 46 | fetchPolicy: 'no-cache', 47 | } 48 | } 49 | 50 | const apolloClient = new ApolloClient({ 51 | link: from([retryLink, httpLink]), 52 | cache: new InMemoryCache(), 53 | defaultOptions: defaultOptions, 54 | }) 55 | 56 | const fetchFromGitHub = async (graphQLQuery) => { 57 | if (process.env.GRAPHQL_TOKEN === undefined) { 58 | throw new Error('token is undefined') 59 | } 60 | return apolloClient.query({ 61 | query: graphQLQuery, 62 | }).then((response) => { 63 | return response 64 | }) 65 | } 66 | 67 | const REGEX_PUBLIC_IMAGES = /https:\/\/github\.com\/[a-zA-Z0-9-]+\/[\w\-.]+\/assets\/\d+\/([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/g 68 | const replacePrivateImage = (markdown, html) => { 69 | const publicMatches = new Map() 70 | for (const match of markdown.matchAll(REGEX_PUBLIC_IMAGES)) { 71 | publicMatches.set(match[0], match[1]) 72 | } 73 | for (const match of publicMatches) { 74 | const regexPrivateImages = new RegExp(`https:\\/\\/private-user-images\\.githubusercontent\\.com\\/\\d+\\/\\d+-${match[1]}\\..*?(?=")`, 'g') 75 | html = html.replaceAll(regexPrivateImages, match[0]) 76 | } 77 | return html 78 | } 79 | 80 | exports.fetchFromGithub = fetchFromGitHub 81 | exports.replacePrivateImage = replacePrivateImage 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modules", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "modules", 6 | "author": "Riko Sakurauchi", 7 | "keywords": [ 8 | "gatsby" 9 | ], 10 | "scripts": { 11 | "develop": "gatsby develop", 12 | "start": "gatsby develop", 13 | "build": "gatsby build", 14 | "serve": "gatsby serve", 15 | "clean": "gatsby clean", 16 | "lint": "eslint --fix ." 17 | }, 18 | "dependencies": { 19 | "@emotion/react": "^11.10.4", 20 | "@material-ui/core": "^4.12.4", 21 | "@material-ui/icons": "^4.11.3", 22 | "@material-ui/lab": "^4.0.0-alpha.61", 23 | "@primer/css": "^20.4.8", 24 | "@types/node": "^18.11.2", 25 | "@types/react": "^18.0.21", 26 | "@types/react-dom": "^18.0.6", 27 | "@typescript-eslint/eslint-plugin": "5.40.1", 28 | "@typescript-eslint/parser": "^5.40.1", 29 | "ellipsize": "^0.5.1", 30 | "eslint": "8.25.0", 31 | "eslint-config-standard-with-typescript": "^23.0.0", 32 | "eslint-plugin-import": "2.26.0", 33 | "eslint-plugin-n": "16.6.2", 34 | "eslint-plugin-node": "11.1.0", 35 | "eslint-plugin-promise": "6.1.1", 36 | "filesize": "^10.0.5", 37 | "flexsearch": "^0.6.32", 38 | "gatsby": "^5.13.3", 39 | "gatsby-plugin-local-search": "^2.0.1", 40 | "gatsby-plugin-manifest": "^5.13.1", 41 | "gatsby-plugin-nprogress": "^5.13.1", 42 | "gatsby-plugin-postcss": "^6.13.1", 43 | "gatsby-plugin-sass": "^6.13.1", 44 | "gatsby-plugin-sitemap": "^6.13.1", 45 | "gatsby-plugin-stylus": "^5.13.1", 46 | "gatsby-remark-external-links": "^0.0.4", 47 | "gatsby-source-filesystem": "^5.13.1", 48 | "gatsby-transformer-remark": "^6.13.1", 49 | "github-syntax-dark": "^0.5.0", 50 | "github-syntax-light": "^0.5.0", 51 | "glob": "^8.0.3", 52 | "postcss": "^8.4.18", 53 | "react": "^18.2.0", 54 | "react-dom": "^18.2.0", 55 | "react-use-flexsearch": "^0.1.1", 56 | "sass": "^1.55.0", 57 | "segmentit": "^2.0.3", 58 | "typescript": "^4.8.4", 59 | "uuid": "^9.0.0" 60 | }, 61 | "packageManager": "yarn@3.1.1", 62 | "devDependencies": { 63 | "@apollo/client": "^3.10.4", 64 | "@babel/plugin-proposal-class-properties": "^7.18.6", 65 | "graphql": "^16.8.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/404.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ReactElement } from 'react' 3 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' 4 | 5 | const useStyles = makeStyles((theme: Theme) => 6 | createStyles({ 7 | landing: { 8 | display: 'flex', 9 | width: '100%', 10 | height: 'calc(100vh - 144px)' 11 | }, 12 | centerBox: { 13 | display: 'flex', 14 | width: '100%', 15 | height: '100%', 16 | textAlign: 'center', 17 | justifyContent: 'center', 18 | alignItems: 'center', 19 | flexDirection: 'column' 20 | }, 21 | h1: { 22 | color: '#bcc6cc', 23 | fontSize: 60 24 | }, 25 | h2: { 26 | color: theme.palette.type === 'light' 27 | ? '#464a4d' 28 | : '#cdd6e0', 29 | fontSize: 20, 30 | fontStyle: 'upper' 31 | } 32 | }) 33 | ) 34 | 35 | export default function _404 (): ReactElement { 36 | const classes = useStyles() 37 | return ( 38 |
39 |
40 |
404
41 |
try somewhere else
42 |
43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/components/module.scss: -------------------------------------------------------------------------------- 1 | @use "~@primer/primitives/dist/scss/colors/_light" as primer-light; 2 | @use "~@primer/primitives/dist/scss/colors/_dark_dimmed" as primer-dark; 3 | @use '~@primer/css/markdown/index'; 4 | 5 | .markdown-body { 6 | @media (prefers-color-scheme: light) { 7 | @include primer-light.primer-colors-light; 8 | @import '~github-syntax-light/lib/github-light'; 9 | } 10 | @media (prefers-color-scheme: dark) { 11 | @include primer-dark.primer-colors-dark-dimmed; 12 | @import '~github-syntax-dark/lib/github-dark'; 13 | } 14 | pre { 15 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace !important; 16 | } 17 | font-family: Roboto, 'FZ SC', sans-serif !important; 18 | } -------------------------------------------------------------------------------- /src/components/module.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useState } from 'react' 2 | import * as React from 'react' 3 | import { Grid, Tooltip } from '@material-ui/core' 4 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' 5 | import './module.scss' 6 | import { filesize } from 'filesize' 7 | 8 | const useStyles = makeStyles((theme: Theme) => 9 | createStyles({ 10 | container: { 11 | margin: '30px 0', 12 | [theme.breakpoints.down('sm')]: { 13 | margin: '10px 0' 14 | }, 15 | '& a': { 16 | color: theme.palette.secondary.main 17 | } 18 | }, 19 | releases: { 20 | '& a': { 21 | color: theme.palette.secondary.main 22 | } 23 | }, 24 | document: { 25 | wordBreak: 'break-word', 26 | '& img': { 27 | maxWidth: '100%' 28 | }, 29 | '& pre': { 30 | whiteSpace: 'pre-wrap' 31 | } 32 | }, 33 | plainDocument: { 34 | wordBreak: 'break-word' 35 | }, 36 | box: { 37 | padding: '24px 0', 38 | borderBottom: '1px solid #eaecef', 39 | '&:first-child': { 40 | paddingTop: '0.5rem' 41 | }, 42 | '&:last-child': { 43 | borderBottom: 'none' 44 | }, 45 | wordBreak: 'break-word' 46 | }, 47 | h2: { 48 | marginTop: 0, 49 | marginBottom: 14 50 | }, 51 | p: { 52 | marginTop: 10, 53 | marginBottom: 10, 54 | '&:last-child': { 55 | marginBottom: 0 56 | } 57 | }, 58 | release: { 59 | [theme.breakpoints.down('sm')]: { 60 | display: 'none' 61 | } 62 | } 63 | }) 64 | ) 65 | 66 | export default function Module ({ data }: any): ReactElement { 67 | const classes = useStyles() 68 | const [showReleaseNum, setShowReleaseNum] = useState(1) 69 | return ( 70 | <> 71 | 72 | 73 |
74 | {data.githubRepository.childGitHubReadme 75 | ? (
) 81 | : (
82 | {data.githubRepository.summary || data.githubRepository.description} 83 |
) 84 | } 85 |
86 | 87 | 88 |
89 |
90 |

Package

91 |

{data.githubRepository.name}

92 |
93 | {(data.githubRepository.collaborators?.edges.length) || 94 | (data.githubRepository.additionalAuthors?.length) 95 | ? (
96 |

Authors

97 | {data.githubRepository.collaborators 98 | ? data.githubRepository.collaborators.edges.map(({ node: collaborator }: any) => ( 99 |

100 | 103 | {collaborator.name || collaborator.login} 104 | 105 |

106 | )) 107 | : '' 108 | } 109 | {data.githubRepository.additionalAuthors 110 | ? data.githubRepository.additionalAuthors.map((author: any) => ( 111 |

112 | 113 | {author.name || author.link} 114 | 115 |

116 | )) 117 | : '' 118 | } 119 |
) 120 | : '' 121 | } 122 | {data.githubRepository.homepageUrl 123 | ? (
124 |

Support / Discussion URL

125 |

126 | {data.githubRepository.homepageUrl} 129 |

130 |
) 131 | : '' 132 | } 133 | {data.githubRepository.sourceUrl 134 | ? (
135 |

Source URL

136 |

137 | {data.githubRepository.sourceUrl} 140 |

141 |
) 142 | : '' 143 | } 144 | {data.githubRepository.releases?.edges.length 145 | ? (
146 |

Releases

147 |

148 | {data.githubRepository.releases.edges[0].node.name} 151 |

152 |

153 | Release Type: {data.githubRepository.releases.edges[0].node.isPrerelease ? 'Pre-release' : 'Stable'} 154 |

155 |

156 | {new Date(data.githubRepository.releases.edges[0].node.publishedAt).toLocaleString()} 157 |

158 |

159 | View all releases 160 |

161 |
) 162 | : '' 163 | } 164 |
165 |
166 | {data.githubRepository.releases?.edges.length 167 | ? ( 168 |
169 |

Releases

170 | {data.githubRepository.releases.edges.slice(0, showReleaseNum).map(({ node: release }: any) => ( 171 |
172 |

{release.name}

173 |

174 | Release Type: {release.isPrerelease ? 'Pre-release' : 'Stable'} 175 |

176 |

177 | {new Date(release.publishedAt).toLocaleString()} 178 |

179 |
185 | {release.releaseAssets?.edges.length 186 | ? ( 187 |
188 |

Downloads

189 |
    190 | {release.releaseAssets.edges.map(({ node: asset }: any) => ( 191 | 192 |
  • 193 | {asset.name} 194 |
  • 195 |
    196 | ))} 197 |
198 |
) 199 | : '' 200 | } 201 |
202 | ))} 203 | {showReleaseNum !== data.githubRepository.releases.edges.length 204 | ? (

205 | { 208 | e.preventDefault() 209 | setShowReleaseNum(data.githubRepository.releases.edges.length) 210 | }} 211 | >Show older versions 212 |

) 213 | : '' 214 | } 215 |
216 | ) 217 | : '' 218 | } 219 | 220 | 221 | ) 222 | } 223 | -------------------------------------------------------------------------------- /src/components/repo-card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Card from '@material-ui/core/Card' 4 | import CardActionArea from '@material-ui/core/CardActionArea' 5 | import CardActions from '@material-ui/core/CardActions' 6 | import CardContent from '@material-ui/core/CardContent' 7 | import Button from '@material-ui/core/Button' 8 | import Typography from '@material-ui/core/Typography' 9 | import { Link } from 'gatsby' 10 | 11 | export interface RepoCardProps { 12 | name: string 13 | title: string 14 | summary: string 15 | url: string 16 | sourceUrl: string 17 | } 18 | 19 | const useStyles = makeStyles({ 20 | root: { 21 | margin: 10, 22 | height: 200, 23 | display: 'flex', 24 | flexDirection: 'column', 25 | justifyContent: 'flex-end', 26 | alignItems: 'left' 27 | }, 28 | actionArea: { 29 | flex: '1 1 auto', 30 | display: 'flex', 31 | flexDirection: 'column', 32 | justifyContent: 'flex-start', 33 | alignItems: 'flex-start', 34 | overflow: 'hidden' 35 | }, 36 | cardContent: { 37 | display: 'flex', 38 | flexDirection: 'column', 39 | justifyContent: 'flex-start', 40 | alignItems: 'flex-start', 41 | overflow: 'hidden' 42 | }, 43 | body: { 44 | overflow: 'hidden' 45 | } 46 | }) 47 | 48 | export default function RepoCard (props: RepoCardProps): React.ReactElement { 49 | const classes = useStyles() 50 | return ( 51 | 52 | 54 | 55 | 56 | {props.title} 57 | 58 | 61 | {props.summary} 62 | 63 | 64 | 65 | 66 | {props.url 67 | ? () 72 | : '' 73 | } 74 | {props.sourceUrl 75 | ? () 80 | : '' 81 | } 82 | 83 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/components/search-result-card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { createStyles, alpha, makeStyles } from '@material-ui/core/styles' 3 | import { SearchResult } from 'react-use-flexsearch' 4 | import { Paper } from '@material-ui/core' 5 | import Typography from '@material-ui/core/Typography' 6 | import { Link } from 'gatsby' 7 | 8 | export interface SearchResultCardProps { 9 | searchKeyword: string 10 | searchResult: SearchResult[] 11 | className?: string 12 | } 13 | 14 | const useStyles = makeStyles((theme) => 15 | createStyles({ 16 | root: { 17 | color: theme.palette.text.primary, 18 | borderRadius: 4, 19 | maxHeight: 'calc(100vh - 100px)', 20 | overflow: 'scroll', 21 | zIndex: theme.zIndex.appBar + 1, 22 | padding: 10, 23 | boxSizing: 'border-box', 24 | [theme.breakpoints.down('xs')]: { 25 | maxHeight: 'calc(100vh - 56px)' 26 | } 27 | }, 28 | result: { 29 | width: 550, 30 | maxWidth: 'calc(100vw - 40px)', 31 | boxSizing: 'border-box', 32 | padding: '20px 15px', 33 | margin: '0 10px', 34 | borderBottom: '1px solid ' + theme.palette.divider, 35 | '&:last-child': { 36 | borderBottom: 'none' 37 | }, 38 | '&:hover': { 39 | background: theme.palette.type === 'light' 40 | ? alpha(theme.palette.common.black, 0.1) 41 | : alpha(theme.palette.common.white, 0.1) 42 | }, 43 | cursor: 'pointer', 44 | display: 'block', 45 | textDecoration: 'none', 46 | color: theme.palette.text.primary 47 | }, 48 | hide: { 49 | display: 'none' 50 | } 51 | }) 52 | ) 53 | 54 | export default function SearchResultCard (props: SearchResultCardProps): React.ReactElement { 55 | const classes = useStyles() 56 | return ( 57 | 61 | {props.searchResult.length 62 | ? props.searchResult.map((result) => ( 63 | 68 | 69 | {result.description} 70 | 71 | 73 | {result.summary || result.readmeExcerpt} 74 | 75 | 76 | )) 77 | :
80 | 83 | No results found 84 | 85 |
86 | } 87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /src/components/seo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import { graphql, useStaticQuery } from 'gatsby' 9 | import * as PropTypes from 'prop-types' 10 | import * as React from 'react' 11 | import { ReactElement } from 'react' 12 | 13 | function SEO ({ description, lang, meta, title, siteTitle, publishedTime, author, cover }: any): ReactElement { 14 | const { site } = useStaticQuery( 15 | graphql` 16 | query { 17 | site { 18 | siteMetadata { 19 | title 20 | description 21 | author 22 | } 23 | } 24 | } 25 | ` 26 | ) 27 | 28 | const metaDescription = description || site.siteMetadata.description 29 | const metaList = [ 30 | { 31 | content: metaDescription, 32 | name: 'description' 33 | }, 34 | { 35 | content: 'Medium', 36 | property: 'al:android:app_name' 37 | }, 38 | { 39 | content: title, 40 | property: 'og:title' 41 | }, 42 | { 43 | content: siteTitle, 44 | property: 'og:site_name' 45 | }, 46 | { 47 | content: metaDescription, 48 | property: 'og:description' 49 | }, 50 | { 51 | content: 'website', 52 | property: 'og:type' 53 | }, 54 | { 55 | content: author, 56 | name: 'author' 57 | }, 58 | { 59 | content: 'summary', 60 | name: 'twitter:card' 61 | }, 62 | { 63 | content: author || site.siteMetadata.author, 64 | name: 'twitter:creator' 65 | }, 66 | { 67 | content: title, 68 | name: 'twitter:title' 69 | }, 70 | { 71 | content: metaDescription, 72 | name: 'twitter:description' 73 | } 74 | ] 75 | if (publishedTime) { 76 | metaList.push({ 77 | content: publishedTime, 78 | name: 'article:published_time' 79 | }) 80 | } 81 | if (cover) { 82 | metaList.push({ 83 | content: cover, 84 | name: 'og:image' 85 | }) 86 | } 87 | const metas = metaList.concat(meta).map(m => ) 88 | 89 | return ( 90 | <> 91 | 92 | {`${title as string} - ${siteTitle as string || site.siteMetadata.title as string}`} 93 | {metas} 94 | 95 | ) 96 | } 97 | 98 | SEO.defaultProps = { 99 | description: '', 100 | lang: 'en', 101 | meta: [] 102 | } 103 | 104 | SEO.propTypes = { 105 | author: PropTypes.string, 106 | cover: PropTypes.string, 107 | description: PropTypes.string, 108 | lang: PropTypes.string, 109 | meta: PropTypes.arrayOf(PropTypes.object), 110 | publishedTime: PropTypes.string, 111 | siteTitle: PropTypes.string, 112 | title: PropTypes.string.isRequired 113 | } 114 | 115 | export default SEO 116 | -------------------------------------------------------------------------------- /src/debounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export function debounce (fn: (...args: T) => void, delay: number): (...args: T) => void { 4 | const callback = fn 5 | let timerId = 0 6 | 7 | function debounced (...args: T): void { 8 | clearTimeout(timerId) 9 | timerId = setTimeout(() => { 10 | callback.apply(this, args) 11 | }, delay) as any 12 | } 13 | 14 | return debounced 15 | } 16 | 17 | export function useDebounce (value: T, delay: number): T { 18 | const [debouncedValue, setDebouncedValue] = useState(value) 19 | useEffect( 20 | () => { 21 | const handler = setTimeout(() => { 22 | setDebouncedValue(value) 23 | }, delay) 24 | return () => { 25 | clearTimeout(handler) 26 | } 27 | }, 28 | [value] 29 | ) 30 | return debouncedValue 31 | } 32 | -------------------------------------------------------------------------------- /src/flexsearch-config.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateOptions } from 'flexsearch' 2 | 3 | declare let options: CreateOptions 4 | export = options 5 | -------------------------------------------------------------------------------- /src/flexsearch-config.js: -------------------------------------------------------------------------------- 1 | const { Segment, useDefault } = require('segmentit') 2 | 3 | module.exports = { 4 | tokenize: function (str) { 5 | const segmentit = useDefault(new Segment()) 6 | const result = segmentit.doSegment(str) 7 | return result.map((token) => token.w) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xposed-Modules-Repo/modules/9830351868b10ddd9a66082b2e14a23e6728fd76/src/images/favicon.png -------------------------------------------------------------------------------- /src/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | Container, 4 | createTheme, 5 | CssBaseline, 6 | Drawer, 7 | List, 8 | ListItem, 9 | ListItemIcon, 10 | ListItemText, 11 | MuiThemeProvider, 12 | useMediaQuery 13 | } from '@material-ui/core' 14 | import { blue } from '@material-ui/core/colors' 15 | import { createStyles, alpha, makeStyles, Theme } from '@material-ui/core/styles' 16 | import AppBar from '@material-ui/core/AppBar' 17 | import Toolbar from '@material-ui/core/Toolbar' 18 | import IconButton from '@material-ui/core/IconButton' 19 | import MenuIcon from '@material-ui/icons/Menu' 20 | import Typography from '@material-ui/core/Typography' 21 | import SearchIcon from '@material-ui/icons/Search' 22 | import InputBase from '@material-ui/core/InputBase' 23 | import AppsIcon from '@material-ui/icons/Apps' 24 | import PublishIcon from '@material-ui/icons/Publish' 25 | import './styles.styl' 26 | import { Link, useStaticQuery, graphql } from 'gatsby' 27 | import { useEffect, useState } from 'react' 28 | import { useFlexSearch } from 'react-use-flexsearch' 29 | import * as flexsearchConfig from './flexsearch-config' 30 | import { useDebounce } from './debounce' 31 | import SearchResultCard from './components/search-result-card' 32 | import FlexSearch from 'flexsearch' 33 | 34 | const useStyles = makeStyles((theme: Theme) => 35 | createStyles({ 36 | root: { 37 | flexGrow: 1 38 | }, 39 | menuButton: { 40 | marginRight: theme.spacing(2) 41 | }, 42 | title: { 43 | flexGrow: 1, 44 | display: 'none', 45 | [theme.breakpoints.up('sm')]: { 46 | display: 'block' 47 | } 48 | }, 49 | h1: { 50 | textDecoration: 'none', 51 | color: 'inherit' 52 | }, 53 | search: { 54 | position: 'relative', 55 | borderRadius: theme.shape.borderRadius, 56 | backgroundColor: alpha(theme.palette.common.white, 0.15), 57 | '&:hover': { 58 | backgroundColor: alpha(theme.palette.common.white, 0.25) 59 | }, 60 | marginLeft: 0, 61 | marginRight: '12px', 62 | width: '100%', 63 | [theme.breakpoints.up('sm')]: { 64 | marginLeft: theme.spacing(1), 65 | width: 'auto' 66 | } 67 | }, 68 | searchIcon: { 69 | padding: theme.spacing(0, 2), 70 | height: '100%', 71 | position: 'absolute', 72 | pointerEvents: 'none', 73 | display: 'flex', 74 | alignItems: 'center', 75 | justifyContent: 'center' 76 | }, 77 | inputRoot: { 78 | color: 'inherit' 79 | }, 80 | inputInput: { 81 | padding: theme.spacing(1, 1, 1, 0), 82 | // vertical padding + font size from searchIcon 83 | paddingLeft: `calc(1em + ${theme.spacing(4)}px)`, 84 | transition: theme.transitions.create('width'), 85 | width: '100%', 86 | [theme.breakpoints.up('sm')]: { 87 | width: '12ch', 88 | '&:focus': { 89 | width: '20ch' 90 | } 91 | } 92 | }, 93 | footer: { 94 | height: 80, 95 | display: 'flex', 96 | alignItems: 'center', 97 | justifyContent: 'center', 98 | fontSize: 14 99 | }, 100 | list: { 101 | width: 250 102 | }, 103 | searchResult: { 104 | position: 'absolute', 105 | right: 0, 106 | top: 'calc(100% + 8px)', 107 | [theme.breakpoints.down('xs')]: { 108 | right: -28 109 | } 110 | }, 111 | hide: { 112 | display: 'none' 113 | } 114 | }) 115 | ) 116 | 117 | let previousLoaded = false 118 | 119 | const index = FlexSearch.create(flexsearchConfig) 120 | 121 | function Layout (props: { children: React.ReactNode }): React.ReactElement { 122 | const classes = useStyles() 123 | const [isDrawerOpen, setIsDrawerOpen] = useState(false) 124 | const [searchKeyword, setSearchKeyword] = useState('') 125 | const [isSearchFocused, _setIsSearchFocused] = useState(false) 126 | const searchRef = React.createRef() 127 | const setIsSearchFocused = (focused: boolean): void => { 128 | _setIsSearchFocused(focused) 129 | if (focused) { 130 | searchRef.current?.focus() 131 | } else { 132 | searchRef.current?.blur() 133 | } 134 | } 135 | useEffect(() => { 136 | const blur = (): void => setIsSearchFocused(false) 137 | window.addEventListener('click', blur) 138 | return () => { 139 | window.removeEventListener('click', blur) 140 | } 141 | }) 142 | const debouncedSearchKeyword = useDebounce(searchKeyword, 300) 143 | const { localSearchRepositories } = useStaticQuery(graphql` 144 | { 145 | localSearchRepositories { 146 | index 147 | store 148 | } 149 | } 150 | `) 151 | useEffect(() => { 152 | index.import(localSearchRepositories.index) 153 | }, [localSearchRepositories.index]) 154 | const searchResult = useFlexSearch( 155 | debouncedSearchKeyword, 156 | index, 157 | localSearchRepositories.store, 158 | 100 159 | ).sort((a, b) => { 160 | const iq = (x: string) => { 161 | if (!x) return false 162 | return x.toLowerCase().includes(debouncedSearchKeyword.toLowerCase()) 163 | } 164 | if (iq(a.name) != iq(b.name)) return iq(b.name) - iq(a.name) 165 | if (iq(a.description) != iq(b.description)) return iq(b.description) - iq(a.description) 166 | if (iq(a.summary) != iq(b.summary)) return iq(b.summary) - iq(a.summary) 167 | return 1 168 | }).slice(0, 6) 169 | const toggleDrawer = (): void => { 170 | setIsDrawerOpen(!isDrawerOpen) 171 | } 172 | return ( 173 |
174 | 175 | 176 | 182 | 183 | 184 |
185 | 188 | Xposed Module Repository 189 | 190 |
191 |
{ setIsSearchFocused(true); e.stopPropagation() }} 194 | > 195 |
196 | 197 |
198 | { setSearchKeyword(e.target.value) }} 208 | /> 209 | 214 |
215 |
216 |
217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | <>{props.children} 231 | 232 |
233 | © 2021 - {new Date().getFullYear()} New Xposed Module Repository 234 |
235 |
236 | ) 237 | } 238 | 239 | export const Splash = React.memo(() => ( 240 | <> 241 |
242 |