Spaces:
Sleeping
Sleeping
自動クローン
Browse files- Dockerfile +3 -1
- README.md +2 -1
- package.json +1 -1
- src/addons/addons/hide-flyout/style.css +2 -2
- src/addons/addons/onion-skinning/style.css +2 -2
- src/addons/addons/paint-gradient-maker/userscript.js +2 -2
- src/addons/addons/paint-snap/userstyle.css +1 -1
- src/components/filter/filter.jsx +4 -10
- src/components/gui/dump.html +1 -1
- src/components/gui/gui.css +8 -8
- src/components/gui/gui.jsx +19 -56
- src/components/icon-button/icon-button.jsx +15 -6
- src/components/library-item/library-item.css +1 -1
- src/components/library/library.jsx +8 -1
- src/components/menu-bar/google-drive-save.css +103 -0
- src/components/menu-bar/google-drive-save.jsx +396 -118
- src/components/sound-editor/icon--copy-to-new.svg +0 -0
- src/components/sound-editor/icon--copy.svg +0 -0
- src/components/sound-editor/icon--delete.svg +0 -0
- src/components/sound-editor/icon--echo.svg +0 -0
- src/components/sound-editor/icon--fade-in.svg +0 -0
- src/components/sound-editor/icon--fade-out.svg +0 -0
- src/components/sound-editor/icon--faster.svg +0 -0
- src/components/sound-editor/icon--format.svg +0 -0
- src/components/sound-editor/icon--highpass.svg +0 -0
- src/components/sound-editor/icon--louder.svg +0 -0
- src/components/sound-editor/icon--lowpass.svg +0 -0
- src/components/sound-editor/icon--modify.svg +0 -0
- src/components/sound-editor/icon--mute.svg +0 -0
- src/components/sound-editor/icon--redo.svg +0 -0
- src/components/sound-editor/icon--reverse.svg +0 -0
- src/components/sound-editor/icon--robot.svg +0 -0
- src/components/sound-editor/icon--slower.svg +0 -0
- src/components/sound-editor/icon--softer.svg +0 -0
- src/components/sound-editor/icon--undo.svg +0 -0
- src/components/sound-editor/sound-editor.css +7 -7
- src/components/sound-editor/sound-editor.jsx +66 -70
- src/components/sprite-info/sprite-info.jsx +5 -11
- src/containers/extension-library.jsx +7 -1
- src/containers/play-button.jsx +1 -1
- src/containers/sound-editor.jsx +1 -1
- src/lib/libraries/extension-tags.js +8 -3
- src/lib/libraries/sound-tags.js +8 -5
- src/lib/tw-load-scratch-blocks-hoc.jsx +20 -16
- src/reducers/custom-ja.js +14 -1
- 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": "
|
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(
|
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: #
|
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(
|
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: #
|
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 ? "#
|
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 ? "#
|
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(
|
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
|
6 |
-
import
|
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 |
-
<
|
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 |
-
<
|
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="{"isPaintingLayer":true}" fill="none" fill-rule="nonzero" stroke="#
|
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="{"isPaintingLayer":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
|
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
|
145 |
width: 1.375rem;
|
146 |
}
|
147 |
|
@@ -189,29 +189,29 @@
|
|
189 |
background-color: $ui-secondary;
|
190 |
}
|
191 |
|
192 |
-
.tab
|
193 |
width: 1.375rem;
|
194 |
filter: grayscale(100%);
|
195 |
}
|
196 |
|
197 |
-
[dir="ltr"] .tab
|
198 |
margin-right: 0.125rem;
|
199 |
}
|
200 |
|
201 |
-
[dir="rtl"] .tab
|
202 |
margin-left: 0.125rem;
|
203 |
}
|
204 |
|
205 |
/* mirror blocks and sound tab icons */
|
206 |
-
[dir="rtl"] .tab:nth-of-type(1)
|
207 |
transform: scaleX(-1);
|
208 |
}
|
209 |
|
210 |
-
[dir="rtl"] .tab:nth-of-type(3)
|
211 |
transform: scaleX(-1);
|
212 |
}
|
213 |
|
214 |
-
.tab.is-selected
|
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
|
54 |
-
import
|
55 |
-
import
|
56 |
-
import
|
57 |
-
import
|
58 |
-
import
|
59 |
-
import
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
<
|
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 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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.
|
|
|
|
|
|
|
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: #
|
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)}>
|
|
|
|
|
|
|
|
|
|
|
|
|
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',
|
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({
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
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 |
-
|
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 |
-
|
434 |
this.setState({showNewFileInput: false});
|
435 |
this.fetchDriveFiles(this.state.accessToken);
|
436 |
} catch (error) {
|
437 |
console.error("新規保存エラー:", error);
|
438 |
-
|
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 |
-
|
463 |
this.fetchDriveFiles(this.state.accessToken);
|
464 |
} catch (error) {
|
465 |
console.error("ファイル上書きエラー:", error);
|
466 |
-
|
467 |
} finally {
|
468 |
this.setState({isProcessing: false});
|
469 |
}
|
470 |
}
|
471 |
};
|
472 |
|
473 |
-
handleShareFile = (fileId) => {
|
474 |
if (this.state.isProcessing) return;
|
475 |
|
476 |
-
|
477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
497 |
this.fetchDriveFiles(this.state.accessToken);
|
498 |
} catch (error) {
|
499 |
console.error("削除エラー:", error);
|
500 |
-
|
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(() =>
|
512 |
-
.catch(() =>
|
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
|
619 |
-
|
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 >
|
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 >
|
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 >
|
151 |
width: 1.25rem;
|
152 |
}
|
153 |
|
@@ -169,7 +169,7 @@ $border-radius: 0.25rem;
|
|
169 |
margin: 0;
|
170 |
}
|
171 |
|
172 |
-
.effect-button
|
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
|
191 |
width: 4rem;
|
192 |
height: 1.5rem;
|
193 |
margin-bottom: -0.375rem;
|
194 |
}
|
195 |
|
196 |
-
[dir="rtl"] .flip-in-rtl
|
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 >
|
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
|
18 |
-
import
|
19 |
-
import
|
20 |
-
import
|
21 |
-
import
|
22 |
-
import
|
23 |
-
import
|
24 |
-
import
|
25 |
-
import
|
26 |
-
import
|
27 |
-
import
|
28 |
-
import
|
29 |
-
import
|
30 |
-
import
|
31 |
-
import
|
32 |
-
import
|
33 |
-
import
|
34 |
-
import
|
35 |
|
36 |
-
import
|
37 |
-
import
|
38 |
-
import
|
39 |
-
import
|
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 |
-
<
|
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 |
-
<
|
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={
|
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={
|
237 |
title={props.intl.formatMessage(messages.paste)}
|
238 |
onClick={props.onPaste}
|
239 |
/>
|
240 |
<IconButton
|
241 |
className={classNames(styles.toolButton, styles.flipInRtl)}
|
242 |
-
img={
|
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={
|
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 |
-
<
|
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 |
-
<
|
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={
|
302 |
-
title={
|
303 |
onClick={props.onModifySound}
|
304 |
/>
|
305 |
<IconButton
|
306 |
className={styles.effectButton}
|
307 |
-
img={
|
308 |
title={<FormattedMessage {...messages.faster} />}
|
309 |
onClick={props.onFaster}
|
310 |
/>
|
311 |
<IconButton
|
312 |
className={styles.effectButton}
|
313 |
-
img={
|
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={
|
321 |
title={<FormattedMessage {...messages.louder} />}
|
322 |
onClick={props.onLouder}
|
323 |
/>
|
324 |
<IconButton
|
325 |
className={classNames(styles.effectButton, styles.flipInRtl)}
|
326 |
-
img={
|
327 |
title={<FormattedMessage {...messages.softer} />}
|
328 |
onClick={props.onSofter}
|
329 |
/>
|
330 |
<IconButton
|
331 |
className={classNames(styles.effectButton, styles.flipInRtl)}
|
332 |
-
img={
|
333 |
title={<FormattedMessage {...messages.mute} />}
|
334 |
onClick={props.onMute}
|
335 |
/>
|
336 |
<IconButton
|
337 |
className={styles.effectButton}
|
338 |
-
img={
|
339 |
title={<FormattedMessage {...messages.fadeIn} />}
|
340 |
onClick={props.onFadeIn}
|
341 |
/>
|
342 |
<IconButton
|
343 |
className={styles.effectButton}
|
344 |
-
img={
|
345 |
title={<FormattedMessage {...messages.fadeOut} />}
|
346 |
onClick={props.onFadeOut}
|
347 |
/>
|
348 |
<IconButton
|
349 |
className={styles.effectButton}
|
350 |
-
img={
|
351 |
title={<FormattedMessage {...messages.reverse} />}
|
352 |
onClick={props.onReverse}
|
353 |
/>
|
354 |
<IconButton
|
355 |
className={styles.effectButton}
|
356 |
-
img={
|
357 |
title={<FormattedMessage {...messages.robot} />}
|
358 |
onClick={props.onRobot}
|
359 |
/>
|
360 |
<IconButton
|
361 |
className={styles.effectButton}
|
362 |
-
img={
|
363 |
title={<FormattedMessage {...messages.echo} />}
|
364 |
onClick={props.onEcho}
|
365 |
/>
|
366 |
<IconButton
|
367 |
className={styles.effectButton}
|
368 |
-
img={
|
369 |
-
title={
|
370 |
onClick={props.onLowPass}
|
371 |
/>
|
372 |
<IconButton
|
373 |
className={styles.effectButton}
|
374 |
-
img={
|
375 |
-
title={
|
376 |
onClick={props.onHighPass}
|
377 |
/>
|
378 |
<IconButton
|
379 |
className={styles.effectButton}
|
380 |
-
img={
|
381 |
-
title={
|
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
|
21 |
-
import
|
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 |
-
<
|
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 |
-
<
|
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={
|
|
|
|
|
|
|
|
|
|
|
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: #
|
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 |
-
{
|
|
|
|
|
|
|
|
|
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'
|
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 |
-
|
|
|
|
|
|
|
|
|
16 |
if (!this.state.loaded) {
|
17 |
LazyScratchBlocks.load()
|
18 |
.then(() => {
|
19 |
-
this.
|
20 |
-
loaded: true
|
21 |
-
}
|
22 |
})
|
23 |
.catch(e => {
|
24 |
log.error(e);
|
25 |
-
this.
|
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: /\.(
|
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/',
|