procon_bypass_man-web 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.babelrc +6 -0
  3. data/.circleci/config.yml +73 -0
  4. data/.gitignore +12 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +26 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +18 -0
  9. data/Gemfile.lock +97 -0
  10. data/LICENSE.txt +21 -0
  11. data/Procfile +2 -0
  12. data/README.md +43 -0
  13. data/Rakefile +4 -0
  14. data/bin/console +15 -0
  15. data/bin/pbm_web +7 -0
  16. data/bin/setup +8 -0
  17. data/jest.config.ts +194 -0
  18. data/lib/procon_bypass_man/web.rb +20 -0
  19. data/lib/procon_bypass_man/web/db.rb +33 -0
  20. data/lib/procon_bypass_man/web/migration/001_create_settings_table.sql +4 -0
  21. data/lib/procon_bypass_man/web/models/base_model.rb +47 -0
  22. data/lib/procon_bypass_man/web/models/setting.rb +22 -0
  23. data/lib/procon_bypass_man/web/public/bundle.js +2 -0
  24. data/lib/procon_bypass_man/web/public/bundle.js.LICENSE.txt +57 -0
  25. data/lib/procon_bypass_man/web/public/index.html +1 -0
  26. data/lib/procon_bypass_man/web/server.rb +139 -0
  27. data/lib/procon_bypass_man/web/setting_parser.rb +190 -0
  28. data/lib/procon_bypass_man/web/storage.rb +25 -0
  29. data/lib/procon_bypass_man/web/version.rb +7 -0
  30. data/package.json +48 -0
  31. data/procon_bypass_man-web.gemspec +36 -0
  32. data/src/app.tsx +5 -0
  33. data/src/components/button_setting.tsx +142 -0
  34. data/src/components/buttons_modal.tsx +110 -0
  35. data/src/components/buttons_setting.tsx +67 -0
  36. data/src/components/installable_macros.tsx +58 -0
  37. data/src/components/installable_modes.tsx +57 -0
  38. data/src/components/macro_settings.tsx +85 -0
  39. data/src/components/mode_settings.tsx +62 -0
  40. data/src/contexts/buttons_setting.ts +2 -0
  41. data/src/index.html +11 -0
  42. data/src/lib/button_state.test.ts +110 -0
  43. data/src/lib/button_state.ts +52 -0
  44. data/src/lib/button_state_diff.test.ts +123 -0
  45. data/src/lib/button_state_diff.ts +63 -0
  46. data/src/lib/buttons_setting_converter.test.ts +185 -0
  47. data/src/lib/buttons_setting_converter.ts +107 -0
  48. data/src/lib/http_client.ts +93 -0
  49. data/src/pages/bpm_page.tsx +92 -0
  50. data/src/pages/buttons_setting_page.tsx +281 -0
  51. data/src/pages/global_setting_page.tsx +83 -0
  52. data/src/pages/home.tsx +17 -0
  53. data/src/pages/recoding_mode_page.tsx +15 -0
  54. data/src/pages/top.tsx +107 -0
  55. data/src/reducers/layer_reducer.ts +120 -0
  56. data/src/types/button.ts +2 -0
  57. data/src/types/buttons_setting_type.ts +63 -0
  58. data/src/types/layer_key.ts +2 -0
  59. data/src/types/pbm_stats.ts +1 -0
  60. data/src/types/plugin.ts +43 -0
  61. data/tmp/.keep +0 -0
  62. data/tsconfig.json +75 -0
  63. data/webpack.config.js +56 -0
  64. data/yarn.lock +6815 -0
  65. metadata +150 -0
@@ -0,0 +1,25 @@
1
+ module ProconBypassMan
2
+ module Web
3
+ class Storage
4
+ def self.instance
5
+ new
6
+ end
7
+
8
+ def root_path
9
+ ProconBypassMan::Web::Setting.find_or_create_by&.root_path
10
+ end
11
+
12
+ def root_path=(value)
13
+ ProconBypassMan::Web::Setting.find_or_create_by&.update!(root_path: value)
14
+ end
15
+
16
+ def setting_path
17
+ ProconBypassMan::Web::Setting.find_or_create_by&.setting_path
18
+ end
19
+
20
+ def setting_path=(value)
21
+ ProconBypassMan::Web::Setting.find_or_create_by&.update!(setting_path: value)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProconBypassMan
4
+ module Web
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "procon_bypass_man-web",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "license": "MIT",
6
+ "devDependencies": {
7
+ "@babel/core": "^7.14.8",
8
+ "@babel/preset-env": "^7.14.9",
9
+ "@babel/preset-react": "^7.14.5",
10
+ "@babel/preset-typescript": "^7.14.5",
11
+ "@emotion/react": "^11.4.0",
12
+ "@types/jest": "^26.0.24",
13
+ "@types/js-yaml": "^4.0.2",
14
+ "@types/lodash": "^4.14.172",
15
+ "@types/md5": "^2.3.1",
16
+ "@types/react": "^17.0.11",
17
+ "@types/react-dom": "^17.0.8",
18
+ "babel-jest": "^27.0.6",
19
+ "babel-loader": "^8.2.2",
20
+ "deep-object-diff": "^1.1.0",
21
+ "file-loader": "^6.2.0",
22
+ "html-webpack-plugin": "^5.3.1",
23
+ "jest": "^27.0.6",
24
+ "js-yaml": "^4.1.0",
25
+ "lodash": "^4.17.21",
26
+ "md5": "^2.3.0",
27
+ "ts-jest": "^27.0.4",
28
+ "ts-loader": "^9.2.3",
29
+ "ts-node": "^10.1.0",
30
+ "typescript": "^4.3.4",
31
+ "webpack": "^5.39.1",
32
+ "webpack-cli": "^4.7.2",
33
+ "webpack-dev-server": "^3.11.2"
34
+ },
35
+ "scripts": {
36
+ "build": "webpack",
37
+ "server": "webpack serve",
38
+ "release-build": "NODE_ENV=production webpack",
39
+ "test": "jest"
40
+ },
41
+ "dependencies": {
42
+ "@types/react-router-dom": "^5.1.7",
43
+ "axios": "^0.21.1",
44
+ "react": "^17.0.2",
45
+ "react-dom": "^17.0.2",
46
+ "react-router-dom": "^5.2.0"
47
+ }
48
+ }
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/procon_bypass_man/web/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "procon_bypass_man-web"
7
+ spec.version = ProconBypassMan::Web::VERSION
8
+ spec.authors = ["jiikko"]
9
+ spec.email = ["n905i.1214@gmail.com"]
10
+
11
+ spec.summary = "PBM for web"
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/splaplapla/procon_bypass_man-web"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ # Uncomment to register a new dependency of your gem
30
+ spec.add_dependency "sinatra"
31
+ spec.add_dependency "webrick"
32
+ spec.add_dependency "sqlite3"
33
+
34
+ # For more information and examples about making a new gem, checkout our
35
+ # guide at: https://bundler.io/guides/creating_gem.html
36
+ end
data/src/app.tsx ADDED
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom";
3
+ import { Top } from "./pages/top";
4
+
5
+ ReactDOM.render(<Top />, document.getElementById("app"));
@@ -0,0 +1,142 @@
1
+ /** @jsx jsx */
2
+
3
+ import { jsx, css } from '@emotion/react'
4
+ import React, { useState, useContext } from "react";
5
+ import { Button } from "../types/button";
6
+ import { ButtonState } from "./../lib/button_state";
7
+ import { ButtonsModal } from "./buttons_modal";
8
+ import { ButtonsSettingContext } from "./../contexts/buttons_setting";
9
+ import { ButtonsSettingType, ButtonsInLayer, ButtonInLayer, Layers, Flip } from "../types/buttons_setting_type";
10
+ import { LayerKey } from "../types/layer_key";
11
+ import { disableFlipType, alwaysFlipType, flipIfPressedSelfType, flipIfPressedSomeButtonsType, ignoreButtonsInFlipingType, remapType, openMenuType, closeMenuType } from "../reducers/layer_reducer";
12
+
13
+ type ButtonMenuProp = {
14
+ name: Button;
15
+ layerKey: LayerKey;
16
+ buttonValue: ButtonInLayer;
17
+ layersDispatch: any;
18
+ };
19
+
20
+ const ButtonMenu = ({ name, layerKey, buttonValue, layersDispatch }: ButtonMenuProp) => {
21
+ const flipRadioName = `${layerKey}_button_menu_${name}`;
22
+ const buttonState = new ButtonState(name, buttonValue.flip, buttonValue.remap);
23
+
24
+ // for modal
25
+ const [openModal, setOpenModal] = useState(false)
26
+ const [modalCallbackOnSubmit, setModalCallbackOnSubmit] = useState(undefined as any)
27
+ const [modalCloseCallback, setModalCloseCallback] = useState(undefined as any)
28
+ const [modalTitle, setModalTitle] = useState("")
29
+ const [modalPrefillButtons, setModalPrefillButtons] = useState<Array<Button>>([])
30
+
31
+ // 無効
32
+ const handleNullFlipValue = (e: React.ChangeEvent<HTMLInputElement>) => {
33
+ layersDispatch({ type: disableFlipType, payload: { layerKey: layerKey, button: name }});
34
+ };
35
+
36
+ // 常に連打
37
+ const handleFlipValue = (e: React.ChangeEvent<HTMLInputElement>) => {
38
+ layersDispatch({ type: alwaysFlipType, payload: { layerKey: layerKey, button: name }});
39
+ };
40
+
41
+ // 自分自身への条件付き連打
42
+ const openIfPressedRadioboxModal = (e: React.ChangeEvent<HTMLInputElement>) => {
43
+ layersDispatch({ type: flipIfPressedSelfType, payload: { layerKey: layerKey, button: name }});
44
+ };
45
+
46
+ // 条件付き連打
47
+ const flipIfPressedSomeButtons = buttonValue?.flip?.if_pressed || [] as Array<Button>;
48
+ const setFlipIfPressedSomeButtonsWithPersistence = (bs: Array<Button>) => {
49
+ layersDispatch({ type: flipIfPressedSomeButtonsType, payload: { layerKey: layerKey, button: name, targetButtons: bs }});
50
+ }
51
+ const openIfPressedSomeButtonsModal = (e: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLInputElement>) => {
52
+ setOpenModal(true)
53
+ setModalTitle("特定のキーを押したときだけ")
54
+ setModalPrefillButtons(flipIfPressedSomeButtons);
55
+ setModalCallbackOnSubmit(() => setFlipIfPressedSomeButtonsWithPersistence);
56
+ setModalCloseCallback(() => setOpenModal);
57
+ }
58
+
59
+ // 無視
60
+ const forceNeutralButtons = buttonValue.flip?.force_neutral || [] as Array<Button>
61
+ const setIgnoreButtonsOnFlipingWithPersistence = (bs: Array<Button>) => {
62
+ layersDispatch({ type: ignoreButtonsInFlipingType, payload: { layerKey: layerKey, button: name, targetButtons: bs }});
63
+ }
64
+ const handleIgnoreButton = (e: React.ChangeEvent<HTMLInputElement>) => {
65
+ setOpenModal(true)
66
+ setModalTitle("連打中は特定のボタンの入力を無視する")
67
+ setModalPrefillButtons(buttonValue.flip?.force_neutral || [] as Array<Button>);
68
+ setModalCallbackOnSubmit(() => setIgnoreButtonsOnFlipingWithPersistence);
69
+ setModalCloseCallback(() => setOpenModal);
70
+ };
71
+
72
+ // リマップ
73
+ const setRemapButtonsWithPersistence = (bs: Array<Button>) => {
74
+ layersDispatch({ type: remapType, payload: { layerKey: layerKey, button: name, targetButtons: bs }});
75
+ }
76
+ const handleRemapButton = (e: React.ChangeEvent<HTMLInputElement>) => {
77
+ setOpenModal(true)
78
+ setModalTitle("リマップ")
79
+ setModalPrefillButtons(buttonValue.remap?.to || []);
80
+ setModalCallbackOnSubmit(() => setRemapButtonsWithPersistence);
81
+ setModalCloseCallback(() => setOpenModal);
82
+ };
83
+
84
+ return(
85
+ <>
86
+ <fieldset><legend><strong>連打設定</strong></legend>
87
+ <label><input type="radio" onChange={handleNullFlipValue} checked={buttonState.isDisabledFlip()}/>無効</label><br />
88
+ <label><input type="radio" onChange={handleFlipValue} checked={buttonState.isAlwaysFlip()}/>常に連打する</label><br />
89
+ <label><input type="radio" onChange={openIfPressedRadioboxModal} checked={buttonState.isFlipIfPressedSelf()}/>このボタンを押している時だけ連打する({name})</label><br />
90
+ <label>
91
+ <input type="radio" onChange={openIfPressedSomeButtonsModal} onClick={openIfPressedSomeButtonsModal} checked={buttonState.isFlipIfPressedSomeButtons()}/>
92
+ 特定のキーを押したときだけ連打する{flipIfPressedSomeButtons.length > 0 && `(${flipIfPressedSomeButtons.join(", ")})`}
93
+ </label>
94
+ </fieldset>
95
+
96
+ <fieldset><legend><strong>連打オプション</strong></legend>
97
+ <label>
98
+ <input type="checkbox" onChange={handleIgnoreButton} checked={forceNeutralButtons.length > 0} disabled={buttonState.isDisabledFlip()} />
99
+ 連打中は特定のボタンの入力を無視する{forceNeutralButtons.length > 0 && `(${forceNeutralButtons.join(", ")})`}
100
+ </label>
101
+ </fieldset>
102
+
103
+ <fieldset><legend><strong>リマップ設定</strong></legend>
104
+ <label>
105
+ <input type="checkbox" onChange={handleRemapButton} checked={buttonState.isRemap()} disabled={!buttonState.isDisabledFlip()} />
106
+ 別のボタンに置き換える{buttonState.isRemap() && `(${buttonValue.remap?.to?.join(", ")})`}
107
+ </label>
108
+ </fieldset>
109
+ <div css={css`position: relative;`}>
110
+ {openModal && <ButtonsModal callbackOnSubmit={modalCallbackOnSubmit} callbackOnClose={modalCloseCallback} title={modalTitle} prefill={modalPrefillButtons} positionOnShown={"relative"} />}
111
+ </div>
112
+ </>
113
+ )
114
+ }
115
+
116
+ type Prop = {
117
+ name: Button;
118
+ layerKey: LayerKey;
119
+ };
120
+
121
+ export const ButtonSetting: React.FC<Prop> = ({ name, layerKey }) => {
122
+ const settingContext = useContext(ButtonsSettingContext);
123
+ const handleToggle = () => {
124
+ if(isOpenMenu()) { // 閉じる
125
+ settingContext.layersDispatch({ type: closeMenuType, payload: { layerKey: layerKey, button: name }});
126
+ } else { // 開く
127
+ settingContext.layersDispatch({ type: openMenuType, payload: { layerKey: layerKey, button: name }});
128
+ }
129
+ }
130
+
131
+ const isOpenMenu = () => {
132
+ return settingContext.layers[layerKey][name].open;
133
+ }
134
+ const buttonValue = settingContext.layers[layerKey][name] || {} as ButtonInLayer;
135
+
136
+ return (
137
+ <>
138
+ <label><input type="checkbox" checked={isOpenMenu()} onChange={handleToggle}/>{name}</label>
139
+ {isOpenMenu() && <ButtonMenu name={name} layerKey={layerKey} buttonValue={buttonValue} layersDispatch={settingContext.layersDispatch} />}
140
+ </>
141
+ );
142
+ };
@@ -0,0 +1,110 @@
1
+ /** @jsx jsx */
2
+
3
+ import { jsx, css } from '@emotion/react'
4
+ import React, { useState } from "react";
5
+ import { Button, buttons } from "../types/button";
6
+
7
+ type Props = {
8
+ callbackOnSubmit: any;
9
+ callbackOnClose: any;
10
+ prefill: Array<Button>;
11
+ title: string;
12
+ positionOnShown: string;
13
+ };
14
+
15
+ type CheckedButtons = {
16
+ [key in Button] : boolean
17
+ }
18
+
19
+ export const ButtonsModal = ({ callbackOnSubmit, callbackOnClose, title, prefill, positionOnShown }: Props) => {
20
+ const [checkedButtonMap, setCheckedButtonMap] = useState(
21
+ prefill.reduce((a, b) => { a[b] = true; return a },
22
+ buttons.reduce((a, b) => { a[b] = false; return a }, {} as CheckedButtons)
23
+ )
24
+ )
25
+ const callback = callbackOnSubmit;
26
+ const handleSubmit = (e: React.MouseEvent<HTMLAnchorElement>) => {
27
+ e.preventDefault();
28
+ const bs = Object.entries(checkedButtonMap).reduce((acc, item) => {
29
+ const checked: boolean = item[1];
30
+ const button = item[0] as Button;
31
+ checked && acc.push(button);
32
+ return acc;
33
+ }, [] as Array<Button>).sort();
34
+
35
+ callbackOnSubmit(bs);
36
+ callbackOnClose(false);
37
+ };
38
+ const handleCancel = (e: React.MouseEvent<HTMLAnchorElement>) => {
39
+ e.preventDefault();
40
+ callbackOnClose(false);
41
+ }
42
+ const titlestyle = css(`
43
+ margin-top: 10px;
44
+ font-size: 1.17em;
45
+ font-weight: bold;
46
+ `)
47
+ const style = () => {
48
+ if(positionOnShown === "relative") {
49
+ return css(`
50
+ position: absolute;
51
+ align: left;
52
+ top: -400px;
53
+ width: 400px;
54
+ height: 400px;
55
+ border: solid;
56
+ background-color: white;
57
+ `);
58
+ } else {
59
+ return css(`
60
+ position: absolute;
61
+ align: left;
62
+ top: 0px;
63
+ left: 20px;
64
+ width: 400px;
65
+ height: 400px;
66
+ border: solid;
67
+ background-color: white;
68
+ `);
69
+ }
70
+ }
71
+ const aStyle = css`
72
+ background-color: #4669ff;
73
+ border-bottom: solid 2px #003aff;
74
+ border-right: solid 2px #003aff;
75
+ border-radius: 20px;
76
+ font-weight: bold;
77
+ color: #FFF;
78
+ text-decoration: none;
79
+ padding: 10px;
80
+ display: inline-block;
81
+ margin-left: 10px;
82
+ `;
83
+
84
+ const handleClick = (e: React.ChangeEvent<HTMLInputElement>) => {
85
+ setCheckedButtonMap((previousButtonStats) => {
86
+ previousButtonStats[e.target.value as Button] = e.target.checked;
87
+ return previousButtonStats;
88
+ })
89
+ }
90
+
91
+ return (
92
+ <>
93
+ <div css={style()}>
94
+ <div css={titlestyle}>{title}</div>
95
+
96
+ {buttons.map((b, index) => (
97
+ <div key={index}>
98
+ <label><input type="checkbox" value={b} defaultChecked={checkedButtonMap[b]} onChange={handleClick} />{b}</label>
99
+ </div>
100
+ ))}
101
+
102
+ <hr />
103
+ <div css={css`display: flex`}>
104
+ <a href={"#"} onClick={handleCancel} css={aStyle}>変更せず閉じる</a>
105
+ <a href={"#"} onClick={handleSubmit} css={aStyle}>決定する</a>
106
+ </div>
107
+ </div>
108
+ </>
109
+ )
110
+ }
@@ -0,0 +1,67 @@
1
+ /** @jsx jsx */
2
+
3
+ import { jsx, css } from '@emotion/react'
4
+ import React, { useState, useContext } from "react";
5
+ import { ButtonSetting } from "./button_setting";
6
+ import { MacroSettings } from "./macro_settings";
7
+ import { ModeSettings } from "./mode_settings";
8
+ import { Button, buttons } from "../types/button";
9
+ import { LayerKey } from "../types/layer_key";
10
+ import { ButtonsSettingContext } from "./../contexts/buttons_setting";
11
+
12
+ type Props = {
13
+ layerKey: LayerKey;
14
+ layerRef: any;
15
+ };
16
+
17
+ export const ButtonsSetting = ({ layerKey, layerRef }:Props) => {
18
+ const [visibility, setVisibility] = useState("hidden");
19
+ const visibilityStyle = () => {
20
+ if(visibility === "hidden") {
21
+ return css`display: none;`;
22
+ }
23
+ }
24
+ const ulStyle = css`
25
+ border: 1px solid #666;
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ justify-content: center;
29
+ list-style-type: none;
30
+ margin: 0 0 0 1em;
31
+ padding: 0;
32
+ width: 900px;
33
+ `;
34
+ const liStyle = css`
35
+ border: 1px solid #aaa;
36
+ margin: 0.2em;
37
+ padding: 0.5em;
38
+ width: 200px;
39
+ `;
40
+ layerRef.setVisibility = setVisibility;
41
+
42
+ const { layers } = useContext(ButtonsSettingContext);
43
+ const isEnableMode = !layers[layerKey].mode.disable;
44
+
45
+ return(
46
+ <div css={visibilityStyle()}>
47
+ <h4>モード</h4>
48
+ <ModeSettings layerKey={layerKey} />
49
+
50
+ <h4>マクロ</h4>
51
+ {isEnableMode && `モードが有効なので選択できません`}
52
+ {!isEnableMode && <MacroSettings layerKey={layerKey} />}
53
+
54
+ <h4>各ボタンの設定</h4>
55
+ {isEnableMode && `モードが有効なので選択できません`}
56
+ {!isEnableMode &&
57
+ <div css={ulStyle}>
58
+ {buttons.map((b, i) => (
59
+ <div key={i} css={liStyle}>
60
+ <ButtonSetting layerKey={layerKey} name={b} />
61
+ </div>
62
+ ))}
63
+ </div>
64
+ }
65
+ </div>
66
+ )
67
+ }