Skip to content

Commit

Permalink
Add custom font management modal (#789)
Browse files Browse the repository at this point in the history
  • Loading branch information
GarboMuffin committed Aug 16, 2023
1 parent 1c6e353 commit 533cff9
Show file tree
Hide file tree
Showing 19 changed files with 1,209 additions and 6 deletions.
4 changes: 4 additions & 0 deletions src/components/gui/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import TWSettingsModal from '../../containers/tw-settings-modal.jsx';
import TWSecurityManager from '../../containers/tw-security-manager.jsx';
import TWCustomExtensionModal from '../../containers/tw-custom-extension-modal.jsx';
import TWRestorePointManager from '../../containers/tw-restore-point-manager.jsx';
import TWFontsModal from '../../containers/tw-fonts-modal.jsx';

import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants';
import {resolveStageSize} from '../../lib/screen-utils';
Expand Down Expand Up @@ -145,6 +146,7 @@ const GUIComponent = props => {
usernameModalVisible,
settingsModalVisible,
customExtensionModalVisible,
fontsModalVisible,
vm,
...componentProps
} = omit(props, 'dispatch');
Expand Down Expand Up @@ -172,6 +174,7 @@ const GUIComponent = props => {
{usernameModalVisible && <TWUsernameModal />}
{settingsModalVisible && <TWSettingsModal />}
{customExtensionModalVisible && <TWCustomExtensionModal />}
{fontsModalVisible && <TWFontsModal />}
</React.Fragment>
);

Expand Down Expand Up @@ -498,6 +501,7 @@ GUIComponent.propTypes = {
usernameModalVisible: PropTypes.bool,
settingsModalVisible: PropTypes.bool,
customExtensionModalVisible: PropTypes.bool,
fontsModalVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
GUIComponent.defaultProps = {
Expand Down
25 changes: 25 additions & 0 deletions src/components/tw-fonts-modal/add-button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import styles from './fonts-modal.css';

const AddButton = props => (
<button
onClick={props.onClick}
disabled={props.disabled}
className={styles.button}
>
<FormattedMessage
defaultMessage="Add"
description="Part of font management modal. This is the button that will actually add the font."
id="tw.fonts.add"
/>
</button>
);

AddButton.propTypes = {
onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool
};

export default AddButton;
199 changes: 199 additions & 0 deletions src/components/tw-fonts-modal/add-custom-font.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
import bindAll from 'lodash.bindall';
import styles from './fonts-modal.css';
import LoadTemporaryFont from './load-temporary-font.jsx';
import FontName from './font-name.jsx';
import FontPlayground from './font-playground.jsx';
import FontFallback from './font-fallback.jsx';
import AddButton from './add-button.jsx';

const messages = defineMessages({
error: {
defaultMessage: 'Failed to read font file: {error}',
description: 'Part of font management modal. Appears when a font from a local file could not be read.',
id: 'tw.fonts.readError'
}
});

export const FONT_FORMATS = [
'ttf',
'otf',
'woff',
'woff2'
];

const formatFontName = filename => {
// Remove file extension
const idx = filename.indexOf('.');
if (idx !== -1) {
filename = filename.substring(0, idx);
}
return filename;
};

const getDataFormat = filename => {
const parts = filename.split('.');
const extension = parts[parts.length - 1];
if (FONT_FORMATS.includes(extension)) {
return extension;
}
// We'll just guess
return 'ttf';
};

class AddCustomFont extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChangeFile',
'handleChangeName',
'handleChangeFallback',
'handleFinish'
]);
this.state = {
file: null,
url: null,
name: '',
format: '',
fallback: FontFallback.DEFAULT,
loading: false
};
}

componentWillUnmount () {
URL.revokeObjectURL(this.state.url);
}

handleChangeFile (e) {
const file = e.target.files[0] || null;
if (file) {
this.setState({
file,
name: formatFontName(file.name),
format: getDataFormat(file.name),
url: URL.createObjectURL(file)
});
} else {
URL.revokeObjectURL(this.state.url);
this.setState({
file,
name: null,
url: null
});
}
}

handleChangeName (name) {
this.setState({
name
});
}

handleChangeFallback (fallback) {
this.setState({
fallback
});
}

handleFinish () {
this.setState({
loading: true
});

const fr = new FileReader();
fr.onload = () => {
const data = new Uint8Array(fr.result);
const storage = this.props.fontManager.runtime.storage;
const asset = storage.createAsset(
storage.AssetType.Font,
this.state.format,
data,
null,
true
);
this.props.fontManager.addCustomFont(this.state.name, this.state.fallback, asset);
this.props.onClose();
};
fr.onerror = () => {
// eslint-disable-next-line no-alert
alert(this.props.intl.formatMessage(messages.error), {
error: fr.error
});

this.setState({
loading: false
});
};
fr.readAsArrayBuffer(this.state.file);
}

render () {
return (
<React.Fragment>
<p>
<FormattedMessage
defaultMessage="Select a font file from your computer:"
description="Part of font management modal."
id="tw.fonts.custom.file"
/>
</p>

<input
type="file"
onChange={this.handleChangeFile}
className={styles.fileInput}
accept={FONT_FORMATS.map(ext => `.${ext}`).join(',')}
readOnly={this.state.loading}
/>

{this.state.file && (
<React.Fragment>
<p>
<FormattedMessage
defaultMessage="Give the font a name:"
description="Part of font management modal."
id="tw.fonts.custom.name"
/>
</p>

<FontName
name={this.state.name}
onChange={this.handleChangeName}
fontManager={this.props.fontManager}
/>

<LoadTemporaryFont url={this.state.url}>{family => (
<FontPlayground family={`${family}, ${this.state.fallback}`} />
)}</LoadTemporaryFont>

<FontFallback
fallback={this.state.fallback}
onChange={this.handleChangeFallback}
/>
</React.Fragment>
)}

<AddButton
onClick={this.handleFinish}
disabled={!this.state.file || !this.state.name || this.state.loading}
/>
</React.Fragment>
);
}
}

AddCustomFont.propTypes = {
intl: intlShape,
fontManager: PropTypes.shape({
addCustomFont: PropTypes.func,
runtime: PropTypes.shape({
// eslint-disable-next-line react/forbid-prop-types
storage: PropTypes.any
})
}),
onClose: PropTypes.func.isRequired
};

export default injectIntl(AddCustomFont);
114 changes: 114 additions & 0 deletions src/components/tw-fonts-modal/add-system-font.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import bindAll from 'lodash.bindall';
import FontName from './font-name.jsx';
import FontPlayground from './font-playground.jsx';
import FontFallback from './font-fallback.jsx';
import AddButton from './add-button.jsx';

class AddSystemFont extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChangeName',
'handleChangeFallback',
'handleFinish'
]);
this.state = {
name: '',
fallback: FontFallback.DEFAULT,
localFonts: null
};
}

componentDidMount () {
// Chrome-only API
if (typeof queryLocalFonts === 'function') {
// eslint-disable-next-line no-undef
queryLocalFonts().then(fonts => {
const uniqueFamilies = [...new Set(fonts.map(i => i.family))];
this.setState({
localFonts: uniqueFamilies
});
});
}
}

handleChangeName (name) {
this.setState({
name
});
}

handleChangeFallback (fallback) {
this.setState({
fallback
});
}

handleFinish () {
this.props.fontManager.addSystemFont(this.state.name, this.state.fallback);
this.props.onClose();
}

render () {
return (
<React.Fragment>
<p>
<FormattedMessage
// eslint-disable-next-line max-len
defaultMessage="Type in the name of any font built in to your computer. The font may not appear correctly for everyone."
description="Part of font management modal."
id="tw.fonts.system.name"
/>
</p>

{/* TODO: datalist is pretty bad at this. we should try our own dropdown? */}
<FontName
name={this.state.name}
onChange={this.handleChangeName}
fontManager={this.props.fontManager}
placeholder="Wingdings"
list="fontslist"
/>
{this.state.localFonts && (
<datalist id="fontslist">
{this.state.localFonts.map(family => (
<option
key={family}
value={family}
/>
))}
</datalist>
)}

{this.state.name && (
<React.Fragment>
<FontPlayground family={`${this.state.name}, ${this.state.fallback}`} />

<FontFallback
fallback={this.state.fallback}
onChange={this.handleChangeFallback}
/>
</React.Fragment>
)}

<AddButton
onClick={this.handleFinish}
disabled={!this.state.name}
/>
</React.Fragment>
);
}
}

AddSystemFont.propTypes = {
fontManager: PropTypes.shape({
addSystemFont: PropTypes.func.isRequired,
hasFont: PropTypes.func.isRequired
}).isRequired,
onClose: PropTypes.func.isRequired
};

export default AddSystemFont;
2 changes: 2 additions & 0 deletions src/components/tw-fonts-modal/custom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/components/tw-fonts-modal/delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/tw-fonts-modal/export.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 533cff9

Please sign in to comment.