├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------