Merge pull request #759 from mattermost/plt-242-3

PLT-242 Implement custom UI theme colors.
Этот коммит содержится в:
Harrison Healey
2015-09-23 15:04:22 -04:00
родитель 301706f255 e4a15076f4
Коммит 15f15de8ca
41 изменённых файлов: 1166 добавлений и 194 удалений

Просмотреть файл

@@ -12,6 +12,7 @@ var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
export default class ChannelLoader extends React.Component {
constructor(props) {
@@ -68,25 +69,10 @@ export default class ChannelLoader extends React.Component {
/* Update CSS classes to match user theme */
var user = UserStore.getCurrentUser();
if (user.props && user.props.theme) {
Utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';');
Utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';');
Utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';');
Utils.changeCss('.mention', 'background: ' + user.props.theme + ';');
Utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';');
Utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}');
Utils.changeCss('.search-item-container:hover', 'background: ' + Utils.changeOpacity(user.props.theme, 0.05) + ';');
}
if (user.props.theme !== '#000000' && user.props.theme !== '#585858') {
Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, -10) + ';');
Utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;');
} else if (user.props.theme === '#000000') {
Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +50) + ';');
$('.team__header').addClass('theme--black');
} else if (user.props.theme === '#585858') {
Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +10) + ';');
$('.team__header').addClass('theme--gray');
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
Utils.applyTheme(user.theme_props);
} else {
Utils.applyTheme(Constants.THEMES.default);
}
/* Setup global mouse events */

Просмотреть файл

@@ -65,7 +65,7 @@ export default class PopoverListMembers extends React.Component {
>
{count}
<span
className='glyphicon glyphicon-user'
className='fa fa-user'
aria-hidden='true'
/>
</div>

Просмотреть файл

@@ -228,7 +228,7 @@ export default class RegisterAppModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>{'x'}</span>
<span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'

Просмотреть файл

@@ -65,6 +65,7 @@ export default class RhsHeaderPost extends React.Component {
aria-label='Close'
onClick={this.handleClose}
>
<i className='fa fa-sign-out'/>
</button>
</div>
);

Просмотреть файл

@@ -50,6 +50,7 @@ export default class SearchResultsHeader extends React.Component {
title='Close'
onClick={this.handleClose}
>
<i className='fa fa-sign-out'/>
</button>
</div>
);

Просмотреть файл

@@ -0,0 +1,108 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Constants = require('../../utils/constants.jsx');
export default class CustomThemeChooser extends React.Component {
constructor(props) {
super(props);
this.onPickerChange = this.onPickerChange.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.pasteBoxChange = this.pasteBoxChange.bind(this);
this.state = {};
}
componentDidMount() {
$('.color-picker').colorpicker().on('changeColor', this.onPickerChange);
}
onPickerChange(e) {
const theme = this.props.theme;
theme[e.target.id] = e.color.toHex();
theme.type = 'custom';
this.props.updateTheme(theme);
}
onInputChange(e) {
const theme = this.props.theme;
theme[e.target.parentNode.id] = e.target.value;
theme.type = 'custom';
this.props.updateTheme(theme);
}
pasteBoxChange(e) {
const text = e.target.value;
if (text.length === 0) {
return;
}
const colors = text.split(',');
const theme = {type: 'custom'};
let index = 0;
Constants.THEME_ELEMENTS.forEach((element) => {
if (index < colors.length) {
theme[element.id] = colors[index];
}
index++;
});
this.props.updateTheme(theme);
}
render() {
const theme = this.props.theme;
const elements = [];
let colors = '';
Constants.THEME_ELEMENTS.forEach((element) => {
elements.push(
<div className='col-sm-4 form-group'>
<label className='custom-label'>{element.uiName}</label>
<div
className='input-group color-picker'
id={element.id}
>
<input
className='form-control'
type='text'
defaultValue={theme[element.id]}
onChange={this.onInputChange}
/>
<span className='input-group-addon'><i></i></span>
</div>
</div>
);
colors += theme[element.id] + ',';
});
const pasteBox = (
<div className='col-sm-12'>
<label className='custom-label'>
{'Copy and paste to share theme colors:'}
</label>
<input
type='text'
className='form-control'
value={colors}
onChange={this.pasteBoxChange}
/>
</div>
);
return (
<div>
<div className='row form-group'>
{elements}
</div>
<div className='row'>
{pasteBox}
</div>
</div>
);
}
}
CustomThemeChooser.propTypes = {
theme: React.PropTypes.object.isRequired,
updateTheme: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -0,0 +1,179 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
const UserStore = require('../../stores/user_store.jsx');
const Utils = require('../../utils/utils.jsx');
const Client = require('../../utils/client.jsx');
const Modal = ReactBootstrap.Modal;
const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
const Constants = require('../../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
export default class ImportThemeModal extends React.Component {
constructor(props) {
super(props);
this.updateShow = this.updateShow.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.state = {
inputError: '',
show: false
};
}
componentDidMount() {
UserStore.addImportModalListener(this.updateShow);
}
componentWillUnmount() {
UserStore.removeImportModalListener(this.updateShow);
}
updateShow(show) {
this.setState({show});
}
handleSubmit(e) {
e.preventDefault();
const text = React.findDOMNode(this.refs.input).value;
if (!this.isInputValid(text)) {
this.setState({inputError: 'Invalid format, please try copying and pasting in again.'});
return;
}
const colors = text.split(',');
const theme = {type: 'custom'};
theme.sidebarBg = colors[0];
theme.sidebarText = colors[5];
theme.sidebarUnreadText = colors[5];
theme.sidebarTextHoverBg = colors[4];
theme.sidebarTextHoverColor = colors[5];
theme.sidebarTextActiveBg = colors[2];
theme.sidebarTextActiveColor = colors[3];
theme.sidebarHeaderBg = colors[1];
theme.sidebarHeaderTextColor = colors[5];
theme.onlineIndicator = colors[6];
theme.mentionBj = colors[7];
theme.mentionColor = '#ffffff';
theme.centerChannelBg = '#ffffff';
theme.centerChannelColor = '#333333';
theme.linkColor = '#2389d7';
theme.buttonBg = '#26a970';
theme.buttonColor = '#ffffff';
let user = UserStore.getCurrentUser();
user.theme_props = theme;
Client.updateUser(user,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_ME,
me: data
});
this.setState({show: false});
Utils.applyTheme(theme);
$('#user_settings').modal('show');
},
(err) => {
var state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
}
);
}
isInputValid(text) {
if (text.length === 0) {
return false;
}
if (text.indexOf(' ') !== -1) {
return false;
}
if (text.length > 0 && text.indexOf(',') === -1) {
return false;
}
if (text.length > 0) {
const colors = text.split(',');
if (colors.length !== 8) {
return false;
}
for (let i = 0; i < colors.length; i++) {
if (colors[i].length !== 7 && colors[i].length !== 4) {
return false;
}
if (colors[i].charAt(0) !== '#') {
return false;
}
}
}
return true;
}
handleChange(e) {
if (this.isInputValid(e.target.value)) {
this.setState({inputError: null});
} else {
this.setState({inputError: 'Invalid format, please try copying and pasting in again.'});
}
}
render() {
return (
<span>
<Modal
show={this.state.show}
onHide={() => this.setState({show: false})}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'Import Slack Theme'}</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<p>
{'To import a theme, go to a Slack team and look for “”Preferences” -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'}
</p>
<div className='form-group less'>
<div className='col-sm-9'>
<input
ref='input'
type='text'
className='form-control'
onChange={this.handleChange}
/>
{this.state.inputError}
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={() => this.setState({show: false})}
>
{'Cancel'}
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
tabIndex='3'
>
{'Submit'}
</button>
</Modal.Footer>
</form>
</Modal>
</span>
);
}
}

Просмотреть файл

@@ -0,0 +1,55 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Utils = require('../../utils/utils.jsx');
var Constants = require('../../utils/constants.jsx');
export default class PremadeThemeChooser extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const theme = this.props.theme;
const premadeThemes = [];
for (const k in Constants.THEMES) {
if (Constants.THEMES.hasOwnProperty(k)) {
const premadeTheme = $.extend(true, {}, Constants.THEMES[k]);
let activeClass = '';
if (premadeTheme.type === theme.type) {
activeClass = 'active';
}
premadeThemes.push(
<div className='col-sm-3 premade-themes'>
<div
className={activeClass}
onClick={() => this.props.updateTheme(premadeTheme)}
>
<label>
<img
className='img-responsive'
src={'/static/images/themes/' + premadeTheme.type + '.png'}
/>
<div className='theme-label'>{Utils.toTitleCase(premadeTheme.type)}</div>
</label>
</div>
</div>
);
}
}
return (
<div className='row'>
{premadeThemes}
</div>
);
}
}
PremadeThemeChooser.propTypes = {
theme: React.PropTypes.object.isRequired,
updateTheme: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -2,64 +2,32 @@
// See License.txt for license information.
var UserStore = require('../../stores/user_store.jsx');
var SettingItemMin = require('../setting_item_min.jsx');
var SettingItemMax = require('../setting_item_max.jsx');
var Client = require('../../utils/client.jsx');
var Utils = require('../../utils/utils.jsx');
var ThemeColors = ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000'];
const CustomThemeChooser = require('./custom_theme_chooser.jsx');
const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
const Constants = require('../../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
export default class UserSettingsAppearance extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.submitTheme = this.submitTheme.bind(this);
this.updateTheme = this.updateTheme.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleImportModal = this.handleImportModal.bind(this);
this.state = this.getStateFromStores();
}
getStateFromStores() {
var user = UserStore.getCurrentUser();
var theme = '#2389d7';
if (ThemeColors != null) {
theme = ThemeColors[0];
}
if (user.props && user.props.theme) {
theme = user.props.theme;
}
return {theme: theme.toLowerCase()};
}
submitTheme(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
if (!user.props) {
user.props = {};
}
user.props.theme = this.state.theme;
Client.updateUser(user,
function success() {
this.props.updateSection('');
window.location.reload();
}.bind(this),
function fail(err) {
var state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
}.bind(this)
);
}
updateTheme(e) {
var hex = Utils.rgb2hex(e.target.style.backgroundColor);
this.setState({theme: hex.toLowerCase()});
}
handleClose() {
this.setState({serverError: null});
this.props.updateTab('general');
this.originalTheme = this.state.theme;
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
if (this.props.activeSection === 'theme') {
$(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
@@ -72,8 +40,81 @@ export default class UserSettingsAppearance extends React.Component {
}
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateSection('');
}
getStateFromStores() {
const user = UserStore.getCurrentUser();
let theme = null;
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
theme = user.theme_props;
} else {
theme = $.extend(true, {}, Constants.THEMES.default);
}
let type = 'premade';
if (theme.type === 'custom') {
type = 'custom';
}
return {theme, type};
}
onChange() {
const newState = this.getStateFromStores();
if (!Utils.areStatesEqual(this.state, newState)) {
this.setState(newState);
}
}
submitTheme(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
user.theme_props = this.state.theme;
Client.updateUser(user,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_ME,
me: data
});
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateTab('general');
$('#user_settings').modal('hide');
},
(err) => {
var state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
}
);
}
updateTheme(theme) {
this.setState({theme});
Utils.applyTheme(theme);
}
updateType(type) {
this.setState({type});
}
handleClose() {
const state = this.getStateFromStores();
state.serverError = null;
Utils.applyTheme(state.theme);
this.setState(state);
$('.ps-container.modal-body').scrollTop(0);
$('.ps-container.modal-body').perfectScrollbar('update');
$('#user_settings').modal('hide');
}
handleImportModal() {
$('#user_settings').modal('hide');
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL,
value: true
});
}
render() {
var serverError;
@@ -81,67 +122,73 @@ export default class UserSettingsAppearance extends React.Component {
serverError = this.state.serverError;
}
var themeSection;
var self = this;
const displayCustom = this.state.type === 'custom';
if (ThemeColors != null) {
if (this.props.activeSection === 'theme') {
var themeButtons = [];
for (var i = 0; i < ThemeColors.length; i++) {
themeButtons.push(
<button
key={ThemeColors[i] + 'key' + i}
ref={ThemeColors[i]}
type='button'
className='btn btn-lg color-btn'
style={{backgroundColor: ThemeColors[i]}}
onClick={this.updateTheme}
/>
);
}
var inputs = [];
inputs.push(
<li
key='themeColorSetting'
className='setting-list-item'
>
<div
className='btn-group'
data-toggle='buttons-radio'
>
{themeButtons}
</div>
</li>
);
themeSection = (
<SettingItemMax
title='Theme Color'
inputs={inputs}
submit={this.submitTheme}
serverError={serverError}
updateSection={function updateSection(e) {
self.props.updateSection('');
e.preventDefault();
}}
/>
);
} else {
themeSection = (
<SettingItemMin
title='Theme Color'
describe={this.state.theme}
updateSection={function updateSection() {
self.props.updateSection('theme');
}}
/>
);
}
let custom;
let premade;
if (displayCustom) {
custom = (
<CustomThemeChooser
theme={this.state.theme}
updateTheme={this.updateTheme}
/>
);
} else {
premade = (
<PremadeThemeChooser
theme={this.state.theme}
updateTheme={this.updateTheme}
/>
);
}
const themeUI = (
<div className='section-max appearance-section'>
<div className='col-sm-12'>
<div className='radio'>
<label>
<input type='radio'
checked={!displayCustom}
onChange={this.updateType.bind(this, 'premade')}
>
{'Theme Colors'}
</input>
</label>
<br/>
</div>
{premade}
<div className='radio'>
<label>
<input type='radio'
checked={displayCustom}
onChange={this.updateType.bind(this, 'custom')}
>
{'Custom Theme'}
</input>
</label>
<br/>
</div>
{custom}
<hr />
{serverError}
<a
className='btn btn-sm btn-primary'
href='#'
onClick={this.submitTheme}
>
{'Submit'}
</a>
<a
className='btn btn-sm theme'
href='#'
onClick={this.handleClose}
>
{'Cancel'}
</a>
</div>
</div>
);
return (
<div>
<div className='modal-header'>
@@ -151,21 +198,28 @@ export default class UserSettingsAppearance extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
<span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
<i className='modal-back'></i>Appearance Settings
<i className='modal-back'></i>{'Appearance Settings'}
</h4>
</div>
<div className='user-settings'>
<h3 className='tab-header'>Appearance Settings</h3>
<h3 className='tab-header'>{'Appearance Settings'}</h3>
<div className='divider-dark first'/>
{themeSection}
{themeUI}
<div className='divider-dark'/>
</div>
<br/>
<a
className='theme'
onClick={this.handleImportModal}
>
{'Import from Slack'}
</a>
</div>
);
}
@@ -176,6 +230,5 @@ UserSettingsAppearance.defaultProps = {
};
UserSettingsAppearance.propTypes = {
activeSection: React.PropTypes.string,
updateSection: React.PropTypes.func,
updateTab: React.PropTypes.func
};

Просмотреть файл

@@ -64,7 +64,7 @@ export default class DeveloperTab extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>{'x'}</span>
<span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'

Просмотреть файл

@@ -60,7 +60,7 @@ export default class UserSettingsModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>{'x'}</span>
<span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'

Просмотреть файл

@@ -4,13 +4,14 @@
"private": true,
"dependencies": {
"autolinker": "0.18.1",
"babel-runtime": "5.8.24",
"bootstrap-colorpicker": "2.2.0",
"flux": "2.1.1",
"keymirror": "0.1.1",
"marked": "0.3.5",
"object-assign": "3.0.0",
"react-zeroclipboard-mixin": "0.1.0",
"twemoji": "1.4.1",
"babel-runtime": "5.8.24",
"marked": "0.3.5"
"twemoji": "1.4.1"
},
"devDependencies": {
"browserify": "11.0.1",
@@ -28,8 +29,15 @@
},
"browserify": {
"transform": [
["babelify", { "optional": ["runtime"] }],
"envify"
[
"babelify",
{
"optional": [
"runtime"
]
}
],
"envify"
]
},
"jest": {

Просмотреть файл

@@ -34,6 +34,7 @@ var ActivityLogModal = require('../components/activity_log_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -84,6 +85,11 @@ function setupChannelPage(props) {
document.getElementById('user_settings_modal')
);
React.render(
<ImportThemeModal />,
document.getElementById('import_theme_modal')
);
React.render(
<TeamSettingsModal teamDisplayName={props.TeamDisplayName} />,
document.getElementById('team_settings_modal')

Просмотреть файл

@@ -14,6 +14,7 @@ var CHANGE_EVENT_SESSIONS = 'change_sessions';
var CHANGE_EVENT_AUDITS = 'change_audits';
var CHANGE_EVENT_TEAMS = 'change_teams';
var CHANGE_EVENT_STATUSES = 'change_statuses';
var TOGGLE_IMPORT_MODAL_EVENT = 'toggle_import_modal';
class UserStoreClass extends EventEmitter {
constructor() {
@@ -34,6 +35,9 @@ class UserStoreClass extends EventEmitter {
this.emitStatusesChange = this.emitStatusesChange.bind(this);
this.addStatusesChangeListener = this.addStatusesChangeListener.bind(this);
this.removeStatusesChangeListener = this.removeStatusesChangeListener.bind(this);
this.emitToggleImportModal = this.emitToggleImportModal.bind(this);
this.addImportModalListener = this.addImportModalListener.bind(this);
this.removeImportModalListener = this.removeImportModalListener.bind(this);
this.setCurrentId = this.setCurrentId.bind(this);
this.getCurrentId = this.getCurrentId.bind(this);
this.getCurrentUser = this.getCurrentUser.bind(this);
@@ -114,6 +118,15 @@ class UserStoreClass extends EventEmitter {
removeStatusesChangeListener(callback) {
this.removeListener(CHANGE_EVENT_STATUSES, callback);
}
emitToggleImportModal(value) {
this.emit(TOGGLE_IMPORT_MODAL_EVENT, value);
}
addImportModalListener(callback) {
this.on(TOGGLE_IMPORT_MODAL_EVENT, callback);
}
removeImportModalListener(callback) {
this.removeListener(TOGGLE_IMPORT_MODAL_EVENT, callback);
}
setCurrentId(id) {
this.gCurrentId = id;
if (id == null) {
@@ -321,6 +334,9 @@ UserStore.dispatchToken = AppDispatcher.register(function registry(payload) {
UserStore.pSetStatuses(action.statuses);
UserStore.emitStatusesChange();
break;
case ActionTypes.TOGGLE_IMPORT_THEME_MODAL:
UserStore.emitToggleImportModal(action.value);
break;
default:
}

Просмотреть файл

@@ -36,7 +36,9 @@ module.exports = {
RECIEVED_CONFIG: null,
RECIEVED_LOGS: null
RECIEVED_LOGS: null,
TOGGLE_IMPORT_THEME_MODAL: null
}),
PayloadSources: keyMirror({
@@ -110,5 +112,138 @@ module.exports = {
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>",
MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>"
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
THEMES: {
default: {
type: 'Mattermost',
sidebarBg: '#fafafa',
sidebarText: '#999999',
sidebarUnreadText: '#333333',
sidebarTextHoverBg: '#e6f2fa',
sidebarTextHoverColor: '#999999',
sidebarTextActiveBg: '#e1e1e1',
sidebarTextActiveColor: '#111111',
sidebarHeaderBg: '#2389d7',
sidebarHeaderTextColor: '#ffffff',
onlineIndicator: '#7DBE00',
mentionBj: '#2389d7',
mentionColor: '#ffffff',
centerChannelBg: '#ffffff',
centerChannelColor: '#333333',
linkColor: '#2389d7',
buttonBg: '#2389d7',
buttonColor: '#FFFFFF'
},
slack: {
type: 'Slack',
sidebarBg: '#4D394B',
sidebarText: '#ab9ba9',
sidebarUnreadText: '#FFFFFF',
sidebarTextHoverBg: '#3e313c',
sidebarTextHoverColor: '#ab9ba9',
sidebarTextActiveBg: '#4c9689',
sidebarTextActiveColor: '#FFFFFF',
sidebarHeaderBg: '#4D394B',
sidebarHeaderTextColor: '#FFFFFF',
onlineIndicator: '#4c9689',
mentionBj: '#eb4d5c',
mentionColor: '#FFFFFF',
centerChannelBg: '#FFFFFF',
centerChannelColor: '#333333',
linkColor: '#2389d7',
buttonBg: '#26a970',
buttonColor: '#FFFFFF'
},
dark: {
type: 'Dark',
sidebarBg: '#1B2C3E',
sidebarText: '#bbbbbb',
sidebarUnreadText: '#fff',
sidebarTextHoverBg: '#4A5664',
sidebarTextHoverColor: '#bbbbbb',
sidebarTextActiveBg: '#39769C',
sidebarTextActiveColor: '#FFFFFF',
sidebarHeaderBg: '#1B2C3E',
sidebarHeaderTextColor: '#FFFFFF',
onlineIndicator: '#4c9689',
mentionBj: '#B74A4A',
mentionColor: '#FFFFFF',
centerChannelBg: '#2F3E4E',
centerChannelColor: '#DDDDDD',
linkColor: '#A4FFEB',
buttonBg: '#2B9C99',
buttonColor: '#FFFFFF'
}
},
THEME_ELEMENTS: [
{
id: 'sidebarBg',
uiName: 'Sidebar BG'
},
{
id: 'sidebarText',
uiName: 'Sidebar text color'
},
{
id: 'sidebarHeaderBg',
uiName: 'Sidebar Header BG'
},
{
id: 'sidebarHeaderTextColor',
uiName: 'Sidebar Header text color'
},
{
id: 'sidebarUnreadText',
uiName: 'Sidebar unread text color'
},
{
id: 'sidebarTextHoverBg',
uiName: 'Sidebar text hover BG'
},
{
id: 'sidebarTextHoverColor',
uiName: 'Sidebar text hover color'
},
{
id: 'sidebarTextActiveBg',
uiName: 'Sidebar text active BG'
},
{
id: 'sidebarTextActiveColor',
uiName: 'Sidebar text active color'
},
{
id: 'onlineIndicator',
uiName: 'Online indicator'
},
{
id: 'mentionBj',
uiName: 'Mention jewel BG'
},
{
id: 'mentionColor',
uiName: 'Mention jewel text color'
},
{
id: 'centerChannelBg',
uiName: 'Center channel BG'
},
{
id: 'centerChannelColor',
uiName: 'Center channel text color'
},
{
id: 'linkColor',
uiName: 'Link color'
},
{
id: 'buttonBg',
uiName: 'Button BG'
},
{
id: 'buttonColor',
uiName: 'Button Color'
}
]
};

Просмотреть файл

@@ -542,7 +542,119 @@ export function toTitleCase(str) {
return str.replace(/\w\S*/g, doTitleCase);
}
export function changeCss(className, classValue) {
export function applyTheme(theme) {
if (theme.sidebarBg) {
changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1);
changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarBg, 1);
}
if (theme.sidebarText) {
changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
changeCss('.sidebar--right .sidebar-right__body', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
}
if (theme.sidebarUnreadText) {
changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1);
}
if (theme.sidebarTextHoverBg) {
changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1);
}
if (theme.sidebarTextHoverColor) {
changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2);
}
if (theme.sidebarTextActiveBg) {
changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1);
}
if (theme.sidebarTextActiveColor) {
changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2);
}
if (theme.sidebarHeaderBg) {
changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
}
if (theme.sidebarHeaderTextColor) {
changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1);
changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-brand .heading, ', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1);
}
if (theme.onlineIndicator) {
changeCss('.sidebar--left .status .online--icon', 'fill:' + theme.onlineIndicator, 1);
}
if (theme.mentionBj) {
changeCss('.sidebar--left .nav-pills__unread-indicator', 'background:' + theme.mentionBj, 1);
changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj, 1);
}
if (theme.mentionColor) {
changeCss('.sidebar--left .nav-pills__unread-indicator', 'color:' + theme.mentionColor, 2);
changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor, 2);
}
if (theme.centerChannelBg) {
changeCss('.app__content', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.search-bar__container .search__form .search-bar', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
changeCss('.app__content', 'color:' + theme.centerChannelColor, 2);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '!important; color: ' + theme.centerChannelColor, 1);
changeCss('.search-bar__container .search__form .search-bar', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: ' + theme.centerChannelColor, 2);
changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
changeCss('.date-separator .separator__hr, .new-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
}
if (theme.linkColor) {
changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1);
changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1);
}
if (theme.buttonBg) {
changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
}
if (theme.buttonColor) {
changeCss('.btn.btn-primary', 'color:' + theme.buttonColor, 2);
}
}
export function changeCss(className, classValue, classRepeat) {
// we need invisible container to store additional css definitions
var cssMainContainer = $('#css-modifier-container');
if (cssMainContainer.length === 0) {
@@ -552,9 +664,9 @@ export function changeCss(className, classValue) {
}
// and we need one div for each class
var classContainer = cssMainContainer.find('div[data-class="' + className + '"]');
var classContainer = cssMainContainer.find('div[data-class="' + className + classRepeat + '"]');
if (classContainer.length === 0) {
classContainer = $('<div data-class="' + className + '"></div>');
classContainer = $('<div data-class="' + className + classRepeat + '"></div>');
classContainer.appendTo(cssMainContainer);
}
@@ -760,57 +872,47 @@ Image.prototype.load = function imageLoad(url, progressCallback) {
Image.prototype.completedPercentage = 0;
export function changeColor(colourIn, amt) {
var usePound = false;
var col = colourIn;
var hex = colourIn;
var lum = amt;
if (col[0] === '#') {
col = col.slice(1);
usePound = true;
// validate hex string
hex = String(hex).replace(/[^0-9a-f]/gi, '');
if (hex.length < 6) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
lum = lum || 0;
// convert to decimal and change luminosity
var rgb = '#';
var c;
var i;
for (i = 0; i < 3; i++) {
c = parseInt(hex.substr(i * 2, 2), 16);
c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
rgb += ('00' + c).substr(c.length);
}
var num = parseInt(col, 16);
var r = (num >> 16) + amt;
if (r > 255) {
r = 255;
} else if (r < 0) {
r = 0;
}
var b = ((num >> 8) & 0x00FF) + amt;
if (b > 255) {
b = 255;
} else if (b < 0) {
b = 0;
}
var g = (num & 0x0000FF) + amt;
if (g > 255) {
g = 255;
} else if (g < 0) {
g = 0;
}
var pound = '#';
if (!usePound) {
pound = '';
}
return pound + String('000000' + (g | (b << 8) | (r << 16)).toString(16)).slice(-6);
return rgb;
}
export function changeOpacity(oldColor, opacity) {
var col = oldColor;
if (col[0] === '#') {
col = col.slice(1);
var color = oldColor;
if (color[0] === '#') {
color = color.slice(1);
}
var r = parseInt(col.substring(0, 2), 16);
var g = parseInt(col.substring(2, 4), 16);
var b = parseInt(col.substring(4, 6), 16);
if (color.length === 3) {
const tempColor = color;
color = '';
color += tempColor[0] + tempColor[0];
color += tempColor[1] + tempColor[1];
color += tempColor[2] + tempColor[2];
}
var r = parseInt(color.substring(0, 2), 16);
var g = parseInt(color.substring(2, 4), 16);
var b = parseInt(color.substring(4, 6), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
}