├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── package.json ├── readme.md ├── src ├── Components │ ├── APP │ │ ├── index.jsx │ │ └── style.scss │ ├── OptionsApp │ │ ├── GithubToken.jsx │ │ ├── Logs.jsx │ │ └── MainPage.jsx │ ├── RepoItem │ │ ├── index.jsx │ │ └── style.scss │ ├── RepoList │ │ ├── index.jsx │ │ └── style.scss │ └── SearchBar │ │ ├── index.jsx │ │ └── style.scss ├── background.js ├── constants.js ├── icon-128.png ├── manifest.json ├── options.html ├── options.jsx ├── popup.html ├── popup.jsx └── store.jsx ├── webpack.config.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "react/prefer-stateless-function": "off", 6 | "react/jsx-filename-extension": [ 7 | 1, { 8 | "extensions": [".js", ".jsx"] 9 | } 10 | ], 11 | "jsx-a11y/anchor-is-valid": [ 12 | "error", { 13 | "components": ["Link"], 14 | "specialLink": ["to", "path"] 15 | } 16 | ], 17 | "jsx-a11y/label-has-for": ["off"], 18 | "no-underscore-dangle": ["off"], 19 | "react/prop-types": ["off"] 20 | }, 21 | "env": { 22 | "browser": true, 23 | "node": true 24 | }, 25 | "plugins": ["react"], 26 | "globals": { 27 | "chrome": true 28 | } 29 | } -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Chrome Extion Upload 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: borales/actions-yarn@v2.0.0 15 | with: 16 | cmd: install # will run `yarn install` command 17 | - name: build dist 18 | uses: borales/actions-yarn@v2.0.0 19 | with: 20 | cmd: build # will run `yarn build` command 21 | - name: zip dist 22 | uses: montudor/action-zip@v0.1.0 23 | with: 24 | args: zip -qq -r ./dist.zip ./dist 25 | - name: upload 26 | uses: trmcnvn/chrome-addon@v1 27 | with: 28 | # extension is only necessary when updating an existing addon, 29 | # omitting it will create a new addon 30 | extension: klajgkhhnnipjkilfgkkjofidahjfobh 31 | zip: dist.zip 32 | client-id: ${{ secrets.CHROME_CLIENT_ID }} 33 | client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} 34 | refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | package-lock.json 4 | dist.zip 5 | yarn.lock 6 | .idea 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "star-manager", 3 | "version": "0.0.7", 4 | "description": "tiny Github star manager", 5 | "main": "index.js", 6 | "author": "Kilerd", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "babel": "^6.23.0", 10 | "babel-core": "^6.26.0", 11 | "babel-eslint": "^8.0.3", 12 | "babel-loader": "^7.1.2", 13 | "babel-plugin-transform-decorators": "^6.24.1", 14 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 15 | "babel-polyfill": "^6.26.0", 16 | "babel-preset-es2015": "^6.24.1", 17 | "babel-preset-react": "^6.24.1", 18 | "babel-preset-stage-0": "^6.24.1", 19 | "copy-webpack-plugin": "^4.3.1", 20 | "css-loader": "^0.28.7", 21 | "eslint": "^4.13.1", 22 | "eslint-config-airbnb": "^16.1.0", 23 | "eslint-plugin-import": "^2.8.0", 24 | "eslint-plugin-jsx-a11y": "^6.0.3", 25 | "eslint-plugin-react": "^7.5.1", 26 | "extract-text-webpack-plugin": "^3.0.2", 27 | "html-webpack-plugin": "^2.30.1", 28 | "node-sass": "^4.7.2", 29 | "sass-loader": "^6.0.6", 30 | "style-loader": "^0.19.1", 31 | "webpack": "^3.10.0" 32 | }, 33 | "dependencies": { 34 | "graphql-request": "^1.8.2", 35 | "react": "^16.12.0", 36 | "react-dom": "^16.12.0", 37 | "styled-components": "^4.4.1" 38 | }, 39 | "scripts": { 40 | "build": "webpack", 41 | "zip": "zip -qq -r ./dist.zip ./dist", 42 | "develop": "webpack --watch" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Star manager 0.0.7 2 | 超轻量级的 GitHub Star 管理插件 3 | 4 | ## 使用方法 5 | 1. 进入 Chrome 插件市场[下载插件](https://chrome.google.com/webstore/detail/star-manager/klajgkhhnnipjkilfgkkjofidahjfobh) 6 | 2. 进入 `chrome://extensions/shortcuts` 为 Star Manager 设置 popup 的快捷键 7 | 3. 打开 插件的设置界面(`chrome-extension://klajgkhhnnipjkilfgkkjofidahjfobh/options.html`),输入用户名和 Github Access Token,提交修改 8 | 4. 等待插件拉取用户 star 即可开始使用 9 | 10 | ## 关键字搜索 11 | 插件支持渐进式的关键字搜索,即下一个关键字会在上一个关键字的基础上做筛选。 12 | 13 | 例如,搜索 `python web async` 插件会先筛选出带有 `python` 关键字的 star 列表,再基础上再筛选 `web` 关键字,最后筛选 `async`。 14 | 15 | 关键字作用范围有: 16 | - repo 名称 E.G. `rust-lang/rust` `psf/black` 17 | - repo 的描述 18 | - repo 在 GitHub 标记的语言 19 | - repo 在 GitHub 登记的主页 20 | - repo 的 关键字 21 | 22 | 23 | ## TODO 24 | - [ ] 搜索框的方向键支持 25 | - [x] options 页面的日志支持和界面优化 26 | - [ ] 英文说明 27 | 28 | ## Changelog 29 | ### 0.0.7 30 | - 修复了 popup 页面跳转至 options 页面时没有采用新窗口打开的方式 31 | ### 0.0.6 32 | - 修复了旧版本更新上来(只存在`username` 没有 `token`)时,不显示设置 tips 的问题 33 | ### 0.0.5 34 | - 新增了 `options.html` 页面的友好提示和日志支持 35 | -------------------------------------------------------------------------------- /src/Components/APP/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchBar from '../SearchBar'; 3 | import RepoList from '../RepoList'; 4 | import './style.scss'; 5 | 6 | 7 | export default function APP() { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/Components/APP/style.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-size: 14px; 9 | } 10 | 11 | 12 | .app { 13 | width: 300px; 14 | height: 400px; 15 | padding: .5rem; 16 | 17 | overflow-y: scroll; 18 | background-color: #FCFCFC; 19 | } -------------------------------------------------------------------------------- /src/Components/OptionsApp/GithubToken.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Store } from '../../store'; 4 | import Logs from './Logs'; 5 | 6 | 7 | const TwoColumn = styled.div` 8 | display: flex; 9 | justify-content: space-between; 10 | `; 11 | 12 | export default function GithubToken() { 13 | const { state, dispatch } = React.useContext(Store); 14 | 15 | const [user, setUser] = React.useState(state.user); 16 | const [token, setToken] = React.useState(state.user); 17 | const [errorMsg, setErrorMsg] = React.useState(''); 18 | 19 | const [isUsernameEdit, setUsernameEdit] = React.useState(false); 20 | const [isTokenEdit, setTokenEdit] = React.useState(false); 21 | 22 | const realEditUsername = isUsernameEdit ? user : state.user; 23 | const realEditToken = isTokenEdit ? token : state.token; 24 | 25 | function handleUserChange(e) { 26 | setUser(e.target.value); 27 | setUsernameEdit(true); 28 | } 29 | 30 | function handleTokenChange(e) { 31 | setToken(e.target.value); 32 | setTokenEdit(true); 33 | } 34 | 35 | function submit() { 36 | if (realEditUsername === '' || realEditToken === '') { 37 | setErrorMsg('Username and token must be set'); 38 | return; 39 | } 40 | dispatch({ 41 | type: 'LOG', 42 | data: `[${new Date().toISOString()}] modify username and token`, 43 | }); 44 | dispatch({ 45 | type: 'UPDATE_USERNAME_AND_TOKEN', 46 | data: { 47 | user: realEditUsername, 48 | token: realEditToken, 49 | }, 50 | }); 51 | } 52 | 53 | return ( 54 | 55 |
56 |

Star Manager

57 |

Setting

58 |
{errorMsg}
59 |
60 | Username: 61 |
62 |
63 | Github Token: 64 |
65 | 66 |

help

67 |
68 | Github Token can be created at [Github]New personal access token 69 |
70 |
71 | NOTICE: the access token DO NOT need ANY extra scopes. 72 |
73 | 74 |
75 | 76 |
77 |

Repos ({Object.keys(state.repos).length})

78 | {Object.values(state.repos) 79 | .map(repo =>
{repo.nameWithOwner}
)} 80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/Components/OptionsApp/Logs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Store } from '../../store'; 3 | 4 | 5 | export default function Logs() { 6 | const { state } = React.useContext(Store); 7 | 8 | const unknowns = (state.logs || []).map(log =>
  • {log}
  • ); 9 | return ( 10 |
    11 |

    Log

    12 | 15 |
    16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/Components/OptionsApp/MainPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GithubToken from './GithubToken'; 3 | 4 | export default function MainPage() { 5 | return ( 6 |
    7 | 8 |
    9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/Components/RepoItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import './style.scss'; 4 | 5 | 6 | const TagSpan = styled.span` 7 | color: grey; 8 | border: 1px solid grey; 9 | font-size: 60%; 10 | border-radius: 3px; 11 | margin-left: 5px; 12 | `; 13 | 14 | export default function RepoItem(props) { 15 | const { repo } = props; 16 | 17 | function onClick() { 18 | window.open(repo.url, '_blank') 19 | .focus(); 20 | } 21 | const language = repo.primaryLanguage ? ( 22 | {repo.primaryLanguage.name}) : ''; 23 | const license = repo.licenseInfo ? ({repo.licenseInfo.name}) : ''; 24 | const isArchive = repo.isArchived ? Archived : ''; 25 | return ( 26 |
    27 |
    28 | {repo.nameWithOwner} {isArchive} 29 |
    30 |
    31 | {repo.description} 32 |
    33 |
    34 | {language} 35 | {repo.stargazers.totalCount} stars 36 | {license} 37 |
    38 |
    39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/Components/RepoItem/style.scss: -------------------------------------------------------------------------------- 1 | .repo-item{ 2 | 3 | padding: .65rem .75rem; 4 | cursor: pointer; 5 | border-bottom: 1px solid #e5e9ef; 6 | transition: .1s; 7 | outline: none; 8 | margin-bottom: .2rem; 9 | &:hover { 10 | box-shadow: 0 3px 3px #e5e9ef; 11 | background-color: #fff; 12 | } 13 | .name { 14 | font-size: 1.1rem; 15 | color: #0366d6; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | span { 20 | padding: 2px 5px; 21 | } 22 | } 23 | .description { 24 | white-space: nowrap; 25 | text-overflow: ellipsis; 26 | overflow: hidden; 27 | font-size: .9rem; 28 | color: #586069; 29 | padding: .2rem 0; 30 | } 31 | .info { 32 | color: #586069; 33 | font-size: .8rem; 34 | span:not(:first-child) { 35 | &:before { 36 | content: " | " 37 | } 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Components/RepoList/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style.scss'; 3 | import RepoItem from '../RepoItem'; 4 | import { Store } from '../../store'; 5 | 6 | export default function RepoList() { 7 | const { state } = React.useContext(Store); 8 | 9 | const { search: filter, repos } = state; 10 | if (state.user === '' || state.token === '') { 11 | return ( 12 |
    13 | please go to the options page to finish setup. 14 |
    15 | ); 16 | } 17 | 18 | let filteredRepos = []; 19 | 20 | if (filter === '') { 21 | filteredRepos = Object.values(repos); 22 | } else { 23 | const keywords = filter 24 | .split(' ') 25 | .filter(keyword => keyword !== ''); 26 | 27 | const finalFilterRepo = keywords.reduce((reducedRepoMap, keyword) => { 28 | Object 29 | .keys(reducedRepoMap) 30 | .forEach((repoId) => { 31 | const repoDetail = reducedRepoMap[repoId]; 32 | 33 | const isFullNameMatch = repoDetail.nameWithOwner.toLowerCase() 34 | .includes(keyword); 35 | const isDescriptionMatch = (repoDetail.description || '').toLowerCase() 36 | .includes(keyword); 37 | const isLinkMatch = (repoDetail.url || '').toLowerCase() 38 | .includes(keyword); 39 | const language = (repoDetail.language || {}).name || ''; 40 | const isLanguageMatch = language.toLowerCase().includes(keyword); 41 | 42 | const isTopicMatch = repoDetail.repositoryTopics.nodes.some(topic => topic.topic.name.toLowerCase().includes(keyword)); 43 | 44 | if (!(isFullNameMatch || isDescriptionMatch || isLanguageMatch || isLinkMatch || isTopicMatch)) { 45 | // eslint-disable-next-line no-param-reassign 46 | delete reducedRepoMap[repoId]; 47 | } 48 | }); 49 | return reducedRepoMap; 50 | }, Object.assign({}, repos)); 51 | 52 | filteredRepos = Object.values(finalFilterRepo); 53 | } 54 | 55 | const RepoItemList = filteredRepos.map(item => ( 56 | 57 | )); 58 | return ( 59 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/Components/RepoList/style.scss: -------------------------------------------------------------------------------- 1 | .blank { 2 | height: 300px; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | text-align: center; 7 | font-size: 1.25rem; 8 | } -------------------------------------------------------------------------------- /src/Components/SearchBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style.scss'; 3 | import { Store } from '../../store'; 4 | 5 | export default function SearchBar() { 6 | const { state, dispatch } = React.useContext(Store); 7 | 8 | function handleSearchChange(e) { 9 | dispatch({ 10 | type: 'SEARCH', 11 | data: e.target.value, 12 | }); 13 | } 14 | 15 | return ( 16 | input && input.focus()} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/Components/SearchBar/style.scss: -------------------------------------------------------------------------------- 1 | .search { 2 | width: 100%; 3 | outline: none; 4 | border: 1px solid #e9e9e9; 5 | border-radius: .28571rem; 6 | padding: .5rem .75rem; 7 | font-size: 1.1rem; 8 | } -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request'; 2 | import { GITHUB_GRAPHQL_API_ENDPOINT, QUERY } from './constants'; 3 | 4 | const storeRepos = (repos) => { 5 | const newRepos = repos.reduce((ress, item) => { 6 | const _res = ress; 7 | _res[item.id] = item; 8 | return _res; 9 | }, {}); 10 | 11 | chrome.storage.local.set({ repos: newRepos }, () => {}); 12 | }; 13 | 14 | 15 | const fetchRepos = async (user, token) => { 16 | const graphQLClient = new GraphQLClient(GITHUB_GRAPHQL_API_ENDPOINT, { 17 | headers: { 18 | authorization: `token ${token}`, 19 | }, 20 | }); 21 | const repos = []; 22 | const pageInfo = { 23 | endCursor: null, 24 | hasNextPage: true, 25 | }; 26 | while (pageInfo.hasNextPage) { 27 | const fetchedRepos = await graphQLClient.request(QUERY, { 28 | user, 29 | endcursor: pageInfo.endCursor, 30 | }); 31 | const { pageInfo: newPageInfo, nodes: pagedRepos } = fetchedRepos.user.starredRepositories; 32 | console.log(pageInfo, pagedRepos); 33 | repos.push(...pagedRepos); 34 | pageInfo.endCursor = newPageInfo.endCursor; 35 | pageInfo.hasNextPage = newPageInfo.hasNextPage; 36 | } 37 | 38 | return repos; 39 | }; 40 | 41 | const fetchProcess = async () => { 42 | chrome.storage.local.get({ user: '', token: '' }, async (result) => { 43 | const { user, token } = result; 44 | if (user === '' || token === '') { 45 | console.log('error: username and token is required'); 46 | return false; 47 | } 48 | const repos = await fetchRepos(user, token); 49 | storeRepos(repos); 50 | return true; 51 | }); 52 | }; 53 | 54 | 55 | const handleUsername = async () => { 56 | fetchProcess(); 57 | setInterval(fetchProcess, 1000 * 60 * 30); 58 | }; 59 | 60 | handleUsername(); 61 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 2 | export const GITHUB_GRAPHQL_API_ENDPOINT = 'https://api.github.com/graphql'; 3 | export const QUERY = ` query getUserStarredRepos($user: String!, $endcursor: String){ 4 | user(login: $user) { 5 | starredRepositories(first:100, after:$endcursor) { 6 | pageInfo { 7 | endCursor 8 | hasNextPage 9 | } 10 | nodes { 11 | url 12 | description 13 | isArchived 14 | isPrivate 15 | id 16 | nameWithOwner 17 | pushedAt 18 | repositoryTopics(first:100) { 19 | nodes { 20 | topic { 21 | name 22 | } 23 | } 24 | } 25 | primaryLanguage { 26 | color 27 | name 28 | } 29 | stargazers { 30 | totalCount 31 | } 32 | languages(first:100) { 33 | nodes { 34 | color, 35 | name 36 | } 37 | } 38 | licenseInfo { 39 | url 40 | name 41 | } 42 | } 43 | } 44 | } 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kilerd/StarManager/fcf8d0244b04231745b98bc67b0678c5bffa92ca/src/icon-128.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Star Manager", 4 | "version": "0.0.7", 5 | "description": "tiny Github star manager", 6 | "icons": { 7 | "128": "icon-128.png" 8 | }, 9 | "options_page": "options.html", 10 | "browser_action": { 11 | "default_popup": "popup.html" 12 | }, 13 | "background": { 14 | "scripts": [ 15 | "background.bundle.js" 16 | ] 17 | }, 18 | "permissions": [ 19 | "storage" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Star Manager Option Setting 8 | 9 | 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /src/options.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import MainPage from './Components/OptionsApp/MainPage'; 4 | import { StoreProvider } from './store'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /src/popup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import APP from './Components/APP'; 4 | import { StoreProvider } from './store'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ); 12 | -------------------------------------------------------------------------------- /src/store.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GraphQLClient } from 'graphql-request'; 3 | import { GITHUB_GRAPHQL_API_ENDPOINT, QUERY } from './constants'; 4 | 5 | export const initialStore = { 6 | search: '', 7 | user: '', 8 | token: '', 9 | repos: {}, 10 | logs: [], 11 | }; 12 | 13 | export const Store = React.createContext(initialStore); 14 | 15 | 16 | export const updateUserStarredRepo = async (data, dispatch) => { 17 | dispatch({ 18 | type: 'LOG', 19 | data: `[${new Date().toISOString()}] initialize graphql client for fetching user's repo data`, 20 | }); 21 | const graphQLClient = new GraphQLClient(GITHUB_GRAPHQL_API_ENDPOINT, { 22 | headers: { 23 | authorization: `token ${data.token}`, 24 | }, 25 | }); 26 | const pageInfo = { 27 | endCursor: null, 28 | hasNextPage: true, 29 | }; 30 | while (pageInfo.hasNextPage) { 31 | dispatch({ 32 | type: 'LOG', 33 | data: `[${new Date().toISOString()}] fetching user's paged repo data, user=${data.user}, offsetCursor=${pageInfo.endCursor}`, 34 | }); 35 | try { 36 | const fetchedRepos = await graphQLClient.request(QUERY, { 37 | user: data.user, 38 | endcursor: pageInfo.endCursor, 39 | }); 40 | const { pageInfo: newPageInfo, nodes: repos } = fetchedRepos.user.starredRepositories; 41 | dispatch({ 42 | type: 'APPEND_REPOS', 43 | data: repos, 44 | }); 45 | pageInfo.endCursor = newPageInfo.endCursor; 46 | pageInfo.hasNextPage = newPageInfo.hasNextPage; 47 | dispatch({ 48 | type: 'LOG', 49 | data: `[${new Date().toISOString()}] fetching successfully, dataCount=${repos.length}, hasNextPage=${pageInfo.hasNextPage}`, 50 | }); 51 | } catch (e) { 52 | dispatch({ 53 | type: 'LOG', 54 | data: `[${new Date().toISOString()}] fetching Error: ${e}`, 55 | }); 56 | pageInfo.hasNextPage = false; 57 | } 58 | } 59 | dispatch({ 60 | type: 'LOG', 61 | data: `[${new Date().toISOString()}] fetching end`, 62 | }); 63 | }; 64 | 65 | const dispatchMiddleware = dispath => (action) => { 66 | switch (action.type) { 67 | case 'UPDATE_USERNAME_AND_TOKEN': 68 | updateUserStarredRepo(action.data, dispath); 69 | return dispath(action); 70 | default: 71 | return dispath(action); 72 | } 73 | }; 74 | 75 | // eslint-disable-next-line no-unused-vars 76 | function reducer(state, action) { 77 | console.log(action); 78 | switch (action.type) { 79 | case 'SEARCH': 80 | return { 81 | ...state, 82 | search: action.data, 83 | }; 84 | 85 | case 'INITIAL_DATA_FROM_CHROME': 86 | return action.data; 87 | case 'LOG': 88 | const { logs } = state; 89 | logs.push(action.data); 90 | return { 91 | ...state, 92 | logs 93 | }; 94 | case 'UPDATE_USERNAME_AND_TOKEN': 95 | const { user, token } = action.data; 96 | chrome.storage.local.set({ 97 | user, 98 | token, 99 | repos: {}, 100 | }, () => { 101 | }); 102 | return { 103 | ...state, 104 | user, 105 | token, 106 | repos: {}, 107 | }; 108 | case 'APPEND_REPOS': 109 | const appendedData = action.data; 110 | const repos = Object.assign({}, state.repos); 111 | appendedData.forEach((repo) => { 112 | repos[repo.id] = repo; 113 | }); 114 | 115 | chrome.storage.local.set({ 116 | repos, 117 | }, () => { 118 | }); 119 | return { 120 | ...state, 121 | repos, 122 | }; 123 | 124 | default: 125 | return state; 126 | } 127 | } 128 | 129 | export function StoreProvider(props) { 130 | const [state, dispatch] = React.useReducer(reducer, initialStore); 131 | const value = { 132 | state, 133 | dispatch: dispatchMiddleware(dispatch), 134 | }; 135 | React.useEffect(() => { 136 | chrome.storage.local.get(initialStore, (result) => { 137 | dispatch({ 138 | type: 'INITIAL_DATA_FROM_CHROME', 139 | data: result, 140 | }); 141 | dispatch({ 142 | type: 'LOG', 143 | data: `[${new Date().toISOString()}] fetch chrome local storage`, 144 | }); 145 | }); 146 | }, []); 147 | return ( 148 | 149 | {props.children} 150 | 151 | ); 152 | } 153 | 154 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | module.exports = { 8 | entry: { 9 | popup: ['babel-polyfill', './src/popup.jsx'], 10 | background: ['babel-polyfill', './src/background.js'], 11 | options: ['babel-polyfill', './src/options.jsx'], 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: '[name].bundle.js', 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx'], 19 | }, 20 | module: { 21 | loaders: [ 22 | // We use Babel to transpile JSX 23 | { 24 | test: /\.js[x]$/, 25 | include: [path.resolve(__dirname, './src')], 26 | exclude: /node_modules/, 27 | loader: 'babel-loader', 28 | query: { 29 | plugins: ['transform-decorators-legacy'], 30 | presets: ['es2015', 'stage-0', 'react'], 31 | }, 32 | }, { 33 | test: /\.css$/, 34 | loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' }), 35 | }, 36 | { 37 | test: /\.scss$/, 38 | loader: ['style-loader', 'css-loader', 'sass-loader'], 39 | }, 40 | { 41 | test: /\.(ico|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/, 42 | use: 'file-loader?limit=100000', 43 | }, { 44 | test: /\.(jpe?g|png|gif|svg)$/i, 45 | use: [ 46 | 'file-loader?limit=100000', { 47 | loader: 'img-loader', 48 | options: { 49 | enabled: true, 50 | optipng: true, 51 | }, 52 | }, 53 | ], 54 | }, 55 | ], 56 | }, 57 | plugins: [ 58 | // create CSS file with all used styles 59 | new ExtractTextPlugin('bundle.css'), 60 | // create popup.html from template and inject styles and script bundles 61 | new HtmlWebpackPlugin({ 62 | inject: true, 63 | chunks: ['options'], 64 | filename: 'options.html', 65 | template: './src/options.html', 66 | }), 67 | new HtmlWebpackPlugin({ 68 | inject: true, 69 | chunks: ['popup'], 70 | filename: 'popup.html', 71 | template: './src/popup.html', 72 | }), 73 | // copy extension manifest and icons 74 | new CopyWebpackPlugin([ 75 | { 76 | from: './src/manifest.json', 77 | }, { 78 | context: './src', 79 | from: 'icon-**', 80 | }, 81 | ]), 82 | ], 83 | }; 84 | --------------------------------------------------------------------------------