pxt-core 8.2.9 → 8.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/built/pxt.js +40 -20
- package/built/pxtsim.d.ts +9 -0
- package/built/pxtsim.js +40 -20
- package/built/target.js +1 -1
- package/built/web/main.js +1 -1
- package/built/web/pxtapp.js +1 -1
- package/built/web/pxtembed.js +1 -1
- package/built/web/pxtsim.js +1 -1
- package/built/web/react-common-authcode.css +160 -32
- package/built/web/react-common-skillmap.css +1 -1
- package/built/web/rtlreact-common-skillmap.css +1 -1
- package/built/web/rtlsemantic.css +1 -1
- package/built/web/semantic.css +1 -1
- package/package.json +1 -1
- package/react-common/components/share/Share.tsx +8 -21
- package/react-common/components/share/ShareInfo.tsx +142 -132
- package/react-common/components/share/SocialButton.tsx +36 -6
- package/react-common/components/share/ThumbnailRecorder.tsx +149 -0
- package/react-common/styles/controls/Button.less +30 -3
- package/react-common/styles/controls/Modal.less +7 -2
- package/react-common/styles/share/share.less +169 -31
- package/react-common/components/share/GifInfo.tsx +0 -63
- package/react-common/components/share/GifRecorder.tsx +0 -97
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference path="../types.d.ts" />
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { SimRecorder } from "./ThumbnailRecorder";
|
|
4
5
|
import { ShareInfo } from "./ShareInfo";
|
|
5
6
|
|
|
6
7
|
export interface ShareData {
|
|
@@ -18,34 +19,20 @@ export interface ShareData {
|
|
|
18
19
|
export interface ShareProps {
|
|
19
20
|
projectName: string;
|
|
20
21
|
screenshotUri?: string;
|
|
21
|
-
|
|
22
|
+
isLoggedIn?: boolean;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
gifRecordAsync: () => Promise<void>;
|
|
25
|
-
gifRenderAsync: () => Promise<string | void>;
|
|
26
|
-
gifAddFrame: (dataUri: ImageData, delay?: number) => boolean;
|
|
24
|
+
simRecorder: SimRecorder;
|
|
27
25
|
publishAsync: (name: string, screenshotUri?: string, forceAnonymous?: boolean) => Promise<ShareData>;
|
|
28
|
-
registerSimulatorMsgHandler?: (handler: (msg: any) => void) => void;
|
|
29
|
-
unregisterSimulatorMsgHandler?: () => void;
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
export const Share = (props: ShareProps) => {
|
|
33
|
-
const { projectName, screenshotUri,
|
|
34
|
-
gifAddFrame, publishAsync, registerSimulatorMsgHandler, unregisterSimulatorMsgHandler } = props;
|
|
29
|
+
const { projectName, screenshotUri, isLoggedIn, simRecorder, publishAsync} = props;
|
|
35
30
|
|
|
36
|
-
return <div className="project-share">
|
|
37
|
-
{(!!screenshotAsync || !!gifRecordAsync) && <div className="project-share-simulator">
|
|
38
|
-
<div id="shareLoanedSimulator" />
|
|
39
|
-
</div>}
|
|
31
|
+
return <div className="project-share">
|
|
40
32
|
<ShareInfo projectName={projectName}
|
|
41
|
-
|
|
33
|
+
isLoggedIn={isLoggedIn}
|
|
42
34
|
screenshotUri={screenshotUri}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
gifRenderAsync={gifRenderAsync}
|
|
46
|
-
gifAddFrame={gifAddFrame}
|
|
47
|
-
publishAsync={publishAsync}
|
|
48
|
-
registerSimulatorMsgHandler={registerSimulatorMsgHandler}
|
|
49
|
-
unregisterSimulatorMsgHandler={unregisterSimulatorMsgHandler} />
|
|
35
|
+
simRecorder={simRecorder}
|
|
36
|
+
publishAsync={publishAsync} />
|
|
50
37
|
</div>
|
|
51
38
|
}
|
|
@@ -2,32 +2,26 @@ import * as React from "react";
|
|
|
2
2
|
import { Button } from "../controls/Button";
|
|
3
3
|
import { EditorToggle } from "../controls/EditorToggle";
|
|
4
4
|
import { Input } from "../controls/Input";
|
|
5
|
-
import { MenuDropdown } from "../controls/MenuDropdown";
|
|
6
5
|
import { Textarea } from "../controls/Textarea";
|
|
7
6
|
import { Modal } from "../controls/Modal";
|
|
8
7
|
|
|
9
8
|
import { ShareData } from "./Share";
|
|
10
|
-
import {
|
|
9
|
+
import { ThumbnailRecorder } from "./ThumbnailRecorder";
|
|
11
10
|
import { SocialButton } from "./SocialButton";
|
|
11
|
+
import { Checkbox } from "../controls/Checkbox";
|
|
12
|
+
import { SimRecorder } from "./ThumbnailRecorder";
|
|
12
13
|
|
|
13
14
|
export interface ShareInfoProps {
|
|
14
15
|
projectName: string;
|
|
15
16
|
description?: string;
|
|
16
17
|
screenshotUri?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
screenshotAsync?: () => Promise<string>;
|
|
20
|
-
gifRecordAsync?: () => Promise<void>;
|
|
21
|
-
gifRenderAsync?: () => Promise<string | void>;
|
|
22
|
-
gifAddFrame?: (dataUri: ImageData, delay?: number) => boolean;
|
|
18
|
+
isLoggedIn?: boolean;
|
|
19
|
+
simRecorder: SimRecorder;
|
|
23
20
|
publishAsync: (name: string, screenshotUri?: string, forceAnonymous?: boolean) => Promise<ShareData>;
|
|
24
|
-
registerSimulatorMsgHandler?: (handler: (msg: any) => void) => void;
|
|
25
|
-
unregisterSimulatorMsgHandler?: () => void;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
export const ShareInfo = (props: ShareInfoProps) => {
|
|
29
|
-
const { projectName, description, screenshotUri,
|
|
30
|
-
gifRenderAsync, gifAddFrame, publishAsync, registerSimulatorMsgHandler, unregisterSimulatorMsgHandler } = props;
|
|
24
|
+
const { projectName, description, screenshotUri, isLoggedIn, simRecorder, publishAsync } = props;
|
|
31
25
|
const [ name, setName ] = React.useState(projectName);
|
|
32
26
|
const [ thumbnailUri, setThumbnailUri ] = React.useState(screenshotUri);
|
|
33
27
|
const [ shareState, setShareState ] = React.useState<"share" | "gifrecord" | "publish" | "publishing">("share");
|
|
@@ -35,8 +29,9 @@ export const ShareInfo = (props: ShareInfoProps) => {
|
|
|
35
29
|
const [ embedState, setEmbedState ] = React.useState<"none" | "code" | "editor" | "simulator">("none");
|
|
36
30
|
const [ showQRCode, setShowQRCode ] = React.useState(false);
|
|
37
31
|
const [ copySuccessful, setCopySuccessful ] = React.useState(false);
|
|
32
|
+
const [ isAnonymous, setIsAnonymous ] = React.useState(!isLoggedIn);
|
|
38
33
|
|
|
39
|
-
const showSimulator = !!
|
|
34
|
+
const showSimulator = !!simRecorder;
|
|
40
35
|
const showDescription = shareState !== "publish";
|
|
41
36
|
let qrCodeButtonRef: HTMLButtonElement;
|
|
42
37
|
let inputRef: HTMLInputElement;
|
|
@@ -54,9 +49,9 @@ export const ShareInfo = (props: ShareInfoProps) => {
|
|
|
54
49
|
exitGifRecord();
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
const handlePublishClick = async (
|
|
52
|
+
const handlePublishClick = async () => {
|
|
58
53
|
setShareState("publishing");
|
|
59
|
-
let publishedShareData = await publishAsync(name, thumbnailUri,
|
|
54
|
+
let publishedShareData = await publishAsync(name, thumbnailUri, isAnonymous);
|
|
60
55
|
setShareData(publishedShareData);
|
|
61
56
|
if (!publishedShareData?.error) setShareState("publish");
|
|
62
57
|
else setShareState("share")
|
|
@@ -132,12 +127,6 @@ export const ShareInfo = (props: ShareInfoProps) => {
|
|
|
132
127
|
onClick: () => setEmbedState("simulator")
|
|
133
128
|
}];
|
|
134
129
|
|
|
135
|
-
const dropdownOptions = [{
|
|
136
|
-
title: lf("Create snapshot"),
|
|
137
|
-
label: lf("Create snapshot"),
|
|
138
|
-
onClick: () => handlePublishClick(true)
|
|
139
|
-
}];
|
|
140
|
-
|
|
141
130
|
const handleQRCodeButtonRef = (ref: HTMLButtonElement) => {
|
|
142
131
|
if (ref) qrCodeButtonRef = ref;
|
|
143
132
|
}
|
|
@@ -153,124 +142,145 @@ export const ShareInfo = (props: ShareInfoProps) => {
|
|
|
153
142
|
|
|
154
143
|
const prePublish = shareState === "share" || shareState === "publishing";
|
|
155
144
|
|
|
145
|
+
const inputTitle = showSimulator && prePublish ? lf("Project Title") : lf("Project Link")
|
|
146
|
+
|
|
156
147
|
return <>
|
|
157
148
|
<div className="project-share-info">
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
placeholder={lf("Name your project")}
|
|
171
|
-
onChange={setName} />
|
|
172
|
-
<Textarea label={lf("Description")}
|
|
173
|
-
initialValue={description}
|
|
174
|
-
placeholder={lf("Tell others about your project")}
|
|
175
|
-
rows={5} />
|
|
176
|
-
</>
|
|
177
|
-
}
|
|
178
|
-
{prePublish && <>
|
|
179
|
-
{showSimulator && <div className="project-share-thumbnail">
|
|
180
|
-
{thumbnailUri
|
|
181
|
-
? <img src={thumbnailUri} />
|
|
182
|
-
: <div className="project-thumbnail-placeholder" />
|
|
183
|
-
}
|
|
184
|
-
<Button title={lf("Update project thumbnail")}
|
|
149
|
+
{showSimulator && shareState !== "gifrecord" &&
|
|
150
|
+
<div className="project-share-thumbnail">
|
|
151
|
+
{thumbnailUri
|
|
152
|
+
? <img src={thumbnailUri} />
|
|
153
|
+
: <div className="project-thumbnail-placeholder">
|
|
154
|
+
<div className="common-spinner" />
|
|
155
|
+
</div>
|
|
156
|
+
}
|
|
157
|
+
{shareState !== "publish" &&
|
|
158
|
+
<Button
|
|
159
|
+
className="link-button"
|
|
160
|
+
title={lf("Update project thumbnail")}
|
|
185
161
|
label={lf("Update project thumbnail")}
|
|
186
162
|
onClick={() => setShareState("gifrecord")} />
|
|
187
|
-
</div>}
|
|
188
|
-
<div>{lf("You need to publish your project to share it or embed it in other web pages. You acknowledge having consent to publish this project.")}</div>
|
|
189
|
-
{shareData?.error && <div className="project-share-error">
|
|
190
|
-
{(shareData.error.statusCode === 413
|
|
191
|
-
&& pxt.appTarget?.cloud?.cloudProviders?.github)
|
|
192
|
-
? lf("Oops! Your project is too big. You can create a GitHub repository to share it.")
|
|
193
|
-
: lf("Oops! There was an error. Please ensure you are connected to the Internet and try again.")}
|
|
194
|
-
</div>}
|
|
195
|
-
{shareState === "share" ?
|
|
196
|
-
<Button className="primary share-publish-button"
|
|
197
|
-
title={lf("Publish to share")}
|
|
198
|
-
label={lf("Publish to share")}
|
|
199
|
-
onClick={handlePublishClick} /> :
|
|
200
|
-
<Button className="primary share-publish-button"
|
|
201
|
-
title={lf("Publishing...")}
|
|
202
|
-
label={ <div className="common-spinner" />}
|
|
203
|
-
onClick={() => {}} />
|
|
204
163
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
</div>
|
|
225
|
-
<div className="project-share-actions">
|
|
226
|
-
<Button className="circle-button gray embed mobile-portrait-hidden"
|
|
227
|
-
title={lf("Show embed code")}
|
|
228
|
-
leftIcon="fas fa-code"
|
|
229
|
-
onClick={handleEmbedClick} />
|
|
230
|
-
<SocialButton className="circle-button facebook"
|
|
231
|
-
url={shareData?.url}
|
|
232
|
-
type='facebook'
|
|
233
|
-
heading={lf("Share on Facebook")} />
|
|
234
|
-
<SocialButton className="circle-button twitter"
|
|
235
|
-
url={shareData?.url}
|
|
236
|
-
type='twitter'
|
|
237
|
-
heading={lf("Share on Twitter")} />
|
|
238
|
-
{navigator.share && <Button className="circle-button device-share"
|
|
239
|
-
title={lf("Show device share options")}
|
|
240
|
-
ariaLabel={lf("Show device share options")}
|
|
241
|
-
leftIcon={"icon share"}
|
|
242
|
-
onClick={handleDeviceShareClick}
|
|
164
|
+
</div>
|
|
165
|
+
}
|
|
166
|
+
<div className="project-share-content">
|
|
167
|
+
{(prePublish || shareState === "publish") && <>
|
|
168
|
+
<div className="project-share-title project-share-label" id="share-input-title">
|
|
169
|
+
{inputTitle}
|
|
170
|
+
</div>
|
|
171
|
+
{showDescription && <>
|
|
172
|
+
<Input
|
|
173
|
+
ariaDescribedBy="share-input-title"
|
|
174
|
+
className="name-input"
|
|
175
|
+
initialValue={name}
|
|
176
|
+
placeholder={lf("Name your project")}
|
|
177
|
+
onChange={setName} />
|
|
178
|
+
{isLoggedIn && <Checkbox
|
|
179
|
+
id="persistent-share-checkbox"
|
|
180
|
+
label={lf("Allow people to see future changes to my project")}
|
|
181
|
+
isChecked={!isAnonymous}
|
|
182
|
+
onChange={val => setIsAnonymous(!val)}
|
|
243
183
|
/>}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
184
|
+
</>
|
|
185
|
+
}
|
|
186
|
+
{prePublish && <>
|
|
187
|
+
{shareData?.error && <div className="project-share-error">
|
|
188
|
+
{(shareData.error.statusCode === 413
|
|
189
|
+
&& pxt.appTarget?.cloud?.cloudProviders?.github)
|
|
190
|
+
? lf("Oops! Your project is too big. You can create a GitHub repository to share it.")
|
|
191
|
+
: lf("Oops! There was an error. Please ensure you are connected to the Internet and try again.")}
|
|
192
|
+
</div>}
|
|
193
|
+
<div>
|
|
194
|
+
{shareState === "share" &&
|
|
195
|
+
<Button className="primary share-publish-button"
|
|
196
|
+
title={lf("Continue")}
|
|
197
|
+
label={lf("Continue")}
|
|
198
|
+
onClick={handlePublishClick} />
|
|
199
|
+
}
|
|
200
|
+
{ shareState === "publishing" &&
|
|
201
|
+
<Button className="primary share-publish-button"
|
|
202
|
+
title={lf("Publishing...")}
|
|
203
|
+
label={ <div className="common-spinner" />}
|
|
204
|
+
onClick={() => {}} />
|
|
205
|
+
}
|
|
251
206
|
</div>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
207
|
+
</>}
|
|
208
|
+
|
|
209
|
+
{shareState === "publish" &&
|
|
210
|
+
<div className="project-share-data">
|
|
211
|
+
<div className="common-input-attached-button">
|
|
212
|
+
<Input
|
|
213
|
+
ariaDescribedBy="share-input-title"
|
|
214
|
+
handleInputRef={handleInputRef}
|
|
215
|
+
initialValue={shareData.url}
|
|
216
|
+
readOnly={true}
|
|
217
|
+
onChange={setName} />
|
|
218
|
+
<Button className={copySuccessful ? "green" : "primary"}
|
|
219
|
+
title={lf("Copy link")}
|
|
220
|
+
label={copySuccessful ? lf("Copied!") : lf("Copy")}
|
|
221
|
+
leftIcon="fas fa-link"
|
|
222
|
+
onClick={handleCopyClick}
|
|
223
|
+
onBlur={handleCopyBlur} />
|
|
224
|
+
</div>
|
|
225
|
+
<div className="project-share-actions">
|
|
226
|
+
<div className="project-share-social">
|
|
227
|
+
<Button className="square-button gray embed mobile-portrait-hidden"
|
|
228
|
+
title={lf("Show embed code")}
|
|
229
|
+
leftIcon="fas fa-code"
|
|
230
|
+
onClick={handleEmbedClick} />
|
|
231
|
+
<SocialButton className="square-button facebook"
|
|
232
|
+
url={shareData?.url}
|
|
233
|
+
type='facebook'
|
|
234
|
+
heading={lf("Share on Facebook")} />
|
|
235
|
+
<SocialButton className="square-button twitter"
|
|
236
|
+
url={shareData?.url}
|
|
237
|
+
type='twitter'
|
|
238
|
+
heading={lf("Share on Twitter")} />
|
|
239
|
+
<SocialButton className="square-button google-classroom"
|
|
240
|
+
url={shareData?.url}
|
|
241
|
+
type='google-classroom'
|
|
242
|
+
heading={lf("Share on Google Classroom")} />
|
|
243
|
+
<SocialButton className="square-button microsoft-teams"
|
|
244
|
+
url={shareData?.url}
|
|
245
|
+
type='microsoft-teams'
|
|
246
|
+
heading={lf("Share on Microsoft Teams")} />
|
|
247
|
+
<SocialButton className="square-button whatsapp"
|
|
248
|
+
url={shareData?.url}
|
|
249
|
+
type='whatsapp'
|
|
250
|
+
heading={lf("Share on WhatsApp")} />
|
|
251
|
+
{navigator.share && <Button className="square-button device-share"
|
|
252
|
+
title={lf("Show device share options")}
|
|
253
|
+
ariaLabel={lf("Show device share options")}
|
|
254
|
+
leftIcon={"icon share"}
|
|
255
|
+
onClick={handleDeviceShareClick}
|
|
256
|
+
/>}
|
|
257
|
+
</div>
|
|
258
|
+
<Button
|
|
259
|
+
className="menu-button project-qrcode"
|
|
260
|
+
buttonRef={handleQRCodeButtonRef}
|
|
261
|
+
title={lf("Show QR Code")}
|
|
262
|
+
label={<img className="qrcode-image" src={shareData?.qr} />}
|
|
263
|
+
onClick={handleQRCodeClick}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
}
|
|
268
|
+
{embedState !== "none" && <div className="project-embed">
|
|
269
|
+
<EditorToggle id="project-embed-toggle"
|
|
270
|
+
className="slim tablet-compact"
|
|
271
|
+
items={embedOptions}
|
|
272
|
+
selected={embedOptions.findIndex(i => i.name === embedState)} />
|
|
273
|
+
<Textarea readOnly={true}
|
|
274
|
+
rows={5}
|
|
275
|
+
initialValue={shareData?.embed[embedState]} />
|
|
276
|
+
</div>}
|
|
277
|
+
</>}
|
|
278
|
+
{shareState === "gifrecord" && <ThumbnailRecorder
|
|
279
|
+
initialUri={thumbnailUri}
|
|
280
|
+
onApply={applyGifChange}
|
|
281
|
+
onCancel={exitGifRecord}
|
|
282
|
+
simRecorder={simRecorder}/>}
|
|
283
|
+
</div>
|
|
274
284
|
|
|
275
285
|
{showQRCode &&
|
|
276
286
|
<Modal title={lf("QR Code")} onClose={handleQRCodeModalClose}>
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Button } from "../controls/Button";
|
|
3
|
+
import { classList } from "../util";
|
|
3
4
|
|
|
4
5
|
interface SocialButtonProps {
|
|
5
6
|
className?: string;
|
|
6
7
|
url?: string;
|
|
7
|
-
type?: "facebook" | "twitter" | "discourse";
|
|
8
|
+
type?: "facebook" | "twitter" | "discourse" | "google-classroom" | "microsoft-teams" | "whatsapp";
|
|
8
9
|
heading?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const SocialButton = (props: SocialButtonProps) => {
|
|
12
13
|
const { className, url, type, heading } = props;
|
|
13
14
|
|
|
15
|
+
const classes = classList(className, "social-button", "type")
|
|
16
|
+
|
|
14
17
|
const handleClick = () => {
|
|
15
18
|
const socialOptions = pxt.appTarget.appTheme.socialOptions;
|
|
16
19
|
let socialUrl = '';
|
|
@@ -41,13 +44,40 @@ export const SocialButton = (props: SocialButtonProps) => {
|
|
|
41
44
|
socialUrl += `&category=${encodeURIComponent(socialOptions.discourseCategory)}`;
|
|
42
45
|
break;
|
|
43
46
|
}
|
|
47
|
+
case "google-classroom":
|
|
48
|
+
socialUrl = `https://classroom.google.com/share?url=${encodeURIComponent(url)}`;
|
|
49
|
+
break;
|
|
50
|
+
case "microsoft-teams":
|
|
51
|
+
socialUrl = `https://teams.microsoft.com/share?href=${encodeURIComponent(url)}`;
|
|
52
|
+
break;
|
|
53
|
+
case "whatsapp":
|
|
54
|
+
socialUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent(url)}`;
|
|
55
|
+
break;
|
|
44
56
|
}
|
|
45
57
|
pxt.BrowserUtils.popupWindow(socialUrl, heading, 600, 600);
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
switch (type) {
|
|
61
|
+
// Icon buttons
|
|
62
|
+
case "facebook":
|
|
63
|
+
case "twitter":
|
|
64
|
+
case "discourse":
|
|
65
|
+
case "whatsapp":
|
|
66
|
+
return <Button className={classes}
|
|
67
|
+
ariaLabel={type}
|
|
68
|
+
title={heading}
|
|
69
|
+
leftIcon={`icon ${type}`}
|
|
70
|
+
onClick={handleClick} />
|
|
71
|
+
|
|
72
|
+
// Image buttons
|
|
73
|
+
case "google-classroom":
|
|
74
|
+
case "microsoft-teams":
|
|
75
|
+
return <Button className={classes}
|
|
76
|
+
ariaLabel={type}
|
|
77
|
+
title={heading}
|
|
78
|
+
label={<img src={`/static/logo/social-buttons/${type}.png`} alt={heading || pxt.U.rlf(type)} />}
|
|
79
|
+
onClick={handleClick} />
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
53
83
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Button } from "../controls/Button";
|
|
3
|
+
import { classList } from "../util";
|
|
4
|
+
|
|
5
|
+
export interface ThumbnailRecorderProps {
|
|
6
|
+
initialUri?: string;
|
|
7
|
+
|
|
8
|
+
onApply: (uri: string) => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
|
|
11
|
+
simRecorder: SimRecorder;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SimRecorderProps {
|
|
15
|
+
onSimRecorderInit: (ref: SimRecorderRef) => void;
|
|
16
|
+
}
|
|
17
|
+
export type SimRecorder = (props: SimRecorderProps) => JSX.Element
|
|
18
|
+
export type SimRecorderState = "default" | "recording" | "rendering"
|
|
19
|
+
export interface SimRecorderRef {
|
|
20
|
+
state: SimRecorderState;
|
|
21
|
+
startRecordingAsync: () => Promise<void>;
|
|
22
|
+
stopRecordingAsync: () => Promise<string>;
|
|
23
|
+
screenshotAsync: () => Promise<string>;
|
|
24
|
+
addStateChangeListener: (handler: (newState: SimRecorderState) => void) => void;
|
|
25
|
+
addThumbnailListener: (handler: (uri: string, type: "gif" | "png") => void) => void;
|
|
26
|
+
addErrorListener: (handler: (message: string) => void) => void;
|
|
27
|
+
removeStateChangeListener: (handler: (newState: SimRecorderState) => void) => void;
|
|
28
|
+
removeThumbnailListener: (handler: (uri: string, type: "gif" | "png") => void) => void;
|
|
29
|
+
removeErrorListener: (handler: (message: string) => void) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export const ThumbnailRecorder = (props: ThumbnailRecorderProps) => {
|
|
34
|
+
const { initialUri, onApply, onCancel, simRecorder } = props;
|
|
35
|
+
const [ uri, setUri ] = React.useState(undefined);
|
|
36
|
+
const [ error, setError] = React.useState<string>(undefined)
|
|
37
|
+
const [ recorderRef, setRecorderRef ] = React.useState<SimRecorderRef>(undefined);
|
|
38
|
+
const [ recorderState, setRecorderState ] = React.useState<SimRecorderState>("default");
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
if (!recorderRef) return undefined;
|
|
42
|
+
recorderRef.addStateChangeListener(setRecorderState);
|
|
43
|
+
recorderRef.addThumbnailListener(setUri);
|
|
44
|
+
recorderRef.addErrorListener(setError);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
recorderRef.removeStateChangeListener(setRecorderState);
|
|
48
|
+
recorderRef.removeThumbnailListener(setUri);
|
|
49
|
+
recorderRef.removeErrorListener(setError);
|
|
50
|
+
}
|
|
51
|
+
}, [recorderRef])
|
|
52
|
+
|
|
53
|
+
const handleApplyClick = (evt?: any) => {
|
|
54
|
+
onApply(uri);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleScreenshotClick = async () => {
|
|
58
|
+
setError(undefined);
|
|
59
|
+
if (recorderRef) recorderRef.screenshotAsync();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleRecordClick = async () => {
|
|
63
|
+
setError(undefined);
|
|
64
|
+
if (!recorderRef) return;
|
|
65
|
+
|
|
66
|
+
if (recorderRef.state === "recording") {
|
|
67
|
+
recorderRef.stopRecordingAsync();
|
|
68
|
+
}
|
|
69
|
+
else if (recorderRef.state === "default") {
|
|
70
|
+
recorderRef.startRecordingAsync();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const targetTheme = pxt.appTarget.appTheme;
|
|
75
|
+
|
|
76
|
+
const handleSimRecorderRef = (ref: SimRecorderRef) => {
|
|
77
|
+
setRecorderRef(ref);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const screenshotLabel = lf("Screenshot ({0})", targetTheme.simScreenshotKey);
|
|
81
|
+
const startRecordingLabel = lf("Record ({0})", targetTheme.simGifKey);
|
|
82
|
+
const stopRecordingLabel = lf("Stop recording ({0})", targetTheme.simGifKey) ;
|
|
83
|
+
|
|
84
|
+
const thumbnailLabel = uri ? lf("New Thumbnail") : lf("Current Thumbnail");
|
|
85
|
+
const classes = classList(
|
|
86
|
+
"gif-recorder-content",
|
|
87
|
+
uri && "has-uri"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return <>
|
|
91
|
+
<div className={classes}>
|
|
92
|
+
<div className="gif-recorder-sim">
|
|
93
|
+
<div className="gif-recorder-sim-embed">
|
|
94
|
+
{React.createElement(simRecorder, { onSimRecorderInit: handleSimRecorderRef })}
|
|
95
|
+
</div>
|
|
96
|
+
<div className="gif-recorder">
|
|
97
|
+
<div className="gif-recorder-actions">
|
|
98
|
+
<Button className="teal inverted"
|
|
99
|
+
title={screenshotLabel}
|
|
100
|
+
label={screenshotLabel}
|
|
101
|
+
leftIcon="fas fa-camera"
|
|
102
|
+
onClick={handleScreenshotClick} />
|
|
103
|
+
<Button className="teal inverted"
|
|
104
|
+
title={recorderState === "recording" ? stopRecordingLabel : startRecordingLabel}
|
|
105
|
+
label={recorderState === "recording" ? stopRecordingLabel : startRecordingLabel}
|
|
106
|
+
leftIcon={`fas fa-${recorderState === "recording" ? "square" : "circle"}`}
|
|
107
|
+
onClick={handleRecordClick} />
|
|
108
|
+
<div className="spacer mobile-only" />
|
|
109
|
+
<Button className="mobile-only"
|
|
110
|
+
title={lf("Cancel")}
|
|
111
|
+
label={lf("Cancel")}
|
|
112
|
+
onClick={onCancel} />
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="thumbnail-controls">
|
|
117
|
+
<div className="thumbnail-preview">
|
|
118
|
+
<div>
|
|
119
|
+
<div className="thumbnail-header">
|
|
120
|
+
<span className="project-share-label">{thumbnailLabel}</span>
|
|
121
|
+
<Button
|
|
122
|
+
className="link-button mobile-only"
|
|
123
|
+
title={lf("Try again?")}
|
|
124
|
+
label={lf("Try again?")}
|
|
125
|
+
onClick={() => setUri(undefined)} />
|
|
126
|
+
</div>
|
|
127
|
+
<div className="thumbnail-image">
|
|
128
|
+
{(uri || initialUri)
|
|
129
|
+
? <img src={uri || initialUri} />
|
|
130
|
+
: <div className="thumbnail-placeholder" />
|
|
131
|
+
}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="thumbnail-actions">
|
|
136
|
+
{uri &&
|
|
137
|
+
<Button className="primary"
|
|
138
|
+
title={lf("Apply")}
|
|
139
|
+
label={lf("Apply")}
|
|
140
|
+
onClick={handleApplyClick} />
|
|
141
|
+
}
|
|
142
|
+
<Button title={lf("Cancel")}
|
|
143
|
+
label={lf("Cancel")}
|
|
144
|
+
onClick={onCancel} />
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</>
|
|
149
|
+
}
|