├── .eslintignore ├── .yarnrc.yml ├── bin └── cmark-gfm ├── src ├── images │ └── favicon.png ├── flexsearch-config.d.ts ├── flexsearch-config.js ├── shims.d.ts ├── pages │ ├── 404.tsx │ └── submission.tsx ├── styles.styl ├── components │ ├── module.scss │ ├── 404.tsx │ ├── repo-card.tsx │ ├── search-result-card.tsx │ ├── seo.tsx │ └── module.tsx ├── debounce.ts ├── templates │ ├── module.tsx │ └── index.tsx └── layout.tsx ├── .eslintrc ├── README.md ├── gh-pages-cache-restore.js ├── tsconfig.json ├── .github ├── workflows │ ├── build.yml │ └── tag.yml └── actions │ └── build │ └── action.yml ├── gatsby-ssr.js ├── .gitignore ├── package.json ├── gh-pages-cache.js ├── github-source.js ├── gatsby-config.js └── gatsby-node.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .cache 2 | public 3 | node_modules 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /bin/cmark-gfm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xposed-Modules-Repo/modules/HEAD/bin/cmark-gfm -------------------------------------------------------------------------------- /src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xposed-Modules-Repo/modules/HEAD/src/images/favicon.png -------------------------------------------------------------------------------- /src/flexsearch-config.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateOptions } from 'flexsearch' 2 | 3 | declare let options: CreateOptions 4 | export = options 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-use-flexsearch' { 2 | export interface SearchResult { 3 | name: string 4 | description: string 5 | summary: string 6 | readmeExcerpt: string 7 | } 8 | const useFlexSearch: (query: string, index: object | string, store: object, limit?: number) => SearchResult[] 9 | } 10 | 11 | declare module 'segmentit' { 12 | const Segment: any 13 | const useDefault: any 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ReactElement } from 'react' 3 | import Layout from '../layout' 4 | import SEO from '../components/seo' 5 | import _404 from '../components/404' 6 | 7 | export default function NotFoundPage (): ReactElement { 8 | return ( 9 | 10 | <_404 /> 11 | 12 | ) 13 | } 14 | 15 | export const Head = (): ReactElement => 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/styles.styl: -------------------------------------------------------------------------------- 1 | body { 2 | margin 0 3 | font-family: Roboto, 'FZ SC', sans-serif !important; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | body { 8 | background-color #303030 !important 9 | } 10 | } 11 | 12 | .splash { 13 | position fixed 14 | left 0 15 | right 0 16 | top 0 17 | bottom 0 18 | z-index -1 19 | display flex 20 | justify-content center 21 | align-items center 22 | color #999 23 | font-size 30px 24 | font-family sans-serif 25 | } 26 | 27 | .ssr { 28 | opacity: 0; 29 | } 30 | .fade { 31 | transition: opacity 200ms; 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "noImplicitAny": true, 5 | "module": "commonjs", 6 | "target": "es6", 7 | "lib": ["es5", "es6", "es2015", "es7", "es2016", "es2017", "es2018", "es2019", "es2020", "esnext", "dom"], 8 | "jsx": "react", 9 | "experimentalDecorators": true, 10 | "declaration": true, 11 | "declarationDir": "./@types", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "strictNullChecks": true 15 | }, 16 | "include": [ 17 | "./src/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/templates/module.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import Layout from '../layout' 3 | import * as React from 'react' 4 | import { graphql } from 'gatsby' 5 | import SEO from '../components/seo' 6 | import Module from '../components/module' 7 | 8 | const getSummary = (repo: any): string => { 9 | let summary = '' 10 | if (repo.summary) summary = repo.summary 11 | else if (repo.readmeHTML) { 12 | summary = repo.readmeHTML 13 | } else if (repo.childGitHubReadme) { 14 | summary = repo.childGitHubReadme.childMarkdownRemark.excerpt 15 | } 16 | return summary 17 | } 18 | 19 | export default function ModulePage ({ data }: any): ReactElement { 20 | return ( 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export const query = graphql` 29 | query ($name: String!) { 30 | githubRepository(name: {eq: $name}) { 31 | name 32 | description 33 | url 34 | homepageUrl 35 | collaborators { 36 | edges { 37 | node { 38 | login 39 | name 40 | } 41 | } 42 | } 43 | readme 44 | readmeHTML 45 | summary 46 | sourceUrl 47 | hide 48 | additionalAuthors { 49 | name 50 | link 51 | type 52 | } 53 | childGitHubReadme { 54 | childMarkdownRemark { 55 | html 56 | excerpt 57 | } 58 | } 59 | releases { 60 | edges { 61 | node { 62 | name 63 | url 64 | description 65 | descriptionHTML 66 | createdAt 67 | publishedAt 68 | updatedAt 69 | tagName 70 | isPrerelease 71 | releaseAssets { 72 | edges { 73 | node { 74 | name 75 | contentType 76 | downloadUrl 77 | downloadCount 78 | size 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | ` 88 | export const Head = ({ data }: any): ReactElement => 89 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/templates/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Layout from '../layout' 3 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' 4 | import Pagination from '@material-ui/lab/Pagination' 5 | import PaginationItem from '@material-ui/lab/PaginationItem' 6 | import RepoCard, { RepoCardProps } from '../components/repo-card' 7 | import { graphql, Link } from 'gatsby' 8 | import SEO from '../components/seo' 9 | import { ReactElement } from 'react' 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | title: { 14 | textAlign: 'center' 15 | }, 16 | container: { 17 | display: 'grid', 18 | gridTemplateColumns: 'repeat(2, 50%)', 19 | [theme.breakpoints.down('xs')]: { 20 | gridTemplateColumns: 'repeat(1, 100%)' 21 | } 22 | }, 23 | pagination: { 24 | display: 'flex', 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | margin: 10 28 | } 29 | }) 30 | ) 31 | 32 | export default function IndexPage ({ data }: any): React.ReactElement { 33 | const classes = useStyles() 34 | const getRepoCardProps = (repo: any): RepoCardProps => { 35 | const title = repo.description 36 | let summary = '' 37 | if (repo.summary) summary = repo.summary 38 | else if (repo.childGitHubReadme) { 39 | summary = repo.childGitHubReadme.childMarkdownRemark.excerpt 40 | } 41 | const url = repo.homepageUrl || repo.url 42 | let sourceUrl 43 | if (repo.sourceUrl) sourceUrl = repo.sourceUrl 44 | else sourceUrl = repo.url 45 | return { 46 | title, summary, url, sourceUrl, name: repo.name 47 | } 48 | } 49 | return ( 50 | 51 |

Xposed Module Repository

52 |
53 | {data.allGithubRepository.edges 54 | .map(({ node: repo }: any) => ( 55 |
56 | 57 |
58 | )) 59 | } 60 |
61 | {data.allGithubRepository.pageInfo.pageCount > 1 62 | ? (
63 | ( 68 | 73 | )} 74 | /> 75 |
) 76 | : '' 77 | } 78 |
79 | ) 80 | } 81 | 82 | export const query = graphql`query ($skip: Int!, $limit: Int!) { 83 | allGithubRepository( 84 | skip: $skip 85 | limit: $limit 86 | filter: {isModule: {eq: true}, hide: {eq: false}} 87 | sort: [ { latestReleaseTime: DESC }, { latestBetaReleaseTime: DESC } ] 88 | ) { 89 | edges { 90 | node { 91 | name 92 | description 93 | url 94 | homepageUrl 95 | collaborators { 96 | edges { 97 | node { 98 | login 99 | name 100 | } 101 | } 102 | } 103 | readme 104 | summary 105 | sourceUrl 106 | hide 107 | additionalAuthors { 108 | name 109 | link 110 | type 111 | } 112 | childGitHubReadme { 113 | childMarkdownRemark { 114 | excerpt(pruneLength: 250, truncate: true) 115 | } 116 | } 117 | } 118 | } 119 | pageInfo { 120 | currentPage 121 | pageCount 122 | } 123 | } 124 | }` 125 | 126 | export const Head = (): ReactElement => 127 | -------------------------------------------------------------------------------- /src/pages/submission.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ReactElement, useState } from 'react' 3 | import Layout from '../layout' 4 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' 5 | import SEO from '../components/seo' 6 | import { Button, Container, FormControl, InputLabel, MenuItem, Select, TextField } from '@material-ui/core' 7 | 8 | const useStyles = makeStyles((theme: Theme) => 9 | createStyles({ 10 | title: { 11 | textAlign: 'center' 12 | }, 13 | formControl: { 14 | margin: theme.spacing(1), 15 | verticalAlign: 'bottom', 16 | flex: '1 1 auto' 17 | }, 18 | formInput: { 19 | margin: theme.spacing(1), 20 | verticalAlign: 'bottom', 21 | flex: '1 1 auto', 22 | [theme.breakpoints.down('xs')]: { 23 | minWidth: '100%', 24 | marginLeft: 0 25 | } 26 | }, 27 | label: { 28 | padding: '1.9rem 0 0.6rem', 29 | lineHeight: '1rem', 30 | display: 'inline-block', 31 | verticalAlign: 'bottom' 32 | }, 33 | labelInput: { 34 | padding: '1.9rem 0 0.6rem', 35 | lineHeight: '1rem', 36 | display: 'inline-block', 37 | verticalAlign: 'bottom', 38 | [theme.breakpoints.down('xs')]: { 39 | paddingBottom: 0 40 | } 41 | }, 42 | flex: { 43 | display: 'flex', 44 | alignItems: 'end', 45 | justifyContent: 'left', 46 | flexWrap: 'wrap' 47 | }, 48 | submit: { 49 | display: 'flex', 50 | justifyContent: 'center', 51 | margin: 20 52 | }, 53 | landing: { 54 | display: 'flex', 55 | flexDirection: 'column', 56 | justifyContent: 'center', 57 | minHeight: 'calc(100vh - 144px)' 58 | } 59 | }) 60 | ) 61 | 62 | export default function SubmissionPage (): ReactElement { 63 | let initialSubmissionType = 'submission' 64 | if (typeof window !== 'undefined') { 65 | const match = window.location.search.match(/\?.*type=([^&#]*)/) 66 | if (match) { 67 | initialSubmissionType = match[1] 68 | } 69 | } 70 | const classes = useStyles() 71 | const [submissionType, _setSubmissionType] = useState(initialSubmissionType) 72 | const setSubmissionType = (type: string): void => { 73 | _setSubmissionType(type) 74 | if (typeof window !== 'undefined') { 75 | history.pushState({}, '', `?type=${type}`) 76 | } 77 | } 78 | const [packageName, setPackageName] = useState('') 79 | const isPackageNameValid = (): boolean => { 80 | if (!packageName.match(/\./)) return false 81 | const groups = packageName.split('.') 82 | for (const group of groups) { 83 | if (!group.match(/^[a-zA-Z_][a-zA-Z_0-9]*$/)) return false 84 | } 85 | return true 86 | } 87 | const [title, setTitle] = useState('') 88 | const [description, setDescription] = useState('') 89 | const showPackageName = (): boolean => { 90 | return ['submission', 'transfer', 'appeal'].includes(submissionType) 91 | } 92 | const isValidForm = (): boolean => { 93 | return showPackageName() ? !!packageName && isPackageNameValid() : !!title 94 | } 95 | const submit = (): void => { 96 | if (typeof window !== 'undefined') { 97 | const issueTitle = showPackageName() 98 | ? `[${submissionType}] ${packageName}` 99 | : `[${submissionType}] ${title}` 100 | window.open(`https://github.com/Xposed-Modules-Repo/submission/issues/new?title=${ 101 | encodeURIComponent(issueTitle) 102 | }&body=${ 103 | encodeURIComponent(description) 104 | }`, '_blank') 105 | } 106 | } 107 | return ( 108 | 109 | 110 |

Submit Your Xposed Module!

111 |
112 |
113 | I'd like to 114 | 115 | Select 116 | 128 | 129 |
130 | {showPackageName() 131 | ? (
132 | Package name: 133 | setPackageName(e.target.value)} 139 | /> 140 |
) 141 | : (
142 | Title: 143 | setTitle(e.target.value)} 149 | /> 150 |
) 151 | } 152 |
153 | Description (Reason): 154 | setDescription(e.target.value)} 160 | /> 161 |
162 |
163 | 169 |
170 |
171 |
172 |
173 | ) 174 | } 175 | 176 | export const Head = (): ReactElement => 177 | -------------------------------------------------------------------------------- /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/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: 32, 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 | Privacy Policy 237 |
238 |
239 | ) 240 | } 241 | 242 | export const Splash = React.memo(() => ( 243 | <> 244 |
245 |