From 6e45329128574ac9aa36be291847ff1ff25bc831 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 24 Aug 2023 00:21:49 -0500 Subject: [PATCH 1/6] Disable dragging on all images in libraries --- src/components/library-item/library-item.jsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 7789a2968eb..ce058ed0bc3 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -42,6 +42,7 @@ class LibraryItemComponent extends React.PureComponent { aspectRatio: this.props.iconAspectRatio ? this.props.iconAspectRatio.toString() : '' }} loading="lazy" + draggable={false} src={this.props.iconURL} /> @@ -50,6 +51,7 @@ class LibraryItemComponent extends React.PureComponent { ) : null} @@ -103,10 +105,16 @@ class LibraryItemComponent extends React.PureComponent { className={styles.featuredExtensionMetadataDetail} > {this.props.bluetoothRequired ? ( - + ) : null} {this.props.internetConnectionRequired ? ( - + ) : null} @@ -160,6 +168,7 @@ class LibraryItemComponent extends React.PureComponent { className={styles.libraryItemImage} loading="lazy" src={this.props.iconURL} + draggable={false} /> From f000776288f8f3360e9bb200447e92ed3cb590ef Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 24 Aug 2023 01:35:26 -0500 Subject: [PATCH 2/6] Simplify sound library --- src/containers/sound-library.jsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index 56eddce6dc9..251e4c5aa3b 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -65,20 +65,13 @@ class SoundLibrary extends React.PureComponent { this.handleStop = null; const soundLibrary = getSoundLibrary(); - this.state = { - data: Array.isArray(soundLibrary) ? - getSoundLibraryThumbnailData(soundLibrary, this.props.isRtl) : - soundLibrary - }; + this.data = Array.isArray(soundLibrary) ? ( + getSoundLibraryThumbnailData(soundLibrary, this.props.isRtl) + ) : ( + soundLibrary.then(data => getSoundLibraryThumbnailData(data, this.props.isRtl)) + ); } componentDidMount () { - if (this.state.data.then) { - this.state.data.then(data => { - this.setState({ - data: getSoundLibraryThumbnailData(data, this.props.isRtl) - }); - }); - } this.audioEngine = new AudioEngine(); this.playingSoundPromise = null; } @@ -180,7 +173,7 @@ class SoundLibrary extends React.PureComponent { return ( Date: Thu, 24 Aug 2023 01:53:57 -0500 Subject: [PATCH 3/6] Add favorites to all libraries --- .../library-item/favorite-active.svg | 35 +++++ .../library-item/favorite-inactive.svg | 35 +++++ src/components/library-item/library-item.css | 21 ++- src/components/library-item/library-item.jsx | 34 ++++- src/components/library/library.jsx | 139 ++++++++++++++---- src/containers/extension-library.jsx | 17 ++- src/containers/library-item.jsx | 10 ++ 7 files changed, 252 insertions(+), 39 deletions(-) create mode 100644 src/components/library-item/favorite-active.svg create mode 100644 src/components/library-item/favorite-inactive.svg diff --git a/src/components/library-item/favorite-active.svg b/src/components/library-item/favorite-active.svg new file mode 100644 index 00000000000..d29fb8b5136 --- /dev/null +++ b/src/components/library-item/favorite-active.svg @@ -0,0 +1,35 @@ + + diff --git a/src/components/library-item/favorite-inactive.svg b/src/components/library-item/favorite-inactive.svg new file mode 100644 index 00000000000..0b147de9800 --- /dev/null +++ b/src/components/library-item/favorite-inactive.svg @@ -0,0 +1,35 @@ + + diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 71d89388a49..2d6987b181e 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -231,8 +231,21 @@ transform: translate(calc(-2 * $space), calc(2 * $space)); } -.incompatible-with-scratch { - padding: 0 1.25rem 1rem 1.25rem; - width: 100%; - text-align: left; +.favorite-container { + display: none; + background: none; + border: none; + padding: 0; + margin: 0; + position: absolute; + top: 0.5rem; + left: 0.5rem; +} +.favorite-icon { + width: 32px; + height: 32px; +} +.favorite-container.active, +.library-item:hover .favorite-container { + display: block; } diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index ce058ed0bc3..94527d4707e 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -1,4 +1,4 @@ -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, intlShape, defineMessages} from 'react-intl'; import PropTypes from 'prop-types'; import React from 'react'; @@ -9,10 +9,35 @@ import classNames from 'classnames'; import bluetoothIconURL from './bluetooth.svg'; import internetConnectionIconURL from './internet-connection.svg'; +import favoriteInactiveIcon from './favorite-inactive.svg'; +import favoriteActiveIcon from './favorite-active.svg'; + +const messages = defineMessages({ + favorite: { + defaultMessage: 'Favorite', + description: 'Alt text of icon in costume, sound, and extension libraries to mark an item as favorite.', + id: 'tw.favorite' + } +}); /* eslint-disable react/prefer-stateless-function */ class LibraryItemComponent extends React.PureComponent { render () { + const favorite = ( + + ); + return this.props.featured ? (
) : null} + + {favorite} ) : ( ) : null} + + {favorite} ); } @@ -188,6 +217,7 @@ class LibraryItemComponent extends React.PureComponent { LibraryItemComponent.propTypes = { + intl: intlShape, bluetoothRequired: PropTypes.bool, collaborator: PropTypes.string, description: PropTypes.oneOfType([ @@ -211,6 +241,8 @@ LibraryItemComponent.propTypes = { PropTypes.string, PropTypes.node ])), + favorite: PropTypes.bool, + onFavorite: PropTypes.func, onBlur: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, onFocus: PropTypes.func.isRequired, diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 9e6b8caf108..e93e7e62b7b 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -10,6 +10,7 @@ import Divider from '../divider/divider.jsx'; import Filter from '../filter/filter.jsx'; import TagButton from '../../containers/tag-button.jsx'; import Spinner from '../spinner/spinner.jsx'; +import Separator from '../tw-extension-separator/separator.jsx'; import styles from './library.css'; @@ -40,15 +41,19 @@ class LibraryComponent extends React.Component { 'handleMouseLeave', 'handlePlayingEnd', 'handleSelect', + 'handleFavorite', 'handleTagClick', 'setFilteredDataRef' ]); + const favorites = this.readFavoritesFromStorage(); this.state = { playingItem: null, filterQuery: '', selectedTag: ALL_TAG.tag, loaded: false, - data: props.data + data: props.data, + favorites, + initialFavorites: favorites }; } componentDidMount () { @@ -63,27 +68,64 @@ class LibraryComponent extends React.Component { } else { // Allow the spinner to display before loading the content setTimeout(() => { - this.setState({loaded: true}); + this.setState({ + loaded: true + }); }); } if (this.props.setStopHandler) this.props.setStopHandler(this.handlePlayingEnd); } + componentWillReceiveProps (nextProps) { + if (nextProps.data !== this.props.data && Array.isArray(nextProps.data)) { + this.setState({ + data: nextProps.data + }); + } + } componentDidUpdate (prevProps, prevState) { if (prevState.filterQuery !== this.state.filterQuery || prevState.selectedTag !== this.state.selectedTag) { this.scrollToTop(); } - if (prevProps.data !== this.props.data) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - data: this.props.data - }); + + if (this.state.favorites !== prevState.favorites) { + try { + localStorage.setItem(this.getFavoriteStorageKey(), JSON.stringify(this.state.favorites)); + } catch (error) { + // ignore + } } } handleSelect (id) { this.handleClose(); this.props.onItemSelected(this.getFilteredData()[id]); } + readFavoritesFromStorage () { + let data; + try { + data = JSON.parse(localStorage.getItem(this.getFavoriteStorageKey())); + } catch (error) { + // ignore + } + if (!Array.isArray(data)) { + data = []; + } + return data; + } + getFavoriteStorageKey () { + return `tw:library-favorites:${this.props.id}`; + } + handleFavorite (id) { + const data = this.getFilteredData()[id]; + const key = data[this.props.persistableKey]; + this.setState(oldState => ({ + favorites: oldState.favorites.includes(key) ? ( + oldState.favorites.filter(i => i !== key) + ) : ( + [...oldState.favorites, key] + ) + })); + } handleClose () { this.props.onRequestClose(); } @@ -145,12 +187,51 @@ class LibraryComponent extends React.Component { this.setState({filterQuery: ''}); } getFilteredData () { - if (this.state.selectedTag === 'all') { - if (!this.state.filterQuery) return this.state.data; - return this.state.data.filter(dataItem => { - if (React.isValidElement(dataItem)) { - return false; - } + // When no filtering, favorites get their own section + if (this.state.selectedTag === 'all' && !this.state.filterQuery) { + const favoriteItems = this.state.data + .filter(dataItem => ( + this.state.initialFavorites.includes(dataItem[this.props.persistableKey]) + )) + .map(dataItem => ({ + ...dataItem, + key: `favorite-${dataItem[this.props.persistableKey]}` + })); + + if (favoriteItems.length) { + favoriteItems.push('---'); + } + + return [ + ...favoriteItems, + ...this.state.data + ]; + } + + // When filtering, favorites are just listed first, not in a separte section. + const favoriteItems = []; + const nonFavoriteItems = []; + for (const dataItem of this.state.data) { + if (dataItem === '---') { + // ignore + } else if (this.state.initialFavorites.includes(dataItem[this.props.persistableKey])) { + favoriteItems.push(dataItem); + } else { + nonFavoriteItems.push(dataItem); + } + } + + let filteredItems = favoriteItems.concat(nonFavoriteItems); + + if (this.state.selectedTag !== 'all') { + filteredItems = filteredItems.filter(dataItem => ( + dataItem.tags && + dataItem.tags.map(i => i.toLowerCase()).includes(this.state.selectedTag) + )); + } + + if (this.state.filterQuery) { + filteredItems = filteredItems.filter(dataItem => { const search = [...dataItem.tags]; if (dataItem.name) { // Use the name if it is a string, else use formatMessage to get the translated name @@ -169,12 +250,8 @@ class LibraryComponent extends React.Component { .includes(this.state.filterQuery.toLowerCase()); }); } - return this.state.data.filter(dataItem => ( - dataItem.tags && - dataItem.tags - .map(String.prototype.toLowerCase.call, String.prototype.toLowerCase) - .indexOf(this.state.selectedTag) !== -1 - )); + + return filteredItems; } scrollToTop () { this.filteredDataRef.scrollTop = 0; @@ -234,10 +311,8 @@ class LibraryComponent extends React.Component { ref={this.setFilteredDataRef} > {this.state.loaded ? this.getFilteredData().map((dataItem, index) => ( - React.isValidElement(dataItem) ? ( - - {dataItem} - + dataItem === '---' ? ( + ) : ( ({ - rawURL: extension.iconURL || extensionIcon, - ...extension -}); +const toLibraryItem = extension => { + if (typeof extension === 'object') { + return ({ + rawURL: extension.iconURL || extensionIcon, + ...extension + }); + } + return extension; +}; let cachedGallery = null; @@ -137,7 +141,7 @@ class ExtensionLibrary extends React.PureComponent { } render () { const library = extensionLibraryContent.map(toLibraryItem); - library.push(); + library.push('---'); if (this.state.gallery) { library.push(...this.state.gallery.map(toLibraryItem)); library.push(toLibraryItem(galleryMore)); @@ -151,6 +155,7 @@ class ExtensionLibrary extends React.PureComponent { Date: Thu, 24 Aug 2023 01:56:53 -0500 Subject: [PATCH 4/6] Change alt text to unfavorite when favorited --- src/components/library-item/library-item.jsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 94527d4707e..dd44eab8c85 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -17,12 +17,20 @@ const messages = defineMessages({ defaultMessage: 'Favorite', description: 'Alt text of icon in costume, sound, and extension libraries to mark an item as favorite.', id: 'tw.favorite' + }, + unfavorite: { + defaultMessage: 'Unfavorite', + description: 'Alt text of icon in costume, sound, and extension libraries to unmark an item as favorite.', + id: 'tw.unfavorite' } }); /* eslint-disable react/prefer-stateless-function */ class LibraryItemComponent extends React.PureComponent { render () { + const favoriteMessage = this.props.intl.formatMessage( + this.props.favorite ? messages.unfavorite : messages.favorite + ); const favorite = ( ); From 93deb83781b4bfbb0728b6003d57a483de4b7602 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 24 Aug 2023 01:58:26 -0500 Subject: [PATCH 5/6] Fix favoriting some extension items --- src/containers/extension-library.jsx | 4 +--- src/lib/libraries/extensions/index.jsx | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index 3003b7ab8c6..dab134f112e 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -109,9 +109,7 @@ class ExtensionLibrary extends React.PureComponent { const extensionId = item.extensionId; - // Don't warn about Scratch compatibility before showing modal - const isCustomURL = !item.disabled && !extensionId; - if (isCustomURL) { + if (extensionId === 'custom_extension') { this.props.onOpenCustomExtensionModal(); return; } diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx index 4b153cb6c95..8fb04be9799 100644 --- a/src/lib/libraries/extensions/index.jsx +++ b/src/lib/libraries/extensions/index.jsx @@ -398,7 +398,7 @@ export default [ id="tw.customExtension.name" /> ), - extensionId: '', + extensionId: 'custom_extension', iconURL: customExtensionIcon, iconAspectRatio: 600 / 372, description: ( @@ -423,7 +423,7 @@ export const galleryLoading = { /> ), href: 'https://extensions.turbowarp.org/', - extensionId: '', + extensionId: 'gallery', iconURL: galleryIcon, iconAspectRatio: 2, description: ( @@ -447,7 +447,7 @@ export const galleryMore = { /> ), href: 'https://extensions.turbowarp.org/', - extensionId: '', + extensionId: 'gallery', iconURL: galleryIcon, iconAspectRatio: 2, description: ( @@ -471,7 +471,7 @@ export const galleryError = { /> ), href: 'https://extensions.turbowarp.org/', - extensionId: '', + extensionId: 'gallery', iconURL: galleryIcon, iconAspectRatio: 2, description: ( From a0ee7cf7c5e296113e2e2489c79e5ef4aa9d5c9d Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 24 Aug 2023 02:00:14 -0500 Subject: [PATCH 6/6] Put the link to extensions.turbowarp.org before the list so that people might actually see it --- src/containers/extension-library.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index dab134f112e..7108b37f811 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -141,8 +141,8 @@ class ExtensionLibrary extends React.PureComponent { const library = extensionLibraryContent.map(toLibraryItem); library.push('---'); if (this.state.gallery) { - library.push(...this.state.gallery.map(toLibraryItem)); library.push(toLibraryItem(galleryMore)); + library.push(...this.state.gallery.map(toLibraryItem)); } else if (this.state.galleryError) { library.push(toLibraryItem(galleryError)); } else {