Skip to content
Closed
103 changes: 99 additions & 4 deletions src/components/menu-bar/menu-bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import ShareButton from './share-button.jsx';
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import Divider from '../divider/divider.jsx';
import SaveStatus from './save-status.jsx';

Check failure on line 18 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'SaveStatus' is defined but never used. Allowed unused vars must match /^_/u
import ProjectWatcher from '../../containers/project-watcher.jsx';
import MenuBarMenu from './menu-bar-menu.jsx';
import MenuLabel from './tw-menu-label.jsx';
Expand Down Expand Up @@ -81,14 +81,15 @@
closeErrorsMenu
} from '../../reducers/menus';
import {setFileHandle} from '../../reducers/tw.js';
import {getFileHandleByName} from '../../lib/recent-files-manager';

import collectMetadata from '../../lib/collect-metadata';

import styles from './menu-bar.css';

import helpIcon from '../../lib/assets/icon--tutorials.svg';

Check failure on line 90 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'helpIcon' is defined but never used. Allowed unused vars must match /^_/u
import mystuffIcon from './icon--mystuff.png';

Check failure on line 91 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'mystuffIcon' is defined but never used. Allowed unused vars must match /^_/u
import profileIcon from './icon--profile.png';

Check failure on line 92 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'profileIcon' is defined but never used. Allowed unused vars must match /^_/u
import remixIcon from './icon--remix.svg';
import dropdownCaret from './dropdown-caret.svg';
import aboutIcon from './icon--about.svg';
Expand All @@ -109,7 +110,7 @@
import {notScratchDesktop} from '../../lib/isScratchDesktop.js';
import {APP_NAME} from '../../lib/brand.js';

const ariaMessages = defineMessages({

Check failure on line 113 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'ariaMessages' is assigned a value but never used. Allowed unused vars must match /^_/u
tutorials: {
id: 'gui.menuBar.tutorialsLibrary',
defaultMessage: 'Tutorials',
Expand Down Expand Up @@ -228,7 +229,8 @@
'handleKeyPress',
'handleRestoreOption',
'getSaveToComputerHandler',
'restoreOptionMessage'
'restoreOptionMessage',
'handleClickRecentFile'
]);
}
componentDidMount () {
Expand Down Expand Up @@ -326,7 +328,7 @@
} else if (mode === '220022BC') {
document.getElementById('logo_img').src = prehistoricLogo;
} else {
document.getElementById('logo_img').src = this.props.logo;

Check failure on line 331 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'logo' is missing in props validation
}

this.props.onSetTimeTravelMode(mode);
Expand Down Expand Up @@ -360,6 +362,71 @@
}
};
}
async handleClickRecentFile (fileName) {
try {
// Close the file menu
this.props.onRequestCloseFile();

// Get the file handle from IndexedDB
const fileHandle = await getFileHandleByName(fileName);
if (!fileHandle) {
console.error('File handle not found for:', fileName);
return;
}

// Request permission to read the file
const permission = await fileHandle.queryPermission({mode: 'read'});
if (permission !== 'granted') {
const newPermission = await fileHandle.requestPermission({mode: 'read'});
if (newPermission !== 'granted') {
console.error('Permission denied to read file:', fileName);
return;
}
}

// Read the file
const file = await fileHandle.getFile();
const reader = new FileReader();

reader.onload = () => {
// Trigger the file upload with the file content
if (this.props.onStartSelectingFileUpload) {
// Create a synthetic file input event
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.files = dataTransfer.files;

// Store the handle for future saves
if (this.props.onSetFileHandle) {
this.props.onSetFileHandle(fileHandle);
}

// Trigger the upload
this.props.onStartSelectingFileUpload();
// Simulate the file selection
setTimeout(() => {
const realFileInput = document.querySelector('input[type="file"]');
if (realFileInput) {
const dt = new DataTransfer();
dt.items.add(file);
realFileInput.files = dt.files;
realFileInput.dispatchEvent(new Event('change', {bubbles: true}));
}
}, 100);
}
};

reader.onerror = error => {
console.error('Error reading file:', error);
};

reader.readAsArrayBuffer(file);
} catch (error) {
console.error('Error opening recent file:', error);
}
}
restoreOptionMessage (deletedItem) {
switch (deletedItem) {
case 'Sprite':
Expand Down Expand Up @@ -723,6 +790,25 @@
)}
</SB3FolderExporter>
</MenuSection>
{this.props.autoOpenEnabled && this.props.recentFiles && this.props.recentFiles.length > 0 && (

Check failure on line 793 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

This line has a length of 131. Maximum allowed is 120
<MenuSection>
<MenuItem className={styles.menuSectionHeader}>
<FormattedMessage
defaultMessage="Recent Files"
description="Section header for recent files in file menu"
id="tw.menuBar.recentFiles"
/>
</MenuItem>
{this.props.recentFiles.slice(0, 5).map((file, index) => (
<MenuItem
key={`recent-${index}`}
onClick={() => this.handleClickRecentFile(file.name)}

Check failure on line 805 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

JSX props should not use arrow functions
>
{file.name}
</MenuItem>
))}
</MenuSection>
)}
{this.props.onClickPackager && (
<MenuSection>
<MenuItem
Expand Down Expand Up @@ -1082,7 +1168,7 @@
enableSeeInside: PropTypes.bool,
onClickSeeInside: PropTypes.func,
aboutMenuOpen: PropTypes.bool,
accountMenuOpen: PropTypes.bool,

Check failure on line 1171 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'accountMenuOpen' PropType is defined but prop is never used
authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
authorThumbnailUrl: PropTypes.string,
authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
Expand All @@ -1106,7 +1192,7 @@
onClickErrors: PropTypes.func,
onRequestCloseErrors: PropTypes.func,
confirmReadyToReplaceProject: PropTypes.func,
currentLocale: PropTypes.string.isRequired,

Check failure on line 1195 in src/components/menu-bar/menu-bar.jsx

View workflow job for this annotation

GitHub Actions / mega-test

'currentLocale' PropType is defined but prop is never used
editMenuOpen: PropTypes.bool,
enableCommunity: PropTypes.bool,
fileMenuOpen: PropTypes.bool,
Expand Down Expand Up @@ -1178,7 +1264,13 @@
showComingSoon: PropTypes.bool,
username: PropTypes.string,
userOwnsProject: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
vm: PropTypes.instanceOf(VM).isRequired,
recentFiles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
timestamp: PropTypes.number
})),
autoOpenEnabled: PropTypes.bool,
onSetFileHandle: PropTypes.func
};

MenuBar.defaultProps = {
Expand Down Expand Up @@ -1217,7 +1309,9 @@
mode1920: isTimeTravel1920(state),
mode1990: isTimeTravel1990(state),
mode2020: isTimeTravel2020(state),
modeNow: isTimeTravelNow(state)
modeNow: isTimeTravelNow(state),
recentFiles: state.scratchGui.tw.recentFiles || [],
autoOpenEnabled: state.scratchGui.tw.autoOpenEnabled || false
};
};

Expand Down Expand Up @@ -1254,7 +1348,8 @@
onClickSave: () => dispatch(manualUpdateProject()),
onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()),
onSeeCommunity: () => dispatch(setPlayer(true)),
onSetTimeTravelMode: mode => dispatch(setTimeTravel(mode))
onSetTimeTravelMode: mode => dispatch(setTimeTravel(mode)),
onSetFileHandle: handle => dispatch(setFileHandle(handle))
});

export default compose(
Expand Down
37 changes: 36 additions & 1 deletion src/components/tw-settings-modal/settings-modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,27 @@ const DisableCompiler = props => (
/>
);

const AutoOpen = props => (
<BooleanSetting
{...props}
label={
<FormattedMessage
defaultMessage="Auto-Open Last File"
description="Auto-Open setting"
id="tw.settingsModal.autoOpen"
/>
}
help={
<FormattedMessage
// eslint-disable-next-line max-len
defaultMessage="Remembers your most recently saved files. When enabled, you can quickly reopen them. This uses the File System Access API to track file names. Your browser must support this feature."
description="Auto-Open setting help"
id="tw.settingsModal.autoOpenHelp"
/>
}
/>
);

const CustomStageSize = ({
customStageSizeEnabled,
stageWidth,
Expand Down Expand Up @@ -499,6 +520,17 @@ const SettingsModalComponent = props => (
value={props.disableCompiler}
onChange={props.onDisableCompilerChange}
/>
<Header>
<FormattedMessage
defaultMessage="File Management"
description="Settings modal section"
id="tw.settingsModal.fileManagement"
/>
</Header>
<AutoOpen
value={props.autoOpenEnabled}
onChange={props.onAutoOpenChange}
/>
{!props.isEmbedded && (
<StoreProjectOptions
{...props}
Expand Down Expand Up @@ -528,7 +560,10 @@ SettingsModalComponent.propTypes = {
warpTimer: PropTypes.bool,
onWarpTimerChange: PropTypes.func,
disableCompiler: PropTypes.bool,
onDisableCompilerChange: PropTypes.func
onDisableCompilerChange: PropTypes.func,
onStoreProjectOptions: PropTypes.func,
autoOpenEnabled: PropTypes.bool,
onAutoOpenChange: PropTypes.func
};

export default injectIntl(SettingsModalComponent);
4 changes: 3 additions & 1 deletion src/containers/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import storage from '../lib/storage';
import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
import vmManagerHOC from '../lib/vm-manager-hoc.jsx';
import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx';
import AutoOpenHOC from '../lib/auto-open-hoc.jsx';

import GUIComponent from '../components/gui/gui.jsx';
import {setIsScratchDesktop} from '../lib/isScratchDesktop.js';
Expand Down Expand Up @@ -222,7 +223,8 @@ const WrappedGui = compose(
vmListenerHOC,
vmManagerHOC,
SBFileUploaderHOC,
cloudManagerHOC
cloudManagerHOC,
AutoOpenHOC
)(ConnectedGUI);

WrappedGui.setAppElement = ReactModal.setAppElement;
Expand Down
15 changes: 12 additions & 3 deletions src/containers/sb3-downloader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {projectTitleInitialState, setProjectTitle} from '../reducers/project-tit
import downloadBlob from '../lib/download-blob';
import {setProjectUnchanged} from '../reducers/project-changed';
import {showStandardAlert, showAlertWithTimeout} from '../reducers/alerts';
import {setFileHandle} from '../reducers/tw';
import {setFileHandle, addRecentFile} from '../reducers/tw';
import {getIsShowingProject} from '../reducers/project-state';
import {addRecentFile as addToRecentFiles} from '../lib/recent-files-manager';
import log from '../lib/log';

// from sb-file-uploader-hoc.jsx
Expand Down Expand Up @@ -110,6 +111,12 @@ class SB3Downloader extends React.Component {
});
await this.saveToHandle(handle);
this.props.onSetFileHandle(handle);
// Add to recent files (async)
addToRecentFiles(handle).then(recentFiles => {
this.props.onAddRecentFile(recentFiles);
}).catch(err => {
console.error('Failed to add recent file:', err);
});
const title = getProjectTitleFromFilename(handle.name);
if (title) {
this.props.onSetProjectTitle(title);
Expand Down Expand Up @@ -291,7 +298,8 @@ SB3Downloader.propTypes = {
onShowSaveSuccessAlert: PropTypes.func,
onShowSaveErrorAlert: PropTypes.func,
onProjectUnchanged: PropTypes.func,
showSaveFilePicker: PropTypes.func
showSaveFilePicker: PropTypes.func,
onAddRecentFile: PropTypes.func
};
SB3Downloader.defaultProps = {
className: '',
Expand All @@ -312,7 +320,8 @@ const mapDispatchToProps = dispatch => ({
onShowSavingAlert: () => showAlertWithTimeout(dispatch, 'saving'),
onShowSaveSuccessAlert: () => showAlertWithTimeout(dispatch, 'twSaveToDiskSuccess'),
onShowSaveErrorAlert: () => dispatch(showStandardAlert('savingError')),
onProjectUnchanged: () => dispatch(setProjectUnchanged())
onProjectUnchanged: () => dispatch(setProjectUnchanged()),
onAddRecentFile: recentFiles => dispatch(addRecentFile(recentFiles))
});

export default connect(
Expand Down
22 changes: 18 additions & 4 deletions src/containers/tw-settings-modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {connect} from 'react-redux';
import {closeSettingsModal} from '../reducers/modals';
import SettingsModalComponent from '../components/tw-settings-modal/settings-modal.jsx';
import {defaultStageSize} from '../reducers/custom-stage-size';
import {setAutoOpenEnabled} from '../reducers/tw';
import {saveAutoOpenSetting} from '../lib/recent-files-manager';

const messages = defineMessages({
newFramerate: {
Expand All @@ -30,7 +32,8 @@ class UsernameModal extends React.Component {
'handleStageWidthChange',
'handleStageHeightChange',
'handleDisableCompilerChange',
'handleStoreProjectOptions'
'handleStoreProjectOptions',
'handleAutoOpenChange'
]);
}
handleFramerateChange (e) {
Expand Down Expand Up @@ -85,6 +88,9 @@ class UsernameModal extends React.Component {
handleStoreProjectOptions () {
this.props.vm.storeProjectOptions();
}
handleAutoOpenChange (e) {
this.props.onAutoOpenChange(e.target.checked);
}
render () {
const {
/* eslint-disable no-unused-vars */
Expand Down Expand Up @@ -114,6 +120,7 @@ class UsernameModal extends React.Component {
this.props.customStageSize.height !== defaultStageSize.height
}
onStoreProjectOptions={this.handleStoreProjectOptions}
onAutoOpenChange={this.handleAutoOpenChange}
{...props}
/>
);
Expand All @@ -123,6 +130,7 @@ class UsernameModal extends React.Component {
UsernameModal.propTypes = {
intl: intlShape,
onClose: PropTypes.func,
onAutoOpenChange: PropTypes.func,
vm: PropTypes.shape({
renderer: PropTypes.shape({
setUseHighQualityRender: PropTypes.func
Expand All @@ -146,7 +154,8 @@ UsernameModal.propTypes = {
width: PropTypes.number,
height: PropTypes.number
}),
disableCompiler: PropTypes.bool
disableCompiler: PropTypes.bool,
autoOpenEnabled: PropTypes.bool
};

const mapStateToProps = state => ({
Expand All @@ -160,11 +169,16 @@ const mapStateToProps = state => ({
removeLimits: !state.scratchGui.tw.runtimeOptions.miscLimits,
warpTimer: state.scratchGui.tw.compilerOptions.warpTimer,
customStageSize: state.scratchGui.customStageSize,
disableCompiler: !state.scratchGui.tw.compilerOptions.enabled
disableCompiler: !state.scratchGui.tw.compilerOptions.enabled,
autoOpenEnabled: state.scratchGui.tw.autoOpenEnabled
});

const mapDispatchToProps = dispatch => ({
onClose: () => dispatch(closeSettingsModal())
onClose: () => dispatch(closeSettingsModal()),
onAutoOpenChange: enabled => {
dispatch(setAutoOpenEnabled(enabled));
saveAutoOpenSetting(enabled);
}
});

export default injectIntl(connect(
Expand Down
Loading
Loading