soiz1 commited on
Commit
e4ae1e1
·
verified ·
1 Parent(s): 51d1fca

自動クローン

Browse files
Files changed (46) hide show
  1. Dockerfile +3 -1
  2. README.md +2 -1
  3. package.json +1 -1
  4. src/addons/addons/hide-flyout/style.css +2 -2
  5. src/addons/addons/onion-skinning/style.css +2 -2
  6. src/addons/addons/paint-gradient-maker/userscript.js +2 -2
  7. src/addons/addons/paint-snap/userstyle.css +1 -1
  8. src/components/filter/filter.jsx +4 -10
  9. src/components/gui/dump.html +1 -1
  10. src/components/gui/gui.css +8 -8
  11. src/components/gui/gui.jsx +19 -56
  12. src/components/icon-button/icon-button.jsx +15 -6
  13. src/components/library-item/library-item.css +1 -1
  14. src/components/library/library.jsx +8 -1
  15. src/components/menu-bar/google-drive-save.css +103 -0
  16. src/components/menu-bar/google-drive-save.jsx +396 -118
  17. src/components/sound-editor/icon--copy-to-new.svg +0 -0
  18. src/components/sound-editor/icon--copy.svg +0 -0
  19. src/components/sound-editor/icon--delete.svg +0 -0
  20. src/components/sound-editor/icon--echo.svg +0 -0
  21. src/components/sound-editor/icon--fade-in.svg +0 -0
  22. src/components/sound-editor/icon--fade-out.svg +0 -0
  23. src/components/sound-editor/icon--faster.svg +0 -0
  24. src/components/sound-editor/icon--format.svg +0 -0
  25. src/components/sound-editor/icon--highpass.svg +0 -0
  26. src/components/sound-editor/icon--louder.svg +0 -0
  27. src/components/sound-editor/icon--lowpass.svg +0 -0
  28. src/components/sound-editor/icon--modify.svg +0 -0
  29. src/components/sound-editor/icon--mute.svg +0 -0
  30. src/components/sound-editor/icon--redo.svg +0 -0
  31. src/components/sound-editor/icon--reverse.svg +0 -0
  32. src/components/sound-editor/icon--robot.svg +0 -0
  33. src/components/sound-editor/icon--slower.svg +0 -0
  34. src/components/sound-editor/icon--softer.svg +0 -0
  35. src/components/sound-editor/icon--undo.svg +0 -0
  36. src/components/sound-editor/sound-editor.css +7 -7
  37. src/components/sound-editor/sound-editor.jsx +66 -70
  38. src/components/sprite-info/sprite-info.jsx +5 -11
  39. src/containers/extension-library.jsx +7 -1
  40. src/containers/play-button.jsx +1 -1
  41. src/containers/sound-editor.jsx +1 -1
  42. src/lib/libraries/extension-tags.js +8 -3
  43. src/lib/libraries/sound-tags.js +8 -5
  44. src/lib/tw-load-scratch-blocks-hoc.jsx +20 -16
  45. src/reducers/custom-ja.js +14 -1
  46. webpack.config.js +5 -1
Dockerfile CHANGED
@@ -14,11 +14,13 @@ RUN chmod -R 777 /app
14
  # 依存関係をインストール(競合を無視)
15
  #RUN PNPM_SKIP_BUILDS_APPROVAL=true pnpm install --prefer-offline --strict-peer-dependencies=false
16
  RUN npm install --prefer-offline --no-audit --legacy-peer-deps
 
17
 
18
  # OpenSSL の互換オプションを有効化
19
  #ENV NODE_OPTIONS="--openssl-legacy-provider"
20
  RUN sed -i 's/\$PORT/3000/g' package.json
21
-
 
22
  #RUN PNPM_SKIP_BUILDS_APPROVAL=true pnpm install scratch-vm@git+https://huggingface.co/datasets/soiz1/s4s-vm
23
  RUN npm install scratch-vm@git+https://huggingface.co/datasets/soiz1/s4s-vm
24
  # コンテナの起動時にサーバーを実行
 
14
  # 依存関係をインストール(競合を無視)
15
  #RUN PNPM_SKIP_BUILDS_APPROVAL=true pnpm install --prefer-offline --strict-peer-dependencies=false
16
  RUN npm install --prefer-offline --no-audit --legacy-peer-deps
17
+ RUN npm install --save-dev @svgr/webpack@5
18
 
19
  # OpenSSL の互換オプションを有効化
20
  #ENV NODE_OPTIONS="--openssl-legacy-provider"
21
  RUN sed -i 's/\$PORT/3000/g' package.json
22
+ RUN export NODE_OPTIONS="--max-old-space-size=12288"
23
+ ENV NODE_OPTIONS="--max-old-space-size=12288"
24
  #RUN PNPM_SKIP_BUILDS_APPROVAL=true pnpm install scratch-vm@git+https://huggingface.co/datasets/soiz1/s4s-vm
25
  RUN npm install scratch-vm@git+https://huggingface.co/datasets/soiz1/s4s-vm
26
  # コンテナの起動時にサーバーを実行
README.md CHANGED
@@ -295,4 +295,5 @@ Here's what will happen in the project state machine:
295
  ## Donate
296
  We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, and resource development efforts. Donations of any size are appreciated. Thank you!
297
 
298
- -->
 
 
295
  ## Donate
296
  We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, and resource development efforts. Donations of any size are appreciated. Thank you!
297
 
298
+ -->
299
+ app_port: 3000
package.json CHANGED
@@ -90,7 +90,7 @@
90
  "redux-throttle": "0.1.1",
91
  "scratch-audio": "github:PenguinMod/PenguinMod-Audio#develop",
92
  "scratch-blocks": "github:PenguinMod/PenguinMod-Blocks#develop-builds",
93
- "scratch-paint": "github:PenguinMod/PenguinMod-Paint#develop",
94
  "scratch-render": "github:PenguinMod/PenguinMod-Render#develop",
95
  "scratch-render-fonts": "github:PenguinMod/penguinmod-render-fonts#master",
96
  "scratch-storage": "github:PenguinMod/PenguinMod-Storage#develop",
 
90
  "redux-throttle": "0.1.1",
91
  "scratch-audio": "github:PenguinMod/PenguinMod-Audio#develop",
92
  "scratch-blocks": "github:PenguinMod/PenguinMod-Blocks#develop-builds",
93
+ "scratch-paint": "git+https://huggingface.co/datasets/soiz1/s4s-paint",
94
  "scratch-render": "github:PenguinMod/PenguinMod-Render#develop",
95
  "scratch-render-fonts": "github:PenguinMod/penguinmod-render-fonts#master",
96
  "scratch-storage": "github:PenguinMod/PenguinMod-Storage#develop",
src/addons/addons/hide-flyout/style.css CHANGED
@@ -24,7 +24,7 @@
24
  right: -1px;
25
  }
26
  .sa-flyout-border-1 {
27
- border-left: 1px solid var(--ui-primary, hsla(215, 100%, 95%, 1));
28
  }
29
  .sa-flyout-border-2 {
30
  border-left: 1px solid var(--ui-black-transparent, rgba(0, 0, 0, 0.15));
@@ -85,7 +85,7 @@
85
  }
86
 
87
  .sa-lock-object.locked .sa-lock-button {
88
- background-color: #00c3ff;
89
  border-color: #0099ff;
90
  }
91
 
 
24
  right: -1px;
25
  }
26
  .sa-flyout-border-1 {
27
+ border-left: 1px solid var(--ui-primary, hsla(259, 100%, 95%, 1));
28
  }
29
  .sa-flyout-border-2 {
30
  border-left: 1px solid var(--ui-black-transparent, rgba(0, 0, 0, 0.15));
 
85
  }
86
 
87
  .sa-lock-object.locked .sa-lock-button {
88
+ background-color: #5100ff;
89
  border-color: #0099ff;
90
  }
91
 
src/addons/addons/onion-skinning/style.css CHANGED
@@ -2,7 +2,7 @@
2
  position: relative;
3
  }
4
  .sa-onion-button:focus-within {
5
- background-color: hsla(194, 100%, 50%, 0.2);
6
  }
7
  [theme="dark"] .sa-onion-image {
8
  filter: brightness(0) invert(0.8);
@@ -11,7 +11,7 @@
11
  filter: brightness(0) invert(1);
12
  }
13
  .sa-onion-button[data-enabled="true"] {
14
- background-color: #00c3ff;
15
  }
16
 
17
  .sa-onion-group {
 
2
  position: relative;
3
  }
4
  .sa-onion-button:focus-within {
5
+ background-color: hsla(259, 100%, 50%, 0.2);
6
  }
7
  [theme="dark"] .sa-onion-image {
8
  filter: brightness(0) invert(0.8);
 
11
  filter: brightness(0) invert(1);
12
  }
13
  .sa-onion-button[data-enabled="true"] {
14
+ background-color: #5100ff;
15
  }
16
 
17
  .sa-onion-group {
src/addons/addons/paint-gradient-maker/userscript.js CHANGED
@@ -284,7 +284,7 @@ export default async function () {
284
 
285
  /* GUI Utils */
286
  function getButtonURI(name, dontCompile) {
287
- const themeHex = isPM ? "#00c3ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
288
  const guiSVG = guiIMGS[name].replace("red", themeHex);
289
  if (dontCompile) return guiSVG;
290
  else return "data:image/svg+xml;base64," + btoa(guiSVG);
@@ -497,7 +497,7 @@ export default async function () {
497
  }
498
 
499
  function genButtonTable(div) {
500
- const themeHex = isPM ? "#00c3ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
501
  const btnStyle = `color: #fff; font-weight: 600; text-align: center; padding: 10px; margin: 10px 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: ${themeHex}; transition: transform 0.2s;`;
502
 
503
  const enterBtn = document.createElement("button");
 
284
 
285
  /* GUI Utils */
286
  function getButtonURI(name, dontCompile) {
287
+ const themeHex = isPM ? "#5100ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
288
  const guiSVG = guiIMGS[name].replace("red", themeHex);
289
  if (dontCompile) return guiSVG;
290
  else return "data:image/svg+xml;base64," + btoa(guiSVG);
 
497
  }
498
 
499
  function genButtonTable(div) {
500
+ const themeHex = isPM ? "#5100ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
501
  const btnStyle = `color: #fff; font-weight: 600; text-align: center; padding: 10px; margin: 10px 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: ${themeHex}; transition: transform 0.2s;`;
502
 
503
  const enterBtn = document.createElement("button");
src/addons/addons/paint-snap/userstyle.css CHANGED
@@ -2,7 +2,7 @@
2
  position: relative;
3
  }
4
  .sa-paint-snap-button:focus-within {
5
- background-color: hsla(194, 100%, 65%, 0.2);
6
  }
7
  .sa-paint-snap-button[data-enabled="true"] .sa-paint-snap-image {
8
  filter: brightness(0) invert(1);
 
2
  position: relative;
3
  }
4
  .sa-paint-snap-button:focus-within {
5
+ background-color: hsla(259, 100%, 65%, 0.2);
6
  }
7
  .sa-paint-snap-button[data-enabled="true"] .sa-paint-snap-image {
8
  filter: brightness(0) invert(1);
src/components/filter/filter.jsx CHANGED
@@ -2,8 +2,8 @@ import classNames from 'classnames';
2
  import PropTypes from 'prop-types';
3
  import React from 'react';
4
 
5
- import filterIcon from './icon--filter.svg';
6
- import xIcon from './icon--x.svg';
7
  import styles from './filter.css';
8
 
9
  const FilterComponent = props => {
@@ -21,10 +21,7 @@ const FilterComponent = props => {
21
  [styles.isActive]: filterQuery.length > 0
22
  })}
23
  >
24
- <img
25
- className={styles.filterIcon}
26
- src={filterIcon}
27
- />
28
  <input
29
  className={classNames(styles.filterInput, inputClassName)}
30
  placeholder={placeholderText}
@@ -36,10 +33,7 @@ const FilterComponent = props => {
36
  className={styles.xIconWrapper}
37
  onClick={onClear}
38
  >
39
- <img
40
- className={styles.xIcon}
41
- src={xIcon}
42
- />
43
  </div>
44
  </div>
45
  );
 
2
  import PropTypes from 'prop-types';
3
  import React from 'react';
4
 
5
+ import { ReactComponent as FilterIcon } from './icon--filter.svg';
6
+ import { ReactComponent as XIcon } from './icon--x.svg';
7
  import styles from './filter.css';
8
 
9
  const FilterComponent = props => {
 
21
  [styles.isActive]: filterQuery.length > 0
22
  })}
23
  >
24
+ <FilterIcon className={styles.filterIcon} />
 
 
 
25
  <input
26
  className={classNames(styles.filterInput, inputClassName)}
27
  placeholder={placeholderText}
 
33
  className={styles.xIconWrapper}
34
  onClick={onClear}
35
  >
36
+ <XIcon className={styles.xIcon} />
 
 
 
37
  </div>
38
  </div>
39
  );
src/components/gui/dump.html CHANGED
@@ -1,7 +1,7 @@
1
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.00999"
2
  height="16.04844" viewBox="0,0,12.00999,16.04844">
3
  <g transform="translate(-233.95947,-171.97578)">
4
- <g data-paper-data="{&quot;isPaintingLayer&quot;:true}" fill="none" fill-rule="nonzero" stroke="#00c3ff"
5
  stroke-width="1.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray=""
6
  stroke-dashoffset="0" style="mix-blend-mode: normal">
7
  <path d="M234.70947,187.27422v-14.54844h5.50805l4.96921,5.02909v9.51935z" />
 
1
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.00999"
2
  height="16.04844" viewBox="0,0,12.00999,16.04844">
3
  <g transform="translate(-233.95947,-171.97578)">
4
+ <g data-paper-data="{&quot;isPaintingLayer&quot;:true}" fill="none" fill-rule="nonzero" stroke="#5100ff"
5
  stroke-width="1.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray=""
6
  stroke-dashoffset="0" style="mix-blend-mode: normal">
7
  <path d="M234.70947,187.27422v-14.54844h5.50805l4.96921,5.02909v9.51935z" />
src/components/gui/gui.css CHANGED
@@ -130,7 +130,7 @@
130
  background: none;
131
  border-color: rgba(255,255,255,.15);
132
  }
133
- [theme="dark"] .add-tab-button img {
134
  filter: invert(1);
135
  }
136
  .add-tab-button-disabled {
@@ -141,7 +141,7 @@
141
  display: flex;
142
  align-items: center;
143
  }
144
- .tab-addition-item img {
145
  width: 1.375rem;
146
  }
147
 
@@ -189,29 +189,29 @@
189
  background-color: $ui-secondary;
190
  }
191
 
192
- .tab img {
193
  width: 1.375rem;
194
  filter: grayscale(100%);
195
  }
196
 
197
- [dir="ltr"] .tab img {
198
  margin-right: 0.125rem;
199
  }
200
 
201
- [dir="rtl"] .tab img {
202
  margin-left: 0.125rem;
203
  }
204
 
205
  /* mirror blocks and sound tab icons */
206
- [dir="rtl"] .tab:nth-of-type(1) img {
207
  transform: scaleX(-1);
208
  }
209
 
210
- [dir="rtl"] .tab:nth-of-type(3) img {
211
  transform: scaleX(-1);
212
  }
213
 
214
- .tab.is-selected img {
215
  filter: none;
216
  }
217
 
 
130
  background: none;
131
  border-color: rgba(255,255,255,.15);
132
  }
133
+ [theme="dark"] .add-tab-button svg {
134
  filter: invert(1);
135
  }
136
  .add-tab-button-disabled {
 
141
  display: flex;
142
  align-items: center;
143
  }
144
+ .tab-addition-item svg {
145
  width: 1.375rem;
146
  }
147
 
 
189
  background-color: $ui-secondary;
190
  }
191
 
192
+ .tab svg {
193
  width: 1.375rem;
194
  filter: grayscale(100%);
195
  }
196
 
197
+ [dir="ltr"] .tab svg {
198
  margin-right: 0.125rem;
199
  }
200
 
201
+ [dir="rtl"] .tab svg {
202
  margin-left: 0.125rem;
203
  }
204
 
205
  /* mirror blocks and sound tab icons */
206
+ [dir="rtl"] .tab:nth-of-type(1) svg {
207
  transform: scaleX(-1);
208
  }
209
 
210
+ [dir="rtl"] .tab:nth-of-type(3) svg {
211
  transform: scaleX(-1);
212
  }
213
 
214
+ .tab.is-selected svg {
215
  filter: none;
216
  }
217
 
src/components/gui/gui.jsx CHANGED
@@ -50,13 +50,13 @@ import {resolveStageSize} from '../../lib/screen-utils';
50
  import {isRendererSupported, isBrowserSupported} from '../../lib/tw-environment-support-prober';
51
 
52
  import styles from './gui.css';
53
- import plusIcon from './add-tab.svg';
54
- import addExtensionIcon from './icon--extensions.svg';
55
- import codeIcon from './icon--code.svg';
56
- import costumesIcon from './icon--costumes.svg';
57
- import soundsIcon from './icon--sounds.svg';
58
- import variablesIcon from './icon--variables.svg';
59
- import filesIcon from './icon--files.svg';
60
 
61
  const urlParams = new URLSearchParams(location.search);
62
 
@@ -226,10 +226,7 @@ const GUIComponent = props => {
226
  // currently each tab can decide whether or not its hidden, remove this once rearranging tabs is supported
227
  const codeTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('code') ? null : styles.tabDisabled)}>
228
  <ContextMenuWrapTab tabId="code">
229
- <img
230
- draggable={false}
231
- src={codeIcon}
232
- />
233
  <FormattedMessage
234
  defaultMessage="Code"
235
  description="Button to get to the code panel"
@@ -239,10 +236,7 @@ const GUIComponent = props => {
239
  </Tab>);
240
  const costumesTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('costume') ? null : styles.tabDisabled)} onClick={onActivateCostumesTab}>
241
  <ContextMenuWrapTab tabId="costume">
242
- <img
243
- draggable={false}
244
- src={costumesIcon}
245
- />
246
  {targetIsStage ? (
247
  <FormattedMessage
248
  defaultMessage="Backdrops"
@@ -260,10 +254,7 @@ const GUIComponent = props => {
260
  </Tab>);
261
  const soundsTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('sound') ? null : styles.tabDisabled)} onClick={onActivateSoundsTab}>
262
  <ContextMenuWrapTab tabId="sound">
263
- <img
264
- draggable={false}
265
- src={soundsIcon}
266
- />
267
  <FormattedMessage
268
  defaultMessage="Sounds"
269
  description="Button to get to the sounds panel"
@@ -273,10 +264,7 @@ const GUIComponent = props => {
273
  </Tab>);
274
  const variablesTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('variable') ? null : styles.tabDisabled)} onClick={onActivateVariablesTab}>
275
  <ContextMenuWrapTab tabId="variable">
276
- <img
277
- draggable={false}
278
- src={variablesIcon}
279
- />
280
  <FormattedMessage
281
  defaultMessage="Variables"
282
  description="Button to get to the variables panel"
@@ -286,10 +274,7 @@ const GUIComponent = props => {
286
  </Tab>);
287
  const filesTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('file') ? null : styles.tabDisabled)} onClick={onActivateFilesTab}>
288
  <ContextMenuWrapTab tabId="file">
289
- <img
290
- draggable={false}
291
- src={filesIcon}
292
- />
293
  <FormattedMessage
294
  defaultMessage="Files"
295
  description="Button to get to the files panel"
@@ -521,20 +506,14 @@ const GUIComponent = props => {
521
  id={`add-editor-tab-button`}
522
  >
523
  <button className={classNames(styles.addTabButton, addTabButtonDisabled ? styles.addTabButtonDisabled : null)}>
524
- <img
525
- draggable={false}
526
- src={plusIcon}
527
- />
528
  </button>
529
  </ContextMenuTrigger>
530
 
531
  <ContextMenu id={`add-editor-tab-button`}>
532
  {!tabOrder.includes('code') && <MenuItem onClick={() => addTabToEditor('code')}>
533
  <div className={styles.tabAdditionItem}>
534
- <img
535
- draggable={false}
536
- src={codeIcon}
537
- />
538
  <FormattedMessage
539
  defaultMessage="Code"
540
  description="Button to get to the code panel"
@@ -544,10 +523,7 @@ const GUIComponent = props => {
544
  </MenuItem>}
545
  {!tabOrder.includes('costume') && <MenuItem onClick={() => addTabToEditor('costume')}>
546
  <div className={styles.tabAdditionItem}>
547
- <img
548
- draggable={false}
549
- src={costumesIcon}
550
- />
551
  <FormattedMessage
552
  defaultMessage="Costumes"
553
  description="Button to get to the costumes panel"
@@ -557,10 +533,7 @@ const GUIComponent = props => {
557
  </MenuItem>}
558
  {!tabOrder.includes('sound') && <MenuItem onClick={() => addTabToEditor('sound')}>
559
  <div className={styles.tabAdditionItem}>
560
- <img
561
- draggable={false}
562
- src={soundsIcon}
563
- />
564
  <FormattedMessage
565
  defaultMessage="Sounds"
566
  description="Button to get to the sounds panel"
@@ -570,10 +543,7 @@ const GUIComponent = props => {
570
  </MenuItem>}
571
  {!tabOrder.includes('variable') && <MenuItem onClick={() => addTabToEditor('variable')}>
572
  <div className={styles.tabAdditionItem}>
573
- <img
574
- draggable={false}
575
- src={variablesIcon}
576
- />
577
  <FormattedMessage
578
  defaultMessage="Variables"
579
  description="Button to get to the variables panel"
@@ -583,10 +553,7 @@ const GUIComponent = props => {
583
  </MenuItem>}
584
  {/* {!tabOrder.includes('file') && <MenuItem onClick={() => addTabToEditor('file')}>
585
  <div className={styles.tabAdditionItem}>
586
- <img
587
- draggable={false}
588
- src={filesIcon}
589
- />
590
  <FormattedMessage
591
  defaultMessage="Files"
592
  description="Button to get to the files panel"
@@ -617,11 +584,7 @@ const GUIComponent = props => {
617
  title={intl.formatMessage(messages.addExtension)}
618
  onClick={onExtensionButtonClick}
619
  >
620
- <img
621
- className={styles.extensionButtonIcon}
622
- draggable={false}
623
- src={addExtensionIcon}
624
- />
625
  </button>
626
  </Box>
627
  <Box className={styles.watermark}>
 
50
  import {isRendererSupported, isBrowserSupported} from '../../lib/tw-environment-support-prober';
51
 
52
  import styles from './gui.css';
53
+ import { ReactComponent as PlusIcon } from './add-tab.svg';
54
+ import { ReactComponent as AddExtensionIcon } from './icon--extensions.svg';
55
+ import { ReactComponent as CodeIcon } from './icon--code.svg';
56
+ import { ReactComponent as CostumesIcon } from './icon--costumes.svg';
57
+ import { ReactComponent as SoundsIcon } from './icon--sounds.svg';
58
+ import { ReactComponent as VariablesIcon } from './icon--variables.svg';
59
+ import { ReactComponent as FilesIcon } from './icon--files.svg';
60
 
61
  const urlParams = new URLSearchParams(location.search);
62
 
 
226
  // currently each tab can decide whether or not its hidden, remove this once rearranging tabs is supported
227
  const codeTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('code') ? null : styles.tabDisabled)}>
228
  <ContextMenuWrapTab tabId="code">
229
+ <CodeIcon />
 
 
 
230
  <FormattedMessage
231
  defaultMessage="Code"
232
  description="Button to get to the code panel"
 
236
  </Tab>);
237
  const costumesTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('costume') ? null : styles.tabDisabled)} onClick={onActivateCostumesTab}>
238
  <ContextMenuWrapTab tabId="costume">
239
+ <CostumesIcon />
 
 
 
240
  {targetIsStage ? (
241
  <FormattedMessage
242
  defaultMessage="Backdrops"
 
254
  </Tab>);
255
  const soundsTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('sound') ? null : styles.tabDisabled)} onClick={onActivateSoundsTab}>
256
  <ContextMenuWrapTab tabId="sound">
257
+ <SoundsIcon />
 
 
 
258
  <FormattedMessage
259
  defaultMessage="Sounds"
260
  description="Button to get to the sounds panel"
 
264
  </Tab>);
265
  const variablesTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('variable') ? null : styles.tabDisabled)} onClick={onActivateVariablesTab}>
266
  <ContextMenuWrapTab tabId="variable">
267
+ <VariablesIcon />
 
 
 
268
  <FormattedMessage
269
  defaultMessage="Variables"
270
  description="Button to get to the variables panel"
 
274
  </Tab>);
275
  const filesTab = (<Tab className={classNames(tabClassNames.tab, tabOrder.includes('file') ? null : styles.tabDisabled)} onClick={onActivateFilesTab}>
276
  <ContextMenuWrapTab tabId="file">
277
+ <FilesIcon />
 
 
 
278
  <FormattedMessage
279
  defaultMessage="Files"
280
  description="Button to get to the files panel"
 
506
  id={`add-editor-tab-button`}
507
  >
508
  <button className={classNames(styles.addTabButton, addTabButtonDisabled ? styles.addTabButtonDisabled : null)}>
509
+ <PlusIcon />
 
 
 
510
  </button>
511
  </ContextMenuTrigger>
512
 
513
  <ContextMenu id={`add-editor-tab-button`}>
514
  {!tabOrder.includes('code') && <MenuItem onClick={() => addTabToEditor('code')}>
515
  <div className={styles.tabAdditionItem}>
516
+ <CodeIcon />
 
 
 
517
  <FormattedMessage
518
  defaultMessage="Code"
519
  description="Button to get to the code panel"
 
523
  </MenuItem>}
524
  {!tabOrder.includes('costume') && <MenuItem onClick={() => addTabToEditor('costume')}>
525
  <div className={styles.tabAdditionItem}>
526
+ <CostumesIcon />
 
 
 
527
  <FormattedMessage
528
  defaultMessage="Costumes"
529
  description="Button to get to the costumes panel"
 
533
  </MenuItem>}
534
  {!tabOrder.includes('sound') && <MenuItem onClick={() => addTabToEditor('sound')}>
535
  <div className={styles.tabAdditionItem}>
536
+ <SoundsIcon />
 
 
 
537
  <FormattedMessage
538
  defaultMessage="Sounds"
539
  description="Button to get to the sounds panel"
 
543
  </MenuItem>}
544
  {!tabOrder.includes('variable') && <MenuItem onClick={() => addTabToEditor('variable')}>
545
  <div className={styles.tabAdditionItem}>
546
+ <VariablesIcon />
 
 
 
547
  <FormattedMessage
548
  defaultMessage="Variables"
549
  description="Button to get to the variables panel"
 
553
  </MenuItem>}
554
  {/* {!tabOrder.includes('file') && <MenuItem onClick={() => addTabToEditor('file')}>
555
  <div className={styles.tabAdditionItem}>
556
+ <FilesIcon />
 
 
 
557
  <FormattedMessage
558
  defaultMessage="Files"
559
  description="Button to get to the files panel"
 
584
  title={intl.formatMessage(messages.addExtension)}
585
  onClick={onExtensionButtonClick}
586
  >
587
+ <AddExtensionIcon className={styles.extensionButtonIcon} />
 
 
 
 
588
  </button>
589
  </Box>
590
  <Box className={styles.watermark}>
src/components/icon-button/icon-button.jsx CHANGED
@@ -19,11 +19,17 @@ const IconButton = ({
19
  role="button"
20
  onClick={disabled ? null : onClick}
21
  >
22
- <img
23
- className={styles.icon}
24
- draggable={false}
25
- src={img}
26
- />
 
 
 
 
 
 
27
  <div className={styles.title}>
28
  {title}
29
  </div>
@@ -33,7 +39,10 @@ const IconButton = ({
33
  IconButton.propTypes = {
34
  className: PropTypes.string,
35
  disabled: PropTypes.bool,
36
- img: PropTypes.string,
 
 
 
37
  onClick: PropTypes.func.isRequired,
38
  title: PropTypes.node.isRequired
39
  };
 
19
  role="button"
20
  onClick={disabled ? null : onClick}
21
  >
22
+ {typeof img === 'string' ? (
23
+ <img
24
+ className={styles.icon}
25
+ draggable={false}
26
+ src={img}
27
+ alt=""
28
+ />
29
+ ) : (
30
+ // Reactコンポーネントとして描画
31
+ React.cloneElement(img, { className: styles.icon })
32
+ )}
33
  <div className={styles.title}>
34
  {title}
35
  </div>
 
39
  IconButton.propTypes = {
40
  className: PropTypes.string,
41
  disabled: PropTypes.bool,
42
+ img: PropTypes.oneOfType([
43
+ PropTypes.string,
44
+ PropTypes.element
45
+ ]),
46
  onClick: PropTypes.func.isRequired,
47
  title: PropTypes.node.isRequired
48
  };
src/components/library-item/library-item.css CHANGED
@@ -183,7 +183,7 @@
183
 
184
  .inspect-extension {
185
  position: relative;
186
- background: #00c3ff;
187
  color: white;
188
  font-weight: bold;
189
  padding: 0;
 
183
 
184
  .inspect-extension {
185
  position: relative;
186
+ background: #5100ff;
187
  color: white;
188
  font-weight: bold;
189
  padding: 0;
src/components/library/library.jsx CHANGED
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
4
  import React from 'react';
5
  import localforage from 'localforage';
6
  import {defineMessages, injectIntl, intlShape} from 'react-intl';
 
7
 
8
  import LibraryItem from '../../containers/library-item.jsx';
9
  import Modal from '../../containers/modal.jsx';
@@ -349,7 +350,13 @@ class LibraryComponent extends React.Component {
349
  {/*
350
  todo: translation?
351
  */}
352
- <h3 className={classNames(styles.whiteTextInDarkMode)}>Filters</h3>
 
 
 
 
 
 
353
  {this.props.filterable && (
354
  <div>
355
  <Filter
 
4
  import React from 'react';
5
  import localforage from 'localforage';
6
  import {defineMessages, injectIntl, intlShape} from 'react-intl';
7
+ import { FormattedMessage } from 'react-intl';
8
 
9
  import LibraryItem from '../../containers/library-item.jsx';
10
  import Modal from '../../containers/modal.jsx';
 
350
  {/*
351
  todo: translation?
352
  */}
353
+ <h3 className={classNames(styles.whiteTextInDarkMode)}>
354
+ <FormattedMessage
355
+ defaultMessage="Filters"
356
+ description="Label for the library filter section"
357
+ id="gui.library.filters"
358
+ />
359
+ </h3>
360
  {this.props.filterable && (
361
  <div>
362
  <Filter
src/components/menu-bar/google-drive-save.css CHANGED
@@ -446,4 +446,107 @@
446
  border: 1px solid #ddd;
447
  border-radius: 3px;
448
  font-size: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  }
 
446
  border: 1px solid #ddd;
447
  border-radius: 3px;
448
  font-size: 12px;
449
+ }
450
+ /* Google Drive Save コンポーネント用の追加スタイル */
451
+ .shareSection {
452
+ margin-bottom: 20px;
453
+ }
454
+
455
+ .shareSection h3 {
456
+ margin: 0 0 10px 0;
457
+ font-size: 14px;
458
+ color: #333;
459
+ }
460
+
461
+ .permissionRow {
462
+ display: flex;
463
+ align-items: center;
464
+ margin-bottom: 10px;
465
+ gap: 10px;
466
+ }
467
+
468
+ .emailInput {
469
+ flex: 1;
470
+ padding: 8px;
471
+ border: 1px solid #ddd;
472
+ border-radius: 4px;
473
+ }
474
+
475
+ .roleSelect {
476
+ padding: 8px;
477
+ border: 1px solid #ddd;
478
+ border-radius: 4px;
479
+ min-width: 120px;
480
+ }
481
+
482
+ .addButton, .removeButton {
483
+ padding: 8px 12px;
484
+ border: 1px solid #ddd;
485
+ background: #f5f5f5;
486
+ border-radius: 4px;
487
+ cursor: pointer;
488
+ }
489
+
490
+ .addButton:hover, .removeButton:hover {
491
+ background: #e0e0e0;
492
+ }
493
+
494
+ .modalFooter {
495
+ display: flex;
496
+ justify-content: flex-end;
497
+ gap: 10px;
498
+ padding: 20px;
499
+ border-top: 1px solid #eee;
500
+ }
501
+
502
+ .applyButton, .cancelButton {
503
+ padding: 10px 20px;
504
+ border: none;
505
+ border-radius: 4px;
506
+ cursor: pointer;
507
+ }
508
+
509
+ .applyButton {
510
+ background: #4CAF50;
511
+ color: white;
512
+ }
513
+
514
+ .applyButton:hover:not(:disabled) {
515
+ background: #45a049;
516
+ }
517
+
518
+ .cancelButton {
519
+ background: #f5f5f5;
520
+ color: #333;
521
+ }
522
+
523
+ .cancelButton:hover:not(:disabled) {
524
+ background: #e0e0e0;
525
+ }
526
+
527
+ .applyButton:disabled, .cancelButton:disabled {
528
+ opacity: 0.6;
529
+ cursor: not-allowed;
530
+ }
531
+
532
+ /* 特定のクラス付き要素も固定 */
533
+ .emailInput,
534
+ .newFileNameInput,
535
+ .permissionDropdown select,
536
+ .roleSelect {
537
+ background-color: #fff !important;
538
+ color: #333 !important;
539
+ }
540
+ .publicButton {
541
+ background-color: #4CAF50;
542
+ color: white;
543
+ }
544
+
545
+ .publicButton:hover:not(:disabled) {
546
+ background-color: #45a049;
547
+ }
548
+
549
+ .publicButton:disabled {
550
+ background-color: #cccccc;
551
+ cursor: not-allowed;
552
  }
src/components/menu-bar/google-drive-save.jsx CHANGED
@@ -20,14 +20,24 @@ class GoogleDriveSave extends React.Component {
20
  isProcessing: false,
21
  newFileName: props.projectTitle || '無題',
22
  showNewFileInput: false,
23
- sharePermission: 'reader', // 'reader', 'writer', or 'owner'
24
- selectedFileId: null
 
 
 
 
 
 
25
  };
26
  this.modalContentRef = React.createRef();
 
27
  }
28
 
29
  componentDidMount() {
30
- // 初期化処理
 
 
 
31
  }
32
 
33
  handleClick = () => {
@@ -36,7 +46,11 @@ class GoogleDriveSave extends React.Component {
36
 
37
  handleCloseModal = () => {
38
  if (!this.state.isProcessing) {
39
- this.setState({isModalOpen: false, showNewFileInput: false});
 
 
 
 
40
  }
41
  };
42
 
@@ -67,6 +81,10 @@ class GoogleDriveSave extends React.Component {
67
  isModalOpen: true
68
  });
69
 
 
 
 
 
70
  this.fetchDriveFiles(event.data.token);
71
  }
72
  };
@@ -98,11 +116,19 @@ class GoogleDriveSave extends React.Component {
98
  this.setState({files: data.files || [], isLoading: false});
99
  } catch (error) {
100
  console.error("ファイル一覧取得エラー:", error);
101
- alert("error", "ファイル一覧の取得に失敗しました");
102
  this.setState({isLoading: false});
103
  }
104
  };
105
 
 
 
 
 
 
 
 
 
106
  renderModal() {
107
  if (!this.state.isModalOpen) return null;
108
 
@@ -218,7 +244,7 @@ class GoogleDriveSave extends React.Component {
218
  <button
219
  onClick={() => this.setState({
220
  showNewFileInput: true,
221
- newFileName: window.vm.runtime.projectName || '無題',
222
  sharePermission: 'reader'
223
  })}
224
  className={styles.newFileButton}
@@ -255,102 +281,74 @@ class GoogleDriveSave extends React.Component {
255
  );
256
  }
257
 
258
- renderFileItem(project, thumbnailFiles) {
259
- const thumbnail = thumbnailFiles.find(
260
- thumb => thumb.name === `Scratch-Thumbnail-${project.id}.png`
261
- );
262
-
263
- return (
264
- <div key={project.id} className={styles.fileItem}>
265
- <div className={styles.thumbnailContainer}>
266
- {thumbnail ? (
267
- <img
268
- src={`https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`}
269
- alt="プロジェクトサムネイル"
270
- className={styles.thumbnail}
271
- />
272
- ) : (
273
- <div className={styles.thumbnailPlaceholder}>
274
- サムネイルなし
275
- </div>
276
- )}
277
- </div>
278
-
279
- <h3 className={styles.fileName}>
280
- {project.name.replace('.s4s.txt', '')}
281
- </h3>
282
-
283
- {this.renderShareLink(project.id)}
284
-
285
- <div className={styles.buttonGroup}>
286
- <button
287
- onClick={() => this.handleLoadFile(project)}
288
- className={styles.actionButton}
289
- disabled={this.state.isProcessing}
290
- >
291
- 読み込む
292
- </button>
293
- <button
294
- onClick={() => this.handleReplaceFile(project)}
295
- className={styles.actionButton}
296
- disabled={this.state.isProcessing}
297
- >
298
- 上書き
299
- </button>
300
- <button
301
- onClick={() => this.handleShareFile(project.id)}
302
- className={classNames(styles.actionButton, styles.shareButton)}
303
- disabled={this.state.isProcessing}
304
- >
305
- 共有
306
- </button>
307
- <button
308
- onClick={() => this.handleDeleteFile(project, thumbnailFiles)}
309
- className={classNames(styles.actionButton, styles.deleteButton)}
310
- disabled={this.state.isProcessing}
311
- >
312
- 削除
313
- </button>
314
- </div>
315
-
316
- {/* ここにアクセス権限設定のドロップダウンを追加 */}
317
- <div className={styles.permissionDropdown}>
318
- <label>アクセス権限: </label>
319
- <select
320
- value={this.state.sharePermission}
321
- onChange={(e) => this.updateFilePermission(project.id, e.target.value)}
322
- disabled={this.state.isProcessing}
323
- >
324
- <option value="reader">閲覧のみ</option>
325
- <option value="writer">編集可能</option>
326
- <option value="owner">所有者</option>
327
- </select>
328
  </div>
329
- </div>
330
- );
331
- }
332
-
333
- updateFilePermission = async (fileId, permission) => {
334
- this.setState({isProcessing: true});
335
- try {
336
- await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}/permissions/anyone`, {
337
- method: "PATCH",
338
- headers: {
339
- Authorization: `Bearer ${this.state.accessToken}`,
340
- "Content-Type": "application/json",
341
- },
342
- body: JSON.stringify({
343
- role: permission,
344
- }),
345
- });
346
- alert("success", "アクセス権限を更新しました");
347
- } catch (error) {
348
- console.error("権限更新エラー:", error);
349
- alert("error", "アクセス権限の更新に失敗しました");
350
- } finally {
351
- this.setState({isProcessing: false});
352
  }
353
- };
354
  renderShareLink(fileId) {
355
  const SHORT_URL = "https://s4.rf.gd/";
356
 
@@ -391,6 +389,239 @@ updateFilePermission = async (fileId, permission) => {
391
  );
392
  }
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  render() {
395
  return (
396
  <div>
@@ -409,6 +640,7 @@ updateFilePermission = async (fileId, permission) => {
409
  </Button>
410
 
411
  {this.renderModal()}
 
412
  </div>
413
  );
414
  }
@@ -419,7 +651,8 @@ updateFilePermission = async (fileId, permission) => {
419
  this.setState({
420
  accessToken: null,
421
  currentAccountEmail: null,
422
- currentAccountName: null
 
423
  });
424
  localStorage.removeItem('googleDriveAccessToken');
425
  localStorage.removeItem('googleDriveAccountEmail');
@@ -430,12 +663,12 @@ updateFilePermission = async (fileId, permission) => {
430
  this.setState({isProcessing: true});
431
  try {
432
  await this.saveToGoogleDrive(null, `${this.state.newFileName}.s4s.txt`, this.state.sharePermission);
433
- alert("success", "新規保存しました");
434
  this.setState({showNewFileInput: false});
435
  this.fetchDriveFiles(this.state.accessToken);
436
  } catch (error) {
437
  console.error("新規保存エラー:", error);
438
- alert("error", "新規保存に失敗しました");
439
  } finally {
440
  this.setState({isProcessing: false});
441
  }
@@ -459,24 +692,56 @@ updateFilePermission = async (fileId, permission) => {
459
  this.setState({isProcessing: true});
460
  try {
461
  await this.saveToGoogleDrive(project.id, project.name);
462
- alert("success", "上書き保存しました");
463
  this.fetchDriveFiles(this.state.accessToken);
464
  } catch (error) {
465
  console.error("ファイル上書きエラー:", error);
466
- alert("error", "ファイルの上書きに失敗しました");
467
  } finally {
468
  this.setState({isProcessing: false});
469
  }
470
  }
471
  };
472
 
473
- handleShareFile = (fileId) => {
474
  if (this.state.isProcessing) return;
475
 
476
- const SHARE_URL = "https://scratch-school.ct.ws/upload?id=";
477
- window.open(`${SHARE_URL}${fileId}`, "_blank");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  };
479
-
480
  handleDeleteFile = async (project, thumbnailFiles) => {
481
  if (this.state.isProcessing) return;
482
 
@@ -493,11 +758,11 @@ updateFilePermission = async (fileId, permission) => {
493
  await this.deleteFile(thumbnailToDelete.id);
494
  }
495
 
496
- alert("success", "ファイルを削除しました");
497
  this.fetchDriveFiles(this.state.accessToken);
498
  } catch (error) {
499
  console.error("削除エラー:", error);
500
- alert("error", "ファイルの削除に失敗しました");
501
  } finally {
502
  this.setState({isProcessing: false});
503
  }
@@ -508,8 +773,8 @@ updateFilePermission = async (fileId, permission) => {
508
  if (this.state.isProcessing) return;
509
 
510
  navigator.clipboard.writeText(text)
511
- .then(() => alert("success", "リンクをクリップボードにコピーしました"))
512
- .catch(() => alert("error", "リンクのコピーに失敗しました"));
513
  };
514
 
515
  async deleteFile(fileId) {
@@ -526,6 +791,10 @@ updateFilePermission = async (fileId, permission) => {
526
  }
527
 
528
  async saveToGoogleDrive(fileId, fileName, permission = 'reader') {
 
 
 
 
529
  const blob = await window.vm.saveProjectSb3();
530
 
531
  const metadata = {
@@ -606,18 +875,25 @@ updateFilePermission = async (fileId, permission) => {
606
  "Content-Type": "application/json",
607
  },
608
  body: JSON.stringify({
609
- role: permission, // ここで公開設定を使用
610
  type: "anyone",
611
  }),
612
  });
613
  }
 
 
614
  }
615
 
616
  getProjectThumbnail() {
617
- return new Promise(resolve => {
618
- window.vm.renderer.requestSnapshot(uri => {
619
- resolve(uri);
620
- });
 
 
 
 
 
621
  });
622
  }
623
  }
@@ -627,7 +903,9 @@ GoogleDriveSave.propTypes = {
627
  showAlert: PropTypes.func.isRequired,
628
  projectTitle: PropTypes.string
629
  };
 
630
  const mapStateToProps = state => ({
631
  projectTitle: state.scratchGui.projectTitle
632
  });
 
633
  export default connect(mapStateToProps)(GoogleDriveSave);
 
20
  isProcessing: false,
21
  newFileName: props.projectTitle || '無題',
22
  showNewFileInput: false,
23
+ sharePermission: 'reader',
24
+ selectedFileId: null,
25
+ // 共有モーダル用の状態
26
+ isShareModalOpen: false,
27
+ currentSharingFileId: null,
28
+ emailPermissions: [{ email: '', role: 'reader' }],
29
+ linkPermission: 'reader',
30
+ groupPermission: 'reader'
31
  };
32
  this.modalContentRef = React.createRef();
33
+ this.shareModalContentRef = React.createRef();
34
  }
35
 
36
  componentDidMount() {
37
+ // アクセストークンがある場合はファイル一覧を取得
38
+ if (this.state.accessToken) {
39
+ this.fetchDriveFiles(this.state.accessToken);
40
+ }
41
  }
42
 
43
  handleClick = () => {
 
46
 
47
  handleCloseModal = () => {
48
  if (!this.state.isProcessing) {
49
+ this.setState({
50
+ isModalOpen: false,
51
+ showNewFileInput: false,
52
+ isShareModalOpen: false
53
+ });
54
  }
55
  };
56
 
 
81
  isModalOpen: true
82
  });
83
 
84
+ localStorage.setItem('googleDriveAccessToken', event.data.token);
85
+ if (event.data.email) localStorage.setItem('googleDriveAccountEmail', event.data.email);
86
+ if (event.data.name) localStorage.setItem('googleDriveAccountName', event.data.name);
87
+
88
  this.fetchDriveFiles(event.data.token);
89
  }
90
  };
 
116
  this.setState({files: data.files || [], isLoading: false});
117
  } catch (error) {
118
  console.error("ファイル一覧取得エラー:", error);
119
+ this.showAlert("error", "ファイル一覧の取得に失敗しました");
120
  this.setState({isLoading: false});
121
  }
122
  };
123
 
124
+ showAlert = (type, message) => {
125
+ if (this.props.showAlert) {
126
+ this.props.showAlert(type, message);
127
+ } else {
128
+ alert(`${type}: ${message}`);
129
+ }
130
+ };
131
+
132
  renderModal() {
133
  if (!this.state.isModalOpen) return null;
134
 
 
244
  <button
245
  onClick={() => this.setState({
246
  showNewFileInput: true,
247
+ newFileName: window.vm && window.vm.runtime ? window.vm.runtime.projectName || '無題' : '無題',
248
  sharePermission: 'reader'
249
  })}
250
  className={styles.newFileButton}
 
281
  );
282
  }
283
 
284
+ renderFileItem(project, thumbnailFiles) {
285
+ const thumbnail = thumbnailFiles.find(
286
+ thumb => thumb.name === `Scratch-Thumbnail-${project.id}.png`
287
+ );
288
+
289
+ return (
290
+ <div key={project.id} className={styles.fileItem}>
291
+ <div className={styles.thumbnailContainer}>
292
+ {thumbnail ? (
293
+ <img
294
+ src={`https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`}
295
+ alt="プロジェクトサムネイル"
296
+ className={styles.thumbnail}
297
+ />
298
+ ) : (
299
+ <div className={styles.thumbnailPlaceholder}>
300
+ サムネイルなし
301
+ </div>
302
+ )}
303
+ </div>
304
+
305
+ <h3 className={styles.fileName}>
306
+ {project.name.replace('.s4s.txt', '')}
307
+ </h3>
308
+
309
+ {this.renderShareLink(project.id)}
310
+
311
+ <div className={styles.buttonGroup}>
312
+ <button
313
+ onClick={() => this.handleLoadFile(project)}
314
+ className={styles.actionButton}
315
+ disabled={this.state.isProcessing}
316
+ >
317
+ 読み込む
318
+ </button>
319
+ <button
320
+ onClick={() => this.handleReplaceFile(project)}
321
+ className={styles.actionButton}
322
+ disabled={this.state.isProcessing}
323
+ >
324
+ 上書き
325
+ </button>
326
+ <button
327
+ onClick={() => this.openShareModal(project.id)}
328
+ className={classNames(styles.actionButton, styles.shareButton)}
329
+ disabled={this.state.isProcessing}
330
+ >
331
+ 共有
332
+ </button>
333
+ <button
334
+ onClick={() => this.handleShareFile(project.id)}
335
+ className={classNames(styles.actionButton, styles.publicButton)}
336
+ disabled={this.state.isProcessing}
337
+ >
338
+ 公開
339
+ </button>
340
+ <button
341
+ onClick={() => this.handleDeleteFile(project, thumbnailFiles)}
342
+ className={classNames(styles.actionButton, styles.deleteButton)}
343
+ disabled={this.state.isProcessing}
344
+ >
345
+ 削除
346
+ </button>
347
+ </div>
 
 
 
 
 
 
348
  </div>
349
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  }
351
+
352
  renderShareLink(fileId) {
353
  const SHORT_URL = "https://s4.rf.gd/";
354
 
 
389
  );
390
  }
391
 
392
+ // 共有モーダルを開く
393
+ openShareModal = (fileId) => {
394
+ this.setState({
395
+ isShareModalOpen: true,
396
+ currentSharingFileId: fileId,
397
+ emailPermissions: [{ email: '', role: 'reader' }],
398
+ linkPermission: 'reader',
399
+ groupPermission: 'reader'
400
+ });
401
+ };
402
+
403
+ // 共有モーダルを閉じる
404
+ closeShareModal = () => {
405
+ if (!this.state.isProcessing) {
406
+ this.setState({ isShareModalOpen: false });
407
+ }
408
+ };
409
+
410
+ // オーバーレイクリックで共有モーダルを閉じる
411
+ handleShareOverlayClick = (e) => {
412
+ if (!this.state.isProcessing && this.shareModalContentRef.current &&
413
+ !this.shareModalContentRef.current.contains(e.target)) {
414
+ this.closeShareModal();
415
+ }
416
+ };
417
+
418
+ // メール権限入力欄を追加
419
+ addEmailPermission = () => {
420
+ this.setState(prevState => ({
421
+ emailPermissions: [...prevState.emailPermissions, { email: '', role: 'reader' }]
422
+ }));
423
+ };
424
+
425
+ // メール権限入力欄を更新
426
+ updateEmailPermission = (index, field, value) => {
427
+ this.setState(prevState => ({
428
+ emailPermissions: prevState.emailPermissions.map((item, i) =>
429
+ i === index ? { ...item, [field]: value } : item
430
+ )
431
+ }));
432
+ };
433
+
434
+ // メール権限入力欄を削除
435
+ removeEmailPermission = (index) => {
436
+ this.setState(prevState => ({
437
+ emailPermissions: prevState.emailPermissions.filter((_, i) => i !== index)
438
+ }));
439
+ };
440
+
441
+ // 権限設定を適用
442
+ applyPermissions = async () => {
443
+ this.setState({ isProcessing: true });
444
+ try {
445
+ const { currentSharingFileId, accessToken, emailPermissions, linkPermission } = this.state;
446
+
447
+ // メールごとの権限設定
448
+ for (const permission of emailPermissions) {
449
+ if (permission.email.trim()) {
450
+ await this.setFilePermission(
451
+ currentSharingFileId,
452
+ accessToken,
453
+ permission.email.trim(),
454
+ permission.role
455
+ );
456
+ }
457
+ }
458
+
459
+ // リンクを知っている全員への権限設定
460
+ await fetch(`https://www.googleapis.com/drive/v3/files/${currentSharingFileId}/permissions`, {
461
+ method: "POST",
462
+ headers: {
463
+ "Authorization": `Bearer ${accessToken}`,
464
+ "Content-Type": "application/json",
465
+ },
466
+ body: JSON.stringify({
467
+ type: "anyone",
468
+ role: linkPermission,
469
+ }),
470
+ });
471
+
472
+ this.showAlert("success", "共有設定を適用しました");
473
+ this.closeShareModal();
474
+ } catch (error) {
475
+ console.error("権限設定エラー:", error);
476
+ this.showAlert("error", "共有設定の適用に失敗しました");
477
+ } finally {
478
+ this.setState({ isProcessing: false });
479
+ }
480
+ };
481
+
482
+ // ファイル権限設定メソッド
483
+ async setFilePermission(fileId, accessToken, email, role = "reader") {
484
+ const response = await fetch(
485
+ `https://www.googleapis.com/drive/v3/files/${fileId}/permissions`,
486
+ {
487
+ method: "POST",
488
+ headers: {
489
+ "Authorization": `Bearer ${accessToken}`,
490
+ "Content-Type": "application/json",
491
+ },
492
+ body: JSON.stringify({
493
+ type: "user",
494
+ role: role,
495
+ emailAddress: email
496
+ }),
497
+ }
498
+ );
499
+
500
+ if (!response.ok) {
501
+ throw new Error(await response.text());
502
+ }
503
+ return await response.json();
504
+ }
505
+
506
+ // 共有モーダルのレンダリング
507
+ renderShareModal() {
508
+ if (!this.state.isShareModalOpen) return null;
509
+
510
+ return (
511
+ <div className={styles.modalOverlay} onClick={this.handleShareOverlayClick}>
512
+ <div className={styles.modalContent} ref={this.shareModalContentRef} style={{maxWidth: '500px'}}>
513
+ <div className={styles.modalHeader}>
514
+ <h2>共有設定</h2>
515
+ <button
516
+ onClick={this.closeShareModal}
517
+ className={styles.closeButton}
518
+ disabled={this.state.isProcessing}
519
+ >
520
+ ×
521
+ </button>
522
+ </div>
523
+
524
+ <div className={styles.modalBody}>
525
+ <div className={styles.shareSection}>
526
+ <h3>ユーザーごとの共有</h3>
527
+ {this.state.emailPermissions.map((permission, index) => (
528
+ <div key={index} className={styles.permissionRow}>
529
+ <input
530
+ type="email"
531
+ placeholder="メールアドレス"
532
+ value={permission.email}
533
+ onChange={(e) => this.updateEmailPermission(index, 'email', e.target.value)}
534
+ className={styles.emailInput}
535
+ disabled={this.state.isProcessing}
536
+ />
537
+ <select
538
+ value={permission.role}
539
+ onChange={(e) => this.updateEmailPermission(index, 'role', e.target.value)}
540
+ className={styles.roleSelect}
541
+ disabled={this.state.isProcessing}
542
+ >
543
+ <option value="reader">閲覧可能</option>
544
+ <option value="writer">編集可能</option>
545
+ </select>
546
+ {this.state.emailPermissions.length > 1 && (
547
+ <button
548
+ onClick={() => this.removeEmailPermission(index)}
549
+ className={styles.removeButton}
550
+ disabled={this.state.isProcessing}
551
+ >
552
+ ×
553
+ </button>
554
+ )}
555
+ </div>
556
+ ))}
557
+ <button
558
+ onClick={this.addEmailPermission}
559
+ className={styles.addButton}
560
+ disabled={this.state.isProcessing}
561
+ >
562
+
563
+ </button>
564
+ </div>
565
+
566
+ <div className={styles.shareSection}>
567
+ <h3>リンクを知っている全員</h3>
568
+ <div className={styles.permissionRow}>
569
+ <select
570
+ value={this.state.linkPermission}
571
+ onChange={(e) => this.setState({ linkPermission: e.target.value })}
572
+ className={styles.roleSelect}
573
+ disabled={this.state.isProcessing}
574
+ >
575
+ <option value="reader">閲覧のみ</option>
576
+ <option value="writer">編集可能</option>
577
+ </select>
578
+ </div>
579
+ </div>
580
+
581
+ <div className={styles.shareSection}>
582
+ <h3>グループ内</h3>
583
+ <div className={styles.permissionRow}>
584
+ <select
585
+ value={this.state.groupPermission}
586
+ onChange={(e) => this.setState({ groupPermission: e.target.value })}
587
+ className={styles.roleSelect}
588
+ disabled={this.state.isProcessing}
589
+ >
590
+ <option value="reader">閲覧のみ</option>
591
+ <option value="writer">編集可能</option>
592
+ </select>
593
+ </div>
594
+ </div>
595
+ </div>
596
+
597
+ <div className={styles.modalFooter}>
598
+ <button
599
+ onClick={this.applyPermissions}
600
+ className={styles.applyButton}
601
+ disabled={this.state.isProcessing}
602
+ >
603
+ 適用
604
+ </button>
605
+ <button
606
+ onClick={this.closeShareModal}
607
+ className={styles.cancelButton}
608
+ disabled={this.state.isProcessing}
609
+ >
610
+ キャンセル
611
+ </button>
612
+ </div>
613
+
614
+ {this.state.isProcessing && (
615
+ <div className={styles.processingOverlay}>
616
+ <div className={styles.spinner}></div>
617
+ <div>処理中...</div>
618
+ </div>
619
+ )}
620
+ </div>
621
+ </div>
622
+ );
623
+ }
624
+
625
  render() {
626
  return (
627
  <div>
 
640
  </Button>
641
 
642
  {this.renderModal()}
643
+ {this.renderShareModal()}
644
  </div>
645
  );
646
  }
 
651
  this.setState({
652
  accessToken: null,
653
  currentAccountEmail: null,
654
+ currentAccountName: null,
655
+ files: []
656
  });
657
  localStorage.removeItem('googleDriveAccessToken');
658
  localStorage.removeItem('googleDriveAccountEmail');
 
663
  this.setState({isProcessing: true});
664
  try {
665
  await this.saveToGoogleDrive(null, `${this.state.newFileName}.s4s.txt`, this.state.sharePermission);
666
+ this.showAlert("success", "新規保存しました");
667
  this.setState({showNewFileInput: false});
668
  this.fetchDriveFiles(this.state.accessToken);
669
  } catch (error) {
670
  console.error("新規保存エラー:", error);
671
+ this.showAlert("error", "新規保存に失敗しました");
672
  } finally {
673
  this.setState({isProcessing: false});
674
  }
 
692
  this.setState({isProcessing: true});
693
  try {
694
  await this.saveToGoogleDrive(project.id, project.name);
695
+ this.showAlert("success", "上書き保存しました");
696
  this.fetchDriveFiles(this.state.accessToken);
697
  } catch (error) {
698
  console.error("ファイル上書きエラー:", error);
699
+ this.showAlert("error", "ファイルの上書きに失敗しました");
700
  } finally {
701
  this.setState({isProcessing: false});
702
  }
703
  }
704
  };
705
 
706
+ handleShareFile = async (fileId) => {
707
  if (this.state.isProcessing) return;
708
 
709
+ this.setState({ isProcessing: true });
710
+ try {
711
+ // ファイルを公開設定(anyoneに閲覧権限を付与)
712
+ await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}/permissions`, {
713
+ method: "POST",
714
+ headers: {
715
+ "Authorization": `Bearer ${this.state.accessToken}`,
716
+ "Content-Type": "application/json",
717
+ },
718
+ body: JSON.stringify({
719
+ type: "anyone",
720
+ role: "reader",
721
+ }),
722
+ });
723
+
724
+ const SHARE_URL = "https://scratch-school.ct.ws/upload?id=";
725
+ const shortUrl = `${SHARE_URL}${fileId}`;
726
+
727
+ // 短縮URLをクリップボードにコピー
728
+ navigator.clipboard.writeText(shortUrl)
729
+ .then(() => {
730
+ this.showAlert("success", "公開リンクをクリップボードにコピーしました");
731
+ // 新しいタブで公開ページを開く
732
+ window.open(shortUrl, "_blank");
733
+ })
734
+ .catch(() => {
735
+ this.showAlert("error", "リンクのコピーに失敗しました");
736
+ });
737
+
738
+ } catch (error) {
739
+ console.error("公開エラー:", error);
740
+ this.showAlert("error", "ファイルの公開に失敗しました");
741
+ } finally {
742
+ this.setState({ isProcessing: false });
743
+ }
744
  };
 
745
  handleDeleteFile = async (project, thumbnailFiles) => {
746
  if (this.state.isProcessing) return;
747
 
 
758
  await this.deleteFile(thumbnailToDelete.id);
759
  }
760
 
761
+ this.showAlert("success", "ファイルを削除しました");
762
  this.fetchDriveFiles(this.state.accessToken);
763
  } catch (error) {
764
  console.error("削除エラー:", error);
765
+ this.showAlert("error", "ファイルの削除に失敗しました");
766
  } finally {
767
  this.setState({isProcessing: false});
768
  }
 
773
  if (this.state.isProcessing) return;
774
 
775
  navigator.clipboard.writeText(text)
776
+ .then(() => this.showAlert("success", "リンクをクリップボードにコピーしました"))
777
+ .catch(() => this.showAlert("error", "リンクのコピーに失敗しました"));
778
  };
779
 
780
  async deleteFile(fileId) {
 
791
  }
792
 
793
  async saveToGoogleDrive(fileId, fileName, permission = 'reader') {
794
+ if (!window.vm) {
795
+ throw new Error("VMが初期化されていません");
796
+ }
797
+
798
  const blob = await window.vm.saveProjectSb3();
799
 
800
  const metadata = {
 
875
  "Content-Type": "application/json",
876
  },
877
  body: JSON.stringify({
878
+ role: permission,
879
  type: "anyone",
880
  }),
881
  });
882
  }
883
+
884
+ return fileData;
885
  }
886
 
887
  getProjectThumbnail() {
888
+ return new Promise((resolve) => {
889
+ if (window.vm && window.vm.renderer && window.vm.renderer.requestSnapshot) {
890
+ window.vm.renderer.requestSnapshot(uri => {
891
+ resolve(uri);
892
+ });
893
+ } else {
894
+ // デフォルトのサムネイル
895
+ resolve('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==');
896
+ }
897
  });
898
  }
899
  }
 
903
  showAlert: PropTypes.func.isRequired,
904
  projectTitle: PropTypes.string
905
  };
906
+
907
  const mapStateToProps = state => ({
908
  projectTitle: state.scratchGui.projectTitle
909
  });
910
+
911
  export default connect(mapStateToProps)(GoogleDriveSave);
src/components/sound-editor/icon--copy-to-new.svg CHANGED
src/components/sound-editor/icon--copy.svg CHANGED
src/components/sound-editor/icon--delete.svg CHANGED
src/components/sound-editor/icon--echo.svg CHANGED
src/components/sound-editor/icon--fade-in.svg CHANGED
src/components/sound-editor/icon--fade-out.svg CHANGED
src/components/sound-editor/icon--faster.svg CHANGED
src/components/sound-editor/icon--format.svg CHANGED
src/components/sound-editor/icon--highpass.svg CHANGED
src/components/sound-editor/icon--louder.svg CHANGED
src/components/sound-editor/icon--lowpass.svg CHANGED
src/components/sound-editor/icon--modify.svg CHANGED
src/components/sound-editor/icon--mute.svg CHANGED
src/components/sound-editor/icon--redo.svg CHANGED
src/components/sound-editor/icon--reverse.svg CHANGED
src/components/sound-editor/icon--robot.svg CHANGED
src/components/sound-editor/icon--slower.svg CHANGED
src/components/sound-editor/icon--softer.svg CHANGED
src/components/sound-editor/icon--undo.svg CHANGED
src/components/sound-editor/sound-editor.css CHANGED
@@ -91,7 +91,7 @@ $border-radius: 0.25rem;
91
  background-color: $ui-primary;
92
  }
93
 
94
- .button > img {
95
  flex-grow: 1;
96
  max-width: 100%;
97
  max-height: 100%;
@@ -119,7 +119,7 @@ $border-radius: 0.25rem;
119
  border-color: transparent;
120
  }
121
 
122
- .round-button > img {
123
  flex-grow: 1;
124
  max-width: 100%;
125
  max-height: 100%;
@@ -147,7 +147,7 @@ $border-radius: 0.25rem;
147
  margin-right: 1rem;
148
  }
149
 
150
- .trim-button > img {
151
  width: 1.25rem;
152
  }
153
 
@@ -169,7 +169,7 @@ $border-radius: 0.25rem;
169
  margin: 0;
170
  }
171
 
172
- .effect-button img {
173
  width: 1.25rem;
174
  height: 1.25rem;
175
  margin-bottom: -0.375rem;
@@ -187,13 +187,13 @@ $border-radius: 0.25rem;
187
  margin: 0;
188
  }
189
 
190
- .tool-button img {
191
  width: 4rem;
192
  height: 1.5rem;
193
  margin-bottom: -0.375rem;
194
  }
195
 
196
- [dir="rtl"] .flip-in-rtl img {
197
  transform: scaleX(-1);
198
  }
199
 
@@ -242,7 +242,7 @@ $border-radius: 0.25rem;
242
  border-bottom-right-radius: $border-radius;
243
  }
244
 
245
- .button:disabled > img {
246
  opacity: 0.25;
247
  }
248
 
 
91
  background-color: $ui-primary;
92
  }
93
 
94
+ .button > svg {
95
  flex-grow: 1;
96
  max-width: 100%;
97
  max-height: 100%;
 
119
  border-color: transparent;
120
  }
121
 
122
+ .round-button > svg {
123
  flex-grow: 1;
124
  max-width: 100%;
125
  max-height: 100%;
 
147
  margin-right: 1rem;
148
  }
149
 
150
+ .trim-button > svg {
151
  width: 1.25rem;
152
  }
153
 
 
169
  margin: 0;
170
  }
171
 
172
+ .effect-button svg {
173
  width: 1.25rem;
174
  height: 1.25rem;
175
  margin-bottom: -0.375rem;
 
187
  margin: 0;
188
  }
189
 
190
+ .tool-button svg {
191
  width: 4rem;
192
  height: 1.5rem;
193
  margin-bottom: -0.375rem;
194
  }
195
 
196
+ [dir="rtl"] .flip-in-rtl svg {
197
  transform: scaleX(-1);
198
  }
199
 
 
242
  border-bottom-right-radius: $border-radius;
243
  }
244
 
245
+ .button:disabled > svg {
246
  opacity: 0.25;
247
  }
248
 
src/components/sound-editor/sound-editor.jsx CHANGED
@@ -14,33 +14,50 @@ import {SOUND_BYTE_LIMIT} from '../../lib/audio/audio-util.js';
14
 
15
  import styles from './sound-editor.css';
16
 
17
- import playIcon from './icon--play.svg';
18
- import stopIcon from './icon--stop.svg';
19
- import redoIcon from './icon--redo.svg';
20
- import undoIcon from './icon--undo.svg';
21
- import modifyIcon from './icon--modify.svg';
22
- import formatIcon from './icon--format.svg';
23
- import fasterIcon from './icon--faster.svg';
24
- import slowerIcon from './icon--slower.svg';
25
- import louderIcon from './icon--louder.svg';
26
- import softerIcon from './icon--softer.svg';
27
- import robotIcon from './icon--robot.svg';
28
- import echoIcon from './icon--echo.svg';
29
- import highpassIcon from './icon--highpass.svg';
30
- import lowpassIcon from './icon--lowpass.svg';
31
- import reverseIcon from './icon--reverse.svg';
32
- import fadeOutIcon from './icon--fade-out.svg';
33
- import fadeInIcon from './icon--fade-in.svg';
34
- import muteIcon from './icon--mute.svg';
35
 
36
- import deleteIcon from './icon--delete.svg';
37
- import copyIcon from './icon--copy.svg';
38
- import pasteIcon from './icon--paste.svg';
39
- import copyToNewIcon from './icon--copy-to-new.svg';
40
 
41
  const BufferedInput = BufferedInputHOC(Input);
42
 
43
  const messages = defineMessages({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  sound: {
45
  id: 'gui.soundEditor.sound',
46
  description: 'Label for the name of the sound',
@@ -154,21 +171,13 @@ const formatTime = timeSeconds => {
154
  };
155
 
156
  const formatDuration = (playheadPercent, trimStartPercent, trimEndPercent, durationSeconds) => {
157
- // If no selection, the trim is the entire sound.
158
  trimStartPercent = trimStartPercent === null ? 0 : trimStartPercent;
159
  trimEndPercent = trimEndPercent === null ? 1 : trimEndPercent;
160
-
161
- // If the playhead doesn't exist, assume it's at the start of the selection.
162
  playheadPercent = playheadPercent === null ? trimStartPercent : playheadPercent;
163
-
164
- // If selection has zero length, treat it as the entire sound being selected.
165
- // This happens when the user first clicks to start making a selection.
166
  const trimSize = (trimEndPercent - trimStartPercent) || 1;
167
  const trimDuration = trimSize * durationSeconds;
168
-
169
  const progressInTrim = (playheadPercent - trimStartPercent) / trimSize;
170
  const currentTime = progressInTrim * trimDuration;
171
-
172
  return `${formatTime(currentTime)} / ${formatTime(trimDuration)}`;
173
  };
174
 
@@ -203,11 +212,7 @@ const SoundEditor = props => (
203
  title={props.intl.formatMessage(messages.undo)}
204
  onClick={props.onUndo}
205
  >
206
- <img
207
- className={styles.undoIcon}
208
- draggable={false}
209
- src={undoIcon}
210
- />
211
  </button>
212
  <button
213
  className={styles.button}
@@ -215,31 +220,27 @@ const SoundEditor = props => (
215
  title={props.intl.formatMessage(messages.redo)}
216
  onClick={props.onRedo}
217
  >
218
- <img
219
- className={styles.redoIcon}
220
- draggable={false}
221
- src={redoIcon}
222
- />
223
  </button>
224
  </div>
225
  </div>
226
  <div className={styles.inputGroup}>
227
  <IconButton
228
  className={styles.toolButton}
229
- img={copyIcon}
230
  title={props.intl.formatMessage(messages.copy)}
231
  onClick={props.onCopy}
232
  />
233
  <IconButton
234
  className={styles.toolButton}
235
  disabled={props.canPaste === false}
236
- img={pasteIcon}
237
  title={props.intl.formatMessage(messages.paste)}
238
  onClick={props.onPaste}
239
  />
240
  <IconButton
241
  className={classNames(styles.toolButton, styles.flipInRtl)}
242
- img={copyToNewIcon}
243
  title={props.intl.formatMessage(messages.copyToNew)}
244
  onClick={props.onCopyToNew}
245
  />
@@ -247,7 +248,7 @@ const SoundEditor = props => (
247
  <IconButton
248
  className={styles.toolButton}
249
  disabled={props.trimStart === null}
250
- img={deleteIcon}
251
  title={props.intl.formatMessage(messages.delete)}
252
  onClick={props.onDelete}
253
  />
@@ -277,10 +278,7 @@ const SoundEditor = props => (
277
  title={props.intl.formatMessage(messages.stop)}
278
  onClick={props.onStop}
279
  >
280
- <img
281
- draggable={false}
282
- src={stopIcon}
283
- />
284
  </button>
285
  ) : (
286
  <button
@@ -288,97 +286,94 @@ const SoundEditor = props => (
288
  title={props.intl.formatMessage(messages.play)}
289
  onClick={props.onPlay}
290
  >
291
- <img
292
- draggable={false}
293
- src={playIcon}
294
- />
295
  </button>
296
  )}
297
  </div>
298
  <div className={styles.effects}>
299
  <IconButton
300
  className={styles.effectButton}
301
- img={modifyIcon}
302
- title={"Modify"}
303
  onClick={props.onModifySound}
304
  />
305
  <IconButton
306
  className={styles.effectButton}
307
- img={fasterIcon}
308
  title={<FormattedMessage {...messages.faster} />}
309
  onClick={props.onFaster}
310
  />
311
  <IconButton
312
  className={styles.effectButton}
313
- img={slowerIcon}
314
  title={<FormattedMessage {...messages.slower} />}
315
  onClick={props.onSlower}
316
  />
317
  <IconButton
318
  disabled={props.tooLoud}
319
  className={classNames(styles.effectButton, styles.flipInRtl)}
320
- img={louderIcon}
321
  title={<FormattedMessage {...messages.louder} />}
322
  onClick={props.onLouder}
323
  />
324
  <IconButton
325
  className={classNames(styles.effectButton, styles.flipInRtl)}
326
- img={softerIcon}
327
  title={<FormattedMessage {...messages.softer} />}
328
  onClick={props.onSofter}
329
  />
330
  <IconButton
331
  className={classNames(styles.effectButton, styles.flipInRtl)}
332
- img={muteIcon}
333
  title={<FormattedMessage {...messages.mute} />}
334
  onClick={props.onMute}
335
  />
336
  <IconButton
337
  className={styles.effectButton}
338
- img={fadeInIcon}
339
  title={<FormattedMessage {...messages.fadeIn} />}
340
  onClick={props.onFadeIn}
341
  />
342
  <IconButton
343
  className={styles.effectButton}
344
- img={fadeOutIcon}
345
  title={<FormattedMessage {...messages.fadeOut} />}
346
  onClick={props.onFadeOut}
347
  />
348
  <IconButton
349
  className={styles.effectButton}
350
- img={reverseIcon}
351
  title={<FormattedMessage {...messages.reverse} />}
352
  onClick={props.onReverse}
353
  />
354
  <IconButton
355
  className={styles.effectButton}
356
- img={robotIcon}
357
  title={<FormattedMessage {...messages.robot} />}
358
  onClick={props.onRobot}
359
  />
360
  <IconButton
361
  className={styles.effectButton}
362
- img={echoIcon}
363
  title={<FormattedMessage {...messages.echo} />}
364
  onClick={props.onEcho}
365
  />
366
  <IconButton
367
  className={styles.effectButton}
368
- img={lowpassIcon}
369
- title={"Low Pass"}
370
  onClick={props.onLowPass}
371
  />
372
  <IconButton
373
  className={styles.effectButton}
374
- img={highpassIcon}
375
- title={"High Pass"}
376
  onClick={props.onHighPass}
377
  />
378
  <IconButton
379
  className={styles.effectButton}
380
- img={formatIcon}
381
- title={"Format"}
382
  onClick={props.onFormatSound}
383
  />
384
  </div>
@@ -446,6 +441,7 @@ const SoundEditor = props => (
446
  </div>
447
  );
448
 
 
449
  SoundEditor.propTypes = {
450
  isStereo: PropTypes.bool.isRequired,
451
  duration: PropTypes.number.isRequired,
 
14
 
15
  import styles from './sound-editor.css';
16
 
17
+ import { ReactComponent as PlayIcon } from './icon--play.svg';
18
+ import { ReactComponent as StopIcon } from './icon--stop.svg';
19
+ import { ReactComponent as RedoIcon } from './icon--redo.svg';
20
+ import { ReactComponent as UndoIcon } from './icon--undo.svg';
21
+ import { ReactComponent as ModifyIcon } from './icon--modify.svg';
22
+ import { ReactComponent as FormatIcon } from './icon--format.svg';
23
+ import { ReactComponent as FasterIcon } from './icon--faster.svg';
24
+ import { ReactComponent as SlowerIcon } from './icon--slower.svg';
25
+ import { ReactComponent as LouderIcon } from './icon--louder.svg';
26
+ import { ReactComponent as SofterIcon } from './icon--softer.svg';
27
+ import { ReactComponent as RobotIcon } from './icon--robot.svg';
28
+ import { ReactComponent as EchoIcon } from './icon--echo.svg';
29
+ import { ReactComponent as HighpassIcon } from './icon--highpass.svg';
30
+ import { ReactComponent as LowpassIcon } from './icon--lowpass.svg';
31
+ import { ReactComponent as ReverseIcon } from './icon--reverse.svg';
32
+ import { ReactComponent as FadeOutIcon } from './icon--fade-out.svg';
33
+ import { ReactComponent as FadeInIcon } from './icon--fade-in.svg';
34
+ import { ReactComponent as MuteIcon } from './icon--mute.svg';
35
 
36
+ import { ReactComponent as DeleteIcon } from './icon--delete.svg';
37
+ import { ReactComponent as CopyIcon } from './icon--copy.svg';
38
+ import { ReactComponent as PasteIcon } from './icon--paste.svg';
39
+ import { ReactComponent as CopyToNewIcon } from './icon--copy-to-new.svg';
40
 
41
  const BufferedInput = BufferedInputHOC(Input);
42
 
43
  const messages = defineMessages({
44
+ modify: {
45
+ id: 'gui.soundEditor.modify',
46
+ description: 'Title of the button to modify the sound',
47
+ defaultMessage: 'Modify'
48
+ },
49
+ lowpass: {
50
+ id: 'gui.soundEditor.lowpass',
51
+ defaultMessage: 'Low pass'
52
+ },
53
+ highpass: {
54
+ id: 'gui.soundEditor.highpass',
55
+ defaultMessage: 'High pass'
56
+ },
57
+ format: {
58
+ id: 'gui.soundEditor.format',
59
+ defaultMessage: 'Format'
60
+ },
61
  sound: {
62
  id: 'gui.soundEditor.sound',
63
  description: 'Label for the name of the sound',
 
171
  };
172
 
173
  const formatDuration = (playheadPercent, trimStartPercent, trimEndPercent, durationSeconds) => {
 
174
  trimStartPercent = trimStartPercent === null ? 0 : trimStartPercent;
175
  trimEndPercent = trimEndPercent === null ? 1 : trimEndPercent;
 
 
176
  playheadPercent = playheadPercent === null ? trimStartPercent : playheadPercent;
 
 
 
177
  const trimSize = (trimEndPercent - trimStartPercent) || 1;
178
  const trimDuration = trimSize * durationSeconds;
 
179
  const progressInTrim = (playheadPercent - trimStartPercent) / trimSize;
180
  const currentTime = progressInTrim * trimDuration;
 
181
  return `${formatTime(currentTime)} / ${formatTime(trimDuration)}`;
182
  };
183
 
 
212
  title={props.intl.formatMessage(messages.undo)}
213
  onClick={props.onUndo}
214
  >
215
+ <UndoIcon className={styles.undoIcon} />
 
 
 
 
216
  </button>
217
  <button
218
  className={styles.button}
 
220
  title={props.intl.formatMessage(messages.redo)}
221
  onClick={props.onRedo}
222
  >
223
+ <RedoIcon className={styles.redoIcon} />
 
 
 
 
224
  </button>
225
  </div>
226
  </div>
227
  <div className={styles.inputGroup}>
228
  <IconButton
229
  className={styles.toolButton}
230
+ img={<CopyIcon />}
231
  title={props.intl.formatMessage(messages.copy)}
232
  onClick={props.onCopy}
233
  />
234
  <IconButton
235
  className={styles.toolButton}
236
  disabled={props.canPaste === false}
237
+ img={<PasteIcon />}
238
  title={props.intl.formatMessage(messages.paste)}
239
  onClick={props.onPaste}
240
  />
241
  <IconButton
242
  className={classNames(styles.toolButton, styles.flipInRtl)}
243
+ img={<CopyToNewIcon />}
244
  title={props.intl.formatMessage(messages.copyToNew)}
245
  onClick={props.onCopyToNew}
246
  />
 
248
  <IconButton
249
  className={styles.toolButton}
250
  disabled={props.trimStart === null}
251
+ img={<DeleteIcon />}
252
  title={props.intl.formatMessage(messages.delete)}
253
  onClick={props.onDelete}
254
  />
 
278
  title={props.intl.formatMessage(messages.stop)}
279
  onClick={props.onStop}
280
  >
281
+ <StopIcon />
 
 
 
282
  </button>
283
  ) : (
284
  <button
 
286
  title={props.intl.formatMessage(messages.play)}
287
  onClick={props.onPlay}
288
  >
289
+ <PlayIcon />
 
 
 
290
  </button>
291
  )}
292
  </div>
293
  <div className={styles.effects}>
294
  <IconButton
295
  className={styles.effectButton}
296
+ img={<ModifyIcon />}
297
+ title={<FormattedMessage {...messages.modify} />}
298
  onClick={props.onModifySound}
299
  />
300
  <IconButton
301
  className={styles.effectButton}
302
+ img={<FasterIcon />}
303
  title={<FormattedMessage {...messages.faster} />}
304
  onClick={props.onFaster}
305
  />
306
  <IconButton
307
  className={styles.effectButton}
308
+ img={<SlowerIcon />}
309
  title={<FormattedMessage {...messages.slower} />}
310
  onClick={props.onSlower}
311
  />
312
  <IconButton
313
  disabled={props.tooLoud}
314
  className={classNames(styles.effectButton, styles.flipInRtl)}
315
+ img={<LouderIcon />}
316
  title={<FormattedMessage {...messages.louder} />}
317
  onClick={props.onLouder}
318
  />
319
  <IconButton
320
  className={classNames(styles.effectButton, styles.flipInRtl)}
321
+ img={<SofterIcon />}
322
  title={<FormattedMessage {...messages.softer} />}
323
  onClick={props.onSofter}
324
  />
325
  <IconButton
326
  className={classNames(styles.effectButton, styles.flipInRtl)}
327
+ img={<MuteIcon />}
328
  title={<FormattedMessage {...messages.mute} />}
329
  onClick={props.onMute}
330
  />
331
  <IconButton
332
  className={styles.effectButton}
333
+ img={<FadeInIcon />}
334
  title={<FormattedMessage {...messages.fadeIn} />}
335
  onClick={props.onFadeIn}
336
  />
337
  <IconButton
338
  className={styles.effectButton}
339
+ img={<FadeOutIcon />}
340
  title={<FormattedMessage {...messages.fadeOut} />}
341
  onClick={props.onFadeOut}
342
  />
343
  <IconButton
344
  className={styles.effectButton}
345
+ img={<ReverseIcon />}
346
  title={<FormattedMessage {...messages.reverse} />}
347
  onClick={props.onReverse}
348
  />
349
  <IconButton
350
  className={styles.effectButton}
351
+ img={<RobotIcon />}
352
  title={<FormattedMessage {...messages.robot} />}
353
  onClick={props.onRobot}
354
  />
355
  <IconButton
356
  className={styles.effectButton}
357
+ img={<EchoIcon />}
358
  title={<FormattedMessage {...messages.echo} />}
359
  onClick={props.onEcho}
360
  />
361
  <IconButton
362
  className={styles.effectButton}
363
+ img={<LowpassIcon />}
364
+ title={<FormattedMessage {...messages.lowpass} />}
365
  onClick={props.onLowPass}
366
  />
367
  <IconButton
368
  className={styles.effectButton}
369
+ img={<HighpassIcon />}
370
+ title={<FormattedMessage {...messages.highpass} />}
371
  onClick={props.onHighPass}
372
  />
373
  <IconButton
374
  className={styles.effectButton}
375
+ img={<FormatIcon />}
376
+ title={<FormattedMessage {...messages.format} />}
377
  onClick={props.onFormatSound}
378
  />
379
  </div>
 
441
  </div>
442
  );
443
 
444
+
445
  SoundEditor.propTypes = {
446
  isStereo: PropTypes.bool.isRequired,
447
  duration: PropTypes.number.isRequired,
src/components/sprite-info/sprite-info.jsx CHANGED
@@ -17,8 +17,8 @@ import styles from './sprite-info.css';
17
 
18
  import xIcon from './icon--x.svg';
19
  import yIcon from './icon--y.svg';
20
- import showIcon from './icon--show.svg';
21
- import hideIcon from './icon--hide.svg';
22
 
23
  const BufferedInput = BufferedInputHOC(Input);
24
 
@@ -200,10 +200,7 @@ class SpriteInfo extends React.Component {
200
  onClick={this.props.onClickVisible}
201
  onKeyPress={this.props.onPressVisible}
202
  >
203
- <img
204
- className={styles.icon}
205
- src={showIcon}
206
- />
207
  </div>
208
  <div
209
  className={classNames(
@@ -219,10 +216,7 @@ class SpriteInfo extends React.Component {
219
  onClick={this.props.onClickNotVisible}
220
  onKeyPress={this.props.onPressNotVisible}
221
  >
222
- <img
223
- className={styles.icon}
224
- src={hideIcon}
225
- />
226
  </div>
227
  </div>
228
  </div>
@@ -294,4 +288,4 @@ SpriteInfo.propTypes = {
294
  ])
295
  };
296
 
297
- export default injectIntl(SpriteInfo);
 
17
 
18
  import xIcon from './icon--x.svg';
19
  import yIcon from './icon--y.svg';
20
+ import { ReactComponent as ShowIcon } from './icon--show.svg';
21
+ import { ReactComponent as HideIcon } from './icon--hide.svg';
22
 
23
  const BufferedInput = BufferedInputHOC(Input);
24
 
 
200
  onClick={this.props.onClickVisible}
201
  onKeyPress={this.props.onPressVisible}
202
  >
203
+ <ShowIcon className={styles.icon} />
 
 
 
204
  </div>
205
  <div
206
  className={classNames(
 
216
  onClick={this.props.onClickNotVisible}
217
  onKeyPress={this.props.onPressNotVisible}
218
  >
219
+ <HideIcon className={styles.icon} />
 
 
 
220
  </div>
221
  </div>
222
  </div>
 
288
  ])
289
  };
290
 
291
+ export default injectIntl(SpriteInfo);
src/containers/extension-library.jsx CHANGED
@@ -5,6 +5,7 @@ import VM from 'scratch-vm';
5
  import { defineMessages, injectIntl, intlShape } from 'react-intl';
6
  import log from '../lib/log';
7
  import { manuallyTrustExtension } from './tw-security-manager.jsx';
 
8
 
9
  import extensionLibraryContent from '../lib/libraries/extensions/index.jsx';
10
  import extensionTags from '../lib/libraries/extension-tags';
@@ -212,7 +213,12 @@ class ExtensionLibrary extends React.PureComponent {
212
  tags={extensionTags}
213
  id="extensionLibrary"
214
  actor="ExtensionLibrary"
215
- header={"Extensions"}
 
 
 
 
 
216
  title={this.props.intl.formatMessage(messages.extensionTitle)}
217
  visible={this.props.visible}
218
  onItemSelected={this.handleItemSelect}
 
5
  import { defineMessages, injectIntl, intlShape } from 'react-intl';
6
  import log from '../lib/log';
7
  import { manuallyTrustExtension } from './tw-security-manager.jsx';
8
+ import { FormattedMessage } from 'react-intl';
9
 
10
  import extensionLibraryContent from '../lib/libraries/extensions/index.jsx';
11
  import extensionTags from '../lib/libraries/extension-tags';
 
213
  tags={extensionTags}
214
  id="extensionLibrary"
215
  actor="ExtensionLibrary"
216
+ header={
217
+ <FormattedMessage
218
+ defaultMessage="Extensions"
219
+ description="Name for the 'Extension'"
220
+ id="gui.extensions"
221
+ />}
222
  title={this.props.intl.formatMessage(messages.extensionTitle)}
223
  visible={this.props.visible}
224
  onItemSelected={this.handleItemSelect}
src/containers/play-button.jsx CHANGED
@@ -19,7 +19,7 @@ class PlayButton extends React.Component {
19
  touchStarted: false
20
  };
21
  }
22
- getDerivedStateFromProps (props, state) {
23
  // if touchStarted is true and it's not playing, the sound must have ended.
24
  // reset the touchStarted state to allow the sound to be replayed
25
  if (state.touchStarted && !props.isPlaying) {
 
19
  touchStarted: false
20
  };
21
  }
22
+ static getDerivedStateFromProps (props, state) {
23
  // if touchStarted is true and it's not playing, the sound must have ended.
24
  // reset the touchStarted state to allow the sound to be replayed
25
  if (state.touchStarted && !props.isPlaying) {
src/containers/sound-editor.jsx CHANGED
@@ -526,7 +526,7 @@ class SoundEditor extends React.Component {
526
  menu.textarea.append(pitchDiv, volumeDiv);
527
 
528
  const previewButton = document.createElement("button");
529
- previewButton.style = "border-radius: 1000px;padding: 5px;width: 45px;height: 45px;margin-right: 10px;border-style: none;background: #00c3ff;";
530
  previewButton.innerHTML = `<img draggable="false" style="max-width: 100%;max-height: 100%" src="${playURI}">`;
531
  menu.textarea.append(previewButton);
532
 
 
526
  menu.textarea.append(pitchDiv, volumeDiv);
527
 
528
  const previewButton = document.createElement("button");
529
+ previewButton.style = "border-radius: 1000px;padding: 5px;width: 45px;height: 45px;margin-right: 10px;border-style: none;background: #5100ff;";
530
  previewButton.innerHTML = `<img draggable="false" style="max-width: 100%;max-height: 100%" src="${playURI}">`;
531
  menu.textarea.append(previewButton);
532
 
src/lib/libraries/extension-tags.js CHANGED
@@ -1,4 +1,6 @@
 
1
  import messages from './tag-messages.js';
 
2
 
3
  let tags = [
4
  { tag: 'penguinmod', intlLabel: messages.penguinmod },
@@ -17,16 +19,19 @@ let tags = [
17
  { tag: 'library', intlLabel: messages.library },
18
  { tag: 'extcreate', intlLabel: messages.extcreate },
19
  { tag: 'divider3', intlLabel: messages.scratch, type: 'divider' },
20
- { tag: 'divider1', intlLabel: 'Actions', type: 'title' },
 
 
 
 
21
  { tag: 'custom', intlLabel: messages.customextension, type: 'custom', func: (library) => {
22
  library.select('');
23
  } },
24
  ];
25
 
26
- // 条件によって tags をフィルター
27
  if (typeof ENV !== 'undefined' && ENV.CanLoadCustomExtension === false) {
28
  tags = tags.filter(item => {
29
- if (item.tag === 'divider1' && item.intlLabel === 'Actions') return false;
30
  if (item.tag === 'custom') return false;
31
  return true;
32
  });
 
1
+ import React from 'react';
2
  import messages from './tag-messages.js';
3
+ import { FormattedMessage } from 'react-intl';
4
 
5
  let tags = [
6
  { tag: 'penguinmod', intlLabel: messages.penguinmod },
 
19
  { tag: 'library', intlLabel: messages.library },
20
  { tag: 'extcreate', intlLabel: messages.extcreate },
21
  { tag: 'divider3', intlLabel: messages.scratch, type: 'divider' },
22
+ {
23
+ tag: 'divider1',
24
+ intlLabel: { id: 'gui.library.actions', defaultMessage: 'Actions' },
25
+ type: 'divider'
26
+ },
27
  { tag: 'custom', intlLabel: messages.customextension, type: 'custom', func: (library) => {
28
  library.select('');
29
  } },
30
  ];
31
 
 
32
  if (typeof ENV !== 'undefined' && ENV.CanLoadCustomExtension === false) {
33
  tags = tags.filter(item => {
34
+ if (item.tag === 'divider1') return false; // divider1 はすべて除外
35
  if (item.tag === 'custom') return false;
36
  return true;
37
  });
src/lib/libraries/sound-tags.js CHANGED
@@ -1,13 +1,16 @@
 
1
  import messages from './tag-messages.js';
 
 
2
  export default [
3
  {tag: 'penguinmod', intlLabel: messages.penguinmod},
4
  {type: 'divider'},
5
- {type: 'subtitle', intlLabel: "Types"},
6
  {tag: 'themes', intlLabel: messages.themes},
7
  {tag: 'loops', intlLabel: messages.loops},
8
  {tag: 'effects', intlLabel: messages.effects},
9
  {type: 'divider'},
10
- {type: 'subtitle', intlLabel: "General"},
11
  {tag: 'animals', intlLabel: messages.animals},
12
  {tag: 'monster', intlLabel: messages.monsters},
13
  {tag: 'notes', intlLabel: messages.notes},
@@ -16,13 +19,13 @@ export default [
16
  {tag: 'wacky', intlLabel: messages.wacky},
17
  {tag: 'ui', intlLabel: messages.ui},
18
  {type: 'divider'},
19
- {type: 'subtitle', intlLabel: "Objects"},
20
  {tag: 'footsteps', intlLabel: messages.footsteps},
21
  {tag: 'space', intlLabel: messages.space},
22
  {tag: 'sports', intlLabel: messages.sports},
23
  {tag: 'swords', intlLabel: messages.swords},
24
  {tag: 'guns', intlLabel: messages.guns},
25
  {type: 'divider'},
26
- {type: 'subtitle', intlLabel: "Materials"},
27
- {tag: 'metal', intlLabel: messages.metal},
28
  ];
 
1
+ import React from 'react';
2
  import messages from './tag-messages.js';
3
+ import {FormattedMessage} from 'react-intl';
4
+
5
  export default [
6
  {tag: 'penguinmod', intlLabel: messages.penguinmod},
7
  {type: 'divider'},
8
+ {type: 'subtitle', intlLabel: <FormattedMessage id="gui.extension.types" defaultMessage="Types" />},
9
  {tag: 'themes', intlLabel: messages.themes},
10
  {tag: 'loops', intlLabel: messages.loops},
11
  {tag: 'effects', intlLabel: messages.effects},
12
  {type: 'divider'},
13
+ {type: 'subtitle', intlLabel: <FormattedMessage id="gui.extension.general" defaultMessage="General" />},
14
  {tag: 'animals', intlLabel: messages.animals},
15
  {tag: 'monster', intlLabel: messages.monsters},
16
  {tag: 'notes', intlLabel: messages.notes},
 
19
  {tag: 'wacky', intlLabel: messages.wacky},
20
  {tag: 'ui', intlLabel: messages.ui},
21
  {type: 'divider'},
22
+ {type: 'subtitle', intlLabel: <FormattedMessage id="gui.extension.objects" defaultMessage="Objects" />},
23
  {tag: 'footsteps', intlLabel: messages.footsteps},
24
  {tag: 'space', intlLabel: messages.space},
25
  {tag: 'sports', intlLabel: messages.sports},
26
  {tag: 'swords', intlLabel: messages.swords},
27
  {tag: 'guns', intlLabel: messages.guns},
28
  {type: 'divider'},
29
+ {type: 'subtitle', intlLabel: <FormattedMessage id="gui.extension.materials" defaultMessage="Materials" />},
30
+ {tag: 'metal', intlLabel: messages.metal}
31
  ];
src/lib/tw-load-scratch-blocks-hoc.jsx CHANGED
@@ -12,25 +12,35 @@ const LoadScratchBlocksHOC = function (WrappedComponent) {
12
  loaded: LazyScratchBlocks.isLoaded(),
13
  error: null
14
  };
15
- //console.log(this.state.loaded);
 
 
 
 
16
  if (!this.state.loaded) {
17
  LazyScratchBlocks.load()
18
  .then(() => {
19
- this.setState({
20
- loaded: true
21
- });
22
  })
23
  .catch(e => {
24
  log.error(e);
25
- this.setState({
26
- error: e
27
- });
28
  });
29
  }
30
  }
 
 
 
 
 
31
  handleReload () {
32
  location.reload();
33
  }
 
34
  render () {
35
  if (this.state.error !== null) {
36
  return (
@@ -41,18 +51,12 @@ const LoadScratchBlocksHOC = function (WrappedComponent) {
41
  );
42
  }
43
  if (!this.state.loaded) {
44
- return (
45
- <LoadingSpinner />
46
- );
47
  }
48
- return (
49
- <WrappedComponent
50
- {...this.props}
51
- />
52
- );
53
  }
54
  }
55
  return LoadScratchBlocks;
56
  };
57
 
58
- export default LoadScratchBlocksHOC;
 
12
  loaded: LazyScratchBlocks.isLoaded(),
13
  error: null
14
  };
15
+ this._isMounted = false;
16
+ }
17
+
18
+ componentDidMount () {
19
+ this._isMounted = true;
20
  if (!this.state.loaded) {
21
  LazyScratchBlocks.load()
22
  .then(() => {
23
+ if (this._isMounted) {
24
+ this.setState({ loaded: true });
25
+ }
26
  })
27
  .catch(e => {
28
  log.error(e);
29
+ if (this._isMounted) {
30
+ this.setState({ error: e });
31
+ }
32
  });
33
  }
34
  }
35
+
36
+ componentWillUnmount () {
37
+ this._isMounted = false;
38
+ }
39
+
40
  handleReload () {
41
  location.reload();
42
  }
43
+
44
  render () {
45
  if (this.state.error !== null) {
46
  return (
 
51
  );
52
  }
53
  if (!this.state.loaded) {
54
+ return <LoadingSpinner />;
 
 
55
  }
56
+ return <WrappedComponent {...this.props} />;
 
 
 
 
57
  }
58
  }
59
  return LoadScratchBlocks;
60
  };
61
 
62
+ export default LoadScratchBlocksHOC;
src/reducers/custom-ja.js CHANGED
@@ -279,7 +279,20 @@ const customJa = {
279
  "pm.settingsModal.stageSize": "ステージの大きさ",
280
  "gui.defaultProject.variable": "変数",
281
  "pm.menuBar.loadFromFolder": "フォルダから読み込む",
282
- "pm.menuBar.exportToFolder": "フォルダに保存"
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  }
284
  };
285
 
 
279
  "pm.settingsModal.stageSize": "ステージの大きさ",
280
  "gui.defaultProject.variable": "変数",
281
  "pm.menuBar.loadFromFolder": "フォルダから読み込む",
282
+ "pm.menuBar.exportToFolder": "フォルダに保存",
283
+ "pm.confirmBuggyUnstableExtension": "この拡張機能は実際のプロジェクトには推奨されません。不安定になり、後でプロジェクトに問題を引き起こす可能性があります。有効にしてもよろしいですか?",
284
+ "gui.extensions": "拡張機能",
285
+ "gui.library.filters": "フィルター",
286
+ "gui.library.actions": "",
287
+ "paint.paintEditor.more": "その他",
288
+ "gui.extension.types": "タイプ",
289
+ "gui.extension.general": "一般",
290
+ "gui.extension.objects": "物",
291
+ "gui.extension.materials": "金属",
292
+ "gui.soundEditor.modify": "修正",
293
+ "gui.soundEditor.lowpass": "高音除去",
294
+ "gui.soundEditor.highpass": "低音除去",
295
+ "gui.soundEditor.format": "フォーマット"
296
  }
297
  };
298
 
webpack.config.js CHANGED
@@ -152,6 +152,10 @@ const base = {
152
  }
153
  }
154
  }]
 
 
 
 
155
  }]
156
  },
157
  plugins: [
@@ -310,7 +314,7 @@ module.exports = [
310
  module: {
311
  rules: base.module.rules.concat([
312
  {
313
- test: /\.(svg|png|wav|gif|jpg|mp3|ttf|otf|ico)$/,
314
  loader: 'file-loader',
315
  options: {
316
  outputPath: 'static/assets/',
 
152
  }
153
  }
154
  }]
155
+ },
156
+ {
157
+ test: /\.svg$/,
158
+ use: ['@svgr/webpack']
159
  }]
160
  },
161
  plugins: [
 
314
  module: {
315
  rules: base.module.rules.concat([
316
  {
317
+ test: /\.(png|wav|gif|jpg|mp3|ttf|otf|ico)$/,
318
  loader: 'file-loader',
319
  options: {
320
  outputPath: 'static/assets/',