init
This commit is contained in:
commit
d71716b35b
24 changed files with 9549 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
dist/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 sup39
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# いぬぼき学習進捗管理
|
||||
[いぬぼき学習サイト](https://inuboki.com/)で学習進捗を管理するための拡張機能
|
||||
|
||||
## 対応ブラウザ
|
||||
- [Brave](https://brave.com/ja/download/)
|
||||
- Chrome
|
||||
|
||||
## 機能
|
||||
- 文章一覧ページで各文章の学習状況(なし、0%、50%、100%)を記録
|
||||
- 文章一覧ページで学習状況の集計(学習済み・未学習の数と割合)
|
||||
|
||||
### TODO
|
||||
- 文章ページで学習状況を記録
|
||||
|
||||
## インストール
|
||||
まず、releaseページでzipファイルをダウンロードして解凍します。
|
||||
|
||||
次に、Braveなら[brave://extensions](brave://extensions)、
|
||||
Chromeなら[chrome://extensions](chrome://extensions)をアクセスし、
|
||||
右上の「デベロッパーモード」スイッチを有効にすると、
|
||||
左上に三つのボタンが出てくるはずです。
|
||||
|
||||
「パッケージ化されていない拡張機能を読み込む」ボタンを押し、
|
||||
先程解凍したフォルダを選択するとインストールされます。
|
20
background.js
Normal file
20
background.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/* chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.storage.sync.set({});
|
||||
}); */
|
||||
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
const {name: pageKey} = port;
|
||||
port.onMessage.addListener(({topic, payload}) => {
|
||||
if (topic === 'setProgress') {
|
||||
chrome.storage.sync.get(pageKey, ({[pageKey]: data}) => {
|
||||
const {key, item} = payload;
|
||||
chrome.storage.sync.set({[pageKey]: {...data, [key]: item}});
|
||||
port.postMessage({topic, payload});
|
||||
});
|
||||
} else if (topic === 'getAllProgress') {
|
||||
chrome.storage.sync.get(pageKey, ({[pageKey]: data}) => {
|
||||
port.postMessage({topic: 'setAllProgress', payload: data ?? {}});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
5
deploy
Executable file
5
deploy
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
cd "$(realpath "$(dirname "$0")")"
|
||||
|
||||
mkdir -p dist/
|
||||
cp -Lr manifest.json background.js images/ main.js main.css dist/
|
3
frontend/.gitignore
vendored
Normal file
3
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
build/
|
||||
yarn-error.log
|
20
frontend/config-overrides.js
Normal file
20
frontend/config-overrides.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
module.exports = function override(config, _env) {
|
||||
// Get rid of hash for js files
|
||||
config.output.filename = '[name].js';
|
||||
config.output.chunkFilename = '[name].chunk.js';
|
||||
|
||||
// Get rid of hash for css files
|
||||
const cssPlugin = config.plugins.find(element => element.constructor.name === 'MiniCssExtractPlugin');
|
||||
cssPlugin.options.filename = '[name].css';
|
||||
cssPlugin.options.chunkFilename = '[name].css';
|
||||
|
||||
// Get rid of hash for media files
|
||||
/* config.module.rules[1]?.oneOf.forEach(oneOf => {
|
||||
if (!oneOf.options || oneOf.options.name !== 'static/media/[name].[hash:8].[ext]') {
|
||||
return;
|
||||
}
|
||||
oneOf.options.name = 'static/media/[name].[ext]';
|
||||
}); */
|
||||
|
||||
return config;
|
||||
};
|
58
frontend/package.json
Normal file
58
frontend/package.json
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "inuboki-frontend",
|
||||
"version": "0.1.0",
|
||||
"homepage": ".",
|
||||
"license": "MIT",
|
||||
"author": "sup39 <dev@sup39.dev> (https://github.com/sup39/supStudy-inuboki)",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.2",
|
||||
"@sup39/eslint-config-typescript": "^0.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@types/chrome": "^0.0.190",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^17.0.20",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"dotenv-cli": "^5.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"sass": "^1.51.0",
|
||||
"typescript": "^4.4.2",
|
||||
"web-vitals": "^2.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "GENERATE_SOURCEMAP=false react-app-rewired build",
|
||||
"build:dev": "GENERATE_SOURCEMAP=false dotenv -e .env.development react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"@sup39/eslint-config-typescript"
|
||||
],
|
||||
"globals": {
|
||||
"chrome": false
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
6
frontend/public/index.html
Normal file
6
frontend/public/index.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div class="sidelong__article">
|
||||
<a href="https://inuboki.com/2q-syoubo-kouza/chapter0-1/">0-1</a>
|
||||
</div>
|
||||
<div class="sidelong__article">
|
||||
<a href="https://inuboki.com/2q-syoubo-kouza/chapter0-2/">0-2</a>
|
||||
</div>
|
19
frontend/src/App.tsx
Normal file
19
frontend/src/App.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {useSelector} from './store';
|
||||
import {ProgressItem, selectProgress} from './reducer';
|
||||
|
||||
type Port = ReturnType<typeof chrome.runtime.connect>;
|
||||
|
||||
const progressValues = [null, 0, 1, 2] as (null|ProgressItem['progress'])[];
|
||||
export default function App({itemKey, port}: {itemKey: string, port: Port}) {
|
||||
const progress = useSelector(selectProgress)[itemKey]?.progress ?? null;
|
||||
|
||||
return <table className="sidelong__link ProgressItemRoot"><tr>{progressValues.map(val =>
|
||||
<td key={val}
|
||||
className={'ProgressItem_'+val+(val===progress ? ' selected' : '')}
|
||||
onClick={() => port.postMessage({topic: 'setProgress', payload: {
|
||||
key: itemKey,
|
||||
item: val == null ? null : {progress: val, lastStudy: new Date()},
|
||||
}})}
|
||||
/>,
|
||||
)}</tr></table>;
|
||||
}
|
25
frontend/src/Summary.tsx
Normal file
25
frontend/src/Summary.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import {useSelector} from './store';
|
||||
import {selectProgress} from './reducer';
|
||||
|
||||
export default function Summary({totalCount}: {totalCount: number}) {
|
||||
const doneCount = Object.values(useSelector(selectProgress))
|
||||
.reduce((a, v)=>a+(v?.progress??0), 0)/2;
|
||||
const leftCount = totalCount-doneCount;
|
||||
|
||||
return <p>
|
||||
<table className="ProgressSummary">
|
||||
<tr>
|
||||
<td>完成</td>
|
||||
<td><span>{doneCount.toFixed(1)}</span> / <span>{totalCount.toFixed(1)}</span></td>
|
||||
<td>=</td>
|
||||
<td><span>{(doneCount/totalCount*100).toFixed(2)}%</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>残り</td>
|
||||
<td><span>{leftCount.toFixed(1)}</span> / <span>{totalCount.toFixed(1)}</span></td>
|
||||
<td>=</td>
|
||||
<td><span>{(leftCount/totalCount*100).toFixed(2)}%</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</p>;
|
||||
}
|
56
frontend/src/index.sass
Normal file
56
frontend/src/index.sass
Normal file
|
@ -0,0 +1,56 @@
|
|||
.ProgressItemRoot
|
||||
border-collapse: collapse
|
||||
margin: 0
|
||||
padding: 0 10px 10px
|
||||
td
|
||||
width: 4em
|
||||
height: 4em
|
||||
position: relative
|
||||
border: 1px solid #666
|
||||
cursor: pointer
|
||||
td:before
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
width: 34%
|
||||
height: 34%
|
||||
border-radius: 8px
|
||||
|
||||
/* gray */
|
||||
.ProgressItem_null:before
|
||||
background: #bbb
|
||||
.ProgressItem_null:hover
|
||||
background: #eee
|
||||
.ProgressItem_null:active, .ProgressItem_null.selected
|
||||
background: #ddd
|
||||
/* red */
|
||||
.ProgressItem_0:before
|
||||
background: #fbb
|
||||
.ProgressItem_0:hover
|
||||
background: #fee
|
||||
.ProgressItem_0:active, .ProgressItem_0.selected
|
||||
background: #fdd
|
||||
/* yellow */
|
||||
.ProgressItem_1:before
|
||||
background: #f4f494
|
||||
.ProgressItem_1:hover
|
||||
background: #ffe
|
||||
.ProgressItem_1:active, .ProgressItem_1.selected
|
||||
background: #ffc
|
||||
/* blue */
|
||||
.ProgressItem_2:before
|
||||
background: #bff
|
||||
.ProgressItem_2:hover
|
||||
background: #eff
|
||||
.ProgressItem_2:active, .ProgressItem_2.selected
|
||||
background: #dff
|
||||
|
||||
.ProgressSummary
|
||||
border: none !important
|
||||
width: unset !important
|
||||
tr, td
|
||||
border: none !important
|
||||
td
|
||||
text-align: right
|
62
frontend/src/index.tsx
Normal file
62
frontend/src/index.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
import store from './store';
|
||||
import {setAllProgress, setProgress} from './reducer';
|
||||
import './index.sass';
|
||||
import App from './App';
|
||||
import Summary from './Summary';
|
||||
|
||||
const articles = document.querySelectorAll('.sidelong__article');
|
||||
if (articles.length) {
|
||||
const pageKey = window.location.pathname.replace(/^\/|\/$/g, '');
|
||||
|
||||
const port = chrome.runtime.connect({name: pageKey});
|
||||
port.onMessage.addListener(({topic, payload}) => {
|
||||
if (topic === 'setAllProgress') {
|
||||
store.dispatch(setAllProgress(payload));
|
||||
} else if (topic === 'setProgress') {
|
||||
store.dispatch(setProgress(payload));
|
||||
}
|
||||
});
|
||||
port.postMessage({topic: 'getAllProgress'});
|
||||
|
||||
/* const port: any = {
|
||||
postMessage({topic, payload}: {topic: string, payload: any}) {
|
||||
if (topic !== 'setProgress') return;
|
||||
setTimeout(() => {
|
||||
store.dispatch(setProgress(payload));
|
||||
}, 1000);
|
||||
},
|
||||
}; */
|
||||
|
||||
document.querySelectorAll('.sidelong__article').forEach(p => {
|
||||
const href = p.querySelector('a')?.href;
|
||||
if (href == null) return;
|
||||
const itemKey = href.split('/').slice(-2)[0];
|
||||
|
||||
const div = document.createElement('div');
|
||||
p.appendChild(div);
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<App itemKey={itemKey} port={port}/>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
div,
|
||||
);
|
||||
});
|
||||
|
||||
const summary = document.querySelector('.entry-content > p');
|
||||
if (summary) { // TODO
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<Summary totalCount={articles.length} />
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
summary,
|
||||
);
|
||||
}
|
||||
}
|
31
frontend/src/reducer.ts
Normal file
31
frontend/src/reducer.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
|
||||
import {RootState} from './store';
|
||||
|
||||
export type ProgressItem = {
|
||||
lastStudy: Date,
|
||||
progress: 0 | 1 | 2,
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
items: {} as {[key: string]: ProgressItem|null},
|
||||
};
|
||||
|
||||
const mainSlice = createSlice({
|
||||
name: 'main',
|
||||
initialState,
|
||||
reducers: {
|
||||
setProgress(state, {payload: {key, item}}: PayloadAction<{
|
||||
key: string
|
||||
item: null|ProgressItem
|
||||
}>) {
|
||||
state.items[key] = item;
|
||||
},
|
||||
setAllProgress(state, {payload}: PayloadAction<typeof state.items>) {
|
||||
state.items = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const mainReducer = mainSlice.reducer;
|
||||
export const {setProgress, setAllProgress} = mainSlice.actions;
|
||||
export const selectProgress = (state: RootState) => state.main.items;
|
13
frontend/src/store.ts
Normal file
13
frontend/src/store.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {configureStore} from '@reduxjs/toolkit';
|
||||
import {mainReducer} from './reducer';
|
||||
import * as Redux from 'react-redux';
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
main: mainReducer,
|
||||
},
|
||||
});
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export const useDispatch = () => Redux.useDispatch<typeof store.dispatch>();
|
||||
export const useSelector: Redux.TypedUseSelectorHook<RootState> = Redux.useSelector;
|
||||
export default store;
|
27
frontend/tsconfig.json
Normal file
27
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
9133
frontend/yarn.lock
Normal file
9133
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
BIN
images/icon128.png
Normal file
BIN
images/icon128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
images/icon16.png
Normal file
BIN
images/icon16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
images/icon32.png
Normal file
BIN
images/icon32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
images/icon48.png
Normal file
BIN
images/icon48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
1
main.css
Symbolic link
1
main.css
Symbolic link
|
@ -0,0 +1 @@
|
|||
frontend/build/main.css
|
1
main.js
Symbolic link
1
main.js
Symbolic link
|
@ -0,0 +1 @@
|
|||
frontend/build/main.js
|
23
manifest.json
Normal file
23
manifest.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "いぬぼき学習進捗管理",
|
||||
"description": "いぬぼき学習サイトで学習進捗を管理するための拡張機能",
|
||||
"version": "0.1.0",
|
||||
"manifest_version": 3,
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://inuboki.com/*"],
|
||||
"css": ["main.css"],
|
||||
"js": ["main.js"]
|
||||
}
|
||||
],
|
||||
"permissions": ["storage"],
|
||||
"icons": {
|
||||
"16": "/images/icon16.png",
|
||||
"32": "/images/icon32.png",
|
||||
"48": "/images/icon48.png",
|
||||
"128": "/images/icon128.png"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue