pxt-core 8.2.9 → 8.2.10
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 +6 -9
- package/built/pxtsim.js +6 -9
- 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 +83 -13
- 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 +136 -132
- package/react-common/components/share/SocialButton.tsx +36 -6
- package/react-common/components/share/ThumbnailRecorder.tsx +137 -0
- package/react-common/styles/controls/Button.less +30 -3
- package/react-common/styles/controls/Modal.less +6 -1
- package/react-common/styles/share/share.less +63 -7
- 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,139 @@ 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
|
-
<Input label={lf("Project Name")}
|
|
169
|
-
initialValue={name}
|
|
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
|
+
}
|
|
155
|
+
{shareState !== "publish" &&
|
|
156
|
+
<Button
|
|
157
|
+
className="link-button"
|
|
158
|
+
title={lf("Update project thumbnail")}
|
|
185
159
|
label={lf("Update project thumbnail")}
|
|
186
160
|
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
161
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
onBlur={handleCopyBlur} />
|
|
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}
|
|
162
|
+
</div>
|
|
163
|
+
}
|
|
164
|
+
<div className="project-share-content">
|
|
165
|
+
{(prePublish || shareState === "publish") && <>
|
|
166
|
+
<div className="project-share-title">
|
|
167
|
+
<h2>{inputTitle}</h2>
|
|
168
|
+
</div>
|
|
169
|
+
{showDescription && <>
|
|
170
|
+
<Input
|
|
171
|
+
className="name-input"
|
|
172
|
+
initialValue={name}
|
|
173
|
+
placeholder={lf("Name your project")}
|
|
174
|
+
onChange={setName} />
|
|
175
|
+
{isLoggedIn && <Checkbox
|
|
176
|
+
id="persistent-share-checkbox"
|
|
177
|
+
label={lf("Allow people to see future changes to my project")}
|
|
178
|
+
isChecked={!isAnonymous}
|
|
179
|
+
onChange={val => setIsAnonymous(!val)}
|
|
243
180
|
/>}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
181
|
+
</>
|
|
182
|
+
}
|
|
183
|
+
{prePublish && <>
|
|
184
|
+
{shareData?.error && <div className="project-share-error">
|
|
185
|
+
{(shareData.error.statusCode === 413
|
|
186
|
+
&& pxt.appTarget?.cloud?.cloudProviders?.github)
|
|
187
|
+
? lf("Oops! Your project is too big. You can create a GitHub repository to share it.")
|
|
188
|
+
: lf("Oops! There was an error. Please ensure you are connected to the Internet and try again.")}
|
|
189
|
+
</div>}
|
|
190
|
+
<div>
|
|
191
|
+
{shareState === "share" &&
|
|
192
|
+
<Button className="primary share-publish-button"
|
|
193
|
+
title={lf("Continue")}
|
|
194
|
+
label={lf("Continue")}
|
|
195
|
+
onClick={handlePublishClick} />
|
|
196
|
+
}
|
|
197
|
+
{ shareState === "publishing" &&
|
|
198
|
+
<Button className="primary share-publish-button"
|
|
199
|
+
title={lf("Publishing...")}
|
|
200
|
+
label={ <div className="common-spinner" />}
|
|
201
|
+
onClick={() => {}} />
|
|
202
|
+
}
|
|
251
203
|
</div>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
204
|
+
</>}
|
|
205
|
+
|
|
206
|
+
{shareState === "publish" &&
|
|
207
|
+
<div className="project-share-data">
|
|
208
|
+
<div className="common-input-attached-button">
|
|
209
|
+
<Input
|
|
210
|
+
handleInputRef={handleInputRef}
|
|
211
|
+
initialValue={shareData.url}
|
|
212
|
+
readOnly={true}
|
|
213
|
+
onChange={setName} />
|
|
214
|
+
<Button className={copySuccessful ? "green" : "primary"}
|
|
215
|
+
title={lf("Copy link")}
|
|
216
|
+
label={copySuccessful ? lf("Copied!") : lf("Copy")}
|
|
217
|
+
leftIcon="fas fa-link"
|
|
218
|
+
onClick={handleCopyClick}
|
|
219
|
+
onBlur={handleCopyBlur} />
|
|
220
|
+
</div>
|
|
221
|
+
<div className="project-share-actions">
|
|
222
|
+
<Button className="square-button gray embed mobile-portrait-hidden"
|
|
223
|
+
title={lf("Show embed code")}
|
|
224
|
+
leftIcon="fas fa-code"
|
|
225
|
+
onClick={handleEmbedClick} />
|
|
226
|
+
<SocialButton className="square-button facebook"
|
|
227
|
+
url={shareData?.url}
|
|
228
|
+
type='facebook'
|
|
229
|
+
heading={lf("Share on Facebook")} />
|
|
230
|
+
<SocialButton className="square-button twitter"
|
|
231
|
+
url={shareData?.url}
|
|
232
|
+
type='twitter'
|
|
233
|
+
heading={lf("Share on Twitter")} />
|
|
234
|
+
<SocialButton className="square-button google-classroom"
|
|
235
|
+
url={shareData?.url}
|
|
236
|
+
type='google-classroom'
|
|
237
|
+
heading={lf("Share on Google Classroom")} />
|
|
238
|
+
<SocialButton className="square-button microsoft-teams"
|
|
239
|
+
url={shareData?.url}
|
|
240
|
+
type='microsoft-teams'
|
|
241
|
+
heading={lf("Share on Microsoft Teams")} />
|
|
242
|
+
<SocialButton className="square-button whatsapp"
|
|
243
|
+
url={shareData?.url}
|
|
244
|
+
type='whatsapp'
|
|
245
|
+
heading={lf("Share on WhatsApp")} />
|
|
246
|
+
{navigator.share && <Button className="square-button device-share"
|
|
247
|
+
title={lf("Show device share options")}
|
|
248
|
+
ariaLabel={lf("Show device share options")}
|
|
249
|
+
leftIcon={"icon share"}
|
|
250
|
+
onClick={handleDeviceShareClick}
|
|
251
|
+
/>}
|
|
252
|
+
<Button
|
|
253
|
+
className="menu-button project-qrcode"
|
|
254
|
+
buttonRef={handleQRCodeButtonRef}
|
|
255
|
+
title={lf("Show QR Code")}
|
|
256
|
+
label={<img className="qrcode-image" src={shareData?.qr} />}
|
|
257
|
+
onClick={handleQRCodeClick}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
}
|
|
262
|
+
{embedState !== "none" && <div className="project-embed">
|
|
263
|
+
<EditorToggle id="project-embed-toggle"
|
|
264
|
+
className="slim tablet-compact"
|
|
265
|
+
items={embedOptions}
|
|
266
|
+
selected={embedOptions.findIndex(i => i.name === embedState)} />
|
|
267
|
+
<Textarea readOnly={true}
|
|
268
|
+
rows={5}
|
|
269
|
+
initialValue={shareData?.embed[embedState]} />
|
|
270
|
+
</div>}
|
|
271
|
+
</>}
|
|
272
|
+
{shareState === "gifrecord" && <ThumbnailRecorder
|
|
273
|
+
initialUri={thumbnailUri}
|
|
274
|
+
onApply={applyGifChange}
|
|
275
|
+
onCancel={exitGifRecord}
|
|
276
|
+
simRecorder={simRecorder}/>}
|
|
277
|
+
</div>
|
|
274
278
|
|
|
275
279
|
{showQRCode &&
|
|
276
280
|
<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,137 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Button } from "../controls/Button";
|
|
3
|
+
|
|
4
|
+
export interface ThumbnailRecorderProps {
|
|
5
|
+
initialUri?: string;
|
|
6
|
+
|
|
7
|
+
onApply: (uri: string) => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
|
|
10
|
+
simRecorder: SimRecorder;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SimRecorderProps {
|
|
14
|
+
onSimRecorderInit: (ref: SimRecorderRef) => void;
|
|
15
|
+
}
|
|
16
|
+
export type SimRecorder = (props: SimRecorderProps) => JSX.Element
|
|
17
|
+
export type SimRecorderState = "default" | "recording" | "rendering"
|
|
18
|
+
export interface SimRecorderRef {
|
|
19
|
+
state: SimRecorderState;
|
|
20
|
+
startRecordingAsync: () => Promise<void>;
|
|
21
|
+
stopRecordingAsync: () => Promise<string>;
|
|
22
|
+
screenshotAsync: () => Promise<string>;
|
|
23
|
+
addStateChangeListener: (handler: (newState: SimRecorderState) => void) => void;
|
|
24
|
+
addThumbnailListener: (handler: (uri: string, type: "gif" | "png") => void) => void;
|
|
25
|
+
addErrorListener: (handler: (message: string) => void) => void;
|
|
26
|
+
removeStateChangeListener: (handler: (newState: SimRecorderState) => void) => void;
|
|
27
|
+
removeThumbnailListener: (handler: (uri: string, type: "gif" | "png") => void) => void;
|
|
28
|
+
removeErrorListener: (handler: (message: string) => void) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export const ThumbnailRecorder = (props: ThumbnailRecorderProps) => {
|
|
33
|
+
const { initialUri, onApply, onCancel, simRecorder } = props;
|
|
34
|
+
const [ uri, setUri ] = React.useState(initialUri);
|
|
35
|
+
const [ error, setError] = React.useState<string>(undefined)
|
|
36
|
+
const [ recorderRef, setRecorderRef ] = React.useState<SimRecorderRef>(undefined);
|
|
37
|
+
const [ recorderState, setRecorderState ] = React.useState<SimRecorderState>("default");
|
|
38
|
+
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
if (!recorderRef) return undefined;
|
|
41
|
+
recorderRef.addStateChangeListener(setRecorderState);
|
|
42
|
+
recorderRef.addThumbnailListener(setUri);
|
|
43
|
+
recorderRef.addErrorListener(setError);
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
recorderRef.removeStateChangeListener(setRecorderState);
|
|
47
|
+
recorderRef.removeThumbnailListener(setUri);
|
|
48
|
+
recorderRef.removeErrorListener(setError);
|
|
49
|
+
}
|
|
50
|
+
}, [recorderRef])
|
|
51
|
+
|
|
52
|
+
const handleApplyClick = (evt?: any) => {
|
|
53
|
+
onApply(uri);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleScreenshotClick = async () => {
|
|
57
|
+
setError(undefined);
|
|
58
|
+
if (recorderRef) recorderRef.screenshotAsync();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleRecordClick = async () => {
|
|
62
|
+
setError(undefined);
|
|
63
|
+
if (!recorderRef) return;
|
|
64
|
+
|
|
65
|
+
if (recorderRef.state === "recording") {
|
|
66
|
+
recorderRef.stopRecordingAsync();
|
|
67
|
+
}
|
|
68
|
+
else if (recorderRef.state === "default") {
|
|
69
|
+
recorderRef.startRecordingAsync();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const targetTheme = pxt.appTarget.appTheme;
|
|
74
|
+
|
|
75
|
+
const handleSimRecorderRef = (ref: SimRecorderRef) => {
|
|
76
|
+
setRecorderRef(ref);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const screenshotLabel = lf("Take screenshot ({0})", targetTheme.simScreenshotKey);
|
|
80
|
+
const startRecordingLabel = lf("Record gameplay ({0})", targetTheme.simGifKey);
|
|
81
|
+
const stopRecordingLabel = lf("Stop recording ({0})", targetTheme.simGifKey) ;
|
|
82
|
+
|
|
83
|
+
return <>
|
|
84
|
+
<div className="gif-recorder-content">
|
|
85
|
+
<div className="gif-recorder-sim-embed">
|
|
86
|
+
{React.createElement(simRecorder, { onSimRecorderInit: handleSimRecorderRef })}
|
|
87
|
+
</div>
|
|
88
|
+
<div className="thumbnail-controls">
|
|
89
|
+
<div className="thumbnail-preview">
|
|
90
|
+
<div>
|
|
91
|
+
<span className="thumbnail-label">{lf("Current Thumbnail")}</span>
|
|
92
|
+
<div className="thumbnail-image">
|
|
93
|
+
{initialUri
|
|
94
|
+
? <img src={initialUri} />
|
|
95
|
+
: <div className="thumbnail-placeholder" />
|
|
96
|
+
}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div>
|
|
100
|
+
<span className="thumbnail-label">{lf("New Thumbnail")}</span>
|
|
101
|
+
<div className="thumbnail-image">
|
|
102
|
+
{uri
|
|
103
|
+
? <img src={uri} />
|
|
104
|
+
: <div className="thumbnail-placeholder" />
|
|
105
|
+
}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="thumbnail-actions">
|
|
110
|
+
<Button className="primary"
|
|
111
|
+
title={lf("Apply")}
|
|
112
|
+
label={lf("Apply")}
|
|
113
|
+
onClick={handleApplyClick} />
|
|
114
|
+
<Button title={lf("Cancel")}
|
|
115
|
+
label={lf("Cancel")}
|
|
116
|
+
onClick={onCancel} />
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="gif-recorder">
|
|
122
|
+
<div className="gif-recorder-label">{lf("Pick your project thumbnail")}</div>
|
|
123
|
+
<div className="gif-recorder-actions">
|
|
124
|
+
{<Button className="teal inverted"
|
|
125
|
+
title={screenshotLabel}
|
|
126
|
+
label={screenshotLabel}
|
|
127
|
+
leftIcon="fas fa-camera"
|
|
128
|
+
onClick={handleScreenshotClick} />}
|
|
129
|
+
{<Button className="teal inverted"
|
|
130
|
+
title={recorderState === "recording" ? stopRecordingLabel : startRecordingLabel}
|
|
131
|
+
label={recorderState === "recording" ? stopRecordingLabel : startRecordingLabel}
|
|
132
|
+
leftIcon={`fas fa-${recorderState === "recording" ? "square" : "circle"}`}
|
|
133
|
+
onClick={handleRecordClick} />}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</>
|
|
137
|
+
}
|
|
@@ -230,21 +230,30 @@
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/****************************************************
|
|
233
|
-
*
|
|
233
|
+
* Square Buttons *
|
|
234
234
|
****************************************************/
|
|
235
235
|
|
|
236
|
-
.common-button.
|
|
236
|
+
.common-button.square-button {
|
|
237
237
|
width: 3rem;
|
|
238
238
|
height: 3rem;
|
|
239
239
|
overflow: hidden;
|
|
240
240
|
padding: 0;
|
|
241
|
-
border-radius: 2rem;
|
|
242
241
|
|
|
243
242
|
i, i.fas, i.far {
|
|
244
243
|
margin: 0;
|
|
245
244
|
}
|
|
246
245
|
}
|
|
247
246
|
|
|
247
|
+
|
|
248
|
+
/****************************************************
|
|
249
|
+
* Circle Buttons *
|
|
250
|
+
****************************************************/
|
|
251
|
+
|
|
252
|
+
.common-button.circle-button {
|
|
253
|
+
&:extend(.common-button.square-button);
|
|
254
|
+
border-radius: 2rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
248
257
|
.common-button.circle-button:focus::after {
|
|
249
258
|
border-radius: 2rem;
|
|
250
259
|
}
|
|
@@ -253,6 +262,14 @@
|
|
|
253
262
|
* Social Buttons *
|
|
254
263
|
****************************************************/
|
|
255
264
|
|
|
265
|
+
.common-button.social-button {
|
|
266
|
+
font-size: 1.5rem;
|
|
267
|
+
|
|
268
|
+
img {
|
|
269
|
+
max-width: 100%;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
256
273
|
.common-button.facebook {
|
|
257
274
|
background: #4267B2;
|
|
258
275
|
color: @buttonTextColorDarkBackground;
|
|
@@ -273,6 +290,16 @@
|
|
|
273
290
|
color: @buttonTextColorDarkBackground;
|
|
274
291
|
}
|
|
275
292
|
|
|
293
|
+
.common-button.whatsapp {
|
|
294
|
+
background: #59CE72;
|
|
295
|
+
color: @buttonTextColorDarkBackground;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.common-button.microsoft-teams {
|
|
299
|
+
background: #35258F;
|
|
300
|
+
color: @buttonTextColorDarkBackground;
|
|
301
|
+
}
|
|
302
|
+
|
|
276
303
|
/****************************************************
|
|
277
304
|
* High Contrast *
|
|
278
305
|
****************************************************/
|