From 1f6802548b01a49c1643bfd4612eb373aa00f0f3 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:24:42 +1000 Subject: [PATCH] Port PenguinMod's library UI. --- src/components/library/library.css | 94 +++- src/components/library/library.jsx | 430 +++++++++++++----- src/components/tag-checkbox/tag-button.css | 62 +++ src/components/tag-checkbox/tag-button.jsx | 63 +++ .../tw-extension-separator/separator.css | 8 + .../tw-extension-separator/separator.jsx | 8 + src/containers/tag-checkbox.jsx | 32 ++ 7 files changed, 589 insertions(+), 108 deletions(-) create mode 100644 src/components/tag-checkbox/tag-button.css create mode 100644 src/components/tag-checkbox/tag-button.jsx create mode 100644 src/components/tw-extension-separator/separator.css create mode 100644 src/components/tw-extension-separator/separator.jsx create mode 100644 src/containers/tag-checkbox.jsx diff --git a/src/components/library/library.css b/src/components/library/library.css index 51287daa721..e57b02dc83b 100644 --- a/src/components/library/library.css +++ b/src/components/library/library.css @@ -1,7 +1,62 @@ @import "../../css/units.css"; @import "../../css/colors.css"; +.library-content-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + height: calc(100% - ($library-header-height)); +} +.library-filter-bar { + width: 342px; + height: calc(100%); + padding: 6px; + margin-left: 4px; + border-radius: 8px; + background: white; + overflow: auto; + border: 2px solid $ui-black-transparent; +} +[theme="dark"] .library-filter-bar { + background: $ui-primary; +} + +.library-header { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 6px; +} +.library-item-count { + opacity: 0.5; + margin-block: 0; + margin-left: 4px; +} +[theme="dark"] .library-header { + color: white; +} +[dir="rtl"] .library-header { + margin-left: initial; + margin-right: 6px; +} +[dir="rtl"] .library-item-count { + margin-left: initial; + margin-right: 4px; +} + +.library-tag-count { + margin-right: 2.5%; + text-align: right; +} +[dir="rtl"] .library-tag-count { + margin-right: initial; + margin-left: 2.5%; + text-align: left; +} + .library-scroll-grid { + width: calc(100% - 346px); display: flex; justify-content: flex-start; align-content: flex-start; @@ -12,7 +67,7 @@ overflow-y: auto; height: auto; padding: 0.5rem; - height: calc(100% - $library-header-height); + height: calc(100%); } .library-scroll-grid.withFilterBar { @@ -31,7 +86,7 @@ } .filter-bar-item { - margin-right: .75rem; + border: 1px solid $ui-black-transparent; } .filter { @@ -48,9 +103,40 @@ width: 18.75rem; } +.tag-checkbox-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} +[theme="dark"] .tag-checkbox-wrapper { + color: white; +} + +[theme="dark"] .white-text-in-dark-mode { + color: white; +} + +.library-filter-collapse { + width: 35px; + height: 40px; + transform: scaleX(-0.65); + background: transparent; + border: 0; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTVweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSItNC41IC02IDEwIDI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA1MC4yICg1NTA0NykgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+cG9seWdvbi1leHBhbmQ8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0icG9seWdvbi1leHBhbmQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxwYXRoIGQ9Ik0zLDZjMCwwLjM2NjM5IC0wLjEzNzQsMC43MzExNiAtMC40MTM4LDEuMDExNjNsLTMuODQzOTksMy45MDIyMmMtMC41NTEyLDAuNTU5MzEgLTEuNDQyNjksMC41NTkzMSAtMS45OTU0OSwwYy0wLjU1MTIsLTAuNTU3NjkgLTAuNTUxMiwtOS4yNzAwMSAwLC05LjgyNzdjMC41NTEyLC0wLjU1OTMxIDEuNDQ0MjksLTAuNTU5MzEgMS45OTU0OSwwbDMuODQzOTksMy45MDA2YzAuMjc2NCwwLjI4MDQ3IDAuNDEzOCwwLjY0NTI0IDAuNDEzOCwxLjAxMzI1IiBpZD0iZXhwYW5kIiBmaWxsPSIjNTc1RTc1Ij48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPgo='); + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} +[theme="dark"] .library-filter-collapse { + filter: brightness(50); +} + .divider { - transform: scaleY(1.39); - height: $library-filter-bar-height; + width: 100%; + border: 0; + border-bottom: 1px dashed $ui-black-transparent; + margin: 6px 0; } .tag-wrapper { diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 1a3c5e0d0f9..c3d652a0493 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; +import localforage from 'localforage'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import LibraryItem from '../../containers/library-item.jsx'; @@ -9,7 +10,9 @@ import Modal from '../../containers/modal.jsx'; import Divider from '../divider/divider.jsx'; import Filter from '../filter/filter.jsx'; import TagButton from '../../containers/tag-button.jsx'; +import TagCheckbox from '../../containers/tag-checkbox.jsx'; import Spinner from '../spinner/spinner.jsx'; +import Separator from '../tw-extension-separator/separator.jsx'; import styles from './library.css'; @@ -26,8 +29,26 @@ const messages = defineMessages({ } }); +const PM_LIBRARY_API = "https://library.penguinmod.com/"; + const ALL_TAG = {tag: 'all', intlLabel: messages.allTag}; -const tagListPrefix = [ALL_TAG]; +const tagListPrefix = []; + +/** + * Returns true if the array includes items from the other array. + * @param {Array} array The array to check + * @param {Array} from The array with the items that need to be included + * @returns {boolean} + */ +const arrayIncludesItemsFrom = (array, from) => { + if (!Array.isArray(array)) array = []; + if (!Array.isArray(from)) from = []; + const value = from.every((value) => { + return array.indexOf(value) >= 0; + }); + // console.log(array, from, value); + return value; +}; class LibraryComponent extends React.Component { constructor (props) { @@ -41,38 +62,116 @@ class LibraryComponent extends React.Component { 'handlePlayingEnd', 'handleSelect', 'handleTagClick', - 'setFilteredDataRef' + 'setFilteredDataRef', + 'loadLibraryData', + 'loadLibraryFavorites', + 'waitForLoading', + 'handleFavoritesUpdate', + 'createFilteredData', + 'getFilteredData' ]); this.state = { playingItem: null, filterQuery: '', - selectedTag: ALL_TAG.tag, + selectedTags: [], + favorites: [], + collapsed: false, loaded: false, data: props.data }; + + // used for actor libraries + // they have special things like favorited items + // the way they load though breaks stuff + this.usesSpecialLoading = [ + "ExtensionLibrary" + ]; } - componentDidMount () { - if (this.state.data.then) { - // If data is a promise, wait for the promise to resolve - this.state.data.then(data => { - this.setState({ - loaded: true, - data + + loadLibraryData () { + return new Promise((resolve) => { + if (this.state.data.then) { + // If data is a promise, wait for the promise to resolve + this.state.data.then(data => { + resolve({ key: "data", value: data }); }); - }); - } else { - // Allow the spinner to display before loading the content - setTimeout(() => { - this.setState({loaded: true}); - }); + } else { + // Allow the spinner to display before loading the content + setTimeout(() => { + const data = this.state.data; + resolve({ key: "data", value: data }); + }); + } + }); + } + async loadLibraryFavorites () { + const favorites = await localforage.getItem("pm:favorited_extensions"); + return { key: "favorites", value: favorites ? favorites : [] }; + } + async handleFavoritesUpdate () { + const favorites = await localforage.getItem("pm:favorited_extensions"); + this.setState({ + favorites + }); + } + + async waitForLoading (processes) { + // we store values in here + const packet = {}; + for (const process of processes) { + // result = { key: "data", value: ... } + const result = await process(); + packet[result.key] = result.value; + } + return packet; + } + + componentDidMount() { + if (!this.usesSpecialLoading.includes(this.props.actor)) { + // regular loading + if (this.state.data.then) { + // If data is a promise, wait for the promise to resolve + this.state.data.then(data => { + this.setState({ + loaded: true, + data + }); + }); + } else { + // Allow the spinner to display before loading the content + setTimeout(() => { + this.setState({ loaded: true }); + }); + } } if (this.props.setStopHandler) this.props.setStopHandler(this.handlePlayingEnd); + if (!this.usesSpecialLoading.includes(this.props.actor)) return; + // special loading + const spinnerProcesses = [this.loadLibraryData]; + // pm: actors can load extra stuff + // pm: if we are acting as the extension library, load favorited extensions + if (this.props.actor === "ExtensionLibrary") { + spinnerProcesses.push(this.loadLibraryFavorites); + } + // wait for spinner stuff + this.waitForLoading(spinnerProcesses).then((packet) => { + const data = { loaded: true, ...packet }; + this.setState(data); + }); } + // uncomment this if favorites start exploding the website lol! + // componentWillUnmount () { + // // pm: clear favorites from.... memory idk + // this.setState({ + // favorites: [] + // }); + // } componentDidUpdate (prevProps, prevState) { if (prevState.filterQuery !== this.state.filterQuery || - prevState.selectedTag !== this.state.selectedTag) { + prevState.selectedTags.length !== this.state.selectedTags.length) { this.scrollToTop(); } + if (prevProps.data !== this.props.data) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ @@ -80,25 +179,34 @@ class LibraryComponent extends React.Component { }); } } - handleSelect (id) { - this.handleClose(); + handleSelect (id, event) { + if (event.shiftKey !== true) { + this.handleClose(); + } this.props.onItemSelected(this.getFilteredData()[id]); } handleClose () { this.props.onRequestClose(); } - handleTagClick (tag) { + handleTagClick (tag, enabled) { + // console.log(tag, enabled); if (this.state.playingItem === null) { this.setState({ filterQuery: '', - selectedTag: tag.toLowerCase() + selectedTags: this.state.selectedTags.concat([tag.toLowerCase()]) }); } else { this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]); this.setState({ filterQuery: '', playingItem: null, - selectedTag: tag.toLowerCase() + selectedTags: this.state.selectedTags.concat([tag.toLowerCase()]) + }); + } + if (!enabled) { + const tags = this.state.selectedTags.filter(t => (t !== tag)); + this.setState({ + selectedTags: tags }); } } @@ -130,22 +238,22 @@ class LibraryComponent extends React.Component { if (this.state.playingItem === null) { this.setState({ filterQuery: event.target.value, - selectedTag: ALL_TAG.tag + selectedTags: [] }); } else { this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]); this.setState({ filterQuery: event.target.value, playingItem: null, - selectedTag: ALL_TAG.tag + selectedTags: [] }); } } handleFilterClear () { this.setState({filterQuery: ''}); } - getFilteredData () { - if (this.state.selectedTag === 'all') { + createFilteredData () { + if (this.state.selectedTags.length <= 0) { if (!this.state.filterQuery) return this.state.data; return this.state.data.filter(dataItem => ( (dataItem.tags || []) @@ -161,12 +269,29 @@ class LibraryComponent extends React.Component { .indexOf(this.state.filterQuery.toLowerCase()) !== -1 )); } - return this.state.data.filter(dataItem => ( + return this.state.data.filter(dataItem => (arrayIncludesItemsFrom( dataItem.tags && dataItem.tags - .map(String.prototype.toLowerCase.call, String.prototype.toLowerCase) - .indexOf(this.state.selectedTag) !== -1 - )); + .map(String.prototype.toLowerCase.call, String.prototype.toLowerCase), + this.state.selectedTags))); + } + getFilteredData () { + const filtered = this.createFilteredData(); + + if (this.props.actor !== "ExtensionLibrary") { + return filtered; + } + + const final = [].concat( + this.state.favorites + .filter(item => (typeof item !== "string")) + .map(item => ({ ...item, custom: true })) + .reverse(), + filtered.filter(item => (this.state.favorites.includes(item.extensionId))), + filtered.filter(item => (!this.state.favorites.includes(item.extensionId))) + ).map(item => ({ ...item, custom: typeof item.custom === "boolean" ? item.custom : false })); + + return final; } scrollToTop () { this.filteredDataRef.scrollTop = 0; @@ -182,89 +307,186 @@ class LibraryComponent extends React.Component { id={this.props.id} onRequestClose={this.handleClose} > - {(this.props.filterable || this.props.tags) && ( -