commit: 1b9805b550444403792db2304b5a6d5681e8f30d
parent 3ab128e73924ce34d190ff609cb9b081cdffe402
Author: Shpuld Shpludson <shp@cock.li>
Date: Fri, 28 Feb 2020 20:27:02 +0000
Merge branch 'develop' into 'master'
Update master with 2.0.0
See merge request pleroma/pleroma-fe!1074
Diffstat:
168 files changed, 4969 insertions(+), 3508 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -4,19 +4,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+
+## [2.0.0] - 2020-02-28
### Added
+- Tons of color slots including ones for hover/pressed/toggled buttons
+- Experimental `--variable[,mod]` syntax support for color slots in themes. the `mod` makes color brighter/darker depending on background color (makes darker color brighter/darker depending on background color)
+- Paper theme by Shpuld
- Icons in nav panel
- Private mode support
- Support for 'Move' type notifications
- Pleroma AMOLED dark theme
+- User level domain mutes, under User Settings -> Mutes
+- Emoji reactions for statuses
+- MRF keyword policy disclosure
### Changed
+- Updated Pleroma default themes
+- theme engine update to 3 (themes v2.1 introduction)
+- massive internal changes in theme engine - slowly away from "generate things separately with spaghetti code" towards "feed all data into single 'generateTheme' function and declare slot inheritance and all in a separate file"
+- Breezy theme updates to make it closer to actual Breeze in some aspects
+- when using `--variable` in shadows it no longer uses the actual CSS3 variable, instead it generates color from other slots
+- theme doesn't get saved to local storage when opening FE anonymously
- Captcha now resets on failed registrations
- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
- 403 messaging
### Fixed
+- anon viewers won't get theme data saved to local storage, so admin changing default theme will have an effect for users coming back to instance.
- Single notifications left unread when hitting read on another device/tab
- Registration fixed
- Deactivation of remote accounts from frontend
+- Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying
+- Improved performance of anything that uses popovers (most notably statuses)
## [1.1.7 and earlier] - 2019-12-14
### Added
diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js
@@ -35,6 +35,7 @@ module.exports = {
],
alias: {
'vue$': 'vue/dist/vue.runtime.common',
+ 'static': path.resolve(__dirname, '../static'),
'src': path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../src/assets'),
'components': path.resolve(__dirname, '../src/components')
diff --git a/package.json b/package.json
@@ -21,6 +21,7 @@
"chromatism": "^3.0.0",
"cropperjs": "^1.4.3",
"diff": "^3.0.1",
+ "escape-html": "^1.0.3",
"karma-mocha-reporter": "^2.2.1",
"localforage": "^1.5.0",
"object-path": "^0.11.3",
@@ -28,7 +29,6 @@
"portal-vue": "^2.1.4",
"sanitize-html": "^1.13.0",
"v-click-outside": "^2.1.1",
- "v-tooltip": "^2.0.2",
"vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2",
@@ -43,6 +43,7 @@
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4",
+ "@ungap/event-target": "^0.1.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
"@vue/test-utils": "^1.0.0-beta.26",
@@ -56,6 +57,7 @@
"connect-history-api-fallback": "^1.1.0",
"cross-spawn": "^4.0.2",
"css-loader": "^0.28.0",
+ "custom-event-polyfill": "^1.0.7",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"eslint-friendly-formatter": "^2.0.5",
diff --git a/src/App.scss b/src/App.scss
@@ -31,9 +31,12 @@ h4 {
margin: auto;
min-height: 100vh;
max-width: 980px;
- background-color: rgba(0,0,0,0.15);
align-content: flex-start;
}
+.underlay {
+ background-color: rgba(0,0,0,0.15);
+ background-color: var(--underlay, rgba(0,0,0,0.15));
+}
.text-center {
text-align: center;
@@ -75,7 +78,7 @@ button {
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
cursor: pointer;
- box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+ box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
font-size: 14px;
font-family: sans-serif;
@@ -98,18 +101,39 @@ button {
&:active {
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
+ color: $fallback--text;
+ color: var(--btnPressedText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--btnPressed, $fallback--fg);
+ i {
+ color: $fallback--text;
+ color: var(--btnPressedText, $fallback--text);
+ }
}
&:disabled {
cursor: not-allowed;
- opacity: 0.5;
+ color: $fallback--text;
+ color: var(--btnDisabledText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--btnDisabled, $fallback--fg);
+ i {
+ color: $fallback--text;
+ color: var(--btnDisabledText, $fallback--text);
+ }
}
- &.pressed {
- color: $fallback--faint;
- color: var(--faint, $fallback--faint);
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg)
+ &.toggled {
+ color: $fallback--text;
+ color: var(--btnToggledText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--btnToggled, $fallback--fg);
+ box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
+ box-shadow: var(--buttonPressedShadow);
+ i {
+ color: $fallback--text;
+ color: var(--btnToggledText, $fallback--text);
+ }
}
&.danger {
@@ -121,12 +145,15 @@ button {
}
}
-label.select {
- padding: 0;
+input, textarea, .select, .input {
-}
+ &.unstyled {
+ border-radius: 0;
+ background: none;
+ box-shadow: none;
+ height: unset;
+ }
-input, textarea, .select {
border: none;
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
@@ -140,13 +167,17 @@ input, textarea, .select {
font-family: var(--inputFont, sans-serif);
font-size: 14px;
margin: 0;
- padding: 8px .5em;
box-sizing: border-box;
display: inline-block;
position: relative;
height: 28px;
line-height: 16px;
hyphens: none;
+ padding: 8px .5em;
+
+ &.select {
+ padding: 0;
+ }
&:disabled, &[disabled=disabled] {
cursor: not-allowed;
@@ -160,7 +191,7 @@ input, textarea, .select {
right: 5px;
height: 100%;
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--inputText, $fallback--text);
line-height: 28px;
z-index: 0;
pointer-events: none;
@@ -198,7 +229,7 @@ input, textarea, .select {
&:checked + label::before {
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
- background-color: var(--link, $fallback--link);
+ background-color: var(--accent, $fallback--link);
}
&:disabled {
&,
@@ -235,7 +266,7 @@ input, textarea, .select {
display: none;
&:checked + label::before {
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--inputText, $fallback--text);
}
&:disabled {
&,
@@ -353,6 +384,33 @@ i[class*=icon-] {
height: 50px;
box-sizing: border-box;
+ button {
+ &, i[class*=icon-] {
+ color: $fallback--text;
+ color: var(--btnTopBarText, $fallback--text);
+ }
+
+ &:active {
+ background-color: $fallback--fg;
+ background-color: var(--btnPressedTopBar, $fallback--fg);
+ color: $fallback--text;
+ color: var(--btnPressedTopBarText, $fallback--text);
+ }
+
+ &:disabled {
+ color: $fallback--text;
+ color: var(--btnDisabledTopBarText, $fallback--text);
+ }
+
+ &.toggled {
+ color: $fallback--text;
+ color: var(--btnToggledTopBarText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--btnToggledTopBar, $fallback--fg)
+ }
+ }
+
+
.logo {
display: flex;
position: absolute;
@@ -487,6 +545,10 @@ main-router {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
+ .faint-link {
+ color: $fallback--faint;
+ color: var(--faintLink, $fallback--faint);
+ }
.alert {
white-space: nowrap;
@@ -509,6 +571,30 @@ main-router {
align-self: stretch;
}
+ button {
+ &, i[class*=icon-] {
+ color: $fallback--text;
+ color: var(--btnPanelText, $fallback--text);
+ }
+
+ &:active {
+ background-color: $fallback--fg;
+ background-color: var(--btnPressedPanel, $fallback--fg);
+ color: $fallback--text;
+ color: var(--btnPressedPanelText, $fallback--text);
+ }
+
+ &:disabled {
+ color: $fallback--text;
+ color: var(--btnDisabledPanelText, $fallback--text);
+ }
+
+ &.toggled {
+ color: $fallback--text;
+ color: var(--btnToggledPanelText, $fallback--text);
+ }
+ }
+
a {
color: $fallback--link;
color: var(--panelLink, $fallback--link)
diff --git a/src/App.vue b/src/App.vue
@@ -78,7 +78,7 @@
</nav>
<div
id="content"
- class="container"
+ class="container underlay"
>
<div class="sidebar-flexer mobile-hidden">
<div class="sidebar-bounds">
diff --git a/src/_variables.scss b/src/_variables.scss
@@ -27,3 +27,5 @@ $fallback--tooltipRadius: 5px;
$fallback--avatarRadius: 4px;
$fallback--avatarAltRadius: 10px;
$fallback--attachmentRadius: 10px;
+
+$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
@@ -5,6 +5,8 @@ import App from '../App.vue'
import { windowWidth } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
+import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
+import { applyTheme } from '../services/style_setter/style_setter.js'
const getStatusnetConfig = async ({ store }) => {
try {
@@ -185,12 +187,9 @@ const getAppSecret = async ({ store }) => {
})
}
-const resolveStaffAccounts = async ({ store, accounts }) => {
- const backendInteractor = store.state.api.backendInteractor
- let nicknames = accounts.map(uri => uri.split('/').pop())
- .map(id => backendInteractor.fetchUser({ id }))
- nicknames = await Promise.all(nicknames)
-
+const resolveStaffAccounts = ({ store, accounts }) => {
+ const nicknames = accounts.map(uri => uri.split('/').pop())
+ nicknames.map(nickname => store.dispatch('fetchUser', nickname))
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
}
@@ -224,9 +223,16 @@ const getNodeInfo = async ({ store }) => {
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
- store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
const federation = metadata.federation
+
+ store.dispatch('setInstanceOption', {
+ name: 'tagPolicyAvailable',
+ value: typeof federation.mrf_policies === 'undefined'
+ ? false
+ : metadata.federation.mrf_policies.includes('TagPolicy')
+ })
+
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
store.dispatch('setInstanceOption', {
name: 'federating',
@@ -236,7 +242,7 @@ const getNodeInfo = async ({ store }) => {
})
const accounts = metadata.staffAccounts
- await resolveStaffAccounts({ store, accounts })
+ resolveStaffAccounts({ store, accounts })
} else {
throw (res)
}
@@ -261,7 +267,7 @@ const checkOAuthToken = async ({ store }) => {
try {
await store.dispatch('loginUser', store.getters.getUserToken())
} catch (e) {
- console.log(e)
+ console.error(e)
}
}
resolve()
@@ -269,23 +275,29 @@ const checkOAuthToken = async ({ store }) => {
}
const afterStoreSetup = async ({ store, i18n }) => {
- if (store.state.config.customTheme) {
- // This is a hack to deal with async loading of config.json and themes
- // See: style_setter.js, setPreset()
- window.themeLoaded = true
- store.dispatch('setOption', {
- name: 'customTheme',
- value: store.state.config.customTheme
- })
- }
-
const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800)
+ await setConfig({ store })
+
+ const { customTheme, customThemeSource } = store.state.config
+ const { theme } = store.state.instance
+ const customThemePresent = customThemeSource || customTheme
+
+ if (customThemePresent) {
+ if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
+ applyTheme(customThemeSource)
+ } else {
+ applyTheme(customTheme)
+ }
+ } else if (theme) {
+ // do nothing, it will load asynchronously
+ } else {
+ console.error('Failed to load any theme!')
+ }
// Now we can try getting the server settings and logging in
await Promise.all([
checkOAuthToken({ store }),
- setConfig({ store }),
getTOS({ store }),
getInstancePanel({ store }),
getStickers({ store }),
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
@@ -1,4 +1,5 @@
import ProgressButton from '../progress_button/progress_button.vue'
+import Popover from '../popover/popover.vue'
const AccountActions = {
props: [
@@ -8,7 +9,8 @@ const AccountActions = {
return { }
},
components: {
- ProgressButton
+ ProgressButton,
+ Popover
},
methods: {
showRepeats () {
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
@@ -1,13 +1,13 @@
<template>
<div class="account-actions">
- <v-popover
+ <Popover
trigger="click"
- class="account-tools-popover"
- :container="false"
- placement="bottom-end"
- :offset="5"
+ placement="bottom"
>
- <div slot="popover">
+ <div
+ slot="content"
+ class="account-tools-popover"
+ >
<div class="dropdown-menu">
<template v-if="user.following">
<button
@@ -51,10 +51,13 @@
</button>
</div>
</div>
- <div class="btn btn-default ellipsis-button">
+ <div
+ slot="trigger"
+ class="btn btn-default ellipsis-button"
+ >
<i class="icon-ellipsis trigger-button" />
</div>
- </v-popover>
+ </Popover>
</div>
</template>
@@ -62,7 +65,6 @@
<style lang="scss">
@import '../../_variables.scss';
-@import '../popper/popper.scss';
.account-actions {
margin: 0 .8em;
}
@@ -70,6 +72,7 @@
.account-actions button.dropdown-item {
margin-left: 0;
}
+
.account-actions .trigger-button {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
@@ -2,6 +2,7 @@ import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js'
+import { mapGetters } from 'vuex'
const Attachment = {
props: [
@@ -49,7 +50,8 @@ const Attachment = {
},
fullwidth () {
return this.type === 'html' || this.type === 'audio'
- }
+ },
+ ...mapGetters(['mergedConfig'])
},
methods: {
linkClicked ({ target }) {
@@ -58,7 +60,7 @@ const Attachment = {
}
},
openModal (event) {
- const modalTypes = this.$store.getters.mergedConfig.playVideosInModal
+ const modalTypes = this.mergedConfig.playVideosInModal
? ['image', 'video']
: ['image']
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
@@ -71,7 +73,10 @@ const Attachment = {
}
},
toggleHidden (event) {
- if (this.$store.getters.mergedConfig.useOneClickNsfw && !this.showHidden) {
+ if (
+ (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
+ (this.type !== 'video' || this.mergedConfig.playVideosInModal)
+ ) {
this.openModal(event)
return
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
@@ -130,6 +130,8 @@
.placeholder {
margin-right: 8px;
margin-bottom: 4px;
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
}
.nsfw-placeholder {
diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue
@@ -40,8 +40,8 @@
top: 100%;
right: 0;
max-height: 400px;
- background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
@@ -87,13 +87,13 @@ export default {
&:checked + .checkbox-indicator::before {
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--inputText, $fallback--text);
}
&:indeterminate + .checkbox-indicator::before {
content: '–';
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--inputText, $fallback--text);
}
}
diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss
@@ -0,0 +1,68 @@
+@import '../../_variables.scss';
+
+.color-input {
+ display: inline-flex;
+
+ &-field.input {
+ display: inline-flex;
+ flex: 0 0 0;
+ max-width: 9em;
+ align-items: stretch;
+ padding: .2em 8px;
+
+ input {
+ background: none;
+ color: $fallback--lightText;
+ color: var(--inputText, $fallback--lightText);
+ border: none;
+ padding: 0;
+ margin: 0;
+
+ &.textColor {
+ flex: 1 0 3em;
+ min-width: 3em;
+ padding: 0;
+ }
+
+ &.nativeColor {
+ flex: 0 0 2em;
+ min-width: 2em;
+ align-self: center;
+ height: 100%;
+ }
+ }
+ .computedIndicator,
+ .transparentIndicator {
+ flex: 0 0 2em;
+ min-width: 2em;
+ align-self: center;
+ height: 100%;
+ }
+ .transparentIndicator {
+ // forgot to install counter-strike source, ooops
+ background-color: #FF00FF;
+ position: relative;
+ &::before, &::after {
+ display: block;
+ content: '';
+ background-color: #000000;
+ position: absolute;
+ height: 50%;
+ width: 50%;
+ }
+ &::after {
+ top: 0;
+ left: 0;
+ }
+ &::before {
+ bottom: 0;
+ right: 0;
+ }
+ }
+ }
+
+ .label {
+ flex: 1 1 auto;
+ }
+
+}
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
@@ -1,6 +1,6 @@
<template>
<div
- class="color-control style-control"
+ class="color-input style-control"
:class="{ disabled: !present || disabled }"
>
<label
@@ -9,46 +9,100 @@
>
{{ label }}
</label>
- <input
- v-if="typeof fallback !== 'undefined'"
- :id="name + '-o'"
- class="opt exlcude-disabled"
- type="checkbox"
+ <Checkbox
+ v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
:checked="present"
- @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
- >
- <label
- v-if="typeof fallback !== 'undefined'"
- class="opt-l"
- :for="name + '-o'"
+ :disabled="disabled"
+ class="opt"
+ @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
/>
- <input
- :id="name"
- class="color-input"
- type="color"
- :value="value || fallback"
- :disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
- >
- <input
- :id="name + '-t'"
- class="text-input"
- type="text"
- :value="value || fallback"
- :disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
- >
+ <div class="input color-input-field">
+ <input
+ :id="name + '-t'"
+ class="textColor unstyled"
+ type="text"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ @input="$emit('input', $event.target.value)"
+ >
+ <input
+ v-if="validColor"
+ :id="name"
+ class="nativeColor unstyled"
+ type="color"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ @input="$emit('input', $event.target.value)"
+ >
+ <div
+ v-if="transparentColor"
+ class="transparentIndicator"
+ />
+ <div
+ v-if="computedColor"
+ class="computedIndicator"
+ :style="{backgroundColor: fallback}"
+ />
+ </div>
</div>
</template>
-
+<style lang="scss" src="./color_input.scss"></style>
<script>
+import Checkbox from '../checkbox/checkbox.vue'
+import { hex2rgb } from '../../services/color_convert/color_convert.js'
export default {
- props: [
- 'name', 'label', 'value', 'fallback', 'disabled'
- ],
+ components: {
+ Checkbox
+ },
+ props: {
+ // Name of color, used for identifying
+ name: {
+ required: true,
+ type: String
+ },
+ // Readable label
+ label: {
+ required: true,
+ type: String
+ },
+ // Color value, should be required but vue cannot tell the difference
+ // between "property missing" and "property set to undefined"
+ value: {
+ required: false,
+ type: String,
+ default: undefined
+ },
+ // Color fallback to use when value is not defeind
+ fallback: {
+ required: false,
+ type: String,
+ default: undefined
+ },
+ // Disable the control
+ disabled: {
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ // Show "optional" tickbox, for when value might become mandatory
+ showOptionalTickbox: {
+ required: false,
+ type: Boolean,
+ default: true
+ }
+ },
computed: {
present () {
return typeof this.value !== 'undefined'
+ },
+ validColor () {
+ return hex2rgb(this.value || this.fallback)
+ },
+ transparentColor () {
+ return this.value === 'transparent'
+ },
+ computedColor () {
+ return this.value && this.value.startsWith('--')
}
}
}
diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue
@@ -37,9 +37,17 @@
<script>
export default {
- props: [
- 'large', 'contrast'
- ],
+ props: {
+ large: {
+ required: false
+ },
+ // TODO: Make theme switcher compute theme initially so that contrast
+ // component won't be called without contrast data
+ contrast: {
+ required: false,
+ type: Object
+ }
+ },
computed: {
hint () {
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
@@ -150,6 +150,7 @@ const conversation = {
if (!id) return
this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
+ this.$store.dispatch('fetchEmojiReactionsBy', id)
},
getHighlight () {
return this.isExpanded ? this.highlight : null
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
@@ -75,18 +75,18 @@
.dialog-modal-content {
margin: 0;
padding: 1rem 1rem;
- background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
white-space: normal;
}
.dialog-modal-footer {
margin: 0;
padding: .5em .5em;
- background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
- border-top: 1px solid $fallback--bg;
- border-top: 1px solid var(--bg, $fallback--bg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+ border-top: 1px solid $fallback--border;
+ border-top: 1px solid var(--border, $fallback--border);
display: flex;
justify-content: flex-end;
diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js
@@ -0,0 +1,15 @@
+import ProgressButton from '../progress_button/progress_button.vue'
+
+const DomainMuteCard = {
+ props: ['domain'],
+ components: {
+ ProgressButton
+ },
+ methods: {
+ unmuteDomain () {
+ return this.$store.dispatch('unmuteDomain', this.domain)
+ }
+ }
+}
+
+export default DomainMuteCard
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
@@ -0,0 +1,38 @@
+<template>
+ <div class="domain-mute-card">
+ <div class="domain-mute-card-domain">
+ {{ domain }}
+ </div>
+ <ProgressButton
+ :click="unmuteDomain"
+ class="btn btn-default"
+ >
+ {{ $t('domain_mute_card.unmute') }}
+ <template slot="progress">
+ {{ $t('domain_mute_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+</template>
+
+<script src="./domain_mute_card.js"></script>
+
+<style lang="scss">
+.domain-mute-card {
+ flex: 1 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.6em 1em 0.6em 0;
+
+ &-domain {
+ margin-right: 1em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ button {
+ width: 10em;
+ }
+}
+</style>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
@@ -147,7 +147,7 @@ const EmojiInput = {
input.elm.addEventListener('keydown', this.onKeyDown)
input.elm.addEventListener('click', this.onClickInput)
input.elm.addEventListener('transitionend', this.onTransition)
- input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
+ input.elm.addEventListener('input', this.onInput)
},
unmounted () {
const { input } = this
@@ -159,7 +159,7 @@ const EmojiInput = {
input.elm.removeEventListener('keydown', this.onKeyDown)
input.elm.removeEventListener('click', this.onClickInput)
input.elm.removeEventListener('transitionend', this.onTransition)
- input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
+ input.elm.removeEventListener('input', this.onInput)
}
},
methods: {
@@ -406,12 +406,6 @@ const EmojiInput = {
this.resize()
this.$emit('input', e.target.value)
},
- onCompositionUpdate (e) {
- this.showPicker = false
- this.setCaret(e)
- this.resize()
- this.$emit('input', e.target.value)
- },
onClickInput (e) {
this.showPicker = false
},
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
@@ -109,10 +109,16 @@
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
min-width: 75%;
- background: $fallback--bg;
- background: var(--bg, $fallback--bg);
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
+ background-color: $fallback--bg;
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--link;
+ color: var(--popoverText, $fallback--link);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --postLink: var(--popoverPostLink, $fallback--link);
+ --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
+ --icon: var(--popoverIcon, $fallback--icon);
}
}
@@ -157,7 +163,12 @@
&.highlighted {
background-color: $fallback--fg;
- background-color: var(--lightBg, $fallback--fg);
+ background-color: var(--selectedMenuPopover, $fallback--fg);
+ color: var(--selectedMenuPopoverText, $fallback--text);
+ --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
}
}
}
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
@@ -8,6 +8,15 @@
left: 0;
margin: 0 !important;
z-index: 1;
+ background-color: $fallback--bg;
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--link;
+ color: var(--popoverText, $fallback--link);
+ --lightText: var(--popoverLightText, $fallback--faint);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --icon: var(--popoverIcon, $fallback--icon);
.keep-open,
.too-many-emoji {
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
@@ -0,0 +1,69 @@
+import UserAvatar from '../user_avatar/user_avatar.vue'
+import Popover from '../popover/popover.vue'
+
+const EMOJI_REACTION_COUNT_CUTOFF = 12
+
+const EmojiReactions = {
+ name: 'EmojiReactions',
+ components: {
+ UserAvatar,
+ Popover
+ },
+ props: ['status'],
+ data: () => ({
+ showAll: false
+ }),
+ computed: {
+ tooManyReactions () {
+ return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
+ },
+ emojiReactions () {
+ return this.showAll
+ ? this.status.emoji_reactions
+ : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
+ },
+ showMoreString () {
+ return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
+ },
+ accountsForEmoji () {
+ return this.status.emoji_reactions.reduce((acc, reaction) => {
+ acc[reaction.name] = reaction.accounts || []
+ return acc
+ }, {})
+ },
+ loggedIn () {
+ return !!this.$store.state.users.currentUser
+ }
+ },
+ methods: {
+ toggleShowAll () {
+ this.showAll = !this.showAll
+ },
+ reactedWith (emoji) {
+ return this.status.emoji_reactions.find(r => r.name === emoji).me
+ },
+ fetchEmojiReactionsByIfMissing () {
+ const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
+ if (hasNoAccounts) {
+ this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
+ }
+ },
+ reactWith (emoji) {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ },
+ unreact (emoji) {
+ this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+ },
+ emojiOnClick (emoji, event) {
+ if (!this.loggedIn) return
+
+ if (this.reactedWith(emoji)) {
+ this.unreact(emoji)
+ } else {
+ this.reactWith(emoji)
+ }
+ }
+ }
+}
+
+export default EmojiReactions
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
@@ -0,0 +1,141 @@
+<template>
+ <div class="emoji-reactions">
+ <Popover
+ v-for="(reaction) in emojiReactions"
+ :key="reaction.name"
+ trigger="hover"
+ placement="top"
+ :offset="{ y: 5 }"
+ >
+ <div
+ slot="content"
+ class="reacted-users"
+ >
+ <div v-if="accountsForEmoji[reaction.name].length">
+ <div
+ v-for="(account) in accountsForEmoji[reaction.name]"
+ :key="account.id"
+ class="reacted-user"
+ >
+ <UserAvatar
+ :user="account"
+ class="avatar-small"
+ :compact="true"
+ />
+ <div class="reacted-user-names">
+ <!-- eslint-disable vue/no-v-html -->
+ <span
+ class="reacted-user-name"
+ v-html="account.name_html"
+ />
+ <!-- eslint-enable vue/no-v-html -->
+ <span class="reacted-user-screen-name">{{ account.screen_name }}</span>
+ </div>
+ </div>
+ </div>
+ <div v-else>
+ <i class="icon-spin4 animate-spin" />
+ </div>
+ </div>
+ <button
+ slot="trigger"
+ class="emoji-reaction btn btn-default"
+ :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
+ @click="emojiOnClick(reaction.name, $event)"
+ @mouseenter="fetchEmojiReactionsByIfMissing()"
+ >
+ <span class="reaction-emoji">{{ reaction.name }}</span>
+ <span>{{ reaction.count }}</span>
+ </button>
+ </Popover>
+ <a
+ v-if="tooManyReactions"
+ class="emoji-reaction-expand faint"
+ href="javascript:void(0)"
+ @click="toggleShowAll"
+ >
+ {{ showAll ? $t('general.show_less') : showMoreString }}
+ </a>
+ </div>
+</template>
+
+<script src="./emoji_reactions.js" ></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.emoji-reactions {
+ display: flex;
+ margin-top: 0.25em;
+ flex-wrap: wrap;
+}
+
+.reacted-users {
+ padding: 0.5em;
+}
+
+.reacted-user {
+ padding: 0.25em;
+ display: flex;
+ flex-direction: row;
+
+ .reacted-user-names {
+ display: flex;
+ flex-direction: column;
+ margin-left: 0.5em;
+ min-width: 5em;
+
+ img {
+ width: 1em;
+ height: 1em;
+ }
+ }
+
+ .reacted-user-screen-name {
+ font-size: 9px;
+ }
+}
+
+.emoji-reaction {
+ padding: 0 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ .reaction-emoji {
+ width: 1.25em;
+ margin-right: 0.25em;
+ }
+ &:focus {
+ outline: none;
+ }
+
+ &.not-clickable {
+ cursor: default;
+ &:hover {
+ box-shadow: $fallback--buttonShadow;
+ box-shadow: var(--buttonShadow);
+ }
+ }
+}
+
+.emoji-reaction-expand {
+ padding: 0 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.picked-reaction {
+ border: 1px solid var(--accent, $fallback--link);
+ margin-left: -1px; // offset the border, can't use inset shadows either
+ margin-right: calc(0.5em - 1px);
+}
+
+</style>
diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue
@@ -42,7 +42,7 @@ export default {
},
methods: {
exportData () {
- const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces
+ const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
@@ -1,5 +1,8 @@
+import Popover from '../popover/popover.vue'
+
const ExtraButtons = {
props: [ 'status' ],
+ components: { Popover },
methods: {
deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm'))
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
@@ -1,11 +1,11 @@
<template>
- <v-popover
+ <Popover
v-if="canDelete || canMute || canPin"
trigger="click"
placement="top"
class="extra-button-popover"
>
- <div slot="popover">
+ <div slot="content">
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
@@ -47,17 +47,17 @@
</button>
</div>
</div>
- <div class="button-icon">
- <i class="icon-ellipsis" />
- </div>
- </v-popover>
+ <i
+ slot="trigger"
+ class="icon-ellipsis button-icon"
+ />
+ </Popover>
</template>
<script src="./extra_buttons.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
-@import '../popper/popper.scss';
.icon-ellipsis {
cursor: pointer;
diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue
@@ -1,7 +1,7 @@
<template>
<button
class="btn btn-default follow-button"
- :class="{ pressed: isPressed }"
+ :class="{ toggled: isPressed }"
:disabled="inProgress"
:title="title"
@click="onClick"
diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js
@@ -10,6 +10,7 @@ const tabModeDict = {
const Interactions = {
data () {
return {
+ allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict['mentions']
}
},
diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue
@@ -22,6 +22,7 @@
:label="$t('interactions.follows')"
/>
<span
+ v-if="!allowFollowingMove"
key="moves"
:label="$t('interactions.moves')"
/>
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
@@ -1,4 +1,5 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
+import Popover from '../popover/popover.vue'
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip'
@@ -14,7 +15,6 @@ const ModerationTools = {
],
data () {
return {
- showDropDown: false,
tags: {
FORCE_NSFW,
STRIP_MEDIA,
@@ -24,11 +24,13 @@ const ModerationTools = {
SANDBOX,
QUARANTINE
},
- showDeleteUserDialog: false
+ showDeleteUserDialog: false,
+ toggled: false
}
},
components: {
- DialogModal
+ DialogModal,
+ Popover
},
computed: {
tagsSet () {
@@ -89,6 +91,9 @@ const ModerationTools = {
window.history.back()
}
})
+ },
+ setToggled (value) {
+ this.toggled = value
}
}
}
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
@@ -1,13 +1,14 @@
<template>
<div>
- <v-popover
+ <Popover
trigger="click"
class="moderation-tools-popover"
- placement="bottom-end"
- @show="showDropDown = true"
- @hide="showDropDown = false"
+ placement="bottom"
+ :offset="{ y: 5 }"
+ @show="setToggled(true)"
+ @close="setToggled(false)"
>
- <div slot="popover">
+ <div slot="content">
<div class="dropdown-menu">
<span v-if="user.is_local">
<button
@@ -122,12 +123,13 @@
</div>
</div>
<button
+ slot="trigger"
class="btn btn-default btn-block"
- :class="{ pressed: showDropDown }"
+ :class="{ toggled }"
>
{{ $t('user_card.admin_menu.moderation') }}
</button>
- </v-popover>
+ </Popover>
<portal to="modal">
<DialogModal
v-if="showDeleteUserDialog"
@@ -160,7 +162,6 @@
<style lang="scss">
@import '../../_variables.scss';
-@import '../popper/popper.scss';
.menu-checkbox {
float: right;
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js
@@ -11,7 +11,10 @@ const MRFTransparencyPanel = {
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
- mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', [])
+ mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
+ keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
+ keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
+ keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])
}),
hasInstanceSpecificPolicies () {
return this.quarantineInstances.length ||
@@ -20,6 +23,11 @@ const MRFTransparencyPanel = {
this.ftlRemovalInstances.length ||
this.mediaNsfwInstances.length ||
this.mediaRemovalInstances.length
+ },
+ hasKeywordPolicies () {
+ return this.keywordsFtlRemoval.length ||
+ this.keywordsReject.length ||
+ this.keywordsReplace.length
}
}
}
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
@@ -6,13 +6,13 @@
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background">
<div class="title">
- {{ $t("about.federation") }}
+ {{ $t("about.mrf.federation") }}
</div>
</div>
<div class="panel-body">
<div class="mrf-section">
- <h2>{{ $t("about.mrf_policies") }}</h2>
- <p>{{ $t("about.mrf_policies_desc") }}</p>
+ <h2>{{ $t("about.mrf.mrf_policies") }}</h2>
+ <p>{{ $t("about.mrf.mrf_policies_desc") }}</p>
<ul>
<li
@@ -23,13 +23,13 @@
</ul>
<h2 v-if="hasInstanceSpecificPolicies">
- {{ $t("about.mrf_policy_simple") }}
+ {{ $t("about.mrf.simple.simple_policies") }}
</h2>
<div v-if="acceptInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>
+ <h4>{{ $t("about.mrf.simple.accept") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_accept_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.accept_desc") }}</p>
<ul>
<li
@@ -41,9 +41,9 @@
</div>
<div v-if="rejectInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_reject") }}</h4>
+ <h4>{{ $t("about.mrf.simple.reject") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_reject_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.reject_desc") }}</p>
<ul>
<li
@@ -55,9 +55,9 @@
</div>
<div v-if="quarantineInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_quarantine") }}</h4>
+ <h4>{{ $t("about.mrf.simple.quarantine") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_quarantine_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
<ul>
<li
@@ -69,9 +69,9 @@
</div>
<div v-if="ftlRemovalInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_ftl_removal") }}</h4>
+ <h4>{{ $t("about.mrf.simple.ftl_removal") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_ftl_removal_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
<ul>
<li
@@ -83,9 +83,9 @@
</div>
<div v-if="mediaNsfwInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_media_nsfw") }}</h4>
+ <h4>{{ $t("about.mrf.simple.media_nsfw") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_media_nsfw_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
<ul>
<li
@@ -97,9 +97,9 @@
</div>
<div v-if="mediaRemovalInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_media_removal") }}</h4>
+ <h4>{{ $t("about.mrf.simple.media_removal") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_media_removal_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
<ul>
<li
@@ -109,6 +109,49 @@
/>
</ul>
</div>
+
+ <h2 v-if="hasKeywordPolicies">
+ {{ $t("about.mrf.keyword.keyword_policies") }}
+ </h2>
+
+ <div v-if="keywordsFtlRemoval.length">
+ <h4>{{ $t("about.mrf.keyword.ftl_removal") }}</h4>
+
+ <ul>
+ <li
+ v-for="keyword in keywordsFtlRemoval"
+ :key="keyword"
+ v-text="keyword"
+ />
+ </ul>
+ </div>
+
+ <div v-if="keywordsReject.length">
+ <h4>{{ $t("about.mrf.keyword.reject") }}</h4>
+
+ <ul>
+ <li
+ v-for="keyword in keywordsReject"
+ :key="keyword"
+ v-text="keyword"
+ />
+ </ul>
+ </div>
+
+ <div v-if="keywordsReplace.length">
+ <h4>{{ $t("about.mrf.keyword.replace") }}</h4>
+
+ <ul>
+ <li
+ v-for="keyword in keywordsReplace"
+ :key="keyword"
+ >
+ {{ keyword.pattern }}
+ {{ $t("about.mrf.keyword.is_replaced_by") }}
+ {{ keyword.replacement }}
+ </li>
+ </ul>
+ </div>
</div>
</div>
</div>
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
@@ -3,7 +3,7 @@ import { mapState } from 'vuex'
const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
- this.$store.dispatch('startFetchingFollowRequest')
+ this.$store.dispatch('startFetchingFollowRequests')
}
},
computed: mapState({
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
@@ -33,7 +33,7 @@
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link>
</li>
- <li v-if="federating && !privateMode">
+ <li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }">
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link>
@@ -100,13 +100,25 @@
&:hover {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: $fallback--link;
+ color: var(--selectedMenuText, $fallback--link);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
}
&.router-link-active {
font-weight: bolder;
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedMenuText, $fallback--text);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
&:hover {
text-decoration: underline;
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
@@ -78,6 +78,13 @@
<i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
+ <span v-if="notification.type === 'pleroma:emoji_reaction'">
+ <small>
+ <i18n path="notifications.reacted_with">
+ <span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
+ </i18n>
+ </small>
+ </span>
</div>
<div
v-if="notification.type === 'follow' || notification.type === 'move'"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
@@ -68,6 +68,9 @@
a {
color: var(--faintLink);
}
+ .status-content a {
+ color: var(--postFaintLink);
+ }
}
padding: 0;
.media-body {
@@ -94,6 +97,10 @@
min-width: 0;
}
+ .emoji-reaction-emoji {
+ font-size: 16px;
+ }
+
.notification-details {
min-width: 0px;
word-wrap: break-word;
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
@@ -9,18 +9,12 @@
>
{{ $t('settings.style.common.opacity') }}
</label>
- <input
+ <Checkbox
v-if="typeof fallback !== 'undefined'"
- :id="name + '-o'"
- class="opt exclude-disabled"
- type="checkbox"
:checked="present"
- @input="$emit('input', !present ? fallback : undefined)"
- >
- <label
- v-if="typeof fallback !== 'undefined'"
- class="opt-l"
- :for="name + '-o'"
+ :disabled="disabled"
+ class="opt"
+ @change="$emit('input', !present ? fallback : undefined)"
/>
<input
:id="name"
@@ -37,7 +31,11 @@
</template>
<script>
+import Checkbox from '../checkbox/checkbox.vue'
export default {
+ components: {
+ Checkbox
+ },
props: [
'name', 'value', 'fallback', 'disabled'
],
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
@@ -104,8 +104,10 @@
.result-fill {
height: 100%;
position: absolute;
+ color: $fallback--text;
+ color: var(--pollText, $fallback--text);
background-color: $fallback--lightBg;
- background-color: var(--linkBg, $fallback--lightBg);
+ background-color: var(--poll, $fallback--lightBg);
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
top: 0;
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
@@ -0,0 +1,156 @@
+
+const Popover = {
+ name: 'Popover',
+ props: {
+ // Action to trigger popover: either 'hover' or 'click'
+ trigger: String,
+ // Either 'top' or 'bottom'
+ placement: String,
+ // Takes object with properties 'x' and 'y', values of these can be
+ // 'container' for using offsetParent as boundaries for either axis
+ // or 'viewport'
+ boundTo: Object,
+ // Takes a top/bottom/left/right object, how much space to leave
+ // between boundary and popover element
+ margin: Object,
+ // Takes a x/y object and tells how many pixels to offset from
+ // anchor point on either axis
+ offset: Object,
+ // Additional styles you may want for the popover container
+ popoverClass: String
+ },
+ data () {
+ return {
+ hidden: true,
+ styles: { opacity: 0 },
+ oldSize: { width: 0, height: 0 }
+ }
+ },
+ methods: {
+ updateStyles () {
+ if (this.hidden) {
+ this.styles = {
+ opacity: 0
+ }
+ return
+ }
+
+ // Popover will be anchored around this element, trigger ref is the container, so
+ // its children are what are inside the slot. Expect only one slot="trigger".
+ const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
+ const screenBox = anchorEl.getBoundingClientRect()
+ // Screen position of the origin point for popover
+ const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
+ const content = this.$refs.content
+ // Minor optimization, don't call a slow reflow call if we don't have to
+ const parentBounds = this.boundTo &&
+ (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
+ this.$el.offsetParent.getBoundingClientRect()
+ const margin = this.margin || {}
+
+ // What are the screen bounds for the popover? Viewport vs container
+ // when using viewport, using default margin values to dodge the navbar
+ const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
+ min: parentBounds.left + (margin.left || 0),
+ max: parentBounds.right - (margin.right || 0)
+ } : {
+ min: 0 + (margin.left || 10),
+ max: window.innerWidth - (margin.right || 10)
+ }
+
+ const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
+ min: parentBounds.top + (margin.top || 0),
+ max: parentBounds.bottom - (margin.bottom || 0)
+ } : {
+ min: 0 + (margin.top || 50),
+ max: window.innerHeight - (margin.bottom || 5)
+ }
+
+ let horizOffset = 0
+
+ // If overflowing from left, move it so that it doesn't
+ if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
+ horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
+ }
+
+ // If overflowing from right, move it so that it doesn't
+ if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
+ horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
+ }
+
+ // Default to whatever user wished with placement prop
+ let usingTop = this.placement !== 'bottom'
+
+ // Handle special cases, first force to displaying on top if there's not space on bottom,
+ // regardless of what placement value was. Then check if there's not space on top, and
+ // force to bottom, again regardless of what placement value was.
+ if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
+ if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
+
+ const yOffset = (this.offset && this.offset.y) || 0
+ const translateY = usingTop
+ ? -anchorEl.offsetHeight - yOffset - content.offsetHeight
+ : yOffset
+
+ const xOffset = (this.offset && this.offset.x) || 0
+ const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
+
+ // Note, separate translateX and translateY avoids blurry text on chromium,
+ // single translate or translate3d resulted in blurry text.
+ this.styles = {
+ opacity: 1,
+ transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`
+ }
+ },
+ showPopover () {
+ if (this.hidden) this.$emit('show')
+ this.hidden = false
+ this.$nextTick(this.updateStyles)
+ },
+ hidePopover () {
+ if (!this.hidden) this.$emit('close')
+ this.hidden = true
+ this.styles = { opacity: 0 }
+ },
+ onMouseenter (e) {
+ if (this.trigger === 'hover') this.showPopover()
+ },
+ onMouseleave (e) {
+ if (this.trigger === 'hover') this.hidePopover()
+ },
+ onClick (e) {
+ if (this.trigger === 'click') {
+ if (this.hidden) {
+ this.showPopover()
+ } else {
+ this.hidePopover()
+ }
+ }
+ },
+ onClickOutside (e) {
+ if (this.hidden) return
+ if (this.$el.contains(e.target)) return
+ this.hidePopover()
+ }
+ },
+ updated () {
+ // Monitor changes to content size, update styles only when content sizes have changed,
+ // that should be the only time we need to move the popover box if we don't care about scroll
+ // or resize
+ const content = this.$refs.content
+ if (!content) return
+ if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
+ this.updateStyles()
+ this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
+ }
+ },
+ created () {
+ document.addEventListener('click', this.onClickOutside)
+ },
+ destroyed () {
+ document.removeEventListener('click', this.onClickOutside)
+ this.hidePopover()
+ }
+}
+
+export default Popover
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
@@ -0,0 +1,118 @@
+<template>
+ <div
+ @mouseenter="onMouseenter"
+ @mouseleave="onMouseleave"
+ >
+ <div
+ ref="trigger"
+ @click="onClick"
+ >
+ <slot name="trigger" />
+ </div>
+ <div
+ v-if="!hidden"
+ ref="content"
+ :style="styles"
+ class="popover"
+ :class="popoverClass"
+ >
+ <slot
+ name="content"
+ class="popover-inner"
+ :close="hidePopover"
+ />
+ </div>
+ </div>
+</template>
+
+<script src="./popover.js" />
+
+<style lang=scss>
+@import '../../_variables.scss';
+
+.popover {
+ z-index: 8;
+ position: absolute;
+ min-width: 0;
+ transition: opacity 0.3s;
+
+ box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+ box-shadow: var(--panelShadow);
+ border-radius: $fallback--btnRadius;
+ border-radius: var(--btnRadius, $fallback--btnRadius);
+
+ background-color: $fallback--bg;
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--text;
+ color: var(--popoverText, $fallback--text);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --postLink: var(--popoverPostLink, $fallback--link);
+ --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
+ --icon: var(--popoverIcon, $fallback--icon);
+}
+
+.dropdown-menu {
+ display: block;
+ padding: .5rem 0;
+ font-size: 1rem;
+ text-align: left;
+ list-style: none;
+ max-width: 100vw;
+ z-index: 10;
+ white-space: nowrap;
+
+ .dropdown-divider {
+ height: 0;
+ margin: .5rem 0;
+ overflow: hidden;
+ border-top: 1px solid $fallback--border;
+ border-top: 1px solid var(--border, $fallback--border);
+ }
+
+ .dropdown-item {
+ line-height: 21px;
+ margin-right: 5px;
+ overflow: auto;
+ display: block;
+ padding: .25rem 1.0rem .25rem 1.5rem;
+ clear: both;
+ font-weight: 400;
+ text-align: inherit;
+ white-space: nowrap;
+ border: none;
+ border-radius: 0px;
+ background-color: transparent;
+ box-shadow: none;
+ width: 100%;
+ height: 100%;
+
+ --btnText: var(--popoverText, $fallback--text);
+
+ &-icon {
+ padding-left: 0.5rem;
+
+ i {
+ margin-right: 0.25rem;
+ color: var(--menuPopoverIcon, $fallback--icon)
+ }
+ }
+
+ &:active, &:hover {
+ background-color: $fallback--lightBg;
+ background-color: var(--selectedMenuPopover, $fallback--lightBg);
+ color: $fallback--link;
+ color: var(--selectedMenuPopoverText, $fallback--link);
+ --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
+ i {
+ color: var(--selectedMenuPopoverIcon, $fallback--icon);
+ }
+ }
+
+ }
+}
+</style>
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
@@ -1,147 +0,0 @@
-@import '../../_variables.scss';
-
-.tooltip.popover {
- z-index: 8;
-
- .popover-inner {
- box-shadow: 1px 1px 4px rgba(0,0,0,.6);
- box-shadow: var(--panelShadow);
- border-radius: $fallback--btnRadius;
- border-radius: var(--btnRadius, $fallback--btnRadius);
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
- }
-
- .popover-arrow {
- width: 0;
- height: 0;
- border-style: solid;
- position: absolute;
- margin: 5px;
- border-color: $fallback--bg;
- border-color: var(--bg, $fallback--bg);
- }
-
- &[x-placement^="top"] {
- margin-bottom: 5px;
-
- .popover-arrow {
- border-width: 5px 5px 0 5px;
- border-left-color: transparent !important;
- border-right-color: transparent !important;
- border-bottom-color: transparent !important;
- bottom: -4px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
- }
- }
-
- &[x-placement^="bottom"] {
- margin-top: 5px;
-
- .popover-arrow {
- border-width: 0 5px 5px 5px;
- border-left-color: transparent !important;
- border-right-color: transparent !important;
- border-top-color: transparent !important;
- top: -4px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
- }
- }
-
- &[x-placement^="right"] {
- margin-left: 5px;
-
- .popover-arrow {
- border-width: 5px 5px 5px 0;
- border-left-color: transparent !important;
- border-top-color: transparent !important;
- border-bottom-color: transparent !important;
- left: -4px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
- }
- }
-
- &[x-placement^="left"] {
- margin-right: 5px;
-
- .popover-arrow {
- border-width: 5px 0 5px 5px;
- border-top-color: transparent !important;
- border-right-color: transparent !important;
- border-bottom-color: transparent !important;
- right: -4px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
- }
- }
-
- &[aria-hidden='true'] {
- visibility: hidden;
- opacity: 0;
- transition: opacity .15s, visibility .15s;
- }
-
- &[aria-hidden='false'] {
- visibility: visible;
- opacity: 1;
- transition: opacity .15s;
- }
-}
-
-.dropdown-menu {
- display: block;
- padding: .5rem 0;
- font-size: 1rem;
- text-align: left;
- list-style: none;
- max-width: 100vw;
- z-index: 10;
-
- .dropdown-divider {
- height: 0;
- margin: .5rem 0;
- overflow: hidden;
- border-top: 1px solid $fallback--border;
- border-top: 1px solid var(--border, $fallback--border);
- }
-
- .dropdown-item {
- line-height: 21px;
- margin-right: 5px;
- overflow: auto;
- display: block;
- padding: .25rem 1.0rem .25rem 1.5rem;
- clear: both;
- font-weight: 400;
- text-align: inherit;
- white-space: normal;
- border: none;
- border-radius: 0px;
- background-color: transparent;
- box-shadow: none;
- width: 100%;
- height: 100%;
-
- &-icon {
- padding-left: 0.5rem;
-
- i {
- margin-right: 0.25rem;
- }
- }
-
- &:hover {
- // TODO: improve the look on breeze themes
- background-color: $fallback--fg;
- background-color: var(--btn, $fallback--fg);
- box-shadow: none;
- }
- }
-}
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
@@ -12,7 +12,7 @@
<input
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
- class="opt exclude-disabled"
+ class="opt"
type="checkbox"
:checked="present"
@input="$emit('input', !present ? fallback : undefined)"
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
@@ -0,0 +1,39 @@
+import Popover from '../popover/popover.vue'
+import { mapGetters } from 'vuex'
+
+const ReactButton = {
+ props: ['status', 'loggedIn'],
+ data () {
+ return {
+ filterWord: ''
+ }
+ },
+ components: {
+ Popover
+ },
+ methods: {
+ addReaction (event, emoji, close) {
+ const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
+ if (existingReaction && existingReaction.me) {
+ this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+ } else {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ }
+ close()
+ }
+ },
+ computed: {
+ commonEmojis () {
+ return ['❤️', '😠', '👀', '😂', '🔥']
+ },
+ emojis () {
+ if (this.filterWord !== '') {
+ return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
+ }
+ return this.$store.state.instance.emoji || []
+ },
+ ...mapGetters(['mergedConfig'])
+ }
+}
+
+export default ReactButton
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
@@ -0,0 +1,111 @@
+<template>
+ <Popover
+ trigger="click"
+ placement="top"
+ :offset="{ y: 5 }"
+ class="react-button-popover"
+ >
+ <div
+ slot="content"
+ slot-scope="{close}"
+ >
+ <div class="reaction-picker-filter">
+ <input
+ v-model="filterWord"
+ :placeholder="$t('emoji.search_emoji')"
+ >
+ </div>
+ <div class="reaction-picker">
+ <span
+ v-for="emoji in commonEmojis"
+ :key="emoji"
+ class="emoji-button"
+ @click="addReaction($event, emoji, close)"
+ >
+ {{ emoji }}
+ </span>
+ <div class="reaction-picker-divider" />
+ <span
+ v-for="(emoji, key) in emojis"
+ :key="key"
+ class="emoji-button"
+ @click="addReaction($event, emoji.replacement, close)"
+ >
+ {{ emoji.replacement }}
+ </span>
+ <div class="reaction-bottom-fader" />
+ </div>
+ </div>
+ <i
+ v-if="loggedIn"
+ slot="trigger"
+ class="icon-smile button-icon add-reaction-button"
+ :title="$t('tool_tip.add_reaction')"
+ />
+ </Popover>
+</template>
+
+<script src="./react_button.js" ></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.reaction-picker-filter {
+ padding: 0.5em;
+ display: flex;
+ input {
+ flex: 1;
+ }
+}
+
+.reaction-picker-divider {
+ height: 1px;
+ width: 100%;
+ margin: 0.5em;
+ background-color: var(--border, $fallback--border);
+}
+
+.reaction-picker {
+ width: 10em;
+ height: 9em;
+ font-size: 1.5em;
+ overflow-y: scroll;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0.5em;
+ text-align: center;
+ align-content: flex-start;
+ user-select: none;
+
+ mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
+ linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
+ linear-gradient(to top, white, white);
+ transition: mask-size 150ms;
+ mask-size: 100% 20px, 100% 20px, auto;
+ // Autoprefixed seem to ignore this one, and also syntax is different
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+
+ .emoji-button {
+ cursor: pointer;
+
+ flex-basis: 20%;
+ line-height: 1.5em;
+ align-content: center;
+
+ &:hover {
+ transform: scale(1.25);
+ }
+ }
+}
+
+.add-reaction-button {
+ cursor: pointer;
+
+ &:hover {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+}
+
+</style>
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
@@ -68,7 +68,12 @@
&-item-selected-inner {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: var(--selectedMenuText, $fallback--text);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
}
&-header {
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
@@ -76,9 +76,9 @@
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
- <br/>
+ <br>
<small>
- {{ $t('settings.useStreamingApiWarning') }}
+ {{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
@@ -92,6 +92,11 @@
{{ $t('settings.reply_link_preview') }}
</Checkbox>
</li>
+ <li>
+ <Checkbox v-model="emojiReactionsOnTimeline">
+ {{ $t('settings.emoji_reactions_on_timeline') }}
+ </Checkbox>
+ </li>
</ul>
</div>
@@ -328,6 +333,11 @@
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</li>
+ <li>
+ <Checkbox v-model="notificationVisibility.emojiReactions">
+ {{ $t('settings.notification_visibility_emoji_reactions') }}
+ </Checkbox>
+ </li>
</ul>
</div>
<div>
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
@@ -3,6 +3,17 @@ import OpacityInput from '../opacity_input/opacity_input.vue'
import { getCssShadow } from '../../services/style_setter/style_setter.js'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
+const toModel = (object = {}) => ({
+ x: 0,
+ y: 0,
+ blur: 0,
+ spread: 0,
+ inset: false,
+ color: '#000000',
+ alpha: 1,
+ ...object
+})
+
export default {
// 'Value' and 'Fallback' can be undefined, but if they are
// initially vue won't detect it when they become something else
@@ -15,7 +26,7 @@ export default {
return {
selectedId: 0,
// TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason)
- cValue: this.value || this.fallback || []
+ cValue: (this.value || this.fallback || []).map(toModel)
}
},
components: {
@@ -24,12 +35,12 @@ export default {
},
methods: {
add () {
- this.cValue.push(Object.assign({}, this.selected))
+ this.cValue.push(toModel(this.selected))
this.selectedId = this.cValue.length - 1
},
del () {
this.cValue.splice(this.selectedId, 1)
- this.selectedId = this.cValue.length === 0 ? undefined : this.selectedId - 1
+ this.selectedId = this.cValue.length === 0 ? undefined : Math.max(this.selectedId - 1, 0)
},
moveUp () {
const movable = this.cValue.splice(this.selectedId, 1)[0]
@@ -46,19 +57,24 @@ export default {
this.cValue = this.value || this.fallback
},
computed: {
+ anyShadows () {
+ return this.cValue.length > 0
+ },
+ anyShadowsFallback () {
+ return this.fallback.length > 0
+ },
selected () {
- if (this.ready && this.cValue.length > 0) {
+ if (this.ready && this.anyShadows) {
return this.cValue[this.selectedId]
} else {
- return {
- x: 0,
- y: 0,
- blur: 0,
- spread: 0,
- inset: false,
- color: '#000000',
- alpha: 1
- }
+ return toModel({})
+ }
+ },
+ currentFallback () {
+ if (this.ready && this.anyShadowsFallback) {
+ return this.fallback[this.selectedId]
+ } else {
+ return toModel({})
}
},
moveUpValid () {
@@ -80,7 +96,7 @@ export default {
},
style () {
return this.ready ? {
- boxShadow: getCssShadow(this.cValue)
+ boxShadow: getCssShadow(this.fallback)
} : {}
}
}
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
@@ -191,15 +191,20 @@
v-model="selected.color"
:disabled="!present"
:label="$t('settings.style.common.color')"
+ :fallback="currentFallback.color"
+ :show-optional-tickbox="false"
name="shadow"
/>
<OpacityInput
v-model="selected.alpha"
:disabled="!present"
/>
- <p>
- {{ $t('settings.style.shadows.hint') }}
- </p>
+ <i18n
+ path="settings.style.shadows.hintV3"
+ tag="p"
+ >
+ <code>--variable,mod</code>
+ </i18n>
</div>
</div>
</template>
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
@@ -12,7 +12,7 @@ const SideDrawer = {
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
if (this.currentUser && this.currentUser.locked) {
- this.$store.dispatch('startFetchingFollowRequest')
+ this.$store.dispatch('startFetchingFollowRequests')
}
},
components: { UserCard },
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
@@ -88,7 +88,7 @@
</router-link>
</li>
<li
- v-if="federating && !privateMode"
+ v-if="federating && (currentUser || !privateMode)"
@click="toggleDrawer"
>
<router-link to="/main/all">
@@ -223,7 +223,13 @@
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--link;
+ color: var(--popoverText, $fallback--link);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --icon: var(--popoverIcon, $fallback--icon);
.button-icon:before {
width: 1.1em;
@@ -289,7 +295,13 @@
&:hover {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenuPopover, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedMenuPopoverText, $fallback--text);
+ --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
}
}
}
diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js
@@ -1,3 +1,4 @@
+import map from 'lodash/map'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = {
@@ -6,7 +7,7 @@ const StaffPanel = {
},
computed: {
staffAccounts () {
- return this.$store.state.instance.staffAccounts
+ return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
}
}
}
diff --git a/src/components/status/status.js b/src/components/status/status.js
@@ -1,5 +1,6 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
+import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
@@ -11,6 +12,7 @@ import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
+import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
@@ -254,6 +256,16 @@ const Status = {
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
+ hasImageAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'image'
+ )
+ },
+ hasVideoAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'video'
+ )
+ },
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
@@ -319,6 +331,7 @@ const Status = {
components: {
Attachment,
FavoriteButton,
+ ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm,
@@ -329,7 +342,8 @@ const Status = {
LinkPreview,
AvatarList,
Timeago,
- StatusPopover
+ StatusPopover,
+ EmojiReactions
},
methods: {
visibilityIcon (visibility) {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
@@ -177,6 +177,8 @@
<StatusPopover
v-if="!isPreview"
:status-id="status.in_reply_to_status_id"
+ class="reply-to-popover"
+ style="min-width: 0"
>
<a
class="reply-to"
@@ -277,7 +279,21 @@
href="#"
class="cw-status-hider"
@click.prevent="toggleShowMore"
- >{{ $t("general.show_more") }}</a>
+ >
+ {{ $t("general.show_more") }}
+ <span
+ v-if="hasImageAttachments"
+ class="icon-picture"
+ />
+ <span
+ v-if="hasVideoAttachments"
+ class="icon-video"
+ />
+ <span
+ v-if="status.card"
+ class="icon-link"
+ />
+ </a>
<a
v-if="showingMore"
href="#"
@@ -354,6 +370,11 @@
</div>
</transition>
+ <EmojiReactions
+ v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
+ :status="status"
+ />
+
<div
v-if="!noHeading && !isPreview"
class="status-actions media-body"
@@ -382,6 +403,10 @@
:logged-in="loggedIn"
:status="status"
/>
+ <ReactButton
+ :logged-in="loggedIn"
+ :status="status"
+ />
<extra-buttons
:status="status"
@onError="showError"
@@ -445,7 +470,15 @@ $status-margin: 0.75em;
&_focused {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedPost, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedPostText, $fallback--text);
+ --lightText: var(--selectedPostLightText, $fallback--light);
+ --faint: var(--selectedPostFaintText, $fallback--faint);
+ --faintLink: var(--selectedPostFaintLink, $fallback--faint);
+ --postLink: var(--selectedPostPostLink, $fallback--faint);
+ --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
+ --icon: var(--selectedPostIcon, $fallback--icon);
}
.timeline & {
@@ -541,11 +574,10 @@ $status-margin: 0.75em;
align-items: stretch;
> .reply-to-and-accountname > a {
+ overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
- overflow: hidden;
white-space: nowrap;
- display: inline-block;
word-break: break-all;
}
}
@@ -554,7 +586,6 @@ $status-margin: 0.75em;
display: flex;
height: 18px;
margin-right: 0.5em;
- overflow: hidden;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
@@ -565,6 +596,10 @@ $status-margin: 0.75em;
display: flex;
}
+ .reply-to-popover {
+ min-width: 0;
+ }
+
.reply-to {
display: flex;
}
@@ -572,9 +607,8 @@ $status-margin: 0.75em;
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
margin: 0 0.4em 0 0.2em;
- color: $fallback--faint;
- color: var(--faint, $fallback--faint);
}
.replies-separator {
@@ -636,6 +670,11 @@ $status-margin: 0.75em;
line-height: 1.4em;
white-space: pre-wrap;
+ a {
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
+ }
+
img, video {
max-width: 100%;
max-height: 400px;
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
@@ -5,22 +5,14 @@ const StatusPopover = {
props: [
'statusId'
],
- data () {
- return {
- popperOptions: {
- modifiers: {
- preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
- }
- }
- }
- },
computed: {
status () {
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
}
},
components: {
- Status: () => import('../status/status.vue')
+ Status: () => import('../status/status.vue'),
+ Popover: () => import('../popover/popover.vue')
},
methods: {
enter () {
diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue
@@ -1,11 +1,16 @@
<template>
- <v-popover
+ <Popover
+ trigger="hover"
popover-class="status-popover"
- placement="top-start"
- :popper-options="popperOptions"
- @show="enter()"
+ :bound-to="{ x: 'container' }"
+ @show="enter"
>
- <template slot="popover">
+ <template slot="trigger">
+ <slot />
+ </template>
+ <div
+ slot="content"
+ >
<Status
v-if="status"
:is-preview="true"
@@ -18,10 +23,8 @@
>
<i class="icon-spin4 animate-spin" />
</div>
- </template>
-
- <slot />
- </v-popover>
+ </div>
+ </Popover>
</template>
<script src="./status_popover.js" ></script>
@@ -29,44 +32,19 @@
<style lang="scss">
@import '../../_variables.scss';
-.tooltip.popover.status-popover {
+.status-popover {
font-size: 1rem;
min-width: 15em;
max-width: 95%;
- margin-left: 0.5em;
-
- .popover-inner {
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- border-style: solid;
- border-width: 1px;
- border-radius: $fallback--tooltipRadius;
- border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
- box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
- box-shadow: var(--popupShadow);
- }
- .popover-arrow::before {
- position: absolute;
- content: '';
- left: -7px;
- border: solid 7px transparent;
- z-index: -1;
- }
-
- &[x-placement^="bottom-start"] .popover-arrow::before {
- top: -2px;
- border-top-width: 0;
- border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
- }
-
- &[x-placement^="top-start"] .popover-arrow::before {
- bottom: -2px;
- border-bottom-width: 0;
- border-top-color: $fallback--border;
- border-top-color: var(--border, $fallback--border);
- }
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ border-style: solid;
+ border-width: 1px;
+ border-radius: $fallback--tooltipRadius;
+ border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
+ box-shadow: var(--popupShadow);
.status-el.status-el {
border: none;
diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue
@@ -51,7 +51,7 @@
img {
height: 100%;
&:hover {
- filter: drop-shadow(0 0 5px var(--link, $fallback--link));
+ filter: drop-shadow(0 0 5px var(--accent, $fallback--link));
}
}
}
diff --git a/src/components/style_switcher/preview.vue b/src/components/style_switcher/preview.vue
@@ -1,101 +1,117 @@
<template>
- <div class="panel dummy">
- <div class="panel-heading">
- <div class="title">
- {{ $t('settings.style.preview.header') }}
- <span class="badge badge-notification">
- 99
+ <div class="preview-container">
+ <div class="underlay underlay-preview" />
+ <div class="panel dummy">
+ <div class="panel-heading">
+ <div class="title">
+ {{ $t('settings.style.preview.header') }}
+ <span class="badge badge-notification">
+ 99
+ </span>
+ </div>
+ <span class="faint">
+ {{ $t('settings.style.preview.header_faint') }}
+ </span>
+ <span class="alert error">
+ {{ $t('settings.style.preview.error') }}
</span>
+ <button class="btn">
+ {{ $t('settings.style.preview.button') }}
+ </button>
</div>
- <span class="faint">
- {{ $t('settings.style.preview.header_faint') }}
- </span>
- <span class="alert error">
- {{ $t('settings.style.preview.error') }}
- </span>
- <button class="btn">
- {{ $t('settings.style.preview.button') }}
- </button>
- </div>
- <div class="panel-body theme-preview-content">
- <div class="post">
- <div class="avatar">
- ( ͡° ͜ʖ ͡°)
- </div>
- <div class="content">
- <h4>
- {{ $t('settings.style.preview.content') }}
- </h4>
+ <div class="panel-body theme-preview-content">
+ <div class="post">
+ <div class="avatar still-image">
+ ( ͡° ͜ʖ ͡°)
+ </div>
+ <div class="content">
+ <h4>
+ {{ $t('settings.style.preview.content') }}
+ </h4>
- <i18n path="settings.style.preview.text">
- <code style="font-family: var(--postCodeFont)">
- {{ $t('settings.style.preview.mono') }}
- </code>
- <a style="color: var(--link)">
- {{ $t('settings.style.preview.link') }}
- </a>
- </i18n>
+ <i18n path="settings.style.preview.text">
+ <code style="font-family: var(--postCodeFont)">
+ {{ $t('settings.style.preview.mono') }}
+ </code>
+ <a style="color: var(--link)">
+ {{ $t('settings.style.preview.link') }}
+ </a>
+ </i18n>
- <div class="icons">
- <i
- style="color: var(--cBlue)"
- class="button-icon icon-reply"
- />
- <i
- style="color: var(--cGreen)"
- class="button-icon icon-retweet"
- />
- <i
- style="color: var(--cOrange)"
- class="button-icon icon-star"
- />
- <i
- style="color: var(--cRed)"
- class="button-icon icon-cancel"
- />
+ <div class="icons">
+ <i
+ style="color: var(--cBlue)"
+ class="button-icon icon-reply"
+ />
+ <i
+ style="color: var(--cGreen)"
+ class="button-icon icon-retweet"
+ />
+ <i
+ style="color: var(--cOrange)"
+ class="button-icon icon-star"
+ />
+ <i
+ style="color: var(--cRed)"
+ class="button-icon icon-cancel"
+ />
+ </div>
</div>
</div>
- </div>
- <div class="after-post">
- <div class="avatar-alt">
- :^)
- </div>
- <div class="content">
- <i18n
- path="settings.style.preview.fine_print"
- tag="span"
- class="faint"
- >
- <a style="color: var(--faintLink)">
- {{ $t('settings.style.preview.faint_link') }}
- </a>
- </i18n>
+ <div class="after-post">
+ <div class="avatar-alt">
+ :^)
+ </div>
+ <div class="content">
+ <i18n
+ path="settings.style.preview.fine_print"
+ tag="span"
+ class="faint"
+ >
+ <a style="color: var(--faintLink)">
+ {{ $t('settings.style.preview.faint_link') }}
+ </a>
+ </i18n>
+ </div>
</div>
- </div>
- <div class="separator" />
+ <div class="separator" />
- <span class="alert error">
- {{ $t('settings.style.preview.error') }}
- </span>
- <input
- :value="$t('settings.style.preview.input')"
- type="text"
- >
-
- <div class="actions">
- <span class="checkbox">
- <input
- id="preview_checkbox"
- checked="very yes"
- type="checkbox"
- >
- <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
+ <span class="alert error">
+ {{ $t('settings.style.preview.error') }}
</span>
- <button class="btn">
- {{ $t('settings.style.preview.button') }}
- </button>
+ <input
+ :value="$t('settings.style.preview.input')"
+ type="text"
+ >
+
+ <div class="actions">
+ <span class="checkbox">
+ <input
+ id="preview_checkbox"
+ checked="very yes"
+ type="checkbox"
+ >
+ <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
+ </span>
+ <button class="btn">
+ {{ $t('settings.style.preview.button') }}
+ </button>
+ </div>
</div>
</div>
</div>
</template>
+
+<style lang="scss">
+.preview-container {
+ position: relative;
+}
+.underlay-preview {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 10px;
+ right: 10px;
+}
+</style>
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
@@ -1,6 +1,29 @@
-import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
import { set, delete as del } from 'vue'
-import { generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
+import {
+ rgb2hex,
+ hex2rgb,
+ getContrastRatioLayers
+} from '../../services/color_convert/color_convert.js'
+import {
+ DEFAULT_SHADOWS,
+ generateColors,
+ generateShadows,
+ generateRadii,
+ generateFonts,
+ composePreset,
+ getThemes,
+ shadows2to3,
+ colors2to3
+} from '../../services/style_setter/style_setter.js'
+import {
+ SLOT_INHERITANCE
+} from '../../services/theme_data/pleromafe.js'
+import {
+ CURRENT_VERSION,
+ OPACITIES,
+ getLayers,
+ getOpacitySlot
+} from '../../services/theme_data/theme_data.service.js'
import ColorInput from '../color_input/color_input.vue'
import RangeInput from '../range_input/range_input.vue'
import OpacityInput from '../opacity_input/opacity_input.vue'
@@ -24,11 +47,22 @@ const v1OnlyNames = [
'cOrange'
].map(_ => _ + 'ColorLocal')
+const colorConvert = (color) => {
+ if (color.startsWith('--') || color === 'transparent') {
+ return color
+ } else {
+ return hex2rgb(color)
+ }
+}
+
export default {
data () {
return {
availableStyles: [],
selected: this.$store.getters.mergedConfig.theme,
+ themeWarning: undefined,
+ tempImportFile: undefined,
+ engineVersion: 0,
previewShadows: {},
previewColors: {},
@@ -45,51 +79,13 @@ export default {
keepRoundness: false,
keepFonts: false,
- textColorLocal: '',
- linkColorLocal: '',
-
- bgColorLocal: '',
- bgOpacityLocal: undefined,
-
- fgColorLocal: '',
- fgTextColorLocal: undefined,
- fgLinkColorLocal: undefined,
-
- btnColorLocal: undefined,
- btnTextColorLocal: undefined,
- btnOpacityLocal: undefined,
-
- inputColorLocal: undefined,
- inputTextColorLocal: undefined,
- inputOpacityLocal: undefined,
+ ...Object.keys(SLOT_INHERITANCE)
+ .map(key => [key, ''])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
- panelColorLocal: undefined,
- panelTextColorLocal: undefined,
- panelLinkColorLocal: undefined,
- panelFaintColorLocal: undefined,
- panelOpacityLocal: undefined,
-
- topBarColorLocal: undefined,
- topBarTextColorLocal: undefined,
- topBarLinkColorLocal: undefined,
-
- alertErrorColorLocal: undefined,
- alertWarningColorLocal: undefined,
-
- badgeOpacityLocal: undefined,
- badgeNotificationColorLocal: undefined,
-
- borderColorLocal: undefined,
- borderOpacityLocal: undefined,
-
- faintColorLocal: undefined,
- faintOpacityLocal: undefined,
- faintLinkColorLocal: undefined,
-
- cRedColorLocal: '',
- cBlueColorLocal: '',
- cGreenColorLocal: '',
- cOrangeColorLocal: '',
+ ...Object.keys(OPACITIES)
+ .map(key => [key, ''])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
shadowSelected: undefined,
shadowsLocal: {},
@@ -108,69 +104,105 @@ export default {
created () {
const self = this
- getThemes().then((themesComplete) => {
- self.availableStyles = themesComplete
- })
+ getThemes()
+ .then((promises) => {
+ return Promise.all(
+ Object.entries(promises)
+ .map(([k, v]) => v.then(res => [k, res]))
+ )
+ })
+ .then(themes => themes.reduce((acc, [k, v]) => {
+ if (v) {
+ return {
+ ...acc,
+ [k]: v
+ }
+ } else {
+ return acc
+ }
+ }, {}))
+ .then((themesComplete) => {
+ self.availableStyles = themesComplete
+ })
},
mounted () {
- this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
+ this.loadThemeFromLocalStorage()
if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0]
}
},
computed: {
+ themeWarningHelp () {
+ if (!this.themeWarning) return
+ const t = this.$t
+ const pre = 'settings.style.switcher.help.'
+ const {
+ origin,
+ themeEngineVersion,
+ type,
+ noActionsPossible
+ } = this.themeWarning
+ if (origin === 'file') {
+ // Loaded v2 theme from file
+ if (themeEngineVersion === 2 && type === 'wrong_version') {
+ return t(pre + 'v2_imported')
+ }
+ if (themeEngineVersion > CURRENT_VERSION) {
+ return t(pre + 'future_version_imported') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present')
+ )
+ }
+ if (themeEngineVersion < CURRENT_VERSION) {
+ return t(pre + 'future_version_imported') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present')
+ )
+ }
+ } else if (origin === 'localStorage') {
+ if (type === 'snapshot_source_mismatch') {
+ return t(pre + 'snapshot_source_mismatch')
+ }
+ // FE upgraded from v2
+ if (themeEngineVersion === 2) {
+ return t(pre + 'upgraded_from_v2')
+ }
+ // Admin downgraded FE
+ if (themeEngineVersion > CURRENT_VERSION) {
+ return t(pre + 'fe_downgraded') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone')
+ )
+ }
+ // Admin upgraded FE
+ if (themeEngineVersion < CURRENT_VERSION) {
+ return t(pre + 'fe_upgraded') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone')
+ )
+ }
+ }
+ },
selectedVersion () {
return Array.isArray(this.selected) ? 1 : 2
},
currentColors () {
- return {
- bg: this.bgColorLocal,
- text: this.textColorLocal,
- link: this.linkColorLocal,
-
- fg: this.fgColorLocal,
- fgText: this.fgTextColorLocal,
- fgLink: this.fgLinkColorLocal,
-
- panel: this.panelColorLocal,
- panelText: this.panelTextColorLocal,
- panelLink: this.panelLinkColorLocal,
- panelFaint: this.panelFaintColorLocal,
-
- input: this.inputColorLocal,
- inputText: this.inputTextColorLocal,
-
- topBar: this.topBarColorLocal,
- topBarText: this.topBarTextColorLocal,
- topBarLink: this.topBarLinkColorLocal,
-
- btn: this.btnColorLocal,
- btnText: this.btnTextColorLocal,
-
- alertError: this.alertErrorColorLocal,
- alertWarning: this.alertWarningColorLocal,
- badgeNotification: this.badgeNotificationColorLocal,
-
- faint: this.faintColorLocal,
- faintLink: this.faintLinkColorLocal,
- border: this.borderColorLocal,
-
- cRed: this.cRedColorLocal,
- cBlue: this.cBlueColorLocal,
- cGreen: this.cGreenColorLocal,
- cOrange: this.cOrangeColorLocal
- }
+ return Object.keys(SLOT_INHERITANCE)
+ .map(key => [key, this[key + 'ColorLocal']])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
},
currentOpacity () {
- return {
- bg: this.bgOpacityLocal,
- btn: this.btnOpacityLocal,
- input: this.inputOpacityLocal,
- panel: this.panelOpacityLocal,
- topBar: this.topBarOpacityLocal,
- border: this.borderOpacityLocal,
- faint: this.faintOpacityLocal
- }
+ return Object.keys(OPACITIES)
+ .map(key => [key, this[key + 'OpacityLocal']])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
},
currentRadii () {
return {
@@ -193,75 +225,66 @@ export default {
},
// This needs optimization maybe
previewContrast () {
- if (!this.previewTheme.colors.bg) return {}
- const colors = this.previewTheme.colors
- const opacity = this.previewTheme.opacity
- if (!colors.bg) return {}
- const hints = (ratio) => ({
- text: ratio.toPrecision(3) + ':1',
- // AA level, AAA level
- aa: ratio >= 4.5,
- aaa: ratio >= 7,
- // same but for 18pt+ texts
- laa: ratio >= 3,
- laaa: ratio >= 4.5
- })
-
- // fgsfds :DDDD
- const fgs = {
- text: hex2rgb(colors.text),
- panelText: hex2rgb(colors.panelText),
- panelLink: hex2rgb(colors.panelLink),
- btnText: hex2rgb(colors.btnText),
- topBarText: hex2rgb(colors.topBarText),
- inputText: hex2rgb(colors.inputText),
-
- link: hex2rgb(colors.link),
- topBarLink: hex2rgb(colors.topBarLink),
-
- red: hex2rgb(colors.cRed),
- green: hex2rgb(colors.cGreen),
- blue: hex2rgb(colors.cBlue),
- orange: hex2rgb(colors.cOrange)
- }
-
- const bgs = {
- bg: hex2rgb(colors.bg),
- btn: hex2rgb(colors.btn),
- panel: hex2rgb(colors.panel),
- topBar: hex2rgb(colors.topBar),
- input: hex2rgb(colors.input),
- alertError: hex2rgb(colors.alertError),
- alertWarning: hex2rgb(colors.alertWarning),
- badgeNotification: hex2rgb(colors.badgeNotification)
- }
-
- /* This is a bit confusing because "bottom layer" used is text color
- * This is done to get worst case scenario when background below transparent
- * layer matches text color, making it harder to read the lower alpha is.
- */
- const ratios = {
- bgText: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.text), fgs.text),
- bgLink: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.link), fgs.link),
- bgRed: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.red), fgs.red),
- bgGreen: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.green), fgs.green),
- bgBlue: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.blue), fgs.blue),
- bgOrange: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.orange), fgs.orange),
-
- tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text),
-
- panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText),
- panelLink: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelLink), fgs.panelLink),
-
- btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText),
-
- inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText),
-
- topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText),
- topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink)
+ try {
+ if (!this.previewTheme.colors.bg) return {}
+ const colors = this.previewTheme.colors
+ const opacity = this.previewTheme.opacity
+ if (!colors.bg) return {}
+ const hints = (ratio) => ({
+ text: ratio.toPrecision(3) + ':1',
+ // AA level, AAA level
+ aa: ratio >= 4.5,
+ aaa: ratio >= 7,
+ // same but for 18pt+ texts
+ laa: ratio >= 3,
+ laaa: ratio >= 4.5
+ })
+ const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
+
+ const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
+ const slotIsBaseText = key === 'text' || key === 'link'
+ const slotIsText = slotIsBaseText || (
+ typeof value === 'object' && value !== null && value.textColor
+ )
+ if (!slotIsText) return acc
+ const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
+ const background = variant || layer
+ const opacitySlot = getOpacitySlot(background)
+ const textColors = [
+ key,
+ ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
+ ]
+
+ const layers = getLayers(
+ layer,
+ variant || layer,
+ opacitySlot,
+ colorsConverted,
+ opacity
+ )
+
+ return {
+ ...acc,
+ ...textColors.reduce((acc, textColorKey) => {
+ const newKey = slotIsBaseText
+ ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
+ : textColorKey
+ return {
+ ...acc,
+ [newKey]: getContrastRatioLayers(
+ colorsConverted[textColorKey],
+ layers,
+ colorsConverted[textColorKey]
+ )
+ }
+ }, {})
+ }
+ }, {})
+
+ return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
+ } catch (e) {
+ console.warn('Failure computing contrasts', e)
}
-
- return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
},
previewRules () {
if (!this.preview.rules) return ''
@@ -272,7 +295,7 @@ export default {
].join(';')
},
shadowsAvailable () {
- return Object.keys(this.previewTheme.shadows).sort()
+ return Object.keys(DEFAULT_SHADOWS).sort()
},
currentShadowOverriden: {
get () {
@@ -287,7 +310,7 @@ export default {
}
},
currentShadowFallback () {
- return this.previewTheme.shadows[this.shadowSelected]
+ return (this.previewTheme.shadows || {})[this.shadowSelected]
},
currentShadow: {
get () {
@@ -309,27 +332,34 @@ export default {
!this.keepColor
)
- const theme = {}
+ const source = {
+ themeEngineVersion: CURRENT_VERSION
+ }
if (this.keepFonts || saveEverything) {
- theme.fonts = this.fontsLocal
+ source.fonts = this.fontsLocal
}
if (this.keepShadows || saveEverything) {
- theme.shadows = this.shadowsLocal
+ source.shadows = this.shadowsLocal
}
if (this.keepOpacity || saveEverything) {
- theme.opacity = this.currentOpacity
+ source.opacity = this.currentOpacity
}
if (this.keepColor || saveEverything) {
- theme.colors = this.currentColors
+ source.colors = this.currentColors
}
if (this.keepRoundness || saveEverything) {
- theme.radii = this.currentRadii
+ source.radii = this.currentRadii
+ }
+
+ const theme = {
+ themeEngineVersion: CURRENT_VERSION,
+ ...this.previewTheme
}
return {
- // To separate from other random JSON files and possible future theme formats
- _pleroma_theme_version: 2, theme
+ // To separate from other random JSON files and possible future source formats
+ _pleroma_theme_version: 2, theme, source
}
}
},
@@ -346,10 +376,128 @@ export default {
Checkbox
},
methods: {
+ loadTheme (
+ {
+ theme,
+ source,
+ _pleroma_theme_version: fileVersion
+ },
+ origin,
+ forceUseSource = false
+ ) {
+ this.dismissWarning()
+ if (!source && !theme) {
+ throw new Error('Can\'t load theme: empty')
+ }
+ const version = (origin === 'localStorage' && !theme.colors)
+ ? 'l1'
+ : fileVersion
+ const snapshotEngineVersion = (theme || {}).themeEngineVersion
+ const themeEngineVersion = (source || {}).themeEngineVersion || 2
+ const versionsMatch = themeEngineVersion === CURRENT_VERSION
+ const sourceSnapshotMismatch = (
+ theme !== undefined &&
+ source !== undefined &&
+ themeEngineVersion !== snapshotEngineVersion
+ )
+ // Force loading of source if user requested it or if snapshot
+ // is unavailable
+ const forcedSourceLoad = (source && forceUseSource) || !theme
+ if (!(versionsMatch && !sourceSnapshotMismatch) &&
+ !forcedSourceLoad &&
+ version !== 'l1' &&
+ origin !== 'defaults'
+ ) {
+ if (sourceSnapshotMismatch && origin === 'localStorage') {
+ this.themeWarning = {
+ origin,
+ themeEngineVersion,
+ type: 'snapshot_source_mismatch'
+ }
+ } else if (!theme) {
+ this.themeWarning = {
+ origin,
+ noActionsPossible: true,
+ themeEngineVersion,
+ type: 'no_snapshot_old_version'
+ }
+ } else if (!versionsMatch) {
+ this.themeWarning = {
+ origin,
+ noActionsPossible: !source,
+ themeEngineVersion,
+ type: 'wrong_version'
+ }
+ }
+ }
+ this.normalizeLocalState(theme, version, source, forcedSourceLoad)
+ },
+ forceLoadLocalStorage () {
+ this.loadThemeFromLocalStorage(true)
+ },
+ dismissWarning () {
+ this.themeWarning = undefined
+ this.tempImportFile = undefined
+ },
+ forceLoad () {
+ const { origin } = this.themeWarning
+ switch (origin) {
+ case 'localStorage':
+ this.loadThemeFromLocalStorage(true)
+ break
+ case 'file':
+ this.onImport(this.tempImportFile, true)
+ break
+ }
+ this.dismissWarning()
+ },
+ forceSnapshot () {
+ const { origin } = this.themeWarning
+ switch (origin) {
+ case 'localStorage':
+ this.loadThemeFromLocalStorage(false, true)
+ break
+ case 'file':
+ console.err('Forcing snapshout from file is not supported yet')
+ break
+ }
+ this.dismissWarning()
+ },
+ loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
+ const {
+ customTheme: theme,
+ customThemeSource: source
+ } = this.$store.getters.mergedConfig
+ if (!theme && !source) {
+ // Anon user or never touched themes
+ this.loadTheme(
+ this.$store.state.instance.themeData,
+ 'defaults',
+ confirmLoadSource
+ )
+ } else {
+ this.loadTheme(
+ {
+ theme,
+ source: forceSnapshot ? theme : source
+ },
+ 'localStorage',
+ confirmLoadSource
+ )
+ }
+ },
setCustomTheme () {
this.$store.dispatch('setOption', {
name: 'customTheme',
value: {
+ themeEngineVersion: CURRENT_VERSION,
+ ...this.previewTheme
+ }
+ })
+ this.$store.dispatch('setOption', {
+ name: 'customThemeSource',
+ value: {
+ themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal,
fonts: this.fontsLocal,
opacity: this.currentOpacity,
@@ -358,21 +506,27 @@ export default {
}
})
},
- onImport (parsed) {
- if (parsed._pleroma_theme_version === 1) {
- this.normalizeLocalState(parsed, 1)
- } else if (parsed._pleroma_theme_version === 2) {
- this.normalizeLocalState(parsed.theme, 2)
- }
+ updatePreviewColorsAndShadows () {
+ this.previewColors = generateColors({
+ opacity: this.currentOpacity,
+ colors: this.currentColors
+ })
+ this.previewShadows = generateShadows(
+ { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
+ this.previewColors.theme.colors,
+ this.previewColors.mod
+ )
+ },
+ onImport (parsed, forceSource = false) {
+ this.tempImportFile = parsed
+ this.loadTheme(parsed, 'file', forceSource)
},
importValidator (parsed) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
},
clearAll () {
- const state = this.$store.getters.mergedConfig.customTheme
- const version = state.colors ? 2 : 'l1'
- this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
+ this.loadThemeFromLocalStorage()
},
// Clears all the extra stuff when loading V1 theme
@@ -411,19 +565,37 @@ export default {
/**
* This applies stored theme data onto form. Supports three versions of data:
+ * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
* v2 (version = 2) - newer version of themes.
* v1 (version = 1) - older version of themes (import from file)
* v1l (version = l1) - older version of theme (load from local storage)
* v1 and v1l differ because of way themes were stored/exported.
- * @param {Object} input - input data
+ * @param {Object} theme - theme data (snapshot)
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
+ * @param {Object} source - theme source - this will be used if compatible
+ * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
+ * this allows importing source anyway
*/
- normalizeLocalState (input, version = 0) {
- const colors = input.colors || input
+ normalizeLocalState (theme, version = 0, source, forceSource = false) {
+ let input
+ if (typeof source !== 'undefined') {
+ if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
+ input = source
+ version = source.themeEngineVersion
+ } else {
+ input = theme
+ }
+ } else {
+ input = theme
+ }
+
const radii = input.radii || input
const opacity = input.opacity
const shadows = input.shadows || {}
const fonts = input.fonts || {}
+ const colors = !input.themeEngineVersion
+ ? colors2to3(input.colors || input)
+ : input.colors || input
if (version === 0) {
if (input.version) version = input.version
@@ -437,6 +609,8 @@ export default {
}
}
+ this.engineVersion = version
+
// Stuff that differs between V1 and V2
if (version === 1) {
this.fgColorLocal = rgb2hex(colors.btn)
@@ -445,7 +619,7 @@ export default {
if (!this.keepColor) {
this.clearV1()
- const keys = new Set(version !== 1 ? Object.keys(colors) : [])
+ const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
if (version === 1 || version === 'l1') {
keys
.add('bg')
@@ -457,7 +631,17 @@ export default {
}
keys.forEach(key => {
- this[key + 'ColorLocal'] = rgb2hex(colors[key])
+ const color = colors[key]
+ const hex = rgb2hex(colors[key])
+ this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
+ })
+ }
+
+ if (opacity && !this.keepOpacity) {
+ this.clearOpacity()
+ Object.entries(opacity).forEach(([k, v]) => {
+ if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
+ this[k + 'OpacityLocal'] = v
})
}
@@ -472,7 +656,11 @@ export default {
if (!this.keepShadows) {
this.clearShadows()
- this.shadowsLocal = shadows
+ if (version === 2) {
+ this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
+ } else {
+ this.shadowsLocal = shadows
+ }
this.shadowSelected = this.shadowsAvailable[0]
}
@@ -480,14 +668,6 @@ export default {
this.clearFonts()
this.fontsLocal = fonts
}
-
- if (opacity && !this.keepOpacity) {
- this.clearOpacity()
- Object.entries(opacity).forEach(([k, v]) => {
- if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
- this[k + 'OpacityLocal'] = v
- })
- }
}
},
watch: {
@@ -502,8 +682,9 @@ export default {
},
shadowsLocal: {
handler () {
+ if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
try {
- this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
+ this.updatePreviewColorsAndShadows()
this.shadowsInvalid = false
} catch (e) {
this.shadowsInvalid = true
@@ -526,27 +707,24 @@ export default {
},
currentColors () {
try {
- this.previewColors = generateColors({
- opacity: this.currentOpacity,
- colors: this.currentColors
- })
+ this.updatePreviewColorsAndShadows()
this.colorsInvalid = false
+ this.shadowsInvalid = false
} catch (e) {
this.colorsInvalid = true
+ this.shadowsInvalid = true
console.warn(e)
}
},
currentOpacity () {
try {
- this.previewColors = generateColors({
- opacity: this.currentOpacity,
- colors: this.currentColors
- })
+ this.updatePreviewColorsAndShadows()
} catch (e) {
console.warn(e)
}
},
selected () {
+ this.dismissWarning()
if (this.selectedVersion === 1) {
if (!this.keepRoundness) {
this.clearRoundness()
@@ -573,7 +751,7 @@ export default {
this.cOrangeColorLocal = this.selected[8]
}
} else if (this.selectedVersion >= 2) {
- this.normalizeLocalState(this.selected.theme, 2)
+ this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
}
}
}
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
@@ -1,5 +1,15 @@
@import '../../_variables.scss';
.style-switcher {
+ .theme-warning {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: .5em;
+ .buttons {
+ .btn {
+ margin-bottom: .5em;
+ }
+ }
+ }
.preset-switcher {
margin-right: 1em;
}
@@ -15,26 +25,23 @@
&.disabled {
input, select {
- &:not(.exclude-disabled) {
- opacity: .5
- }
+ opacity: .5
}
}
+ .opt {
+ margin: .5em;
+ }
+
+ .color-input {
+ flex: 0 0 0;
+ }
+
input, select {
min-width: 3em;
margin: 0;
flex: 0;
- &[type=color] {
- padding: 1px;
- cursor: pointer;
- height: 29px;
- min-width: 2em;
- border: none;
- align-self: stretch;
- }
-
&[type=number] {
min-width: 5em;
}
@@ -42,13 +49,6 @@
&[type=range] {
flex: 1;
min-width: 3em;
- }
-
- &[type=checkbox] + label {
- margin: 6px 0;
- }
-
- &:not([type=number]):not([type=text]) {
align-self: flex-start;
}
}
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
@@ -2,7 +2,53 @@
<div class="style-switcher">
<div class="presets-container">
<div class="save-load">
- <export-import
+ <div
+ v-if="themeWarning"
+ class="theme-warning"
+ >
+ <div class="alert warning">
+ {{ themeWarningHelp }}
+ </div>
+ <div class="buttons">
+ <template v-if="themeWarning.type === 'snapshot_source_mismatch'">
+ <button
+ class="btn"
+ @click="forceLoad"
+ >
+ {{ $t('settings.style.switcher.use_source') }}
+ </button>
+ <button
+ class="btn"
+ @click="forceSnapshot"
+ >
+ {{ $t('settings.style.switcher.use_snapshot') }}
+ </button>
+ </template>
+ <template v-else-if="themeWarning.noActionsPossible">
+ <button
+ class="btn"
+ @click="dismissWarning"
+ >
+ {{ $t('general.dismiss') }}
+ </button>
+ </template>
+ <template v-else>
+ <button
+ class="btn"
+ @click="forceLoad"
+ >
+ {{ $t('settings.style.switcher.load_theme') }}
+ </button>
+ <button
+ class="btn"
+ @click="dismissWarning"
+ >
+ {{ $t('settings.style.switcher.keep_as_is') }}
+ </button>
+ </template>
+ </div>
+ </div>
+ <ExportImport
:export-object="exportedTheme"
:export-label="$t("settings.export_theme")"
:import-label="$t("settings.import_theme")"
@@ -27,8 +73,8 @@
:key="style.name"
:value="style"
:style="{
- backgroundColor: style[1] || style.theme.colors.bg,
- color: style[3] || style.theme.colors.text
+ backgroundColor: style[1] || (style.theme || style.source).colors.bg,
+ color: style[3] || (style.theme || style.source).colors.text
}"
>
{{ style[0] || style.name }}
@@ -38,7 +84,7 @@
</label>
</div>
</template>
- </export-import>
+ </ExportImport>
</div>
<div class="save-load-options">
<span class="keep-option">
@@ -70,9 +116,7 @@
</div>
</div>
- <div class="preview-container">
- <preview :style="previewRules" />
- </div>
+ <preview :style="previewRules" />
<keep-alive>
<tab-switcher key="style-tweak">
@@ -106,7 +150,7 @@
<OpacityInput
v-model="bgOpacityLocal"
name="bgOpacity"
- :fallback="previewTheme.opacity.bg || 1"
+ :fallback="previewTheme.opacity.bg"
/>
<ColorInput
v-model="textColorLocal"
@@ -115,9 +159,18 @@
/>
<ContrastRatio :contrast="previewContrast.bgText" />
<ColorInput
+ v-model="accentColorLocal"
+ name="accentColor"
+ :fallback="previewTheme.colors.link"
+ :label="$t('settings.accent')"
+ :show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
+ />
+ <ColorInput
v-model="linkColorLocal"
name="linkColor"
+ :fallback="previewTheme.colors.accent"
:label="$t('settings.links')"
+ :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
/>
<ContrastRatio :contrast="previewContrast.bgLink" />
</div>
@@ -148,13 +201,13 @@
name="cRedColor"
:label="$t('settings.cRed')"
/>
- <ContrastRatio :contrast="previewContrast.bgRed" />
+ <ContrastRatio :contrast="previewContrast.bgCRed" />
<ColorInput
v-model="cBlueColorLocal"
name="cBlueColor"
:label="$t('settings.cBlue')"
/>
- <ContrastRatio :contrast="previewContrast.bgBlue" />
+ <ContrastRatio :contrast="previewContrast.bgCBlue" />
</div>
<div class="color-item">
<ColorInput
@@ -162,13 +215,13 @@
name="cGreenColor"
:label="$t('settings.cGreen')"
/>
- <ContrastRatio :contrast="previewContrast.bgGreen" />
+ <ContrastRatio :contrast="previewContrast.bgCGreen" />
<ColorInput
v-model="cOrangeColorLocal"
name="cOrangeColor"
:label="$t('settings.cOrange')"
/>
- <ContrastRatio :contrast="previewContrast.bgOrange" />
+ <ContrastRatio :contrast="previewContrast.bgCOrange" />
</div>
<p>{{ $t('settings.theme_help_v2_2') }}</p>
</div>
@@ -193,6 +246,14 @@
</button>
</div>
<div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
+ <ColorInput
+ v-model="postLinkColorLocal"
+ name="postLinkColor"
+ :fallback="previewTheme.colors.accent"
+ :label="$t('settings.links')"
+ />
+ <ContrastRatio :contrast="previewContrast.postLink" />
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
<ColorInput
v-model="alertErrorColorLocal"
@@ -200,14 +261,53 @@
:label="$t('settings.style.advanced_colors.alert_error')"
:fallback="previewTheme.colors.alertError"
/>
- <ContrastRatio :contrast="previewContrast.alertError" />
+ <ColorInput
+ v-model="alertErrorTextColorLocal"
+ name="alertErrorText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.alertErrorText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.alertErrorText"
+ large="true"
+ />
<ColorInput
v-model="alertWarningColorLocal"
name="alertWarning"
:label="$t('settings.style.advanced_colors.alert_warning')"
:fallback="previewTheme.colors.alertWarning"
/>
- <ContrastRatio :contrast="previewContrast.alertWarning" />
+ <ColorInput
+ v-model="alertWarningTextColorLocal"
+ name="alertWarningText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.alertWarningText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.alertWarningText"
+ large="true"
+ />
+ <ColorInput
+ v-model="alertNeutralColorLocal"
+ name="alertNeutral"
+ :label="$t('settings.style.advanced_colors.alert_neutral')"
+ :fallback="previewTheme.colors.alertNeutral"
+ />
+ <ColorInput
+ v-model="alertNeutralTextColorLocal"
+ name="alertNeutralText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.alertNeutralText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.alertNeutralText"
+ large="true"
+ />
+ <OpacityInput
+ v-model="alertOpacityLocal"
+ name="alertOpacity"
+ :fallback="previewTheme.opacity.alert"
+ />
</div>
<div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
@@ -217,19 +317,30 @@
:label="$t('settings.style.advanced_colors.badge_notification')"
:fallback="previewTheme.colors.badgeNotification"
/>
+ <ColorInput
+ v-model="badgeNotificationTextColorLocal"
+ name="badgeNotificationText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.badgeNotificationText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.badgeNotificationText"
+ large="true"
+ />
</div>
<div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
<ColorInput
v-model="panelColorLocal"
name="panelColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.panel"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="panelOpacityLocal"
name="panelOpacity"
- :fallback="previewTheme.opacity.panel || 1"
+ :fallback="previewTheme.opacity.panel"
+ :disabled="panelColorLocal === 'transparent'"
/>
<ColorInput
v-model="panelTextColorLocal"
@@ -239,7 +350,7 @@
/>
<ContrastRatio
:contrast="previewContrast.panelText"
- large="1"
+ large="true"
/>
<ColorInput
v-model="panelLinkColorLocal"
@@ -249,7 +360,7 @@
/>
<ContrastRatio
:contrast="previewContrast.panelLink"
- large="1"
+ large="true"
/>
</div>
<div class="color-item">
@@ -257,7 +368,7 @@
<ColorInput
v-model="topBarColorLocal"
name="topBarColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.topBar"
:label="$t('settings.background')"
/>
<ColorInput
@@ -280,13 +391,14 @@
<ColorInput
v-model="inputColorLocal"
name="inputColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.input"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="inputOpacityLocal"
name="inputOpacity"
- :fallback="previewTheme.opacity.input || 1"
+ :fallback="previewTheme.opacity.input"
+ :disabled="inputColorLocal === 'transparent'"
/>
<ColorInput
v-model="inputTextColorLocal"
@@ -301,13 +413,14 @@
<ColorInput
v-model="btnColorLocal"
name="btnColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.btn"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="btnOpacityLocal"
name="btnOpacity"
- :fallback="previewTheme.opacity.btn || 1"
+ :fallback="previewTheme.opacity.btn"
+ :disabled="btnColorLocal === 'transparent'"
/>
<ColorInput
v-model="btnTextColorLocal"
@@ -316,6 +429,124 @@
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.btnText" />
+ <ColorInput
+ v-model="btnPanelTextColorLocal"
+ name="btnPanelTextColor"
+ :fallback="previewTheme.colors.btnPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPanelText" />
+ <ColorInput
+ v-model="btnTopBarTextColorLocal"
+ name="btnTopBarTextColor"
+ :fallback="previewTheme.colors.btnTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnTopBarText" />
+ <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
+ <ColorInput
+ v-model="btnPressedColorLocal"
+ name="btnPressedColor"
+ :fallback="previewTheme.colors.btnPressed"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="btnPressedTextColorLocal"
+ name="btnPressedTextColor"
+ :fallback="previewTheme.colors.btnPressedText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPressedText" />
+ <ColorInput
+ v-model="btnPressedPanelTextColorLocal"
+ name="btnPressedPanelTextColor"
+ :fallback="previewTheme.colors.btnPressedPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
+ <ColorInput
+ v-model="btnPressedTopBarTextColorLocal"
+ name="btnPressedTopBarTextColor"
+ :fallback="previewTheme.colors.btnPressedTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
+ <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
+ <ColorInput
+ v-model="btnDisabledColorLocal"
+ name="btnDisabledColor"
+ :fallback="previewTheme.colors.btnDisabled"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="btnDisabledTextColorLocal"
+ name="btnDisabledTextColor"
+ :fallback="previewTheme.colors.btnDisabledText"
+ :label="$t('settings.text')"
+ />
+ <ColorInput
+ v-model="btnDisabledPanelTextColorLocal"
+ name="btnDisabledPanelTextColor"
+ :fallback="previewTheme.colors.btnDisabledPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ColorInput
+ v-model="btnDisabledTopBarTextColorLocal"
+ name="btnDisabledTopBarTextColor"
+ :fallback="previewTheme.colors.btnDisabledTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
+ <ColorInput
+ v-model="btnToggledColorLocal"
+ name="btnToggledColor"
+ :fallback="previewTheme.colors.btnToggled"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="btnToggledTextColorLocal"
+ name="btnToggledTextColor"
+ :fallback="previewTheme.colors.btnToggledText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnToggledText" />
+ <ColorInput
+ v-model="btnToggledPanelTextColorLocal"
+ name="btnToggledPanelTextColor"
+ :fallback="previewTheme.colors.btnToggledPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
+ <ColorInput
+ v-model="btnToggledTopBarTextColorLocal"
+ name="btnToggledTopBarTextColor"
+ :fallback="previewTheme.colors.btnToggledTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
+ <ColorInput
+ v-model="tabColorLocal"
+ name="tabColor"
+ :fallback="previewTheme.colors.tab"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="tabTextColorLocal"
+ name="tabTextColor"
+ :fallback="previewTheme.colors.tabText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.tabText" />
+ <ColorInput
+ v-model="tabActiveTextColorLocal"
+ name="tabActiveTextColor"
+ :fallback="previewTheme.colors.tabActiveText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.tabActiveText" />
</div>
<div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
@@ -328,7 +559,8 @@
<OpacityInput
v-model="borderOpacityLocal"
name="borderOpacity"
- :fallback="previewTheme.opacity.border || 1"
+ :fallback="previewTheme.opacity.border"
+ :disabled="borderColorLocal === 'transparent'"
/>
</div>
<div class="color-item">
@@ -336,7 +568,7 @@
<ColorInput
v-model="faintColorLocal"
name="faintColor"
- :fallback="previewTheme.colors.faint || 1"
+ :fallback="previewTheme.colors.faint"
:label="$t('settings.text')"
/>
<ColorInput
@@ -354,8 +586,145 @@
<OpacityInput
v-model="faintOpacityLocal"
name="faintOpacity"
- :fallback="previewTheme.opacity.faint || 0.5"
+ :fallback="previewTheme.opacity.faint"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4>
+ <ColorInput
+ v-model="underlayColorLocal"
+ name="underlay"
+ :label="$t('settings.style.advanced_colors.underlay')"
+ :fallback="previewTheme.colors.underlay"
+ />
+ <OpacityInput
+ v-model="underlayOpacityLocal"
+ name="underlayOpacity"
+ :fallback="previewTheme.opacity.underlay"
+ :disabled="underlayOpacityLocal === 'transparent'"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
+ <ColorInput
+ v-model="pollColorLocal"
+ name="poll"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.poll"
+ />
+ <ColorInput
+ v-model="pollTextColorLocal"
+ name="pollText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.pollText"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
+ <ColorInput
+ v-model="iconColorLocal"
+ name="icon"
+ :label="$t('settings.style.advanced_colors.icons')"
+ :fallback="previewTheme.colors.icon"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
+ <ColorInput
+ v-model="highlightColorLocal"
+ name="highlight"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.highlight"
+ />
+ <ColorInput
+ v-model="highlightTextColorLocal"
+ name="highlightText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.highlightText"
+ />
+ <ContrastRatio :contrast="previewContrast.highlightText" />
+ <ColorInput
+ v-model="highlightLinkColorLocal"
+ name="highlightLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.highlightLink"
+ />
+ <ContrastRatio :contrast="previewContrast.highlightLink" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.popover') }}</h4>
+ <ColorInput
+ v-model="popoverColorLocal"
+ name="popover"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.popover"
+ />
+ <OpacityInput
+ v-model="popoverOpacityLocal"
+ name="popoverOpacity"
+ :fallback="previewTheme.opacity.popover"
+ :disabled="popoverOpacityLocal === 'transparent'"
+ />
+ <ColorInput
+ v-model="popoverTextColorLocal"
+ name="popoverText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.popoverText"
+ />
+ <ContrastRatio :contrast="previewContrast.popoverText" />
+ <ColorInput
+ v-model="popoverLinkColorLocal"
+ name="popoverLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.popoverLink"
+ />
+ <ContrastRatio :contrast="previewContrast.popoverLink" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
+ <ColorInput
+ v-model="selectedPostColorLocal"
+ name="selectedPost"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.selectedPost"
+ />
+ <ColorInput
+ v-model="selectedPostTextColorLocal"
+ name="selectedPostText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.selectedPostText"
+ />
+ <ContrastRatio :contrast="previewContrast.selectedPostText" />
+ <ColorInput
+ v-model="selectedPostLinkColorLocal"
+ name="selectedPostLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.selectedPostLink"
+ />
+ <ContrastRatio :contrast="previewContrast.selectedPostLink" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
+ <ColorInput
+ v-model="selectedMenuColorLocal"
+ name="selectedMenu"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.selectedMenu"
+ />
+ <ColorInput
+ v-model="selectedMenuTextColorLocal"
+ name="selectedMenuText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.selectedMenuText"
+ />
+ <ContrastRatio :contrast="previewContrast.selectedMenuText" />
+ <ColorInput
+ v-model="selectedMenuLinkColorLocal"
+ name="selectedMenuLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.selectedMenuLink"
/>
+ <ContrastRatio :contrast="previewContrast.selectedMenuLink" />
</div>
</div>
@@ -491,7 +860,7 @@
{{ $t('settings.style.switcher.clear_all') }}
</button>
</div>
- <shadow-control
+ <ShadowControl
v-model="currentShadow"
:ready="!!currentShadowFallback"
:fallback="currentShadowFallback"
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
@@ -52,6 +52,11 @@
margin-bottom: 6px - 99px;
white-space: nowrap;
+ color: $fallback--text;
+ color: var(--tabText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--tab, $fallback--fg);
+
&:not(.active) {
z-index: 4;
@@ -63,6 +68,8 @@
&.active {
background: transparent;
z-index: 5;
+ color: $fallback--text;
+ color: var(--tabActiveText, $fallback--text);
}
img {
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
@@ -4,7 +4,6 @@ import ProgressButton from '../progress_button/progress_button.vue'
import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
-import { hex2rgb } from '../../services/color_convert/color_convert.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex'
@@ -30,21 +29,11 @@ export default {
}]
},
style () {
- const color = this.$store.getters.mergedConfig.customTheme.colors
- ? this.$store.getters.mergedConfig.customTheme.colors.bg // v2
- : this.$store.getters.mergedConfig.colors.bg // v1
-
- if (color) {
- const rgb = (typeof color === 'string') ? hex2rgb(color) : color
- const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
-
- return {
- backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
- backgroundImage: [
- `linear-gradient(to bottom, ${tintColor}, ${tintColor})`,
- `url(${this.user.cover_photo})`
- ].join(', ')
- }
+ return {
+ backgroundImage: [
+ `linear-gradient(to bottom, var(--profileTint), var(--profileTint))`,
+ `url(${this.user.cover_photo})`
+ ].join(', ')
}
},
isOtherUser () {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
@@ -151,7 +151,7 @@
</ProgressButton>
<ProgressButton
v-else
- class="btn btn-default pressed"
+ class="btn btn-default toggled"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
@@ -162,7 +162,7 @@
<div>
<button
v-if="user.muted"
- class="btn btn-default btn-block pressed"
+ class="btn btn-default btn-block toggled"
@click="unmuteUser"
>
{{ $t('user_card.muted') }}
@@ -286,6 +286,7 @@
mask-size: 100% 60%;
border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px);
+ background-color: var(--profileBg);
&.hide-bio {
mask-size: 100% 40px;
@@ -299,6 +300,11 @@
&-bio {
text-align: center;
+ a {
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
+ }
+
img {
object-fit: contain;
vertical-align: middle;
@@ -460,14 +466,13 @@
color: var(--text, $fallback--text);
}
- // TODO use proper colors
.staff {
flex: none;
text-transform: capitalize;
color: $fallback--text;
- color: var(--btnText, $fallback--text);
+ color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;
- background-color: var(--btn, $fallback--fg);
+ background-color: var(--alertNeutral, $fallback--fg);
}
}
@@ -538,12 +543,6 @@
button {
margin: 0;
-
- &.pressed {
- // TODO: This should be themed.
- border-bottom-color: rgba(255, 255, 255, 0.2);
- border-top-color: rgba(0, 0, 0, 0.2);
- }
}
}
}
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
@@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
+import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji_input/emoji_input.vue'
@@ -32,6 +33,12 @@ const MuteList = withSubscription({
childPropName: 'items'
})(SelectableList)
+const DomainMuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+ childPropName: 'items'
+})(SelectableList)
+
const UserSettings = {
data () {
return {
@@ -48,6 +55,7 @@ const UserSettings = {
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
discoverable: this.$store.state.users.currentUser.discoverable,
+ allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
@@ -67,7 +75,8 @@ const UserSettings = {
changedPassword: false,
changePasswordError: false,
activeTab: 'profile',
- notificationSettings: this.$store.state.users.currentUser.notification_settings
+ notificationSettings: this.$store.state.users.currentUser.notification_settings,
+ newDomainToMute: ''
}
},
created () {
@@ -80,10 +89,12 @@ const UserSettings = {
ImageCropper,
BlockList,
MuteList,
+ DomainMuteList,
EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
+ DomainMuteCard,
ProgressButton,
Importer,
Exporter,
@@ -152,6 +163,7 @@ const UserSettings = {
hide_follows: this.hideFollows,
hide_followers: this.hideFollowers,
discoverable: this.discoverable,
+ allow_following_move: this.allowFollowingMove,
hide_follows_count: this.hideFollowsCount,
hide_followers_count: this.hideFollowersCount,
show_role: this.showRole
@@ -297,7 +309,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
- this.$store.state.api.backendInteractor.changePassword({ params })
+ this.$store.state.api.backendInteractor.changePassword(params)
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
@@ -314,7 +326,7 @@ const UserSettings = {
email: this.newEmail,
password: this.changeEmailPassword
}
- this.$store.state.api.backendInteractor.changeEmail({ params })
+ this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
@@ -365,6 +377,13 @@ const UserSettings = {
unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
+ unmuteDomains (domains) {
+ return this.$store.dispatch('unmuteDomains', domains)
+ },
+ muteDomain () {
+ return this.$store.dispatch('muteDomain', this.newDomainToMute)
+ .then(() => { this.newDomainToMute = '' })
+ },
identity (value) {
return value
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
@@ -90,9 +90,7 @@
</Checkbox>
</p>
<p>
- <Checkbox
- v-model="hideFollowers"
- >
+ <Checkbox v-model="hideFollowers">
{{ $t('settings.hide_followers_description') }}
</Checkbox>
</p>
@@ -104,6 +102,11 @@
{{ $t('settings.hide_followers_count_description') }}
</Checkbox>
</p>
+ <p>
+ <Checkbox v-model="allowFollowingMove">
+ {{ $t('settings.allow_following_move') }}
+ </Checkbox>
+ </p>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
<template v-if="role === 'admin'">
@@ -509,59 +512,114 @@
</div>
<div :label="$t('settings.mutes_tab')">
- <div class="profile-edit-usersearch-wrapper">
- <Autosuggest
- :filter="filterUnMutedUsers"
- :query="queryUserIds"
- :placeholder="$t('settings.search_user_to_mute')"
- >
- <MuteCard
- slot-scope="row"
- :user-id="row.item"
- />
- </Autosuggest>
- </div>
- <MuteList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => muteUsers(selected)"
+ <tab-switcher>
+ <div label="Users">
+ <div class="profile-edit-usersearch-wrapper">
+ <Autosuggest
+ :filter="filterUnMutedUsers"
+ :query="queryUserIds"
+ :placeholder="$t('settings.search_user_to_mute')"
+ >
+ <MuteCard
+ slot-scope="row"
+ :user-id="row.item"
+ />
+ </Autosuggest>
+ </div>
+ <MuteList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => muteUsers(selected)"
+ >
+ {{ $t('user_card.mute') }}
+ <template slot="progress">
+ {{ $t('user_card.mute_progress') }}
+ </template>
+ </ProgressButton>
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unmuteUsers(selected)"
+ >
+ {{ $t('user_card.unmute') }}
+ <template slot="progress">
+ {{ $t('user_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <MuteCard :user-id="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_mutes') }}
+ </template>
+ </MuteList>
+ </div>
+
+ <div :label="$t('settings.domain_mutes')">
+ <div class="profile-edit-domain-mute-form">
+ <input
+ v-model="newDomainToMute"
+ :placeholder="$t('settings.type_domains_to_mute')"
+ type="text"
+ @keyup.enter="muteDomain"
>
- {{ $t('user_card.mute') }}
- <template slot="progress">
- {{ $t('user_card.mute_progress') }}
- </template>
- </ProgressButton>
<ProgressButton
- v-if="selected.length > 0"
class="btn btn-default"
- :click="() => unmuteUsers(selected)"
+ :click="muteDomain"
>
- {{ $t('user_card.unmute') }}
+ {{ $t('domain_mute_card.mute') }}
<template slot="progress">
- {{ $t('user_card.unmute_progress') }}
+ {{ $t('domain_mute_card.mute_progress') }}
</template>
</ProgressButton>
</div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <MuteCard :user-id="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_mutes') }}
- </template>
- </MuteList>
+ <DomainMuteList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unmuteDomains(selected)"
+ >
+ {{ $t('domain_mute_card.unmute') }}
+ <template slot="progress">
+ {{ $t('domain_mute_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <DomainMuteCard :domain="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_mutes') }}
+ </template>
+ </DomainMuteList>
+ </div>
+ </tab-switcher>
</div>
</tab-switcher>
</div>
@@ -639,6 +697,18 @@
}
}
+ &-domain-mute-form {
+ padding: 1em;
+ display: flex;
+ flex-direction: column;
+
+ button {
+ align-self: flex-end;
+ margin-top: 1em;
+ width: 10em;
+ }
+ }
+
.setting-subitem {
margin-left: 1.75em;
}
diff --git a/src/i18n/en.json b/src/i18n/en.json
@@ -1,26 +1,43 @@
{
"about": {
- "staff": "Staff",
- "federation": "Federation",
- "mrf_policies": "Enabled MRF Policies",
- "mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
- "mrf_policy_simple": "Instance-specific Policies",
- "mrf_policy_simple_accept": "Accept",
- "mrf_policy_simple_accept_desc": "This instance only accepts messages from the following instances:",
- "mrf_policy_simple_reject": "Reject",
- "mrf_policy_simple_reject_desc": "This instance will not accept messages from the following instances:",
- "mrf_policy_simple_quarantine": "Quarantine",
- "mrf_policy_simple_quarantine_desc": "This instance will send only public posts to the following instances:",
- "mrf_policy_simple_ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
- "mrf_policy_simple_ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
- "mrf_policy_simple_media_removal": "Media Removal",
- "mrf_policy_simple_media_removal_desc": "This instance removes media from posts on the following instances:",
- "mrf_policy_simple_media_nsfw": "Media Force-set As Sensitive",
- "mrf_policy_simple_media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
+ "mrf": {
+ "federation": "Federation",
+ "keyword": {
+ "keyword_policies": "Keyword Policies",
+ "ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
+ "reject": "Reject",
+ "replace": "Replace",
+ "is_replaced_by": "→"
+ },
+ "mrf_policies": "Enabled MRF Policies",
+ "mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
+ "simple": {
+ "simple_policies": "Instance-specific Policies",
+ "accept": "Accept",
+ "accept_desc": "This instance only accepts messages from the following instances:",
+ "reject": "Reject",
+ "reject_desc": "This instance will not accept messages from the following instances:",
+ "quarantine": "Quarantine",
+ "quarantine_desc": "This instance will send only public posts to the following instances:",
+ "ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
+ "ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
+ "media_removal": "Media Removal",
+ "media_removal_desc": "This instance removes media from posts on the following instances:",
+ "media_nsfw": "Media Force-set As Sensitive",
+ "media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
+ }
+ },
+ "staff": "Staff"
},
"chat": {
"title": "Chat"
},
+ "domain_mute_card": {
+ "mute": "Mute",
+ "mute_progress": "Muting...",
+ "unmute": "Unmute",
+ "unmute_progress": "Unmuting..."
+ },
"exporter": {
"export": "Export",
"processing": "Processing, you'll soon be asked to download your file"
@@ -46,6 +63,7 @@
"optional": "optional",
"show_more": "Show more",
"show_less": "Show less",
+ "dismiss": "Dismiss",
"cancel": "Cancel",
"disable": "Disable",
"enable": "Enable",
@@ -111,7 +129,8 @@
"read": "Read!",
"repeated_you": "repeated your status",
"no_more_notifications": "No more notifications",
- "migrated_to": "migrated to"
+ "migrated_to": "migrated to",
+ "reacted_with": "reacted with {0}"
},
"polls": {
"add_poll": "Add Poll",
@@ -226,6 +245,7 @@
"desc": "To enable two-factor authentication, enter the code from your two-factor app:"
}
},
+ "allow_following_move": "Allow auto-follow when following account moves",
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom",
@@ -264,8 +284,10 @@
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"discoverable": "Allow discovery of this account in search results and other services",
+ "domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
+ "emoji_reactions_on_timeline": "Show emoji reactions on timeline",
"export_theme": "Save preset",
"filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line",
@@ -274,6 +296,7 @@
"follow_import": "Follow import",
"follow_import_error": "Error importing followers",
"follows_imported": "Follows imported! Processing them will take a while.",
+ "accent": "Accent",
"foreground": "Foreground",
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
@@ -314,6 +337,7 @@
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates",
+ "notification_visibility_emoji_reactions": "Reactions",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
@@ -361,6 +385,7 @@
"post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
+ "user_mutes": "Users",
"useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text",
@@ -369,6 +394,7 @@
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
+ "type_domains_to_mute": "Type in domains to mute",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
@@ -396,7 +422,24 @@
"save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.",
"reset": "Reset",
"clear_all": "Clear all",
- "clear_opacity": "Clear opacity"
+ "clear_opacity": "Clear opacity",
+ "load_theme": "Load theme",
+ "keep_as_is": "Keep as is",
+ "use_snapshot": "Old version",
+ "use_source": "New version",
+ "help": {
+ "upgraded_from_v2": "PleromaFE has been upgraded, theme could look a little bit different than you remember.",
+ "v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.",
+ "future_version_imported": "File you imported was made in newer version of FE.",
+ "older_version_imported": "File you imported was made in older version of FE.",
+ "snapshot_present": "Theme snapshot is loaded, so all values are overriden. You can load theme's actual data instead.",
+ "snapshot_missing": "No theme snapshot was in the file so it could look different than originally envisioned.",
+ "fe_upgraded": "PleromaFE's theme engine upgraded after version update.",
+ "fe_downgraded": "PleromaFE's version rolled back.",
+ "migration_snapshot_ok": "Just to be safe, theme snapshot loaded. You can try loading theme data.",
+ "migration_napshot_gone": "For whatever reason snapshot was missing, some stuff could look different than you remember.",
+ "snapshot_source_mismatch": "Versions conflict: most likely FE was rolled back and updated again, if you changed theme using older version of FE you most likely want to use old version, otherwise use new version."
+ }
},
"common": {
"color": "Color",
@@ -425,14 +468,27 @@
"alert": "Alert background",
"alert_error": "Error",
"alert_warning": "Warning",
+ "alert_neutral": "Neutral",
+ "post": "Posts/User bios",
"badge": "Badge background",
+ "popover": "Tooltips, menus, popovers",
"badge_notification": "Notification",
"panel_header": "Panel header",
"top_bar": "Top bar",
"borders": "Borders",
"buttons": "Buttons",
"inputs": "Input fields",
- "faint_text": "Faded text"
+ "faint_text": "Faded text",
+ "underlay": "Underlay",
+ "poll": "Poll graph",
+ "icons": "Icons",
+ "highlight": "Highlighted elements",
+ "pressed": "Pressed",
+ "selectedPost": "Selected post",
+ "selectedMenu": "Selected menu item",
+ "disabled": "Disabled",
+ "toggled": "Toggled",
+ "tabs": "Tabs"
},
"radii": {
"_tab_label": "Roundness"
@@ -445,7 +501,7 @@
"blur": "Blur",
"spread": "Spread",
"inset": "Inset",
- "hint": "For shadows you can also use --variable as a color value to use CSS3 variables. Please note that setting opacity won't work in this case.",
+ "hintV3": "For shadows you can also use the {0} notation to use other color slot.",
"filter_hint": {
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
"drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.",
@@ -639,6 +695,7 @@
"repeat": "Repeat",
"reply": "Reply",
"favorite": "Favorite",
+ "add_reaction": "Add Reaction",
"user_settings": "User Settings"
},
"upload":{
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
@@ -53,7 +53,8 @@
"notifications": "Ilmoitukset",
"read": "Lue!",
"repeated_you": "toisti viestisi",
- "no_more_notifications": "Ei enempää ilmoituksia"
+ "no_more_notifications": "Ei enempää ilmoituksia",
+ "reacted_with": "lisäsi reaktion {0}"
},
"polls": {
"add_poll": "Lisää äänestys",
@@ -140,6 +141,7 @@
"delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
"delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
+ "emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
"export_theme": "Tallenna teema",
"filtering": "Suodatus",
"filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.",
@@ -183,6 +185,7 @@
"notification_visibility_likes": "Tykkäykset",
"notification_visibility_mentions": "Maininnat",
"notification_visibility_repeats": "Toistot",
+ "notification_visibility_emoji_reactions": "Reaktiot",
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json
@@ -1,22 +1,26 @@
{
"about": {
- "staff": "スタッフ",
- "federation": "フェデレーション",
- "mrf_policies": "ゆうこうなMRFポリシー",
- "mrf_policies_desc": "MRFポリシーは、このインスタンスのフェデレーションのふるまいを、いじります。これらのMRFポリシーがゆうこうになっています:",
- "mrf_policy_simple": "インスタンスのポリシー",
- "mrf_policy_simple_accept": "うけいれ",
- "mrf_policy_simple_accept_desc": "このインスンスは、これらのインスタンスからのメッセージのみをうけいれます:",
- "mrf_policy_simple_reject": "おことわり",
- "mrf_policy_simple_reject_desc": "このインスタンスは、これらのインスタンスからのメッセージをうけいれません:",
- "mrf_policy_simple_quarantine": "けんえき",
- "mrf_policy_simple_quarantine_desc": "このインスタンスは、これらのインスタンスに、パブリックなとうこうのみを、おくります:",
- "mrf_policy_simple_ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
- "mrf_policy_simple_ftl_removal_desc": "このインスタンスは、つながっているすべてのネットワーク」タイムラインから、これらのインスタンスを、とりのぞきます:",
- "mrf_policy_simple_media_removal": "メディアをのぞく",
- "mrf_policy_simple_media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
- "mrf_policy_simple_media_nsfw": "メディアをすべてセンシティブにする",
- "mrf_policy_simple_media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
+ "mrf": {
+ "federation": "フェデレーション",
+ "mrf_policies": "ゆうこうなMRFポリシー",
+ "mrf_policies_desc": "MRFポリシーは、このインスタンスのフェデレーションのふるまいを、いじります。これらのMRFポリシーがゆうこうになっています:",
+ "simple": {
+ "simple_policies": "インスタンスのポリシー",
+ "accept": "うけいれ",
+ "accept_desc": "このインスンスは、これらのインスタンスからのメッセージのみをうけいれます:",
+ "reject": "おことわり",
+ "reject_desc": "このインスタンスは、これらのインスタンスからのメッセージをうけいれません:",
+ "quarantine": "けんえき",
+ "quarantine_desc": "このインスタンスは、これらのインスタンスに、パブリックなとうこうのみを、おくります:",
+ "ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
+ "ftl_removal_desc": "このインスタンスは、つながっているすべてのネットワーク」タイムラインから、これらのインスタンスを、とりのぞきます:",
+ "media_removal": "メディアをのぞく",
+ "media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
+ "media_nsfw": "メディアをすべてセンシティブにする",
+ "media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
+ }
+ },
+ "staff": "スタッフ"
},
"chat": {
"title": "チャット"
diff --git a/src/lib/event_target_polyfill.js b/src/lib/event_target_polyfill.js
@@ -0,0 +1,9 @@
+import EventTargetPolyfill from '@ungap/event-target'
+
+try {
+ /* eslint-disable no-new */
+ new EventTarget()
+ /* eslint-enable no-new */
+} catch (e) {
+ window.EventTarget = EventTargetPolyfill
+}
diff --git a/src/main.js b/src/main.js
@@ -2,6 +2,9 @@ import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
+import 'custom-event-polyfill'
+import './lib/event_target_polyfill.js'
+
import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js'
@@ -28,7 +31,6 @@ import VueChatScroll from 'vue-chat-scroll'
import VueClickOutside from 'v-click-outside'
import PortalVue from 'portal-vue'
import VBodyScrollLock from './directives/body_scroll_lock'
-import VTooltip from 'v-tooltip'
import afterStoreSetup from './boot/after_store.js'
@@ -41,13 +43,6 @@ Vue.use(VueChatScroll)
Vue.use(VueClickOutside)
Vue.use(PortalVue)
Vue.use(VBodyScrollLock)
-Vue.use(VTooltip, {
- popover: {
- defaultTrigger: 'hover click',
- defaultContainer: false,
- defaultOffset: 5
- }
-})
const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary
diff --git a/src/modules/api.js b/src/modules/api.js
@@ -146,6 +146,7 @@ const api = {
startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
+
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
stopFetchingFollowRequests (store) {
diff --git a/src/modules/config.js b/src/modules/config.js
@@ -5,6 +5,9 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
export const defaultState = {
colors: {},
+ theme: undefined,
+ customTheme: undefined,
+ customThemeSource: undefined,
hideISP: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default
@@ -20,6 +23,7 @@ export const defaultState = {
autoLoad: true,
streaming: false,
hoverPreview: true,
+ emojiReactionsOnTimeline: true,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: false,
@@ -29,7 +33,8 @@ export const defaultState = {
mentions: true,
likes: true,
repeats: true,
- moves: true
+ moves: true,
+ emojiReactions: false
},
webPushNotifications: false,
muteWords: [],
@@ -94,10 +99,10 @@ const config = {
commit('setOption', { name, value })
switch (name) {
case 'theme':
- setPreset(value, commit)
+ setPreset(value)
break
case 'customTheme':
- applyTheme(value, commit)
+ applyTheme(value)
}
}
}
diff --git a/src/modules/instance.js b/src/modules/instance.js
@@ -1,5 +1,6 @@
import { set } from 'vue'
-import { setPreset } from '../services/style_setter/style_setter.js'
+import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
+import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { instanceDefaultProperties } from './config.js'
const defaultState = {
@@ -10,6 +11,7 @@ const defaultState = {
textlimit: 5000,
server: 'http://localhost:4040/',
theme: 'pleroma-dark',
+ themeData: undefined,
background: '/static/aurora_borealis.jpg',
logo: '/static/logo.png',
logoMask: true,
@@ -96,6 +98,9 @@ const instance = {
dispatch('initializeSocket')
}
break
+ case 'theme':
+ dispatch('setTheme', value)
+ break
}
},
async getStaticEmoji ({ commit }) {
@@ -147,9 +152,23 @@ const instance = {
}
},
- setTheme ({ commit }, themeName) {
+ setTheme ({ commit, rootState }, themeName) {
commit('setInstanceOption', { name: 'theme', value: themeName })
- return setPreset(themeName, commit)
+ getPreset(themeName)
+ .then(themeData => {
+ commit('setInstanceOption', { name: 'themeData', value: themeData })
+ // No need to apply theme if there's user theme already
+ const { customTheme } = rootState.config
+ if (customTheme) return
+
+ // New theme presets don't have 'theme' property, they use 'source'
+ const themeSource = themeData.source
+ if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
+ applyTheme(themeSource)
+ } else {
+ applyTheme(themeData.theme)
+ }
+ })
},
fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
@@ -1,4 +1,17 @@
-import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
+import {
+ remove,
+ slice,
+ each,
+ findIndex,
+ find,
+ maxBy,
+ minBy,
+ merge,
+ first,
+ last,
+ isArray,
+ omitBy
+} from 'lodash'
import { set } from 'vue'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@@ -68,7 +81,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow',
- rootState.config.notificationVisibility.moves && 'move'
+ rootState.config.notificationVisibility.moves && 'move',
+ rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
].filter(_ => _)
}
@@ -312,6 +326,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
+ if (notification.type === 'pleroma:emoji_reaction') {
+ dispatch('fetchEmojiReactionsBy', notification.status.id)
+ }
+
// Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
state.notifications.maxId = notification.id > state.notifications.maxId
@@ -345,7 +363,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
break
}
- if (i18nString) {
+ if (notification.type === 'pleroma:emoji_reaction') {
+ notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
+ } else if (i18nString) {
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
} else {
notifObj.body = notification.status.text
@@ -358,10 +378,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
}
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
- let notification = new window.Notification(title, notifObj)
+ let desktopNotification = new window.Notification(title, notifObj)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
- setTimeout(notification.close.bind(notification), 5000)
+ setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
}
}
} else if (notification.seen) {
@@ -518,6 +538,53 @@ export const mutations = {
newStatus.fave_num = newStatus.favoritedBy.length
newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
},
+ addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
+ const status = state.allStatusesObject[id]
+ set(status, 'emoji_reactions', emojiReactions)
+ },
+ addOwnReaction (state, { id, emoji, currentUser }) {
+ const status = state.allStatusesObject[id]
+ const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
+ const reaction = status.emoji_reactions[reactionIndex] || { name: emoji, count: 0, accounts: [] }
+
+ const newReaction = {
+ ...reaction,
+ count: reaction.count + 1,
+ me: true,
+ accounts: [
+ ...reaction.accounts,
+ currentUser
+ ]
+ }
+
+ // Update count of existing reaction if it exists, otherwise append at the end
+ if (reactionIndex >= 0) {
+ set(status.emoji_reactions, reactionIndex, newReaction)
+ } else {
+ set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction])
+ }
+ },
+ removeOwnReaction (state, { id, emoji, currentUser }) {
+ const status = state.allStatusesObject[id]
+ const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
+ if (reactionIndex < 0) return
+
+ const reaction = status.emoji_reactions[reactionIndex]
+ const accounts = reaction.accounts || []
+
+ const newReaction = {
+ ...reaction,
+ count: reaction.count - 1,
+ me: false,
+ accounts: accounts.filter(acc => acc.id !== currentUser.id)
+ }
+
+ if (newReaction.count > 0) {
+ set(status.emoji_reactions, reactionIndex, newReaction)
+ } else {
+ set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji))
+ }
+ },
updateStatusWithPoll (state, { id, poll }) {
const status = state.allStatusesObject[id]
status.poll = poll
@@ -622,6 +689,35 @@ const statuses = {
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
})
},
+ reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
+ const currentUser = rootState.users.currentUser
+ if (!currentUser) return
+
+ commit('addOwnReaction', { id, emoji, currentUser })
+ rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
+ ok => {
+ dispatch('fetchEmojiReactionsBy', id)
+ }
+ )
+ },
+ unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
+ const currentUser = rootState.users.currentUser
+ if (!currentUser) return
+
+ commit('removeOwnReaction', { id, emoji, currentUser })
+ rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
+ ok => {
+ dispatch('fetchEmojiReactionsBy', id)
+ }
+ )
+ },
+ fetchEmojiReactionsBy ({ rootState, commit }, id) {
+ rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
+ emojiReactions => {
+ commit('addEmojiReactionsBy', { id, emojiReactions, currentUser: rootState.users.currentUser })
+ }
+ )
+ },
fetchFavs ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
diff --git a/src/modules/users.js b/src/modules/users.js
@@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
+const muteDomain = (store, domain) => {
+ return store.rootState.api.backendInteractor.muteDomain({ domain })
+ .then(() => store.commit('addDomainMute', domain))
+}
+
+const unmuteDomain = (store, domain) => {
+ return store.rootState.api.backendInteractor.unmuteDomain({ domain })
+ .then(() => store.commit('removeDomainMute', domain))
+}
+
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
@@ -177,6 +187,20 @@ export const mutations = {
state.currentUser.muteIds.push(muteId)
}
},
+ saveDomainMutes (state, domainMutes) {
+ state.currentUser.domainMutes = domainMutes
+ },
+ addDomainMute (state, domain) {
+ if (state.currentUser.domainMutes.indexOf(domain) === -1) {
+ state.currentUser.domainMutes.push(domain)
+ }
+ },
+ removeDomainMute (state, domain) {
+ const index = state.currentUser.domainMutes.indexOf(domain)
+ if (index !== -1) {
+ state.currentUser.domainMutes.splice(index, 1)
+ }
+ },
setPinnedToUser (state, status) {
const user = state.usersObject[status.user.id]
const index = user.pinnedStatusIds.indexOf(status.id)
@@ -297,6 +321,25 @@ const users = {
unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id)))
},
+ fetchDomainMutes (store) {
+ return store.rootState.api.backendInteractor.fetchDomainMutes()
+ .then((domainMutes) => {
+ store.commit('saveDomainMutes', domainMutes)
+ return domainMutes
+ })
+ },
+ muteDomain (store, domain) {
+ return muteDomain(store, domain)
+ },
+ unmuteDomain (store, domain) {
+ return unmuteDomain(store, domain)
+ },
+ muteDomains (store, domains = []) {
+ return Promise.all(domains.map(domain => muteDomain(store, domain)))
+ },
+ unmuteDomains (store, domain = []) {
+ return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
+ },
fetchFriends ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
@@ -331,9 +374,9 @@ const users = {
return rootState.api.backendInteractor.unsubscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
- toggleActivationStatus ({ rootState, commit }, user) {
+ toggleActivationStatus ({ rootState, commit }, { user }) {
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
- api(user)
+ api({ user })
.then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
},
registerPushNotifications (store) {
@@ -460,6 +503,7 @@ const users = {
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
+ user.domainMutes = []
commit('setCurrentUser', user)
commit('addNewUsers', [user])
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
@@ -72,7 +72,11 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_STREAMING = '/api/v1/streaming'
+const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
+const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const oldfetch = window.fetch
@@ -398,8 +402,8 @@ const fetchStatus = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
-const tagUser = ({ tag, credentials, ...options }) => {
- const screenName = options.screen_name
+const tagUser = ({ tag, credentials, user }) => {
+ const screenName = user.screen_name
const form = {
nicknames: [screenName],
tags: [tag]
@@ -415,8 +419,8 @@ const tagUser = ({ tag, credentials, ...options }) => {
})
}
-const untagUser = ({ tag, credentials, ...options }) => {
- const screenName = options.screen_name
+const untagUser = ({ tag, credentials, user }) => {
+ const screenName = user.screen_name
const body = {
nicknames: [screenName],
tags: [tag]
@@ -432,7 +436,7 @@ const untagUser = ({ tag, credentials, ...options }) => {
})
}
-const addRight = ({ right, credentials, ...user }) => {
+const addRight = ({ right, credentials, user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
@@ -442,7 +446,7 @@ const addRight = ({ right, credentials, ...user }) => {
})
}
-const deleteRight = ({ right, credentials, ...user }) => {
+const deleteRight = ({ right, credentials, user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
@@ -474,7 +478,7 @@ const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
}).then(response => get(response, 'users.0'))
}
-const deleteUser = ({ credentials, ...user }) => {
+const deleteUser = ({ credentials, user }) => {
const screenName = user.screen_name
const headers = authHeaders(credentials)
@@ -491,7 +495,8 @@ const fetchTimeline = ({
until = false,
userId = false,
tag = false,
- withMuted = false
+ withMuted = false,
+ withMove = false
}) => {
const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE,
@@ -531,6 +536,9 @@ const fetchTimeline = ({
if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false])
}
+ if (timeline === 'notifications') {
+ params.push(['with_move', withMove])
+ }
params.push(['count', 20])
params.push(['with_muted', withMuted])
@@ -880,6 +888,30 @@ const fetchRebloggedByUsers = ({ id }) => {
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
}
+const fetchEmojiReactions = ({ id, credentials }) => {
+ return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials })
+ .then((reactions) => reactions.map(r => {
+ r.accounts = r.accounts.map(parseUser)
+ return r
+ }))
+}
+
+const reactWithEmoji = ({ id, emoji, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_EMOJI_REACT_URL(id, emoji),
+ method: 'PUT',
+ credentials
+ }).then(parseStatus)
+}
+
+const unreactWithEmoji = ({ id, emoji, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
+ method: 'DELETE',
+ credentials
+ }).then(parseStatus)
+}
+
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
return promisedRequest({
url: MASTODON_REPORT_USER_URL,
@@ -948,6 +980,28 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
})
}
+const fetchDomainMutes = ({ credentials }) => {
+ return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
+}
+
+const muteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'POST',
+ payload: { domain },
+ credentials
+ })
+}
+
+const unmuteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'DELETE',
+ payload: { domain },
+ credentials
+ })
+}
+
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({
...(credentials
@@ -1107,10 +1161,16 @@ const apiService = {
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
+ fetchEmojiReactions,
+ reactWithEmoji,
+ unreactWithEmoji,
reportUser,
updateNotificationSettings,
search2,
- searchUsers
+ searchUsers,
+ fetchDomainMutes,
+ muteDomain,
+ unmuteDomain
}
export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -16,7 +16,7 @@ const backendInteractorService = credentials => ({
return notificationsFetcher.fetchAndUpdate({ store, credentials })
},
- startFetchingFollowRequest ({ store }) {
+ startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
@@ -1,16 +1,27 @@
-import { map } from 'lodash'
+import { invertLightness, contrastRatio } from 'chromatism'
-const rgb2hex = (r, g, b) => {
+// useful for visualizing color when debugging
+export const consoleColor = (color) => console.log('%c##########', 'background: ' + color + '; color: ' + color)
+
+/**
+ * Convert r, g, b values into hex notation. All components are [0-255]
+ *
+ * @param {Number|String|Object} r - Either red component, {r,g,b} object, or hex string
+ * @param {Number} [g] - Green component
+ * @param {Number} [b] - Blue component
+ */
+export const rgb2hex = (r, g, b) => {
if (r === null || typeof r === 'undefined') {
return undefined
}
- if (r[0] === '#') {
+ // TODO: clean up this mess
+ if (r[0] === '#' || r === 'transparent') {
return r
}
if (typeof r === 'object') {
({ r, g, b } = r)
}
- [r, g, b] = map([r, g, b], (val) => {
+ [r, g, b] = [r, g, b].map(val => {
val = Math.ceil(val)
val = val < 0 ? 0 : val
val = val > 255 ? 255 : val
@@ -58,7 +69,7 @@ const srgbToLinear = (srgb) => {
* @param {Object} srgb - sRGB color
* @returns {Number} relative luminance
*/
-const relativeLuminance = (srgb) => {
+export const relativeLuminance = (srgb) => {
const { r, g, b } = srgbToLinear(srgb)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
@@ -71,7 +82,7 @@ const relativeLuminance = (srgb) => {
* @param {Object} b - sRGB color
* @returns {Number} color ratio
*/
-const getContrastRatio = (a, b) => {
+export const getContrastRatio = (a, b) => {
const la = relativeLuminance(a)
const lb = relativeLuminance(b)
const [l1, l2] = la > lb ? [la, lb] : [lb, la]
@@ -80,6 +91,17 @@ const getContrastRatio = (a, b) => {
}
/**
+ * Same as `getContrastRatio` but for multiple layers in-between
+ *
+ * @param {Object} text - text color (topmost layer)
+ * @param {[Object, Number]} layers[] - layers between text and bedrock
+ * @param {Object} bedrock - layer at the very bottom
+ */
+export const getContrastRatioLayers = (text, layers, bedrock) => {
+ return getContrastRatio(alphaBlendLayers(bedrock, layers), text)
+}
+
+/**
* This performs alpha blending between solid background and semi-transparent foreground
*
* @param {Object} fg - top layer color
@@ -87,7 +109,7 @@ const getContrastRatio = (a, b) => {
* @param {Object} bg - bottom layer color
* @returns {Object} sRGB of resulting color
*/
-const alphaBlend = (fg, fga, bg) => {
+export const alphaBlend = (fg, fga, bg) => {
if (fga === 1 || typeof fga === 'undefined') return fg
return 'rgb'.split('').reduce((acc, c) => {
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
@@ -97,14 +119,30 @@ const alphaBlend = (fg, fga, bg) => {
}, {})
}
-const invert = (rgb) => {
+/**
+ * Same as `alphaBlend` but for multiple layers in-between
+ *
+ * @param {Object} bedrock - layer at the very bottom
+ * @param {[Object, Number]} layers[] - layers between text and bedrock
+ */
+export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color, opacity]) => {
+ return alphaBlend(color, opacity, acc)
+}, bedrock)
+
+export const invert = (rgb) => {
return 'rgb'.split('').reduce((acc, c) => {
acc[c] = 255 - rgb[c]
return acc
}, {})
}
-const hex2rgb = (hex) => {
+/**
+ * Converts #rrggbb hex notation into an {r, g, b} object
+ *
+ * @param {String} hex - #rrggbb string
+ * @returns {Object} rgb representation of the color, values are 0-255
+ */
+export const hex2rgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
@@ -113,18 +151,72 @@ const hex2rgb = (hex) => {
} : null
}
-const mixrgb = (a, b) => {
- return Object.keys(a).reduce((acc, k) => {
+/**
+ * Old somewhat weird function for mixing two colors together
+ *
+ * @param {Object} a - one color (rgb)
+ * @param {Object} b - other color (rgb)
+ * @returns {Object} result
+ */
+export const mixrgb = (a, b) => {
+ return 'rgb'.split('').reduce((acc, k) => {
acc[k] = (a[k] + b[k]) / 2
return acc
}, {})
}
+/**
+ * Converts rgb object into a CSS rgba() color
+ *
+ * @param {Object} color - rgb
+ * @returns {String} CSS rgba() color
+ */
+export const rgba2css = function (rgba) {
+ return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})`
+}
-export {
- rgb2hex,
- hex2rgb,
- mixrgb,
- invert,
- getContrastRatio,
- alphaBlend
+/**
+ * Get text color for given background color and intended text color
+ * This checks if text and background don't have enough color and inverts
+ * text color's lightness if needed. If text color is still not enough it
+ * will fall back to black or white
+ *
+ * @param {Object} bg - background color
+ * @param {Object} text - intended text color
+ * @param {Boolean} preserve - try to preserve intended text color's hue/saturation (i.e. no BW)
+ */
+export const getTextColor = function (bg, text, preserve) {
+ const contrast = getContrastRatio(bg, text)
+
+ if (contrast < 4.5) {
+ const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
+ const result = Object.assign(base, invertLightness(text).rgb)
+ if (!preserve && getContrastRatio(bg, result) < 4.5) {
+ // B&W
+ return contrastRatio(bg, text).rgb
+ }
+ // Inverted color
+ return result
+ }
+ return text
+}
+
+/**
+ * Converts color to CSS Color value
+ *
+ * @param {Object|String} input - color
+ * @param {Number} [a] - alpha value
+ * @returns {String} a CSS Color value
+ */
+export const getCssColor = (input, a) => {
+ let rgb = {}
+ if (typeof input === 'object') {
+ rgb = input
+ } else if (typeof input === 'string') {
+ if (input.startsWith('#')) {
+ rgb = hex2rgb(input)
+ } else {
+ return input
+ }
+ }
+ return rgba2css({ ...rgb, a })
}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,3 +1,5 @@
+import escape from 'escape-html'
+
const qvitterStatusType = (status) => {
if (status.is_post_verb) {
return 'status'
@@ -41,7 +43,7 @@ export const parseUser = (data) => {
}
output.name = data.display_name
- output.name_html = addEmojis(data.display_name, data.emojis)
+ output.name_html = addEmojis(escape(data.display_name), data.emojis)
output.description = data.note
output.description_html = addEmojis(data.note, data.emojis)
@@ -81,6 +83,8 @@ export const parseUser = (data) => {
output.subscribed = relationship.subscribing
}
+ output.allow_following_move = data.pleroma.allow_following_move
+
output.hide_follows = data.pleroma.hide_follows
output.hide_followers = data.pleroma.hide_followers
output.hide_follows_count = data.pleroma.hide_follows_count
@@ -242,6 +246,7 @@ export const parseStatus = (data) => {
output.is_local = pleroma.local
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
output.thread_muted = pleroma.thread_muted
+ output.emoji_reactions = pleroma.emoji_reactions
} else {
output.text = data.content
output.summary = data.spoiler_text
@@ -255,7 +260,7 @@ export const parseStatus = (data) => {
output.retweeted_status = parseStatus(data.reblog)
}
- output.summary_html = addEmojis(data.spoiler_text, data.emojis)
+ output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
output.external_url = data.url
output.poll = data.poll
output.pinned = data.pinned
@@ -349,6 +354,7 @@ export const parseNotification = (data) => {
? null
: parseUser(data.target)
output.from_profile = parseUser(data.account)
+ output.emoji = data.emoji
} else {
const parsedNotice = parseStatus(data.notice)
output.type = data.ntype
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
@@ -7,7 +7,8 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow',
- store.state.config.notificationVisibility.moves && 'move'
+ store.state.config.notificationVisibility.moves && 'move',
+ store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
].filter(_ => _))
const sortById = (a, b) => {
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -11,9 +11,12 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
+ const allowFollowingMove = rootState.users.currentUser.allow_following_move
args['withMuted'] = !hideMutedPosts
+ args['withMove'] = !allowFollowingMove
+
args['timeline'] = 'notifications'
if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
@@ -1,78 +1,9 @@
-import { times } from 'lodash'
-import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
-import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.js'
+import { convert } from 'chromatism'
+import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js'
+import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js'
-// While this is not used anymore right now, I left it in if we want to do custom
-// styles that aren't just colors, so user can pick from a few different distinct
-// styles as well as set their own colors in the future.
-
-const setStyle = (href, commit) => {
- /***
- What's going on here?
- I want to make it easy for admins to style this application. To have
- a good set of default themes, I chose the system from base16
- (https://chriskempson.github.io/base16/) to style all elements. They
- all have the base00..0F classes. So the only thing an admin needs to
- do to style Pleroma is to change these colors in that one css file.
- Some default things (body text color, link color) need to be set dy-
- namically, so this is done here by waiting for the stylesheet to be
- loaded and then creating an element with the respective classes.
-
- It is a bit weird, but should make life for admins somewhat easier.
- ***/
- const head = document.head
- const body = document.body
- body.classList.add('hidden')
- const cssEl = document.createElement('link')
- cssEl.setAttribute('rel', 'stylesheet')
- cssEl.setAttribute('href', href)
- head.appendChild(cssEl)
-
- const setDynamic = () => {
- const baseEl = document.createElement('div')
- body.appendChild(baseEl)
-
- let colors = {}
- times(16, (n) => {
- const name = `base0${n.toString(16).toUpperCase()}`
- baseEl.setAttribute('class', name)
- const color = window.getComputedStyle(baseEl).getPropertyValue('color')
- colors[name] = color
- })
-
- body.removeChild(baseEl)
-
- const styleEl = document.createElement('style')
- head.appendChild(styleEl)
- // const styleSheet = styleEl.sheet
-
- body.classList.remove('hidden')
- }
-
- cssEl.addEventListener('load', setDynamic)
-}
-
-const rgb2rgba = function (rgba) {
- return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
-}
-
-const getTextColor = function (bg, text, preserve) {
- const bgIsLight = convert(bg).hsl.l > 50
- const textIsLight = convert(text).hsl.l > 50
-
- if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
- const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
- const result = Object.assign(base, invertLightness(text).rgb)
- if (!preserve && getContrastRatio(bg, result) < 4.5) {
- return contrastRatio(bg, text).rgb
- }
- return result
- }
- return text
-}
-
-const applyTheme = (input, commit) => {
- const { rules, theme } = generatePreset(input)
+export const applyTheme = (input) => {
+ const { rules } = generatePreset(input)
const head = document.head
const body = document.body
body.classList.add('hidden')
@@ -87,14 +18,9 @@ const applyTheme = (input, commit) => {
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
body.classList.remove('hidden')
-
- // commit('setOption', { name: 'colors', value: htmlColors })
- // commit('setOption', { name: 'radii', value: radii })
- commit('setOption', { name: 'customTheme', value: input })
- commit('setOption', { name: 'colors', value: theme.colors })
}
-const getCssShadow = (input, usesDropShadow) => {
+export const getCssShadow = (input, usesDropShadow) => {
if (input.length === 0) {
return 'none'
}
@@ -132,122 +58,18 @@ const getCssShadowFilter = (input) => {
.join(' ')
}
-const getCssColor = (input, a) => {
- let rgb = {}
- if (typeof input === 'object') {
- rgb = input
- } else if (typeof input === 'string') {
- if (input.startsWith('#')) {
- rgb = hex2rgb(input)
- } else if (input.startsWith('--')) {
- return `var(${input})`
- } else {
- return input
- }
- }
- return rgb2rgba({ ...rgb, a })
-}
-
-const generateColors = (input) => {
- const colors = {}
- const opacity = Object.assign({
- alert: 0.5,
- input: 0.5,
- faint: 0.5
- }, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => {
- if (typeof v !== 'undefined') {
- acc[k] = v
- }
- return acc
- }, {}))
- const col = Object.entries(input.colors || input).reduce((acc, [k, v]) => {
- if (typeof v === 'object') {
- acc[k] = v
- } else {
- acc[k] = hex2rgb(v)
- }
- return acc
- }, {})
-
- const isLightOnDark = convert(col.bg).hsl.l < convert(col.text).hsl.l
- const mod = isLightOnDark ? 1 : -1
-
- colors.text = col.text
- colors.lightText = brightness(20 * mod, colors.text).rgb
- colors.link = col.link
- colors.faint = col.faint || Object.assign({}, col.text)
-
- colors.bg = col.bg
- colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb
-
- colors.fg = col.fg
- colors.fgText = col.fgText || getTextColor(colors.fg, colors.text)
- colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true)
-
- colors.border = col.border || brightness(2 * mod, colors.fg).rgb
-
- colors.btn = col.btn || Object.assign({}, col.fg)
- colors.btnText = col.btnText || getTextColor(colors.btn, colors.fgText)
-
- colors.input = col.input || Object.assign({}, col.fg)
- colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText)
-
- colors.panel = col.panel || Object.assign({}, col.fg)
- colors.panelText = col.panelText || getTextColor(colors.panel, colors.fgText)
- colors.panelLink = col.panelLink || getTextColor(colors.panel, colors.fgLink)
- colors.panelFaint = col.panelFaint || getTextColor(colors.panel, colors.faint)
-
- colors.topBar = col.topBar || Object.assign({}, col.fg)
- colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText)
- colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink)
-
- colors.faintLink = col.faintLink || Object.assign({}, col.link)
- colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
-
- colors.icon = mixrgb(colors.bg, colors.text)
-
- colors.cBlue = col.cBlue || hex2rgb('#0000FF')
- colors.cRed = col.cRed || hex2rgb('#FF0000')
- colors.cGreen = col.cGreen || hex2rgb('#00FF00')
- colors.cOrange = col.cOrange || hex2rgb('#E3FF00')
+export const generateColors = (themeData) => {
+ const sourceColors = !themeData.themeEngineVersion
+ ? colors2to3(themeData.colors || themeData)
+ : themeData.colors || themeData
- colors.alertError = col.alertError || Object.assign({}, colors.cRed)
- colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text)
- colors.alertErrorPanelText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.panel), colors.panelText)
-
- colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange)
- colors.alertWarningText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.bg), colors.text)
- colors.alertWarningPanelText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.panel), colors.panelText)
-
- colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed)
- colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb
-
- Object.entries(opacity).forEach(([ k, v ]) => {
- if (typeof v === 'undefined') return
- if (k === 'alert') {
- colors.alertError.a = v
- colors.alertWarning.a = v
- return
- }
- if (k === 'faint') {
- colors[k + 'Link'].a = v
- colors['panelFaint'].a = v
- }
- if (k === 'bg') {
- colors['lightBg'].a = v
- }
- if (colors[k]) {
- colors[k].a = v
- } else {
- console.error('Wrong key ' + k)
- }
- })
+ const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
const htmlColors = Object.entries(colors)
.reduce((acc, [k, v]) => {
if (!v) return acc
acc.solid[k] = rgb2hex(v)
- acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v)
+ acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
return acc
}, { complete: {}, solid: {} })
return {
@@ -264,7 +86,7 @@ const generateColors = (input) => {
}
}
-const generateRadii = (input) => {
+export const generateRadii = (input) => {
let inputRadii = input.radii || {}
// v1 -> v2
if (typeof input.btnRadius !== 'undefined') {
@@ -297,7 +119,7 @@ const generateRadii = (input) => {
}
}
-const generateFonts = (input) => {
+export const generateFonts = (input) => {
const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
acc[k] = v
@@ -332,89 +154,123 @@ const generateFonts = (input) => {
}
}
-const generateShadows = (input) => {
- const border = (top, shadow) => ({
- x: 0,
- y: top ? 1 : -1,
- blur: 0,
+const border = (top, shadow) => ({
+ x: 0,
+ y: top ? 1 : -1,
+ blur: 0,
+ spread: 0,
+ color: shadow ? '#000000' : '#FFFFFF',
+ alpha: 0.2,
+ inset: true
+})
+const buttonInsetFakeBorders = [border(true, false), border(false, true)]
+const inputInsetFakeBorders = [border(true, true), border(false, false)]
+const hoverGlow = {
+ x: 0,
+ y: 0,
+ blur: 4,
+ spread: 0,
+ color: '--faint',
+ alpha: 1
+}
+
+export const DEFAULT_SHADOWS = {
+ panel: [{
+ x: 1,
+ y: 1,
+ blur: 4,
spread: 0,
- color: shadow ? '#000000' : '#FFFFFF',
- alpha: 0.2,
- inset: true
- })
- const buttonInsetFakeBorders = [border(true, false), border(false, true)]
- const inputInsetFakeBorders = [border(true, true), border(false, false)]
- const hoverGlow = {
+ color: '#000000',
+ alpha: 0.6
+ }],
+ topBar: [{
x: 0,
y: 0,
blur: 4,
spread: 0,
- color: '--faint',
+ color: '#000000',
+ alpha: 0.6
+ }],
+ popup: [{
+ x: 2,
+ y: 2,
+ blur: 3,
+ spread: 0,
+ color: '#000000',
+ alpha: 0.5
+ }],
+ avatar: [{
+ x: 0,
+ y: 1,
+ blur: 8,
+ spread: 0,
+ color: '#000000',
+ alpha: 0.7
+ }],
+ avatarStatus: [],
+ panelHeader: [],
+ button: [{
+ x: 0,
+ y: 0,
+ blur: 2,
+ spread: 0,
+ color: '#000000',
alpha: 1
+ }, ...buttonInsetFakeBorders],
+ buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
+ buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
+ input: [...inputInsetFakeBorders, {
+ x: 0,
+ y: 0,
+ blur: 2,
+ inset: true,
+ spread: 0,
+ color: '#000000',
+ alpha: 1
+ }]
+}
+export const generateShadows = (input, colors) => {
+ // TODO this is a small hack for `mod` to work with shadows
+ // this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element
+ const hackContextDict = {
+ button: 'btn',
+ panel: 'bg',
+ top: 'topBar',
+ popup: 'popover',
+ avatar: 'bg',
+ panelHeader: 'panel',
+ input: 'input'
}
-
- const shadows = {
- panel: [{
- x: 1,
- y: 1,
- blur: 4,
- spread: 0,
- color: '#000000',
- alpha: 0.6
- }],
- topBar: [{
- x: 0,
- y: 0,
- blur: 4,
- spread: 0,
- color: '#000000',
- alpha: 0.6
- }],
- popup: [{
- x: 2,
- y: 2,
- blur: 3,
- spread: 0,
- color: '#000000',
- alpha: 0.5
- }],
- avatar: [{
- x: 0,
- y: 1,
- blur: 8,
- spread: 0,
- color: '#000000',
- alpha: 0.7
- }],
- avatarStatus: [],
- panelHeader: [],
- button: [{
- x: 0,
- y: 0,
- blur: 2,
- spread: 0,
- color: '#000000',
- alpha: 1
- }, ...buttonInsetFakeBorders],
- buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
- buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
- input: [...inputInsetFakeBorders, {
- x: 0,
- y: 0,
- blur: 2,
- inset: true,
- spread: 0,
- color: '#000000',
- alpha: 1
- }],
- ...(input.shadows || {})
- }
+ const inputShadows = input.shadows && !input.themeEngineVersion
+ ? shadows2to3(input.shadows, input.opacity)
+ : input.shadows || {}
+ const shadows = Object.entries({
+ ...DEFAULT_SHADOWS,
+ ...inputShadows
+ }).reduce((shadowsAcc, [slotName, shadowDefs]) => {
+ const slotFirstWord = slotName.replace(/[A-Z].*$/, '')
+ const colorSlotName = hackContextDict[slotFirstWord]
+ const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
+ const mod = isLightOnDark ? 1 : -1
+ const newShadow = shadowDefs.reduce((shadowAcc, def) => [
+ ...shadowAcc,
+ {
+ ...def,
+ color: rgb2hex(computeDynamicColor(
+ def.color,
+ (variableSlot) => convert(colors[variableSlot]).rgb,
+ mod
+ ))
+ }
+ ], [])
+ return { ...shadowsAcc, [slotName]: newShadow }
+ }, {})
return {
rules: {
shadows: Object
.entries(shadows)
- // TODO for v2.1: if shadow doesn't have non-inset shadows with spread > 0 - optionally
+ // TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
// convert all non-inset shadows into filter: drop-shadow() to boost performance
.map(([k, v]) => [
`--${k}Shadow: ${getCssShadow(v)}`,
@@ -429,7 +285,7 @@ const generateShadows = (input) => {
}
}
-const composePreset = (colors, radii, shadows, fonts) => {
+export const composePreset = (colors, radii, shadows, fonts) => {
return {
rules: {
...shadows.rules,
@@ -446,98 +302,110 @@ const composePreset = (colors, radii, shadows, fonts) => {
}
}
-const generatePreset = (input) => {
- const shadows = generateShadows(input)
+export const generatePreset = (input) => {
const colors = generateColors(input)
- const radii = generateRadii(input)
- const fonts = generateFonts(input)
-
- return composePreset(colors, radii, shadows, fonts)
+ return composePreset(
+ colors,
+ generateRadii(input),
+ generateShadows(input, colors.theme.colors, colors.mod),
+ generateFonts(input)
+ )
}
-const getThemes = () => {
- return window.fetch('/static/styles.json')
+export const getThemes = () => {
+ const cache = 'no-store'
+
+ return window.fetch('/static/styles.json', { cache })
.then((data) => data.json())
.then((themes) => {
- return Promise.all(Object.entries(themes).map(([k, v]) => {
+ return Object.entries(themes).map(([k, v]) => {
+ let promise = null
if (typeof v === 'object') {
- return Promise.resolve([k, v])
+ promise = Promise.resolve(v)
} else if (typeof v === 'string') {
- return window.fetch(v)
+ promise = window.fetch(v, { cache })
.then((data) => data.json())
- .then((theme) => {
- return [k, theme]
- })
.catch((e) => {
console.error(e)
- return []
+ return null
})
}
- }))
+ return [k, promise]
+ })
})
.then((promises) => {
return promises
- .filter(([k, v]) => v)
.reduce((acc, [k, v]) => {
acc[k] = v
return acc
}, {})
})
}
+export const colors2to3 = (colors) => {
+ return Object.entries(colors).reduce((acc, [slotName, color]) => {
+ const btnPositions = ['', 'Panel', 'TopBar']
+ switch (slotName) {
+ case 'lightBg':
+ return { ...acc, highlight: color }
+ case 'btnText':
+ return {
+ ...acc,
+ ...btnPositions
+ .reduce(
+ (statePositionAcc, position) =>
+ ({ ...statePositionAcc, ['btn' + position + 'Text']: color })
+ , {}
+ )
+ }
+ default:
+ return { ...acc, [slotName]: color }
+ }
+ }, {})
+}
-const setPreset = (val, commit) => {
- return getThemes().then((themes) => {
- const theme = themes[val] ? themes[val] : themes['pleroma-dark']
- const isV1 = Array.isArray(theme)
- const data = isV1 ? {} : theme.theme
-
- if (isV1) {
- const bgRgb = hex2rgb(theme[1])
- const fgRgb = hex2rgb(theme[2])
- const textRgb = hex2rgb(theme[3])
- const linkRgb = hex2rgb(theme[4])
-
- const cRedRgb = hex2rgb(theme[5] || '#FF0000')
- const cGreenRgb = hex2rgb(theme[6] || '#00FF00')
- const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
- const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
+/**
+ * This handles compatibility issues when importing v2 theme's shadows to current format
+ *
+ * Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
+ */
+export const shadows2to3 = (shadows, opacity) => {
+ return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
+ const isDynamic = ({ color }) => color.startsWith('--')
+ const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
+ const newShadow = shadowDefs.reduce((shadowAcc, def) => [
+ ...shadowAcc,
+ {
+ ...def,
+ alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
+ }
+ ], [])
+ return { ...shadowsAcc, [slotName]: newShadow }
+ }, {})
+}
- data.colors = {
- bg: bgRgb,
- fg: fgRgb,
- text: textRgb,
- link: linkRgb,
- cRed: cRedRgb,
- cBlue: cBlueRgb,
- cGreen: cGreenRgb,
- cOrange: cOrangeRgb
+export const getPreset = (val) => {
+ return getThemes()
+ .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
+ .then((theme) => {
+ const isV1 = Array.isArray(theme)
+ const data = isV1 ? {} : theme.theme
+
+ if (isV1) {
+ const bg = hex2rgb(theme[1])
+ const fg = hex2rgb(theme[2])
+ const text = hex2rgb(theme[3])
+ const link = hex2rgb(theme[4])
+
+ const cRed = hex2rgb(theme[5] || '#FF0000')
+ const cGreen = hex2rgb(theme[6] || '#00FF00')
+ const cBlue = hex2rgb(theme[7] || '#0000FF')
+ const cOrange = hex2rgb(theme[8] || '#E3FF00')
+
+ data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
}
- }
- // This is a hack, this function is only called during initial load.
- // We want to cancel loading the theme from config.json if we're already
- // loading a theme from the persisted state.
- // Needed some way of dealing with the async way of things.
- // load config -> set preset -> wait for styles.json to load ->
- // load persisted state -> set colors -> styles.json loaded -> set colors
- if (!window.themeLoaded) {
- applyTheme(data, commit)
- }
- })
+ return { theme: data, source: theme.source }
+ })
}
-export {
- setStyle,
- setPreset,
- applyTheme,
- getTextColor,
- generateColors,
- generateRadii,
- generateShadows,
- generateFonts,
- generatePreset,
- getThemes,
- composePreset,
- getCssShadow,
- getCssShadowFilter
-}
+export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
@@ -0,0 +1,631 @@
+import { invertLightness, brightness } from 'chromatism'
+import { alphaBlend, mixrgb } from '../color_convert/color_convert.js'
+/* This is a definition of all layer combinations
+ * each key is a topmost layer, each value represents layer underneath
+ * this is essentially a simplified tree
+ */
+export const LAYERS = {
+ undelay: null, // root
+ topBar: null, // no transparency support
+ badge: null, // no transparency support
+ profileTint: null, // doesn't matter
+ fg: null,
+ bg: 'underlay',
+ highlight: 'bg',
+ panel: 'bg',
+ popover: 'bg',
+ selectedMenu: 'popover',
+ btn: 'bg',
+ btnPanel: 'panel',
+ btnTopBar: 'topBar',
+ input: 'bg',
+ inputPanel: 'panel',
+ inputTopBar: 'topBar',
+ alert: 'bg',
+ alertPanel: 'panel',
+ poll: 'bg'
+}
+
+/* By default opacity slots have 1 as default opacity
+ * this allows redefining it to something else
+ */
+export const DEFAULT_OPACITY = {
+ profileTint: 0.5,
+ alert: 0.5,
+ input: 0.5,
+ faint: 0.5,
+ underlay: 0.15
+}
+
+/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
+ * Color and opacity slots definitions. Each key represents a slot.
+ *
+ * Short-hands:
+ * String beginning with `--` - value after dashes treated as sole
+ * dependency - i.e. `--value` equivalent to { depends: ['value']}
+ * String beginning with `#` - value would be treated as solid color
+ * defined in hexadecimal representation (i.e. #FFFFFF) and will be
+ * used as default. `#FFFFFF` is equivalent to { default: '#FFFFFF'}
+ *
+ * Full definition:
+ * @property {String[]} depends - color slot names this color depends ones.
+ * cyclic dependencies are supported to some extent but not recommended.
+ * @property {String} [opacity] - opacity slot used by this color slot.
+ * opacity is inherited from parents. To break inheritance graph use null
+ * @property {Number} [priority] - EXPERIMENTAL. used to pre-sort slots so
+ * that slots with higher priority come earlier
+ * @property {Function(mod, ...colors)} [color] - function that will be
+ * used to determine the color. By default it just copies first color in
+ * dependency list.
+ * @argument {Number} mod - `1` (light-on-dark) or `-1` (dark-on-light)
+ * depending on background color (for textColor)/given color.
+ * @argument {...Object} deps - each argument after mod represents each
+ * color from `depends` array. All colors take user customizations into
+ * account and represented by { r, g, b } objects.
+ * @returns {Object} resulting color, should be in { r, g, b } form
+ *
+ * @property {Boolean|String} [textColor] - true to mark color slot as text
+ * color. This enables automatic text color generation for the slot. Use
+ * 'preserve' string if you don't want text color to fall back to
+ * black/white. Use 'bw' to only ever use black or white. This also makes
+ * following properties required:
+ * @property {String} [layer] - which layer the text sit on top on - used
+ * to account for transparency in text color calculation
+ * layer is inherited from parents. To break inheritance graph use null
+ * @property {String} [variant] - which color slot is background (same as
+ * above, used to account for transparency)
+ */
+export const SLOT_INHERITANCE = {
+ bg: {
+ depends: [],
+ opacity: 'bg',
+ priority: 1
+ },
+ fg: {
+ depends: [],
+ priority: 1
+ },
+ text: {
+ depends: [],
+ layer: 'bg',
+ opacity: null,
+ priority: 1
+ },
+ underlay: {
+ default: '#000000',
+ opacity: 'underlay'
+ },
+ link: {
+ depends: ['accent'],
+ priority: 1
+ },
+ accent: {
+ depends: ['link'],
+ priority: 1
+ },
+ faint: {
+ depends: ['text'],
+ opacity: 'faint'
+ },
+ faintLink: {
+ depends: ['link'],
+ opacity: 'faint'
+ },
+ postFaintLink: {
+ depends: ['postLink'],
+ opacity: 'faint'
+ },
+
+ cBlue: '#0000ff',
+ cRed: '#FF0000',
+ cGreen: '#00FF00',
+ cOrange: '#E3FF00',
+
+ profileBg: {
+ depends: ['bg'],
+ color: (mod, bg) => ({
+ r: Math.floor(bg.r * 0.53),
+ g: Math.floor(bg.g * 0.56),
+ b: Math.floor(bg.b * 0.59)
+ })
+ },
+ profileTint: {
+ depends: ['bg'],
+ layer: 'profileTint',
+ opacity: 'profileTint'
+ },
+
+ highlight: {
+ depends: ['bg'],
+ color: (mod, bg) => brightness(5 * mod, bg).rgb
+ },
+ highlightLightText: {
+ depends: ['lightText'],
+ layer: 'highlight',
+ textColor: true
+ },
+ highlightPostLink: {
+ depends: ['postLink'],
+ layer: 'highlight',
+ textColor: 'preserve'
+ },
+ highlightFaintText: {
+ depends: ['faint'],
+ layer: 'highlight',
+ textColor: true
+ },
+ highlightFaintLink: {
+ depends: ['faintLink'],
+ layer: 'highlight',
+ textColor: 'preserve'
+ },
+ highlightPostFaintLink: {
+ depends: ['postFaintLink'],
+ layer: 'highlight',
+ textColor: 'preserve'
+ },
+ highlightText: {
+ depends: ['text'],
+ layer: 'highlight',
+ textColor: true
+ },
+ highlightLink: {
+ depends: ['link'],
+ layer: 'highlight',
+ textColor: 'preserve'
+ },
+ highlightIcon: {
+ depends: ['highlight', 'highlightText'],
+ color: (mod, bg, text) => mixrgb(bg, text)
+ },
+
+ popover: {
+ depends: ['bg'],
+ opacity: 'popover'
+ },
+ popoverLightText: {
+ depends: ['lightText'],
+ layer: 'popover',
+ textColor: true
+ },
+ popoverPostLink: {
+ depends: ['postLink'],
+ layer: 'popover',
+ textColor: 'preserve'
+ },
+ popoverFaintText: {
+ depends: ['faint'],
+ layer: 'popover',
+ textColor: true
+ },
+ popoverFaintLink: {
+ depends: ['faintLink'],
+ layer: 'popover',
+ textColor: 'preserve'
+ },
+ popoverPostFaintLink: {
+ depends: ['postFaintLink'],
+ layer: 'popover',
+ textColor: 'preserve'
+ },
+ popoverText: {
+ depends: ['text'],
+ layer: 'popover',
+ textColor: true
+ },
+ popoverLink: {
+ depends: ['link'],
+ layer: 'popover',
+ textColor: 'preserve'
+ },
+ popoverIcon: {
+ depends: ['popover', 'popoverText'],
+ color: (mod, bg, text) => mixrgb(bg, text)
+ },
+
+ selectedPost: '--highlight',
+ selectedPostFaintText: {
+ depends: ['highlightFaintText'],
+ layer: 'highlight',
+ variant: 'selectedPost',
+ textColor: true
+ },
+ selectedPostLightText: {
+ depends: ['highlightLightText'],
+ layer: 'highlight',
+ variant: 'selectedPost',
+ textColor: true
+ },
+ selectedPostPostLink: {
+ depends: ['highlightPostLink'],
+ layer: 'highlight',
+ variant: 'selectedPost',
+ textColor: 'preserve'
+ },
+ selectedPostFaintLink: {
+ depends: ['highlightFaintLink'],
+ layer: 'highlight',
+ variant: 'selectedPost',
+ textColor: 'preserve'
+ },
+ selectedPostText: {
+ depends: ['highlightText'],
+ layer: 'highlight',
+ variant: 'selectedPost',
+ textColor: true
+ },
+ selectedPostLink: {
+ depends: ['highlightLink'],
+ layer: 'highlight',
+ variant: 'selectedPost',
+ textColor: 'preserve'
+ },
+ selectedPostIcon: {
+ depends: ['selectedPost', 'selectedPostText'],
+ color: (mod, bg, text) => mixrgb(bg, text)
+ },
+
+ selectedMenu: {
+ depends: ['bg'],
+ color: (mod, bg) => brightness(5 * mod, bg).rgb
+ },
+ selectedMenuLightText: {
+ depends: ['highlightLightText'],
+ layer: 'selectedMenu',
+ variant: 'selectedMenu',
+ textColor: true
+ },
+ selectedMenuFaintText: {
+ depends: ['highlightFaintText'],
+ layer: 'selectedMenu',
+ variant: 'selectedMenu',
+ textColor: true
+ },
+ selectedMenuFaintLink: {
+ depends: ['highlightFaintLink'],
+ layer: 'selectedMenu',
+ variant: 'selectedMenu',
+ textColor: 'preserve'
+ },
+ selectedMenuText: {
+ depends: ['highlightText'],
+ layer: 'selectedMenu',
+ variant: 'selectedMenu',
+ textColor: true
+ },
+ selectedMenuLink: {
+ depends: ['highlightLink'],
+ layer: 'selectedMenu',
+ variant: 'selectedMenu',
+ textColor: 'preserve'
+ },
+ selectedMenuIcon: {
+ depends: ['selectedMenu', 'selectedMenuText'],
+ color: (mod, bg, text) => mixrgb(bg, text)
+ },
+
+ selectedMenuPopover: {
+ depends: ['popover'],
+ color: (mod, bg) => brightness(5 * mod, bg).rgb
+ },
+ selectedMenuPopoverLightText: {
+ depends: ['selectedMenuLightText'],
+ layer: 'selectedMenuPopover',
+ variant: 'selectedMenuPopover',
+ textColor: true
+ },
+ selectedMenuPopoverFaintText: {
+ depends: ['selectedMenuFaintText'],
+ layer: 'selectedMenuPopover',
+ variant: 'selectedMenuPopover',
+ textColor: true
+ },
+ selectedMenuPopoverFaintLink: {
+ depends: ['selectedMenuFaintLink'],
+ layer: 'selectedMenuPopover',
+ variant: 'selectedMenuPopover',
+ textColor: 'preserve'
+ },
+ selectedMenuPopoverText: {
+ depends: ['selectedMenuText'],
+ layer: 'selectedMenuPopover',
+ variant: 'selectedMenuPopover',
+ textColor: true
+ },
+ selectedMenuPopoverLink: {
+ depends: ['selectedMenuLink'],
+ layer: 'selectedMenuPopover',
+ variant: 'selectedMenuPopover',
+ textColor: 'preserve'
+ },
+ selectedMenuPopoverIcon: {
+ depends: ['selectedMenuPopover', 'selectedMenuText'],
+ color: (mod, bg, text) => mixrgb(bg, text)
+ },
+
+ lightText: {
+ depends: ['text'],
+ layer: 'bg',
+ textColor: 'preserve',
+ color: (mod, text) => brightness(20 * mod, text).rgb
+ },
+
+ postLink: {
+ depends: ['link'],
+ layer: 'bg',
+ textColor: 'preserve'
+ },
+
+ border: {
+ depends: ['fg'],
+ opacity: 'border',
+ color: (mod, fg) => brightness(2 * mod, fg).rgb
+ },
+
+ poll: {
+ depends: ['accent', 'bg'],
+ copacity: 'poll',
+ color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
+ },
+ pollText: {
+ depends: ['text'],
+ layer: 'poll',
+ textColor: true
+ },
+
+ icon: {
+ depends: ['bg', 'text'],
+ inheritsOpacity: false,
+ color: (mod, bg, text) => mixrgb(bg, text)
+ },
+
+ // Foreground
+ fgText: {
+ depends: ['text'],
+ layer: 'fg',
+ textColor: true
+ },
+ fgLink: {
+ depends: ['link'],
+ layer: 'fg',
+ textColor: 'preserve'
+ },
+
+ // Panel header
+ panel: {
+ depends: ['fg'],
+ opacity: 'panel'
+ },
+ panelText: {
+ depends: ['text'],
+ layer: 'panel',
+ textColor: true
+ },
+ panelFaint: {
+ depends: ['fgText'],
+ layer: 'panel',
+ opacity: 'faint',
+ textColor: true
+ },
+ panelLink: {
+ depends: ['fgLink'],
+ layer: 'panel',
+ textColor: 'preserve'
+ },
+
+ // Top bar
+ topBar: '--fg',
+ topBarText: {
+ depends: ['fgText'],
+ layer: 'topBar',
+ textColor: true
+ },
+ topBarLink: {
+ depends: ['fgLink'],
+ layer: 'topBar',
+ textColor: 'preserve'
+ },
+
+ // Tabs
+ tab: {
+ depends: ['btn']
+ },
+ tabText: {
+ depends: ['btnText'],
+ layer: 'btn',
+ textColor: true
+ },
+ tabActiveText: {
+ depends: ['text'],
+ layer: 'bg',
+ textColor: true
+ },
+
+ // Buttons
+ btn: {
+ depends: ['fg'],
+ variant: 'btn',
+ opacity: 'btn'
+ },
+ btnText: {
+ depends: ['fgText'],
+ layer: 'btn',
+ textColor: true
+ },
+ btnPanelText: {
+ depends: ['btnText'],
+ layer: 'btnPanel',
+ variant: 'btn',
+ textColor: true
+ },
+ btnTopBarText: {
+ depends: ['btnText'],
+ layer: 'btnTopBar',
+ variant: 'btn',
+ textColor: true
+ },
+
+ // Buttons: pressed
+ btnPressed: {
+ depends: ['btn'],
+ layer: 'btn'
+ },
+ btnPressedText: {
+ depends: ['btnText'],
+ layer: 'btn',
+ variant: 'btnPressed',
+ textColor: true
+ },
+ btnPressedPanel: {
+ depends: ['btnPressed'],
+ layer: 'btn'
+ },
+ btnPressedPanelText: {
+ depends: ['btnPanelText'],
+ layer: 'btnPanel',
+ variant: 'btnPressed',
+ textColor: true
+ },
+ btnPressedTopBar: {
+ depends: ['btnPressed'],
+ layer: 'btn'
+ },
+ btnPressedTopBarText: {
+ depends: ['btnTopBarText'],
+ layer: 'btnTopBar',
+ variant: 'btnPressed',
+ textColor: true
+ },
+
+ // Buttons: toggled
+ btnToggled: {
+ depends: ['btn'],
+ layer: 'btn',
+ color: (mod, btn) => brightness(mod * 20, btn).rgb
+ },
+ btnToggledText: {
+ depends: ['btnText'],
+ layer: 'btn',
+ variant: 'btnToggled',
+ textColor: true
+ },
+ btnToggledPanelText: {
+ depends: ['btnPanelText'],
+ layer: 'btnPanel',
+ variant: 'btnToggled',
+ textColor: true
+ },
+ btnToggledTopBarText: {
+ depends: ['btnTopBarText'],
+ layer: 'btnTopBar',
+ variant: 'btnToggled',
+ textColor: true
+ },
+
+ // Buttons: disabled
+ btnDisabled: {
+ depends: ['btn', 'bg'],
+ color: (mod, btn, bg) => alphaBlend(btn, 0.25, bg)
+ },
+ btnDisabledText: {
+ depends: ['btnText', 'btnDisabled'],
+ layer: 'btn',
+ variant: 'btnDisabled',
+ color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
+ },
+ btnDisabledPanelText: {
+ depends: ['btnPanelText', 'btnDisabled'],
+ layer: 'btnPanel',
+ variant: 'btnDisabled',
+ color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
+ },
+ btnDisabledTopBarText: {
+ depends: ['btnTopBarText', 'btnDisabled'],
+ layer: 'btnTopBar',
+ variant: 'btnDisabled',
+ color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
+ },
+
+ // Input fields
+ input: {
+ depends: ['fg'],
+ opacity: 'input'
+ },
+ inputText: {
+ depends: ['text'],
+ layer: 'input',
+ textColor: true
+ },
+ inputPanelText: {
+ depends: ['panelText'],
+ layer: 'inputPanel',
+ variant: 'input',
+ textColor: true
+ },
+ inputTopbarText: {
+ depends: ['topBarText'],
+ layer: 'inputTopBar',
+ variant: 'input',
+ textColor: true
+ },
+
+ alertError: {
+ depends: ['cRed'],
+ opacity: 'alert'
+ },
+ alertErrorText: {
+ depends: ['text'],
+ layer: 'alert',
+ variant: 'alertError',
+ textColor: true
+ },
+ alertErrorPanelText: {
+ depends: ['panelText'],
+ layer: 'alertPanel',
+ variant: 'alertError',
+ textColor: true
+ },
+
+ alertWarning: {
+ depends: ['cOrange'],
+ opacity: 'alert'
+ },
+ alertWarningText: {
+ depends: ['text'],
+ layer: 'alert',
+ variant: 'alertWarning',
+ textColor: true
+ },
+ alertWarningPanelText: {
+ depends: ['panelText'],
+ layer: 'alertPanel',
+ variant: 'alertWarning',
+ textColor: true
+ },
+
+ alertNeutral: {
+ depends: ['text'],
+ opacity: 'alert'
+ },
+ alertNeutralText: {
+ depends: ['text'],
+ layer: 'alert',
+ variant: 'alertNeutral',
+ color: (mod, text) => invertLightness(text).rgb,
+ textColor: true
+ },
+ alertNeutralPanelText: {
+ depends: ['panelText'],
+ layer: 'alertPanel',
+ variant: 'alertNeutral',
+ textColor: true
+ },
+
+ badgeNotification: '--cRed',
+ badgeNotificationText: {
+ depends: ['text', 'badgeNotification'],
+ layer: 'badge',
+ variant: 'badgeNotification',
+ textColor: 'bw'
+ }
+}
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
@@ -0,0 +1,374 @@
+import { convert, brightness, contrastRatio } from 'chromatism'
+import { alphaBlendLayers, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
+import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
+
+/*
+ * # What's all this?
+ * Here be theme engine for pleromafe. All of this supposed to ease look
+ * and feel customization, making widget styles and make developer's life
+ * easier when it comes to supporting themes. Like many other theme systems
+ * it operates on color definitions, or "slots" - for example you define
+ * "button" color slot and then in UI component Button's CSS you refer to
+ * it as a CSS3 Variable.
+ *
+ * Some applications allow you to customize colors for certain things.
+ * Some UI toolkits allow you to define colors for each type of widget.
+ * Most of them are pretty barebones and have no assistance for common
+ * problems and cases, and in general themes themselves are very hard to
+ * maintain in all aspects. This theme engine tries to solve all of the
+ * common problems with themes.
+ *
+ * You don't have redefine several similar colors if you just want to
+ * change one color - all color slots are derived from other ones, so you
+ * can have at least one or two "basic" colors defined and have all other
+ * components inherit and modify basic ones.
+ *
+ * You don't have to test contrast ratio for colors or pick text color for
+ * each element even if you have light-on-dark elements in dark-on-light
+ * theme.
+ *
+ * You don't have to maintain order of code for inheriting slots from othet
+ * slots - dependency graph resolving does it for you.
+ */
+
+/* This indicates that this version of code outputs similar theme data and
+ * should be incremented if output changes - for instance if getTextColor
+ * function changes and older themes no longer render text colors as
+ * author intended previously.
+ */
+export const CURRENT_VERSION = 3
+
+export const getLayersArray = (layer, data = LAYERS) => {
+ let array = [layer]
+ let parent = data[layer]
+ while (parent) {
+ array.unshift(parent)
+ parent = data[parent]
+ }
+ return array
+}
+
+export const getLayers = (layer, variant = layer, opacitySlot, colors, opacity) => {
+ return getLayersArray(layer).map((currentLayer) => ([
+ currentLayer === layer
+ ? colors[variant]
+ : colors[currentLayer],
+ currentLayer === layer
+ ? opacity[opacitySlot] || 1
+ : opacity[currentLayer]
+ ]))
+}
+
+const getDependencies = (key, inheritance) => {
+ const data = inheritance[key]
+ if (typeof data === 'string' && data.startsWith('--')) {
+ return [data.substring(2)]
+ } else {
+ if (data === null) return []
+ const { depends, layer, variant } = data
+ const layerDeps = layer
+ ? getLayersArray(layer).map(currentLayer => {
+ return currentLayer === layer
+ ? variant || layer
+ : currentLayer
+ })
+ : []
+ if (Array.isArray(depends)) {
+ return [...depends, ...layerDeps]
+ } else {
+ return [...layerDeps]
+ }
+ }
+}
+
+/**
+ * Sorts inheritance object topologically - dependant slots come after
+ * dependencies
+ *
+ * @property {Object} inheritance - object defining the nodes
+ * @property {Function} getDeps - function that returns dependencies for
+ * given value and inheritance object.
+ * @returns {String[]} keys of inheritance object, sorted in topological
+ * order. Additionally, dependency-less nodes will always be first in line
+ */
+export const topoSort = (
+ inheritance = SLOT_INHERITANCE,
+ getDeps = getDependencies
+) => {
+ // This is an implementation of https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
+
+ const allKeys = Object.keys(inheritance)
+ const whites = new Set(allKeys)
+ const grays = new Set()
+ const blacks = new Set()
+ const unprocessed = [...allKeys]
+ const output = []
+
+ const step = (node) => {
+ if (whites.has(node)) {
+ // Make node "gray"
+ whites.delete(node)
+ grays.add(node)
+ // Do step for each node connected to it (one way)
+ getDeps(node, inheritance).forEach(step)
+ // Make node "black"
+ grays.delete(node)
+ blacks.add(node)
+ // Put it into the output list
+ output.push(node)
+ } else if (grays.has(node)) {
+ console.debug('Cyclic depenency in topoSort, ignoring')
+ output.push(node)
+ } else if (blacks.has(node)) {
+ // do nothing
+ } else {
+ throw new Error('Unintended condition in topoSort!')
+ }
+ }
+ while (unprocessed.length > 0) {
+ step(unprocessed.pop())
+ }
+ return output.sort((a, b) => {
+ const depsA = getDeps(a, inheritance).length
+ const depsB = getDeps(b, inheritance).length
+
+ if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return 0
+ if (depsA === 0 && depsB !== 0) return -1
+ if (depsB === 0 && depsA !== 0) return 1
+ })
+}
+
+const expandSlotValue = (value) => {
+ if (typeof value === 'object') return value
+ return {
+ depends: value.startsWith('--') ? [value.substring(2)] : [],
+ default: value.startsWith('#') ? value : undefined
+ }
+}
+/**
+ * retrieves opacity slot for given slot. This goes up the depenency graph
+ * to find which parent has opacity slot defined for it.
+ * TODO refactor this
+ */
+export const getOpacitySlot = (
+ k,
+ inheritance = SLOT_INHERITANCE,
+ getDeps = getDependencies
+) => {
+ const value = expandSlotValue(inheritance[k])
+ if (value.opacity === null) return
+ if (value.opacity) return value.opacity
+ const findInheritedOpacity = (key, visited = [k]) => {
+ const depSlot = getDeps(key, inheritance)[0]
+ if (depSlot === undefined) return
+ const dependency = inheritance[depSlot]
+ if (dependency === undefined) return
+ if (dependency.opacity || dependency === null) {
+ return dependency.opacity
+ } else if (dependency.depends && visited.includes(depSlot)) {
+ return findInheritedOpacity(depSlot, [...visited, depSlot])
+ } else {
+ return null
+ }
+ }
+ if (value.depends) {
+ return findInheritedOpacity(k)
+ }
+}
+
+/**
+ * retrieves layer slot for given slot. This goes up the depenency graph
+ * to find which parent has opacity slot defined for it.
+ * this is basically copypaste of getOpacitySlot except it checks if key is
+ * in LAYERS
+ * TODO refactor this
+ */
+export const getLayerSlot = (
+ k,
+ inheritance = SLOT_INHERITANCE,
+ getDeps = getDependencies
+) => {
+ const value = expandSlotValue(inheritance[k])
+ if (LAYERS[k]) return k
+ if (value.layer === null) return
+ if (value.layer) return value.layer
+ const findInheritedLayer = (key, visited = [k]) => {
+ const depSlot = getDeps(key, inheritance)[0]
+ if (depSlot === undefined) return
+ const dependency = inheritance[depSlot]
+ if (dependency === undefined) return
+ if (dependency.layer || dependency === null) {
+ return dependency.layer
+ } else if (dependency.depends) {
+ return findInheritedLayer(dependency, [...visited, depSlot])
+ } else {
+ return null
+ }
+ }
+ if (value.depends) {
+ return findInheritedLayer(k)
+ }
+}
+
+/**
+ * topologically sorted SLOT_INHERITANCE
+ */
+export const SLOT_ORDERED = topoSort(
+ Object.entries(SLOT_INHERITANCE)
+ .sort(([aK, aV], [bK, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
+ .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
+)
+
+/**
+ * All opacity slots used in color slots, their default values and affected
+ * color slots.
+ */
+export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) => {
+ const opacity = getOpacitySlot(k, SLOT_INHERITANCE, getDependencies)
+ if (opacity) {
+ return {
+ ...acc,
+ [opacity]: {
+ defaultValue: DEFAULT_OPACITY[opacity] || 1,
+ affectedSlots: [...((acc[opacity] && acc[opacity].affectedSlots) || []), k]
+ }
+ }
+ } else {
+ return acc
+ }
+}, {})
+
+/**
+ * Handle dynamic color
+ */
+export const computeDynamicColor = (sourceColor, getColor, mod) => {
+ if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--')) return sourceColor
+ let targetColor = null
+ // Color references other color
+ const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
+ const variableSlot = variable.substring(2)
+ targetColor = getColor(variableSlot)
+ if (modifier) {
+ targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
+ }
+ return targetColor
+}
+
+/**
+ * THE function you want to use. Takes provided colors and opacities
+ * value and uses inheritance data to figure out color needed for the slot.
+ */
+export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
+ const sourceColor = sourceColors[key]
+ const value = expandSlotValue(SLOT_INHERITANCE[key])
+ const deps = getDependencies(key, SLOT_INHERITANCE)
+ const isTextColor = !!value.textColor
+ const variant = value.variant || value.layer
+
+ let backgroundColor = null
+
+ if (isTextColor) {
+ backgroundColor = alphaBlendLayers(
+ { ...(colors[deps[0]] || convert(sourceColors[key] || '#FF00FF').rgb) },
+ getLayers(
+ getLayerSlot(key) || 'bg',
+ variant || 'bg',
+ getOpacitySlot(variant),
+ colors,
+ opacity
+ )
+ )
+ } else if (variant && variant !== key) {
+ backgroundColor = colors[variant] || convert(sourceColors[variant]).rgb
+ } else {
+ backgroundColor = colors.bg || convert(sourceColors.bg)
+ }
+
+ const isLightOnDark = relativeLuminance(backgroundColor) < 0.5
+ const mod = isLightOnDark ? 1 : -1
+
+ let outputColor = null
+ if (sourceColor) {
+ // Color is defined in source color
+ let targetColor = sourceColor
+ if (targetColor === 'transparent') {
+ // We take only layers below current one
+ const layers = getLayers(
+ getLayerSlot(key),
+ key,
+ getOpacitySlot(key) || key,
+ colors,
+ opacity
+ ).slice(0, -1)
+ targetColor = {
+ ...alphaBlendLayers(
+ convert('#FF00FF').rgb,
+ layers
+ ),
+ a: 0
+ }
+ } else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
+ targetColor = computeDynamicColor(
+ sourceColor,
+ variableSlot => colors[variableSlot] || sourceColors[variableSlot],
+ mod
+ )
+ } else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) {
+ targetColor = convert(targetColor).rgb
+ }
+ outputColor = { ...targetColor }
+ } else if (value.default) {
+ // same as above except in object form
+ outputColor = convert(value.default).rgb
+ } else {
+ // calculate color
+ const defaultColorFunc = (mod, dep) => ({ ...dep })
+ const colorFunc = value.color || defaultColorFunc
+
+ if (value.textColor) {
+ if (value.textColor === 'bw') {
+ outputColor = contrastRatio(backgroundColor).rgb
+ } else {
+ let color = { ...colors[deps[0]] }
+ if (value.color) {
+ color = colorFunc(mod, ...deps.map((dep) => ({ ...colors[dep] })))
+ }
+ outputColor = getTextColor(
+ backgroundColor,
+ { ...color },
+ value.textColor === 'preserve'
+ )
+ }
+ } else {
+ // background color case
+ outputColor = colorFunc(
+ mod,
+ ...deps.map((dep) => ({ ...colors[dep] }))
+ )
+ }
+ }
+ if (!outputColor) {
+ throw new Error('Couldn\'t generate color for ' + key)
+ }
+ const opacitySlot = getOpacitySlot(key)
+ const ownOpacitySlot = value.opacity
+ if (opacitySlot && (outputColor.a === undefined || ownOpacitySlot)) {
+ const dependencySlot = deps[0]
+ if (dependencySlot && colors[dependencySlot] === 'transparent') {
+ outputColor.a = 0
+ } else {
+ outputColor.a = Number(sourceOpacity[opacitySlot]) || OPACITIES[opacitySlot].defaultValue || 1
+ }
+ }
+ if (opacitySlot) {
+ return {
+ colors: { ...colors, [key]: outputColor },
+ opacity: { ...opacity, [opacitySlot]: outputColor.a }
+ }
+ } else {
+ return {
+ colors: { ...colors, [key]: outputColor },
+ opacity
+ }
+ }
+}, { colors: {}, opacity: {} })
diff --git a/static/css/base16-3024.css b/static/css/base16-3024.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #090300; }
-.base01-background { background-color: #3a3432; }
-.base02-background { background-color: #4a4543; }
-.base03-background { background-color: #5c5855; }
-.base04-background { background-color: #807d7c; }
-.base05-background { background-color: #a5a2a2; }
-.base06-background { background-color: #d6d5d4; }
-.base07-background { background-color: #f7f7f7; }
-.base08-background { background-color: #db2d20; }
-.base09-background { background-color: #e8bbd0; }
-.base0A-background { background-color: #fded02; }
-.base0B-background { background-color: #01a252; }
-.base0C-background { background-color: #b5e4f4; }
-.base0D-background { background-color: #01a0e4; }
-.base0E-background { background-color: #a16a94; }
-.base0F-background { background-color: #cdab53; }
-
-.base00 { color: #090300; }
-.base01 { color: #3a3432; }
-.base02 { color: #4a4543; }
-.base03 { color: #5c5855; }
-.base04 { color: #807d7c; }
-.base05 { color: #a5a2a2; }
-.base06 { color: #d6d5d4; }
-.base07 { color: #f7f7f7; }
-.base08 { color: #db2d20; }
-.base09 { color: #e8bbd0; }
-.base0A { color: #fded02; }
-.base0B { color: #01a252; }
-.base0C { color: #b5e4f4; }
-.base0D { color: #01a0e4; }
-.base0E { color: #a16a94; }
-.base0F { color: #cdab53; }
diff --git a/static/css/base16-apathy.css b/static/css/base16-apathy.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #031A16; }
-.base01-background { background-color: #0B342D; }
-.base02-background { background-color: #184E45; }
-.base03-background { background-color: #2B685E; }
-.base04-background { background-color: #5F9C92; }
-.base05-background { background-color: #81B5AC; }
-.base06-background { background-color: #A7CEC8; }
-.base07-background { background-color: #D2E7E4; }
-.base08-background { background-color: #3E9688; }
-.base09-background { background-color: #3E7996; }
-.base0A-background { background-color: #3E4C96; }
-.base0B-background { background-color: #883E96; }
-.base0C-background { background-color: #963E4C; }
-.base0D-background { background-color: #96883E; }
-.base0E-background { background-color: #4C963E; }
-.base0F-background { background-color: #3E965B; }
-
-.base00 { color: #031A16; }
-.base01 { color: #0B342D; }
-.base02 { color: #184E45; }
-.base03 { color: #2B685E; }
-.base04 { color: #5F9C92; }
-.base05 { color: #81B5AC; }
-.base06 { color: #A7CEC8; }
-.base07 { color: #D2E7E4; }
-.base08 { color: #3E9688; }
-.base09 { color: #3E7996; }
-.base0A { color: #3E4C96; }
-.base0B { color: #883E96; }
-.base0C { color: #963E4C; }
-.base0D { color: #96883E; }
-.base0E { color: #4C963E; }
-.base0F { color: #3E965B; }
diff --git a/static/css/base16-ashes.css b/static/css/base16-ashes.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1C2023; }
-.base01-background { background-color: #393F45; }
-.base02-background { background-color: #565E65; }
-.base03-background { background-color: #747C84; }
-.base04-background { background-color: #ADB3BA; }
-.base05-background { background-color: #C7CCD1; }
-.base06-background { background-color: #DFE2E5; }
-.base07-background { background-color: #F3F4F5; }
-.base08-background { background-color: #C7AE95; }
-.base09-background { background-color: #C7C795; }
-.base0A-background { background-color: #AEC795; }
-.base0B-background { background-color: #95C7AE; }
-.base0C-background { background-color: #95AEC7; }
-.base0D-background { background-color: #AE95C7; }
-.base0E-background { background-color: #C795AE; }
-.base0F-background { background-color: #C79595; }
-
-.base00 { color: #1C2023; }
-.base01 { color: #393F45; }
-.base02 { color: #565E65; }
-.base03 { color: #747C84; }
-.base04 { color: #ADB3BA; }
-.base05 { color: #C7CCD1; }
-.base06 { color: #DFE2E5; }
-.base07 { color: #F3F4F5; }
-.base08 { color: #C7AE95; }
-.base09 { color: #C7C795; }
-.base0A { color: #AEC795; }
-.base0B { color: #95C7AE; }
-.base0C { color: #95AEC7; }
-.base0D { color: #AE95C7; }
-.base0E { color: #C795AE; }
-.base0F { color: #C79595; }
diff --git a/static/css/base16-atelier-cave.css b/static/css/base16-atelier-cave.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #19171c; }
-.base01-background { background-color: #26232a; }
-.base02-background { background-color: #585260; }
-.base03-background { background-color: #655f6d; }
-.base04-background { background-color: #7e7887; }
-.base05-background { background-color: #8b8792; }
-.base06-background { background-color: #e2dfe7; }
-.base07-background { background-color: #efecf4; }
-.base08-background { background-color: #be4678; }
-.base09-background { background-color: #aa573c; }
-.base0A-background { background-color: #a06e3b; }
-.base0B-background { background-color: #2a9292; }
-.base0C-background { background-color: #398bc6; }
-.base0D-background { background-color: #576ddb; }
-.base0E-background { background-color: #955ae7; }
-.base0F-background { background-color: #bf40bf; }
-
-.base00 { color: #19171c; }
-.base01 { color: #26232a; }
-.base02 { color: #585260; }
-.base03 { color: #655f6d; }
-.base04 { color: #7e7887; }
-.base05 { color: #8b8792; }
-.base06 { color: #e2dfe7; }
-.base07 { color: #efecf4; }
-.base08 { color: #be4678; }
-.base09 { color: #aa573c; }
-.base0A { color: #a06e3b; }
-.base0B { color: #2a9292; }
-.base0C { color: #398bc6; }
-.base0D { color: #576ddb; }
-.base0E { color: #955ae7; }
-.base0F { color: #bf40bf; }
diff --git a/static/css/base16-atelier-dune.css b/static/css/base16-atelier-dune.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #20201d; }
-.base01-background { background-color: #292824; }
-.base02-background { background-color: #6e6b5e; }
-.base03-background { background-color: #7d7a68; }
-.base04-background { background-color: #999580; }
-.base05-background { background-color: #a6a28c; }
-.base06-background { background-color: #e8e4cf; }
-.base07-background { background-color: #fefbec; }
-.base08-background { background-color: #d73737; }
-.base09-background { background-color: #b65611; }
-.base0A-background { background-color: #ae9513; }
-.base0B-background { background-color: #60ac39; }
-.base0C-background { background-color: #1fad83; }
-.base0D-background { background-color: #6684e1; }
-.base0E-background { background-color: #b854d4; }
-.base0F-background { background-color: #d43552; }
-
-.base00 { color: #20201d; }
-.base01 { color: #292824; }
-.base02 { color: #6e6b5e; }
-.base03 { color: #7d7a68; }
-.base04 { color: #999580; }
-.base05 { color: #a6a28c; }
-.base06 { color: #e8e4cf; }
-.base07 { color: #fefbec; }
-.base08 { color: #d73737; }
-.base09 { color: #b65611; }
-.base0A { color: #ae9513; }
-.base0B { color: #60ac39; }
-.base0C { color: #1fad83; }
-.base0D { color: #6684e1; }
-.base0E { color: #b854d4; }
-.base0F { color: #d43552; }
diff --git a/static/css/base16-atelier-estuary.css b/static/css/base16-atelier-estuary.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #22221b; }
-.base01-background { background-color: #302f27; }
-.base02-background { background-color: #5f5e4e; }
-.base03-background { background-color: #6c6b5a; }
-.base04-background { background-color: #878573; }
-.base05-background { background-color: #929181; }
-.base06-background { background-color: #e7e6df; }
-.base07-background { background-color: #f4f3ec; }
-.base08-background { background-color: #ba6236; }
-.base09-background { background-color: #ae7313; }
-.base0A-background { background-color: #a5980d; }
-.base0B-background { background-color: #7d9726; }
-.base0C-background { background-color: #5b9d48; }
-.base0D-background { background-color: #36a166; }
-.base0E-background { background-color: #5f9182; }
-.base0F-background { background-color: #9d6c7c; }
-
-.base00 { color: #22221b; }
-.base01 { color: #302f27; }
-.base02 { color: #5f5e4e; }
-.base03 { color: #6c6b5a; }
-.base04 { color: #878573; }
-.base05 { color: #929181; }
-.base06 { color: #e7e6df; }
-.base07 { color: #f4f3ec; }
-.base08 { color: #ba6236; }
-.base09 { color: #ae7313; }
-.base0A { color: #a5980d; }
-.base0B { color: #7d9726; }
-.base0C { color: #5b9d48; }
-.base0D { color: #36a166; }
-.base0E { color: #5f9182; }
-.base0F { color: #9d6c7c; }
diff --git a/static/css/base16-atelier-forest.css b/static/css/base16-atelier-forest.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1b1918; }
-.base01-background { background-color: #2c2421; }
-.base02-background { background-color: #68615e; }
-.base03-background { background-color: #766e6b; }
-.base04-background { background-color: #9c9491; }
-.base05-background { background-color: #a8a19f; }
-.base06-background { background-color: #e6e2e0; }
-.base07-background { background-color: #f1efee; }
-.base08-background { background-color: #f22c40; }
-.base09-background { background-color: #df5320; }
-.base0A-background { background-color: #c38418; }
-.base0B-background { background-color: #7b9726; }
-.base0C-background { background-color: #3d97b8; }
-.base0D-background { background-color: #407ee7; }
-.base0E-background { background-color: #6666ea; }
-.base0F-background { background-color: #c33ff3; }
-
-.base00 { color: #1b1918; }
-.base01 { color: #2c2421; }
-.base02 { color: #68615e; }
-.base03 { color: #766e6b; }
-.base04 { color: #9c9491; }
-.base05 { color: #a8a19f; }
-.base06 { color: #e6e2e0; }
-.base07 { color: #f1efee; }
-.base08 { color: #f22c40; }
-.base09 { color: #df5320; }
-.base0A { color: #c38418; }
-.base0B { color: #7b9726; }
-.base0C { color: #3d97b8; }
-.base0D { color: #407ee7; }
-.base0E { color: #6666ea; }
-.base0F { color: #c33ff3; }
diff --git a/static/css/base16-atelier-heath.css b/static/css/base16-atelier-heath.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1b181b; }
-.base01-background { background-color: #292329; }
-.base02-background { background-color: #695d69; }
-.base03-background { background-color: #776977; }
-.base04-background { background-color: #9e8f9e; }
-.base05-background { background-color: #ab9bab; }
-.base06-background { background-color: #d8cad8; }
-.base07-background { background-color: #f7f3f7; }
-.base08-background { background-color: #ca402b; }
-.base09-background { background-color: #a65926; }
-.base0A-background { background-color: #bb8a35; }
-.base0B-background { background-color: #918b3b; }
-.base0C-background { background-color: #159393; }
-.base0D-background { background-color: #516aec; }
-.base0E-background { background-color: #7b59c0; }
-.base0F-background { background-color: #cc33cc; }
-
-.base00 { color: #1b181b; }
-.base01 { color: #292329; }
-.base02 { color: #695d69; }
-.base03 { color: #776977; }
-.base04 { color: #9e8f9e; }
-.base05 { color: #ab9bab; }
-.base06 { color: #d8cad8; }
-.base07 { color: #f7f3f7; }
-.base08 { color: #ca402b; }
-.base09 { color: #a65926; }
-.base0A { color: #bb8a35; }
-.base0B { color: #918b3b; }
-.base0C { color: #159393; }
-.base0D { color: #516aec; }
-.base0E { color: #7b59c0; }
-.base0F { color: #cc33cc; }
diff --git a/static/css/base16-atelier-lakeside.css b/static/css/base16-atelier-lakeside.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #161b1d; }
-.base01-background { background-color: #1f292e; }
-.base02-background { background-color: #516d7b; }
-.base03-background { background-color: #5a7b8c; }
-.base04-background { background-color: #7195a8; }
-.base05-background { background-color: #7ea2b4; }
-.base06-background { background-color: #c1e4f6; }
-.base07-background { background-color: #ebf8ff; }
-.base08-background { background-color: #d22d72; }
-.base09-background { background-color: #935c25; }
-.base0A-background { background-color: #8a8a0f; }
-.base0B-background { background-color: #568c3b; }
-.base0C-background { background-color: #2d8f6f; }
-.base0D-background { background-color: #257fad; }
-.base0E-background { background-color: #6b6bb8; }
-.base0F-background { background-color: #b72dd2; }
-
-.base00 { color: #161b1d; }
-.base01 { color: #1f292e; }
-.base02 { color: #516d7b; }
-.base03 { color: #5a7b8c; }
-.base04 { color: #7195a8; }
-.base05 { color: #7ea2b4; }
-.base06 { color: #c1e4f6; }
-.base07 { color: #ebf8ff; }
-.base08 { color: #d22d72; }
-.base09 { color: #935c25; }
-.base0A { color: #8a8a0f; }
-.base0B { color: #568c3b; }
-.base0C { color: #2d8f6f; }
-.base0D { color: #257fad; }
-.base0E { color: #6b6bb8; }
-.base0F { color: #b72dd2; }
diff --git a/static/css/base16-atelier-plateau.css b/static/css/base16-atelier-plateau.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1b1818; }
-.base01-background { background-color: #292424; }
-.base02-background { background-color: #585050; }
-.base03-background { background-color: #655d5d; }
-.base04-background { background-color: #7e7777; }
-.base05-background { background-color: #8a8585; }
-.base06-background { background-color: #e7dfdf; }
-.base07-background { background-color: #f4ecec; }
-.base08-background { background-color: #ca4949; }
-.base09-background { background-color: #b45a3c; }
-.base0A-background { background-color: #a06e3b; }
-.base0B-background { background-color: #4b8b8b; }
-.base0C-background { background-color: #5485b6; }
-.base0D-background { background-color: #7272ca; }
-.base0E-background { background-color: #8464c4; }
-.base0F-background { background-color: #bd5187; }
-
-.base00 { color: #1b1818; }
-.base01 { color: #292424; }
-.base02 { color: #585050; }
-.base03 { color: #655d5d; }
-.base04 { color: #7e7777; }
-.base05 { color: #8a8585; }
-.base06 { color: #e7dfdf; }
-.base07 { color: #f4ecec; }
-.base08 { color: #ca4949; }
-.base09 { color: #b45a3c; }
-.base0A { color: #a06e3b; }
-.base0B { color: #4b8b8b; }
-.base0C { color: #5485b6; }
-.base0D { color: #7272ca; }
-.base0E { color: #8464c4; }
-.base0F { color: #bd5187; }
diff --git a/static/css/base16-atelier-savanna.css b/static/css/base16-atelier-savanna.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #171c19; }
-.base01-background { background-color: #232a25; }
-.base02-background { background-color: #526057; }
-.base03-background { background-color: #5f6d64; }
-.base04-background { background-color: #78877d; }
-.base05-background { background-color: #87928a; }
-.base06-background { background-color: #dfe7e2; }
-.base07-background { background-color: #ecf4ee; }
-.base08-background { background-color: #b16139; }
-.base09-background { background-color: #9f713c; }
-.base0A-background { background-color: #a07e3b; }
-.base0B-background { background-color: #489963; }
-.base0C-background { background-color: #1c9aa0; }
-.base0D-background { background-color: #478c90; }
-.base0E-background { background-color: #55859b; }
-.base0F-background { background-color: #867469; }
-
-.base00 { color: #171c19; }
-.base01 { color: #232a25; }
-.base02 { color: #526057; }
-.base03 { color: #5f6d64; }
-.base04 { color: #78877d; }
-.base05 { color: #87928a; }
-.base06 { color: #dfe7e2; }
-.base07 { color: #ecf4ee; }
-.base08 { color: #b16139; }
-.base09 { color: #9f713c; }
-.base0A { color: #a07e3b; }
-.base0B { color: #489963; }
-.base0C { color: #1c9aa0; }
-.base0D { color: #478c90; }
-.base0E { color: #55859b; }
-.base0F { color: #867469; }
diff --git a/static/css/base16-atelier-seaside.css b/static/css/base16-atelier-seaside.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #131513; }
-.base01-background { background-color: #242924; }
-.base02-background { background-color: #5e6e5e; }
-.base03-background { background-color: #687d68; }
-.base04-background { background-color: #809980; }
-.base05-background { background-color: #8ca68c; }
-.base06-background { background-color: #cfe8cf; }
-.base07-background { background-color: #f4fbf4; }
-.base08-background { background-color: #e6193c; }
-.base09-background { background-color: #87711d; }
-.base0A-background { background-color: #98981b; }
-.base0B-background { background-color: #29a329; }
-.base0C-background { background-color: #1999b3; }
-.base0D-background { background-color: #3d62f5; }
-.base0E-background { background-color: #ad2bee; }
-.base0F-background { background-color: #e619c3; }
-
-.base00 { color: #131513; }
-.base01 { color: #242924; }
-.base02 { color: #5e6e5e; }
-.base03 { color: #687d68; }
-.base04 { color: #809980; }
-.base05 { color: #8ca68c; }
-.base06 { color: #cfe8cf; }
-.base07 { color: #f4fbf4; }
-.base08 { color: #e6193c; }
-.base09 { color: #87711d; }
-.base0A { color: #98981b; }
-.base0B { color: #29a329; }
-.base0C { color: #1999b3; }
-.base0D { color: #3d62f5; }
-.base0E { color: #ad2bee; }
-.base0F { color: #e619c3; }
diff --git a/static/css/base16-atelier-sulphurpool.css b/static/css/base16-atelier-sulphurpool.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #202746; }
-.base01-background { background-color: #293256; }
-.base02-background { background-color: #5e6687; }
-.base03-background { background-color: #6b7394; }
-.base04-background { background-color: #898ea4; }
-.base05-background { background-color: #979db4; }
-.base06-background { background-color: #dfe2f1; }
-.base07-background { background-color: #f5f7ff; }
-.base08-background { background-color: #c94922; }
-.base09-background { background-color: #c76b29; }
-.base0A-background { background-color: #c08b30; }
-.base0B-background { background-color: #ac9739; }
-.base0C-background { background-color: #22a2c9; }
-.base0D-background { background-color: #3d8fd1; }
-.base0E-background { background-color: #6679cc; }
-.base0F-background { background-color: #9c637a; }
-
-.base00 { color: #202746; }
-.base01 { color: #293256; }
-.base02 { color: #5e6687; }
-.base03 { color: #6b7394; }
-.base04 { color: #898ea4; }
-.base05 { color: #979db4; }
-.base06 { color: #dfe2f1; }
-.base07 { color: #f5f7ff; }
-.base08 { color: #c94922; }
-.base09 { color: #c76b29; }
-.base0A { color: #c08b30; }
-.base0B { color: #ac9739; }
-.base0C { color: #22a2c9; }
-.base0D { color: #3d8fd1; }
-.base0E { color: #6679cc; }
-.base0F { color: #9c637a; }
diff --git a/static/css/base16-bespin.css b/static/css/base16-bespin.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #28211c; }
-.base01-background { background-color: #36312e; }
-.base02-background { background-color: #5e5d5c; }
-.base03-background { background-color: #666666; }
-.base04-background { background-color: #797977; }
-.base05-background { background-color: #8a8986; }
-.base06-background { background-color: #9d9b97; }
-.base07-background { background-color: #baae9e; }
-.base08-background { background-color: #cf6a4c; }
-.base09-background { background-color: #cf7d34; }
-.base0A-background { background-color: #f9ee98; }
-.base0B-background { background-color: #54be0d; }
-.base0C-background { background-color: #afc4db; }
-.base0D-background { background-color: #5ea6ea; }
-.base0E-background { background-color: #9b859d; }
-.base0F-background { background-color: #937121; }
-
-.base00 { color: #28211c; }
-.base01 { color: #36312e; }
-.base02 { color: #5e5d5c; }
-.base03 { color: #666666; }
-.base04 { color: #797977; }
-.base05 { color: #8a8986; }
-.base06 { color: #9d9b97; }
-.base07 { color: #baae9e; }
-.base08 { color: #cf6a4c; }
-.base09 { color: #cf7d34; }
-.base0A { color: #f9ee98; }
-.base0B { color: #54be0d; }
-.base0C { color: #afc4db; }
-.base0D { color: #5ea6ea; }
-.base0E { color: #9b859d; }
-.base0F { color: #937121; }
diff --git a/static/css/base16-brewer.css b/static/css/base16-brewer.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #0c0d0e; }
-.base01-background { background-color: #2e2f30; }
-.base02-background { background-color: #515253; }
-.base03-background { background-color: #737475; }
-.base04-background { background-color: #959697; }
-.base05-background { background-color: #b7b8b9; }
-.base06-background { background-color: #dadbdc; }
-.base07-background { background-color: #fcfdfe; }
-.base08-background { background-color: #e31a1c; }
-.base09-background { background-color: #e6550d; }
-.base0A-background { background-color: #dca060; }
-.base0B-background { background-color: #31a354; }
-.base0C-background { background-color: #80b1d3; }
-.base0D-background { background-color: #3182bd; }
-.base0E-background { background-color: #756bb1; }
-.base0F-background { background-color: #b15928; }
-
-.base00 { color: #0c0d0e; }
-.base01 { color: #2e2f30; }
-.base02 { color: #515253; }
-.base03 { color: #737475; }
-.base04 { color: #959697; }
-.base05 { color: #b7b8b9; }
-.base06 { color: #dadbdc; }
-.base07 { color: #fcfdfe; }
-.base08 { color: #e31a1c; }
-.base09 { color: #e6550d; }
-.base0A { color: #dca060; }
-.base0B { color: #31a354; }
-.base0C { color: #80b1d3; }
-.base0D { color: #3182bd; }
-.base0E { color: #756bb1; }
-.base0F { color: #b15928; }
diff --git a/static/css/base16-bright.css b/static/css/base16-bright.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #000000; }
-.base01-background { background-color: #303030; }
-.base02-background { background-color: #505050; }
-.base03-background { background-color: #b0b0b0; }
-.base04-background { background-color: #d0d0d0; }
-.base05-background { background-color: #e0e0e0; }
-.base06-background { background-color: #f5f5f5; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #fb0120; }
-.base09-background { background-color: #fc6d24; }
-.base0A-background { background-color: #fda331; }
-.base0B-background { background-color: #a1c659; }
-.base0C-background { background-color: #76c7b7; }
-.base0D-background { background-color: #6fb3d2; }
-.base0E-background { background-color: #d381c3; }
-.base0F-background { background-color: #be643c; }
-
-.base00 { color: #000000; }
-.base01 { color: #303030; }
-.base02 { color: #505050; }
-.base03 { color: #b0b0b0; }
-.base04 { color: #d0d0d0; }
-.base05 { color: #e0e0e0; }
-.base06 { color: #f5f5f5; }
-.base07 { color: #ffffff; }
-.base08 { color: #fb0120; }
-.base09 { color: #fc6d24; }
-.base0A { color: #fda331; }
-.base0B { color: #a1c659; }
-.base0C { color: #76c7b7; }
-.base0D { color: #6fb3d2; }
-.base0E { color: #d381c3; }
-.base0F { color: #be643c; }
diff --git a/static/css/base16-chalk.css b/static/css/base16-chalk.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #151515; }
-.base01-background { background-color: #202020; }
-.base02-background { background-color: #303030; }
-.base03-background { background-color: #505050; }
-.base04-background { background-color: #b0b0b0; }
-.base05-background { background-color: #d0d0d0; }
-.base06-background { background-color: #e0e0e0; }
-.base07-background { background-color: #f5f5f5; }
-.base08-background { background-color: #fb9fb1; }
-.base09-background { background-color: #eda987; }
-.base0A-background { background-color: #ddb26f; }
-.base0B-background { background-color: #acc267; }
-.base0C-background { background-color: #12cfc0; }
-.base0D-background { background-color: #6fc2ef; }
-.base0E-background { background-color: #e1a3ee; }
-.base0F-background { background-color: #deaf8f; }
-
-.base00 { color: #151515; }
-.base01 { color: #202020; }
-.base02 { color: #303030; }
-.base03 { color: #505050; }
-.base04 { color: #b0b0b0; }
-.base05 { color: #d0d0d0; }
-.base06 { color: #e0e0e0; }
-.base07 { color: #f5f5f5; }
-.base08 { color: #fb9fb1; }
-.base09 { color: #eda987; }
-.base0A { color: #ddb26f; }
-.base0B { color: #acc267; }
-.base0C { color: #12cfc0; }
-.base0D { color: #6fc2ef; }
-.base0E { color: #e1a3ee; }
-.base0F { color: #deaf8f; }
diff --git a/static/css/base16-codeschool.css b/static/css/base16-codeschool.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #232c31; }
-.base01-background { background-color: #1c3657; }
-.base02-background { background-color: #2a343a; }
-.base03-background { background-color: #3f4944; }
-.base04-background { background-color: #84898c; }
-.base05-background { background-color: #9ea7a6; }
-.base06-background { background-color: #a7cfa3; }
-.base07-background { background-color: #b5d8f6; }
-.base08-background { background-color: #2a5491; }
-.base09-background { background-color: #43820d; }
-.base0A-background { background-color: #a03b1e; }
-.base0B-background { background-color: #237986; }
-.base0C-background { background-color: #b02f30; }
-.base0D-background { background-color: #484d79; }
-.base0E-background { background-color: #c59820; }
-.base0F-background { background-color: #c98344; }
-
-.base00 { color: #232c31; }
-.base01 { color: #1c3657; }
-.base02 { color: #2a343a; }
-.base03 { color: #3f4944; }
-.base04 { color: #84898c; }
-.base05 { color: #9ea7a6; }
-.base06 { color: #a7cfa3; }
-.base07 { color: #b5d8f6; }
-.base08 { color: #2a5491; }
-.base09 { color: #43820d; }
-.base0A { color: #a03b1e; }
-.base0B { color: #237986; }
-.base0C { color: #b02f30; }
-.base0D { color: #484d79; }
-.base0E { color: #c59820; }
-.base0F { color: #c98344; }
diff --git a/static/css/base16-darktooth.css b/static/css/base16-darktooth.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1D2021; }
-.base01-background { background-color: #32302F; }
-.base02-background { background-color: #504945; }
-.base03-background { background-color: #665C54; }
-.base04-background { background-color: #928374; }
-.base05-background { background-color: #A89984; }
-.base06-background { background-color: #D5C4A1; }
-.base07-background { background-color: #FDF4C1; }
-.base08-background { background-color: #FB543F; }
-.base09-background { background-color: #FE8625; }
-.base0A-background { background-color: #FAC03B; }
-.base0B-background { background-color: #95C085; }
-.base0C-background { background-color: #8BA59B; }
-.base0D-background { background-color: #0D6678; }
-.base0E-background { background-color: #8F4673; }
-.base0F-background { background-color: #A87322; }
-
-.base00 { color: #1D2021; }
-.base01 { color: #32302F; }
-.base02 { color: #504945; }
-.base03 { color: #665C54; }
-.base04 { color: #928374; }
-.base05 { color: #A89984; }
-.base06 { color: #D5C4A1; }
-.base07 { color: #FDF4C1; }
-.base08 { color: #FB543F; }
-.base09 { color: #FE8625; }
-.base0A { color: #FAC03B; }
-.base0B { color: #95C085; }
-.base0C { color: #8BA59B; }
-.base0D { color: #0D6678; }
-.base0E { color: #8F4673; }
-.base0F { color: #A87322; }
diff --git a/static/css/base16-default-dark.css b/static/css/base16-default-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #181818; }
-.base01-background { background-color: #282828; }
-.base02-background { background-color: #383838; }
-.base03-background { background-color: #585858; }
-.base04-background { background-color: #b8b8b8; }
-.base05-background { background-color: #d8d8d8; }
-.base06-background { background-color: #e8e8e8; }
-.base07-background { background-color: #f8f8f8; }
-.base08-background { background-color: #ab4642; }
-.base09-background { background-color: #dc9656; }
-.base0A-background { background-color: #f7ca88; }
-.base0B-background { background-color: #a1b56c; }
-.base0C-background { background-color: #86c1b9; }
-.base0D-background { background-color: #7cafc2; }
-.base0E-background { background-color: #ba8baf; }
-.base0F-background { background-color: #a16946; }
-
-.base00 { color: #181818; }
-.base01 { color: #282828; }
-.base02 { color: #383838; }
-.base03 { color: #585858; }
-.base04 { color: #b8b8b8; }
-.base05 { color: #d8d8d8; }
-.base06 { color: #e8e8e8; }
-.base07 { color: #f8f8f8; }
-.base08 { color: #ab4642; }
-.base09 { color: #dc9656; }
-.base0A { color: #f7ca88; }
-.base0B { color: #a1b56c; }
-.base0C { color: #86c1b9; }
-.base0D { color: #7cafc2; }
-.base0E { color: #ba8baf; }
-.base0F { color: #a16946; }
diff --git a/static/css/base16-default-light.css b/static/css/base16-default-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #f8f8f8; }
-.base01-background { background-color: #e8e8e8; }
-.base02-background { background-color: #d8d8d8; }
-.base03-background { background-color: #b8b8b8; }
-.base04-background { background-color: #585858; }
-.base05-background { background-color: #383838; }
-.base06-background { background-color: #282828; }
-.base07-background { background-color: #181818; }
-.base08-background { background-color: #ab4642; }
-.base09-background { background-color: #dc9656; }
-.base0A-background { background-color: #f7ca88; }
-.base0B-background { background-color: #a1b56c; }
-.base0C-background { background-color: #86c1b9; }
-.base0D-background { background-color: #7cafc2; }
-.base0E-background { background-color: #ba8baf; }
-.base0F-background { background-color: #a16946; }
-
-.base00 { color: #f8f8f8; }
-.base01 { color: #e8e8e8; }
-.base02 { color: #d8d8d8; }
-.base03 { color: #b8b8b8; }
-.base04 { color: #585858; }
-.base05 { color: #383838; }
-.base06 { color: #282828; }
-.base07 { color: #181818; }
-.base08 { color: #ab4642; }
-.base09 { color: #dc9656; }
-.base0A { color: #f7ca88; }
-.base0B { color: #a1b56c; }
-.base0C { color: #86c1b9; }
-.base0D { color: #7cafc2; }
-.base0E { color: #ba8baf; }
-.base0F { color: #a16946; }
diff --git a/static/css/base16-eighties.css b/static/css/base16-eighties.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #2d2d2d; }
-.base01-background { background-color: #393939; }
-.base02-background { background-color: #515151; }
-.base03-background { background-color: #747369; }
-.base04-background { background-color: #a09f93; }
-.base05-background { background-color: #d3d0c8; }
-.base06-background { background-color: #e8e6df; }
-.base07-background { background-color: #f2f0ec; }
-.base08-background { background-color: #f2777a; }
-.base09-background { background-color: #f99157; }
-.base0A-background { background-color: #ffcc66; }
-.base0B-background { background-color: #99cc99; }
-.base0C-background { background-color: #66cccc; }
-.base0D-background { background-color: #6699cc; }
-.base0E-background { background-color: #cc99cc; }
-.base0F-background { background-color: #d27b53; }
-
-.base00 { color: #2d2d2d; }
-.base01 { color: #393939; }
-.base02 { color: #515151; }
-.base03 { color: #747369; }
-.base04 { color: #a09f93; }
-.base05 { color: #d3d0c8; }
-.base06 { color: #e8e6df; }
-.base07 { color: #f2f0ec; }
-.base08 { color: #f2777a; }
-.base09 { color: #f99157; }
-.base0A { color: #ffcc66; }
-.base0B { color: #99cc99; }
-.base0C { color: #66cccc; }
-.base0D { color: #6699cc; }
-.base0E { color: #cc99cc; }
-.base0F { color: #d27b53; }
diff --git a/static/css/base16-embers.css b/static/css/base16-embers.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #16130F; }
-.base01-background { background-color: #2C2620; }
-.base02-background { background-color: #433B32; }
-.base03-background { background-color: #5A5047; }
-.base04-background { background-color: #8A8075; }
-.base05-background { background-color: #A39A90; }
-.base06-background { background-color: #BEB6AE; }
-.base07-background { background-color: #DBD6D1; }
-.base08-background { background-color: #826D57; }
-.base09-background { background-color: #828257; }
-.base0A-background { background-color: #6D8257; }
-.base0B-background { background-color: #57826D; }
-.base0C-background { background-color: #576D82; }
-.base0D-background { background-color: #6D5782; }
-.base0E-background { background-color: #82576D; }
-.base0F-background { background-color: #825757; }
-
-.base00 { color: #16130F; }
-.base01 { color: #2C2620; }
-.base02 { color: #433B32; }
-.base03 { color: #5A5047; }
-.base04 { color: #8A8075; }
-.base05 { color: #A39A90; }
-.base06 { color: #BEB6AE; }
-.base07 { color: #DBD6D1; }
-.base08 { color: #826D57; }
-.base09 { color: #828257; }
-.base0A { color: #6D8257; }
-.base0B { color: #57826D; }
-.base0C { color: #576D82; }
-.base0D { color: #6D5782; }
-.base0E { color: #82576D; }
-.base0F { color: #825757; }
diff --git a/static/css/base16-flat.css b/static/css/base16-flat.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #2C3E50; }
-.base01-background { background-color: #34495E; }
-.base02-background { background-color: #7F8C8D; }
-.base03-background { background-color: #95A5A6; }
-.base04-background { background-color: #BDC3C7; }
-.base05-background { background-color: #e0e0e0; }
-.base06-background { background-color: #f5f5f5; }
-.base07-background { background-color: #ECF0F1; }
-.base08-background { background-color: #E74C3C; }
-.base09-background { background-color: #E67E22; }
-.base0A-background { background-color: #F1C40F; }
-.base0B-background { background-color: #2ECC71; }
-.base0C-background { background-color: #1ABC9C; }
-.base0D-background { background-color: #3498DB; }
-.base0E-background { background-color: #9B59B6; }
-.base0F-background { background-color: #be643c; }
-
-.base00 { color: #2C3E50; }
-.base01 { color: #34495E; }
-.base02 { color: #7F8C8D; }
-.base03 { color: #95A5A6; }
-.base04 { color: #BDC3C7; }
-.base05 { color: #e0e0e0; }
-.base06 { color: #f5f5f5; }
-.base07 { color: #ECF0F1; }
-.base08 { color: #E74C3C; }
-.base09 { color: #E67E22; }
-.base0A { color: #F1C40F; }
-.base0B { color: #2ECC71; }
-.base0C { color: #1ABC9C; }
-.base0D { color: #3498DB; }
-.base0E { color: #9B59B6; }
-.base0F { color: #be643c; }
diff --git a/static/css/base16-github.css b/static/css/base16-github.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #ffffff; }
-.base01-background { background-color: #f5f5f5; }
-.base02-background { background-color: #c8c8fa; }
-.base03-background { background-color: #969896; }
-.base04-background { background-color: #e8e8e8; }
-.base05-background { background-color: #333333; }
-.base06-background { background-color: #ffffff; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #ed6a43; }
-.base09-background { background-color: #0086b3; }
-.base0A-background { background-color: #795da3; }
-.base0B-background { background-color: #183691; }
-.base0C-background { background-color: #183691; }
-.base0D-background { background-color: #795da3; }
-.base0E-background { background-color: #a71d5d; }
-.base0F-background { background-color: #333333; }
-
-.base00 { color: #ffffff; }
-.base01 { color: #f5f5f5; }
-.base02 { color: #c8c8fa; }
-.base03 { color: #969896; }
-.base04 { color: #e8e8e8; }
-.base05 { color: #333333; }
-.base06 { color: #ffffff; }
-.base07 { color: #ffffff; }
-.base08 { color: #ed6a43; }
-.base09 { color: #0086b3; }
-.base0A { color: #795da3; }
-.base0B { color: #183691; }
-.base0C { color: #183691; }
-.base0D { color: #795da3; }
-.base0E { color: #a71d5d; }
-.base0F { color: #333333; }
diff --git a/static/css/base16-google-dark.css b/static/css/base16-google-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1d1f21; }
-.base01-background { background-color: #282a2e; }
-.base02-background { background-color: #373b41; }
-.base03-background { background-color: #969896; }
-.base04-background { background-color: #b4b7b4; }
-.base05-background { background-color: #c5c8c6; }
-.base06-background { background-color: #e0e0e0; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #CC342B; }
-.base09-background { background-color: #F96A38; }
-.base0A-background { background-color: #FBA922; }
-.base0B-background { background-color: #198844; }
-.base0C-background { background-color: #3971ED; }
-.base0D-background { background-color: #3971ED; }
-.base0E-background { background-color: #A36AC7; }
-.base0F-background { background-color: #3971ED; }
-
-.base00 { color: #1d1f21; }
-.base01 { color: #282a2e; }
-.base02 { color: #373b41; }
-.base03 { color: #969896; }
-.base04 { color: #b4b7b4; }
-.base05 { color: #c5c8c6; }
-.base06 { color: #e0e0e0; }
-.base07 { color: #ffffff; }
-.base08 { color: #CC342B; }
-.base09 { color: #F96A38; }
-.base0A { color: #FBA922; }
-.base0B { color: #198844; }
-.base0C { color: #3971ED; }
-.base0D { color: #3971ED; }
-.base0E { color: #A36AC7; }
-.base0F { color: #3971ED; }
diff --git a/static/css/base16-google-light.css b/static/css/base16-google-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #ffffff; }
-.base01-background { background-color: #e0e0e0; }
-.base02-background { background-color: #c5c8c6; }
-.base03-background { background-color: #b4b7b4; }
-.base04-background { background-color: #969896; }
-.base05-background { background-color: #373b41; }
-.base06-background { background-color: #282a2e; }
-.base07-background { background-color: #1d1f21; }
-.base08-background { background-color: #CC342B; }
-.base09-background { background-color: #F96A38; }
-.base0A-background { background-color: #FBA922; }
-.base0B-background { background-color: #198844; }
-.base0C-background { background-color: #3971ED; }
-.base0D-background { background-color: #3971ED; }
-.base0E-background { background-color: #A36AC7; }
-.base0F-background { background-color: #3971ED; }
-
-.base00 { color: #ffffff; }
-.base01 { color: #e0e0e0; }
-.base02 { color: #c5c8c6; }
-.base03 { color: #b4b7b4; }
-.base04 { color: #969896; }
-.base05 { color: #373b41; }
-.base06 { color: #282a2e; }
-.base07 { color: #1d1f21; }
-.base08 { color: #CC342B; }
-.base09 { color: #F96A38; }
-.base0A { color: #FBA922; }
-.base0B { color: #198844; }
-.base0C { color: #3971ED; }
-.base0D { color: #3971ED; }
-.base0E { color: #A36AC7; }
-.base0F { color: #3971ED; }
diff --git a/static/css/base16-grayscale-dark.css b/static/css/base16-grayscale-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #101010; }
-.base01-background { background-color: #252525; }
-.base02-background { background-color: #464646; }
-.base03-background { background-color: #525252; }
-.base04-background { background-color: #ababab; }
-.base05-background { background-color: #b9b9b9; }
-.base06-background { background-color: #e3e3e3; }
-.base07-background { background-color: #f7f7f7; }
-.base08-background { background-color: #7c7c7c; }
-.base09-background { background-color: #999999; }
-.base0A-background { background-color: #a0a0a0; }
-.base0B-background { background-color: #8e8e8e; }
-.base0C-background { background-color: #868686; }
-.base0D-background { background-color: #686868; }
-.base0E-background { background-color: #747474; }
-.base0F-background { background-color: #5e5e5e; }
-
-.base00 { color: #101010; }
-.base01 { color: #252525; }
-.base02 { color: #464646; }
-.base03 { color: #525252; }
-.base04 { color: #ababab; }
-.base05 { color: #b9b9b9; }
-.base06 { color: #e3e3e3; }
-.base07 { color: #f7f7f7; }
-.base08 { color: #7c7c7c; }
-.base09 { color: #999999; }
-.base0A { color: #a0a0a0; }
-.base0B { color: #8e8e8e; }
-.base0C { color: #868686; }
-.base0D { color: #686868; }
-.base0E { color: #747474; }
-.base0F { color: #5e5e5e; }
diff --git a/static/css/base16-grayscale-light.css b/static/css/base16-grayscale-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #f7f7f7; }
-.base01-background { background-color: #e3e3e3; }
-.base02-background { background-color: #b9b9b9; }
-.base03-background { background-color: #ababab; }
-.base04-background { background-color: #525252; }
-.base05-background { background-color: #464646; }
-.base06-background { background-color: #252525; }
-.base07-background { background-color: #101010; }
-.base08-background { background-color: #7c7c7c; }
-.base09-background { background-color: #999999; }
-.base0A-background { background-color: #a0a0a0; }
-.base0B-background { background-color: #8e8e8e; }
-.base0C-background { background-color: #868686; }
-.base0D-background { background-color: #686868; }
-.base0E-background { background-color: #747474; }
-.base0F-background { background-color: #5e5e5e; }
-
-.base00 { color: #f7f7f7; }
-.base01 { color: #e3e3e3; }
-.base02 { color: #b9b9b9; }
-.base03 { color: #ababab; }
-.base04 { color: #525252; }
-.base05 { color: #464646; }
-.base06 { color: #252525; }
-.base07 { color: #101010; }
-.base08 { color: #7c7c7c; }
-.base09 { color: #999999; }
-.base0A { color: #a0a0a0; }
-.base0B { color: #8e8e8e; }
-.base0C { color: #868686; }
-.base0D { color: #686868; }
-.base0E { color: #747474; }
-.base0F { color: #5e5e5e; }
diff --git a/static/css/base16-green-screen.css b/static/css/base16-green-screen.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #001100; }
-.base01-background { background-color: #003300; }
-.base02-background { background-color: #005500; }
-.base03-background { background-color: #007700; }
-.base04-background { background-color: #009900; }
-.base05-background { background-color: #00bb00; }
-.base06-background { background-color: #00dd00; }
-.base07-background { background-color: #00ff00; }
-.base08-background { background-color: #007700; }
-.base09-background { background-color: #009900; }
-.base0A-background { background-color: #007700; }
-.base0B-background { background-color: #00bb00; }
-.base0C-background { background-color: #005500; }
-.base0D-background { background-color: #009900; }
-.base0E-background { background-color: #00bb00; }
-.base0F-background { background-color: #005500; }
-
-.base00 { color: #001100; }
-.base01 { color: #003300; }
-.base02 { color: #005500; }
-.base03 { color: #007700; }
-.base04 { color: #009900; }
-.base05 { color: #00bb00; }
-.base06 { color: #00dd00; }
-.base07 { color: #00ff00; }
-.base08 { color: #007700; }
-.base09 { color: #009900; }
-.base0A { color: #007700; }
-.base0B { color: #00bb00; }
-.base0C { color: #005500; }
-.base0D { color: #009900; }
-.base0E { color: #00bb00; }
-.base0F { color: #005500; }
diff --git a/static/css/base16-harmonic16-dark.css b/static/css/base16-harmonic16-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #0b1c2c; }
-.base01-background { background-color: #223b54; }
-.base02-background { background-color: #405c79; }
-.base03-background { background-color: #627e99; }
-.base04-background { background-color: #aabcce; }
-.base05-background { background-color: #cbd6e2; }
-.base06-background { background-color: #e5ebf1; }
-.base07-background { background-color: #f7f9fb; }
-.base08-background { background-color: #bf8b56; }
-.base09-background { background-color: #bfbf56; }
-.base0A-background { background-color: #8bbf56; }
-.base0B-background { background-color: #56bf8b; }
-.base0C-background { background-color: #568bbf; }
-.base0D-background { background-color: #8b56bf; }
-.base0E-background { background-color: #bf568b; }
-.base0F-background { background-color: #bf5656; }
-
-.base00 { color: #0b1c2c; }
-.base01 { color: #223b54; }
-.base02 { color: #405c79; }
-.base03 { color: #627e99; }
-.base04 { color: #aabcce; }
-.base05 { color: #cbd6e2; }
-.base06 { color: #e5ebf1; }
-.base07 { color: #f7f9fb; }
-.base08 { color: #bf8b56; }
-.base09 { color: #bfbf56; }
-.base0A { color: #8bbf56; }
-.base0B { color: #56bf8b; }
-.base0C { color: #568bbf; }
-.base0D { color: #8b56bf; }
-.base0E { color: #bf568b; }
-.base0F { color: #bf5656; }
diff --git a/static/css/base16-harmonic16-light.css b/static/css/base16-harmonic16-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #f7f9fb; }
-.base01-background { background-color: #e5ebf1; }
-.base02-background { background-color: #cbd6e2; }
-.base03-background { background-color: #aabcce; }
-.base04-background { background-color: #627e99; }
-.base05-background { background-color: #405c79; }
-.base06-background { background-color: #223b54; }
-.base07-background { background-color: #0b1c2c; }
-.base08-background { background-color: #bf8b56; }
-.base09-background { background-color: #bfbf56; }
-.base0A-background { background-color: #8bbf56; }
-.base0B-background { background-color: #56bf8b; }
-.base0C-background { background-color: #568bbf; }
-.base0D-background { background-color: #8b56bf; }
-.base0E-background { background-color: #bf568b; }
-.base0F-background { background-color: #bf5656; }
-
-.base00 { color: #f7f9fb; }
-.base01 { color: #e5ebf1; }
-.base02 { color: #cbd6e2; }
-.base03 { color: #aabcce; }
-.base04 { color: #627e99; }
-.base05 { color: #405c79; }
-.base06 { color: #223b54; }
-.base07 { color: #0b1c2c; }
-.base08 { color: #bf8b56; }
-.base09 { color: #bfbf56; }
-.base0A { color: #8bbf56; }
-.base0B { color: #56bf8b; }
-.base0C { color: #568bbf; }
-.base0D { color: #8b56bf; }
-.base0E { color: #bf568b; }
-.base0F { color: #bf5656; }
diff --git a/static/css/base16-hopscotch.css b/static/css/base16-hopscotch.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #322931; }
-.base01-background { background-color: #433b42; }
-.base02-background { background-color: #5c545b; }
-.base03-background { background-color: #797379; }
-.base04-background { background-color: #989498; }
-.base05-background { background-color: #b9b5b8; }
-.base06-background { background-color: #d5d3d5; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #dd464c; }
-.base09-background { background-color: #fd8b19; }
-.base0A-background { background-color: #fdcc59; }
-.base0B-background { background-color: #8fc13e; }
-.base0C-background { background-color: #149b93; }
-.base0D-background { background-color: #1290bf; }
-.base0E-background { background-color: #c85e7c; }
-.base0F-background { background-color: #b33508; }
-
-.base00 { color: #322931; }
-.base01 { color: #433b42; }
-.base02 { color: #5c545b; }
-.base03 { color: #797379; }
-.base04 { color: #989498; }
-.base05 { color: #b9b5b8; }
-.base06 { color: #d5d3d5; }
-.base07 { color: #ffffff; }
-.base08 { color: #dd464c; }
-.base09 { color: #fd8b19; }
-.base0A { color: #fdcc59; }
-.base0B { color: #8fc13e; }
-.base0C { color: #149b93; }
-.base0D { color: #1290bf; }
-.base0E { color: #c85e7c; }
-.base0F { color: #b33508; }
diff --git a/static/css/base16-ir-black.css b/static/css/base16-ir-black.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #000000; }
-.base01-background { background-color: #242422; }
-.base02-background { background-color: #484844; }
-.base03-background { background-color: #6c6c66; }
-.base04-background { background-color: #918f88; }
-.base05-background { background-color: #b5b3aa; }
-.base06-background { background-color: #d9d7cc; }
-.base07-background { background-color: #fdfbee; }
-.base08-background { background-color: #ff6c60; }
-.base09-background { background-color: #e9c062; }
-.base0A-background { background-color: #ffffb6; }
-.base0B-background { background-color: #a8ff60; }
-.base0C-background { background-color: #c6c5fe; }
-.base0D-background { background-color: #96cbfe; }
-.base0E-background { background-color: #ff73fd; }
-.base0F-background { background-color: #b18a3d; }
-
-.base00 { color: #000000; }
-.base01 { color: #242422; }
-.base02 { color: #484844; }
-.base03 { color: #6c6c66; }
-.base04 { color: #918f88; }
-.base05 { color: #b5b3aa; }
-.base06 { color: #d9d7cc; }
-.base07 { color: #fdfbee; }
-.base08 { color: #ff6c60; }
-.base09 { color: #e9c062; }
-.base0A { color: #ffffb6; }
-.base0B { color: #a8ff60; }
-.base0C { color: #c6c5fe; }
-.base0D { color: #96cbfe; }
-.base0E { color: #ff73fd; }
-.base0F { color: #b18a3d; }
diff --git a/static/css/base16-isotope.css b/static/css/base16-isotope.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #000000; }
-.base01-background { background-color: #404040; }
-.base02-background { background-color: #606060; }
-.base03-background { background-color: #808080; }
-.base04-background { background-color: #c0c0c0; }
-.base05-background { background-color: #d0d0d0; }
-.base06-background { background-color: #e0e0e0; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #ff0000; }
-.base09-background { background-color: #ff9900; }
-.base0A-background { background-color: #ff0099; }
-.base0B-background { background-color: #33ff00; }
-.base0C-background { background-color: #00ffff; }
-.base0D-background { background-color: #0066ff; }
-.base0E-background { background-color: #cc00ff; }
-.base0F-background { background-color: #3300ff; }
-
-.base00 { color: #000000; }
-.base01 { color: #404040; }
-.base02 { color: #606060; }
-.base03 { color: #808080; }
-.base04 { color: #c0c0c0; }
-.base05 { color: #d0d0d0; }
-.base06 { color: #e0e0e0; }
-.base07 { color: #ffffff; }
-.base08 { color: #ff0000; }
-.base09 { color: #ff9900; }
-.base0A { color: #ff0099; }
-.base0B { color: #33ff00; }
-.base0C { color: #00ffff; }
-.base0D { color: #0066ff; }
-.base0E { color: #cc00ff; }
-.base0F { color: #3300ff; }
diff --git a/static/css/base16-london-tube.css b/static/css/base16-london-tube.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #231f20; }
-.base01-background { background-color: #1c3f95; }
-.base02-background { background-color: #5a5758; }
-.base03-background { background-color: #737171; }
-.base04-background { background-color: #959ca1; }
-.base05-background { background-color: #d9d8d8; }
-.base06-background { background-color: #e7e7e8; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #ee2e24; }
-.base09-background { background-color: #f386a1; }
-.base0A-background { background-color: #ffd204; }
-.base0B-background { background-color: #00853e; }
-.base0C-background { background-color: #85cebc; }
-.base0D-background { background-color: #009ddc; }
-.base0E-background { background-color: #98005d; }
-.base0F-background { background-color: #b06110; }
-
-.base00 { color: #231f20; }
-.base01 { color: #1c3f95; }
-.base02 { color: #5a5758; }
-.base03 { color: #737171; }
-.base04 { color: #959ca1; }
-.base05 { color: #d9d8d8; }
-.base06 { color: #e7e7e8; }
-.base07 { color: #ffffff; }
-.base08 { color: #ee2e24; }
-.base09 { color: #f386a1; }
-.base0A { color: #ffd204; }
-.base0B { color: #00853e; }
-.base0C { color: #85cebc; }
-.base0D { color: #009ddc; }
-.base0E { color: #98005d; }
-.base0F { color: #b06110; }
diff --git a/static/css/base16-macintosh.css b/static/css/base16-macintosh.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #000000; }
-.base01-background { background-color: #404040; }
-.base02-background { background-color: #404040; }
-.base03-background { background-color: #808080; }
-.base04-background { background-color: #808080; }
-.base05-background { background-color: #c0c0c0; }
-.base06-background { background-color: #c0c0c0; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #dd0907; }
-.base09-background { background-color: #ff6403; }
-.base0A-background { background-color: #fbf305; }
-.base0B-background { background-color: #1fb714; }
-.base0C-background { background-color: #02abea; }
-.base0D-background { background-color: #0000d3; }
-.base0E-background { background-color: #4700a5; }
-.base0F-background { background-color: #90713a; }
-
-.base00 { color: #000000; }
-.base01 { color: #404040; }
-.base02 { color: #404040; }
-.base03 { color: #808080; }
-.base04 { color: #808080; }
-.base05 { color: #c0c0c0; }
-.base06 { color: #c0c0c0; }
-.base07 { color: #ffffff; }
-.base08 { color: #dd0907; }
-.base09 { color: #ff6403; }
-.base0A { color: #fbf305; }
-.base0B { color: #1fb714; }
-.base0C { color: #02abea; }
-.base0D { color: #0000d3; }
-.base0E { color: #4700a5; }
-.base0F { color: #90713a; }
diff --git a/static/css/base16-marrakesh.css b/static/css/base16-marrakesh.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #201602; }
-.base01-background { background-color: #302e00; }
-.base02-background { background-color: #5f5b17; }
-.base03-background { background-color: #6c6823; }
-.base04-background { background-color: #86813b; }
-.base05-background { background-color: #948e48; }
-.base06-background { background-color: #ccc37a; }
-.base07-background { background-color: #faf0a5; }
-.base08-background { background-color: #c35359; }
-.base09-background { background-color: #b36144; }
-.base0A-background { background-color: #a88339; }
-.base0B-background { background-color: #18974e; }
-.base0C-background { background-color: #75a738; }
-.base0D-background { background-color: #477ca1; }
-.base0E-background { background-color: #8868b3; }
-.base0F-background { background-color: #b3588e; }
-
-.base00 { color: #201602; }
-.base01 { color: #302e00; }
-.base02 { color: #5f5b17; }
-.base03 { color: #6c6823; }
-.base04 { color: #86813b; }
-.base05 { color: #948e48; }
-.base06 { color: #ccc37a; }
-.base07 { color: #faf0a5; }
-.base08 { color: #c35359; }
-.base09 { color: #b36144; }
-.base0A { color: #a88339; }
-.base0B { color: #18974e; }
-.base0C { color: #75a738; }
-.base0D { color: #477ca1; }
-.base0E { color: #8868b3; }
-.base0F { color: #b3588e; }
diff --git a/static/css/base16-materia.css b/static/css/base16-materia.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #263238; }
-.base01-background { background-color: #2C393F; }
-.base02-background { background-color: #37474F; }
-.base03-background { background-color: #707880; }
-.base04-background { background-color: #C9CCD3; }
-.base05-background { background-color: #CDD3DE; }
-.base06-background { background-color: #D5DBE5; }
-.base07-background { background-color: #FFFFFF; }
-.base08-background { background-color: #EC5F67; }
-.base09-background { background-color: #EA9560; }
-.base0A-background { background-color: #FFCC00; }
-.base0B-background { background-color: #8BD649; }
-.base0C-background { background-color: #80CBC4; }
-.base0D-background { background-color: #89DDFF; }
-.base0E-background { background-color: #82AAFF; }
-.base0F-background { background-color: #EC5F67; }
-
-.base00 { color: #263238; }
-.base01 { color: #2C393F; }
-.base02 { color: #37474F; }
-.base03 { color: #707880; }
-.base04 { color: #C9CCD3; }
-.base05 { color: #CDD3DE; }
-.base06 { color: #D5DBE5; }
-.base07 { color: #FFFFFF; }
-.base08 { color: #EC5F67; }
-.base09 { color: #EA9560; }
-.base0A { color: #FFCC00; }
-.base0B { color: #8BD649; }
-.base0C { color: #80CBC4; }
-.base0D { color: #89DDFF; }
-.base0E { color: #82AAFF; }
-.base0F { color: #EC5F67; }
diff --git a/static/css/base16-mexico-light.css b/static/css/base16-mexico-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #f8f8f8; }
-.base01-background { background-color: #e8e8e8; }
-.base02-background { background-color: #d8d8d8; }
-.base03-background { background-color: #b8b8b8; }
-.base04-background { background-color: #585858; }
-.base05-background { background-color: #383838; }
-.base06-background { background-color: #282828; }
-.base07-background { background-color: #181818; }
-.base08-background { background-color: #ab4642; }
-.base09-background { background-color: #dc9656; }
-.base0A-background { background-color: #f79a0e; }
-.base0B-background { background-color: #538947; }
-.base0C-background { background-color: #4b8093; }
-.base0D-background { background-color: #7cafc2; }
-.base0E-background { background-color: #96609e; }
-.base0F-background { background-color: #a16946; }
-
-.base00 { color: #f8f8f8; }
-.base01 { color: #e8e8e8; }
-.base02 { color: #d8d8d8; }
-.base03 { color: #b8b8b8; }
-.base04 { color: #585858; }
-.base05 { color: #383838; }
-.base06 { color: #282828; }
-.base07 { color: #181818; }
-.base08 { color: #ab4642; }
-.base09 { color: #dc9656; }
-.base0A { color: #f79a0e; }
-.base0B { color: #538947; }
-.base0C { color: #4b8093; }
-.base0D { color: #7cafc2; }
-.base0E { color: #96609e; }
-.base0F { color: #a16946; }
diff --git a/static/css/base16-mocha.css b/static/css/base16-mocha.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #3B3228; }
-.base01-background { background-color: #534636; }
-.base02-background { background-color: #645240; }
-.base03-background { background-color: #7e705a; }
-.base04-background { background-color: #b8afad; }
-.base05-background { background-color: #d0c8c6; }
-.base06-background { background-color: #e9e1dd; }
-.base07-background { background-color: #f5eeeb; }
-.base08-background { background-color: #cb6077; }
-.base09-background { background-color: #d28b71; }
-.base0A-background { background-color: #f4bc87; }
-.base0B-background { background-color: #beb55b; }
-.base0C-background { background-color: #7bbda4; }
-.base0D-background { background-color: #8ab3b5; }
-.base0E-background { background-color: #a89bb9; }
-.base0F-background { background-color: #bb9584; }
-
-.base00 { color: #3B3228; }
-.base01 { color: #534636; }
-.base02 { color: #645240; }
-.base03 { color: #7e705a; }
-.base04 { color: #b8afad; }
-.base05 { color: #d0c8c6; }
-.base06 { color: #e9e1dd; }
-.base07 { color: #f5eeeb; }
-.base08 { color: #cb6077; }
-.base09 { color: #d28b71; }
-.base0A { color: #f4bc87; }
-.base0B { color: #beb55b; }
-.base0C { color: #7bbda4; }
-.base0D { color: #8ab3b5; }
-.base0E { color: #a89bb9; }
-.base0F { color: #bb9584; }
diff --git a/static/css/base16-monokai.css b/static/css/base16-monokai.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #272822; }
-.base01-background { background-color: #383830; }
-.base02-background { background-color: #49483e; }
-.base03-background { background-color: #75715e; }
-.base04-background { background-color: #a59f85; }
-.base05-background { background-color: #f8f8f2; }
-.base06-background { background-color: #f5f4f1; }
-.base07-background { background-color: #f9f8f5; }
-.base08-background { background-color: #f92672; }
-.base09-background { background-color: #fd971f; }
-.base0A-background { background-color: #f4bf75; }
-.base0B-background { background-color: #a6e22e; }
-.base0C-background { background-color: #a1efe4; }
-.base0D-background { background-color: #66d9ef; }
-.base0E-background { background-color: #ae81ff; }
-.base0F-background { background-color: #cc6633; }
-
-.base00 { color: #272822; }
-.base01 { color: #383830; }
-.base02 { color: #49483e; }
-.base03 { color: #75715e; }
-.base04 { color: #a59f85; }
-.base05 { color: #f8f8f2; }
-.base06 { color: #f5f4f1; }
-.base07 { color: #f9f8f5; }
-.base08 { color: #f92672; }
-.base09 { color: #fd971f; }
-.base0A { color: #f4bf75; }
-.base0B { color: #a6e22e; }
-.base0C { color: #a1efe4; }
-.base0D { color: #66d9ef; }
-.base0E { color: #ae81ff; }
-.base0F { color: #cc6633; }
diff --git a/static/css/base16-ocean.css b/static/css/base16-ocean.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #2b303b; }
-.base01-background { background-color: #343d46; }
-.base02-background { background-color: #4f5b66; }
-.base03-background { background-color: #65737e; }
-.base04-background { background-color: #a7adba; }
-.base05-background { background-color: #c0c5ce; }
-.base06-background { background-color: #dfe1e8; }
-.base07-background { background-color: #eff1f5; }
-.base08-background { background-color: #bf616a; }
-.base09-background { background-color: #d08770; }
-.base0A-background { background-color: #ebcb8b; }
-.base0B-background { background-color: #a3be8c; }
-.base0C-background { background-color: #96b5b4; }
-.base0D-background { background-color: #8fa1b3; }
-.base0E-background { background-color: #b48ead; }
-.base0F-background { background-color: #ab7967; }
-
-.base00 { color: #2b303b; }
-.base01 { color: #343d46; }
-.base02 { color: #4f5b66; }
-.base03 { color: #65737e; }
-.base04 { color: #a7adba; }
-.base05 { color: #c0c5ce; }
-.base06 { color: #dfe1e8; }
-.base07 { color: #eff1f5; }
-.base08 { color: #bf616a; }
-.base09 { color: #d08770; }
-.base0A { color: #ebcb8b; }
-.base0B { color: #a3be8c; }
-.base0C { color: #96b5b4; }
-.base0D { color: #8fa1b3; }
-.base0E { color: #b48ead; }
-.base0F { color: #ab7967; }
diff --git a/static/css/base16-oceanicnext.css b/static/css/base16-oceanicnext.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1B2B34; }
-.base01-background { background-color: #343D46; }
-.base02-background { background-color: #4F5B66; }
-.base03-background { background-color: #65737E; }
-.base04-background { background-color: #A7ADBA; }
-.base05-background { background-color: #C0C5CE; }
-.base06-background { background-color: #CDD3DE; }
-.base07-background { background-color: #D8DEE9; }
-.base08-background { background-color: #EC5f67; }
-.base09-background { background-color: #F99157; }
-.base0A-background { background-color: #FAC863; }
-.base0B-background { background-color: #99C794; }
-.base0C-background { background-color: #5FB3B3; }
-.base0D-background { background-color: #6699CC; }
-.base0E-background { background-color: #C594C5; }
-.base0F-background { background-color: #AB7967; }
-
-.base00 { color: #1B2B34; }
-.base01 { color: #343D46; }
-.base02 { color: #4F5B66; }
-.base03 { color: #65737E; }
-.base04 { color: #A7ADBA; }
-.base05 { color: #C0C5CE; }
-.base06 { color: #CDD3DE; }
-.base07 { color: #D8DEE9; }
-.base08 { color: #EC5f67; }
-.base09 { color: #F99157; }
-.base0A { color: #FAC863; }
-.base0B { color: #99C794; }
-.base0C { color: #5FB3B3; }
-.base0D { color: #6699CC; }
-.base0E { color: #C594C5; }
-.base0F { color: #AB7967; }
diff --git a/static/css/base16-paraiso.css b/static/css/base16-paraiso.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #2f1e2e; }
-.base01-background { background-color: #41323f; }
-.base02-background { background-color: #4f424c; }
-.base03-background { background-color: #776e71; }
-.base04-background { background-color: #8d8687; }
-.base05-background { background-color: #a39e9b; }
-.base06-background { background-color: #b9b6b0; }
-.base07-background { background-color: #e7e9db; }
-.base08-background { background-color: #ef6155; }
-.base09-background { background-color: #f99b15; }
-.base0A-background { background-color: #fec418; }
-.base0B-background { background-color: #48b685; }
-.base0C-background { background-color: #5bc4bf; }
-.base0D-background { background-color: #06b6ef; }
-.base0E-background { background-color: #815ba4; }
-.base0F-background { background-color: #e96ba8; }
-
-.base00 { color: #2f1e2e; }
-.base01 { color: #41323f; }
-.base02 { color: #4f424c; }
-.base03 { color: #776e71; }
-.base04 { color: #8d8687; }
-.base05 { color: #a39e9b; }
-.base06 { color: #b9b6b0; }
-.base07 { color: #e7e9db; }
-.base08 { color: #ef6155; }
-.base09 { color: #f99b15; }
-.base0A { color: #fec418; }
-.base0B { color: #48b685; }
-.base0C { color: #5bc4bf; }
-.base0D { color: #06b6ef; }
-.base0E { color: #815ba4; }
-.base0F { color: #e96ba8; }
diff --git a/static/css/base16-phd.css b/static/css/base16-phd.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #061229; }
-.base01-background { background-color: #2a3448; }
-.base02-background { background-color: #4d5666; }
-.base03-background { background-color: #717885; }
-.base04-background { background-color: #9a99a3; }
-.base05-background { background-color: #b8bbc2; }
-.base06-background { background-color: #dbdde0; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #d07346; }
-.base09-background { background-color: #f0a000; }
-.base0A-background { background-color: #fbd461; }
-.base0B-background { background-color: #99bf52; }
-.base0C-background { background-color: #72b9bf; }
-.base0D-background { background-color: #5299bf; }
-.base0E-background { background-color: #9989cc; }
-.base0F-background { background-color: #b08060; }
-
-.base00 { color: #061229; }
-.base01 { color: #2a3448; }
-.base02 { color: #4d5666; }
-.base03 { color: #717885; }
-.base04 { color: #9a99a3; }
-.base05 { color: #b8bbc2; }
-.base06 { color: #dbdde0; }
-.base07 { color: #ffffff; }
-.base08 { color: #d07346; }
-.base09 { color: #f0a000; }
-.base0A { color: #fbd461; }
-.base0B { color: #99bf52; }
-.base0C { color: #72b9bf; }
-.base0D { color: #5299bf; }
-.base0E { color: #9989cc; }
-.base0F { color: #b08060; }
diff --git a/static/css/base16-pico.css b/static/css/base16-pico.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #000000; }
-.base01-background { background-color: #1d2b53; }
-.base02-background { background-color: #7e2553; }
-.base03-background { background-color: #008751; }
-.base04-background { background-color: #ab5236; }
-.base05-background { background-color: #5f574f; }
-.base06-background { background-color: #c2c3c7; }
-.base07-background { background-color: #fff1e8; }
-.base08-background { background-color: #ff004d; }
-.base09-background { background-color: #ffa300; }
-.base0A-background { background-color: #fff024; }
-.base0B-background { background-color: #00e756; }
-.base0C-background { background-color: #29adff; }
-.base0D-background { background-color: #83769c; }
-.base0E-background { background-color: #ff77a8; }
-.base0F-background { background-color: #ffccaa; }
-
-.base00 { color: #000000; }
-.base01 { color: #1d2b53; }
-.base02 { color: #7e2553; }
-.base03 { color: #008751; }
-.base04 { color: #ab5236; }
-.base05 { color: #5f574f; }
-.base06 { color: #c2c3c7; }
-.base07 { color: #fff1e8; }
-.base08 { color: #ff004d; }
-.base09 { color: #ffa300; }
-.base0A { color: #fff024; }
-.base0B { color: #00e756; }
-.base0C { color: #29adff; }
-.base0D { color: #83769c; }
-.base0E { color: #ff77a8; }
-.base0F { color: #ffccaa; }
diff --git a/static/css/base16-pleroma-dark.css b/static/css/base16-pleroma-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #161c20; }
-.base01-background { background-color: #282e32; }
-.base02-background { background-color: #343a3f; }
-.base03-background { background-color: #4e5256; }
-.base04-background { background-color: #ababab; }
-.base05-background { background-color: #b9b9b9; }
-.base06-background { background-color: #d0d0d0; }
-.base07-background { background-color: #e7e7e7; }
-.base08-background { background-color: #baaa9c; }
-.base09-background { background-color: #999999; }
-.base0A-background { background-color: #a0a0a0; }
-.base0B-background { background-color: #8e8e8e; }
-.base0C-background { background-color: #868686; }
-.base0D-background { background-color: #686868; }
-.base0E-background { background-color: #747474; }
-.base0F-background { background-color: #5e5e5e; }
-
-.base00 { color: #161c20; }
-.base01 { color: #282e32; }
-.base02 { color: #343a3f; }
-.base03 { color: #4e5256; }
-.base04 { color: #ababab; }
-.base05 { color: #b9b9b9; }
-.base06 { color: #d0d0d0; }
-.base07 { color: #e7e7e7; }
-.base08 { color: #baaa9c; }
-.base09 { color: #999999; }
-.base0A { color: #a0a0a0; }
-.base0B { color: #8e8e8e; }
-.base0C { color: #868686; }
-.base0D { color: #686868; }
-.base0E { color: #747474; }
-.base0F { color: #5e5e5e; }
diff --git a/static/css/base16-pleroma-light.css b/static/css/base16-pleroma-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #f2f4f6; }
-.base01-background { background-color: #dde2e6; }
-.base02-background { background-color: #c0c6cb; }
-.base03-background { background-color: #a4a4a4; }
-.base04-background { background-color: #545454; }
-.base05-background { background-color: #304055; }
-.base06-background { background-color: #040404; }
-.base07-background { background-color: #000000; }
-.base08-background { background-color: #e92f2f; }
-.base09-background { background-color: #e09448; }
-.base0A-background { background-color: #dddd13; }
-.base0B-background { background-color: #0ed839; }
-.base0C-background { background-color: #23edda; }
-.base0D-background { background-color: #3b48e3; }
-.base0E-background { background-color: #f996e2; }
-.base0F-background { background-color: #69542d; }
-
-.base00 { color: #f2f4f6; }
-.base01 { color: #dde2e6; }
-.base02 { color: #c0c6cb; }
-.base03 { color: #a4a4a4; }
-.base04 { color: #545454; }
-.base05 { color: #304055; }
-.base06 { color: #040404; }
-.base07 { color: #000000; }
-.base08 { color: #e46f0f; }
-.base09 { color: #e09448; }
-.base0A { color: #dddd13; }
-.base0B { color: #0ed839; }
-.base0C { color: #23edda; }
-.base0D { color: #3b48e3; }
-.base0E { color: #f996e2; }
-.base0F { color: #69542d; }
diff --git a/static/css/base16-pop.css b/static/css/base16-pop.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #000000; }
-.base01-background { background-color: #202020; }
-.base02-background { background-color: #303030; }
-.base03-background { background-color: #505050; }
-.base04-background { background-color: #b0b0b0; }
-.base05-background { background-color: #d0d0d0; }
-.base06-background { background-color: #e0e0e0; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #eb008a; }
-.base09-background { background-color: #f29333; }
-.base0A-background { background-color: #f8ca12; }
-.base0B-background { background-color: #37b349; }
-.base0C-background { background-color: #00aabb; }
-.base0D-background { background-color: #0e5a94; }
-.base0E-background { background-color: #b31e8d; }
-.base0F-background { background-color: #7a2d00; }
-
-.base00 { color: #000000; }
-.base01 { color: #202020; }
-.base02 { color: #303030; }
-.base03 { color: #505050; }
-.base04 { color: #b0b0b0; }
-.base05 { color: #d0d0d0; }
-.base06 { color: #e0e0e0; }
-.base07 { color: #ffffff; }
-.base08 { color: #eb008a; }
-.base09 { color: #f29333; }
-.base0A { color: #f8ca12; }
-.base0B { color: #37b349; }
-.base0C { color: #00aabb; }
-.base0D { color: #0e5a94; }
-.base0E { color: #b31e8d; }
-.base0F { color: #7a2d00; }
diff --git a/static/css/base16-railscasts.css b/static/css/base16-railscasts.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #2b2b2b; }
-.base01-background { background-color: #272935; }
-.base02-background { background-color: #3a4055; }
-.base03-background { background-color: #5a647e; }
-.base04-background { background-color: #d4cfc9; }
-.base05-background { background-color: #e6e1dc; }
-.base06-background { background-color: #f4f1ed; }
-.base07-background { background-color: #f9f7f3; }
-.base08-background { background-color: #da4939; }
-.base09-background { background-color: #cc7833; }
-.base0A-background { background-color: #ffc66d; }
-.base0B-background { background-color: #a5c261; }
-.base0C-background { background-color: #519f50; }
-.base0D-background { background-color: #6d9cbe; }
-.base0E-background { background-color: #b6b3eb; }
-.base0F-background { background-color: #bc9458; }
-
-.base00 { color: #2b2b2b; }
-.base01 { color: #272935; }
-.base02 { color: #3a4055; }
-.base03 { color: #5a647e; }
-.base04 { color: #d4cfc9; }
-.base05 { color: #e6e1dc; }
-.base06 { color: #f4f1ed; }
-.base07 { color: #f9f7f3; }
-.base08 { color: #da4939; }
-.base09 { color: #cc7833; }
-.base0A { color: #ffc66d; }
-.base0B { color: #a5c261; }
-.base0C { color: #519f50; }
-.base0D { color: #6d9cbe; }
-.base0E { color: #b6b3eb; }
-.base0F { color: #bc9458; }
diff --git a/static/css/base16-seti-ui.css b/static/css/base16-seti-ui.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #151718; }
-.base01-background { background-color: #8ec43d; }
-.base02-background { background-color: #3B758C; }
-.base03-background { background-color: #41535B; }
-.base04-background { background-color: #43a5d5; }
-.base05-background { background-color: #d6d6d6; }
-.base06-background { background-color: #eeeeee; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #Cd3f45; }
-.base09-background { background-color: #db7b55; }
-.base0A-background { background-color: #e6cd69; }
-.base0B-background { background-color: #9fca56; }
-.base0C-background { background-color: #55dbbe; }
-.base0D-background { background-color: #55b5db; }
-.base0E-background { background-color: #a074c4; }
-.base0F-background { background-color: #8a553f; }
-
-.base00 { color: #151718; }
-.base01 { color: #8ec43d; }
-.base02 { color: #3B758C; }
-.base03 { color: #41535B; }
-.base04 { color: #43a5d5; }
-.base05 { color: #d6d6d6; }
-.base06 { color: #eeeeee; }
-.base07 { color: #ffffff; }
-.base08 { color: #Cd3f45; }
-.base09 { color: #db7b55; }
-.base0A { color: #e6cd69; }
-.base0B { color: #9fca56; }
-.base0C { color: #55dbbe; }
-.base0D { color: #55b5db; }
-.base0E { color: #a074c4; }
-.base0F { color: #8a553f; }
diff --git a/static/css/base16-shapeshifter.css b/static/css/base16-shapeshifter.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #f9f9f9; }
-.base01-background { background-color: #e0e0e0; }
-.base02-background { background-color: #ababab; }
-.base03-background { background-color: #555555; }
-.base04-background { background-color: #343434; }
-.base05-background { background-color: #102015; }
-.base06-background { background-color: #040404; }
-.base07-background { background-color: #000000; }
-.base08-background { background-color: #e92f2f; }
-.base09-background { background-color: #e09448; }
-.base0A-background { background-color: #dddd13; }
-.base0B-background { background-color: #0ed839; }
-.base0C-background { background-color: #23edda; }
-.base0D-background { background-color: #3b48e3; }
-.base0E-background { background-color: #f996e2; }
-.base0F-background { background-color: #69542d; }
-
-.base00 { color: #f9f9f9; }
-.base01 { color: #e0e0e0; }
-.base02 { color: #ababab; }
-.base03 { color: #555555; }
-.base04 { color: #343434; }
-.base05 { color: #102015; }
-.base06 { color: #040404; }
-.base07 { color: #000000; }
-.base08 { color: #e92f2f; }
-.base09 { color: #e09448; }
-.base0A { color: #dddd13; }
-.base0B { color: #0ed839; }
-.base0C { color: #23edda; }
-.base0D { color: #3b48e3; }
-.base0E { color: #f996e2; }
-.base0F { color: #69542d; }
diff --git a/static/css/base16-solar-flare.css b/static/css/base16-solar-flare.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #18262F; }
-.base01-background { background-color: #222E38; }
-.base02-background { background-color: #586875; }
-.base03-background { background-color: #667581; }
-.base04-background { background-color: #85939E; }
-.base05-background { background-color: #A6AFB8; }
-.base06-background { background-color: #E8E9ED; }
-.base07-background { background-color: #F5F7FA; }
-.base08-background { background-color: #EF5253; }
-.base09-background { background-color: #E66B2B; }
-.base0A-background { background-color: #E4B51C; }
-.base0B-background { background-color: #7CC844; }
-.base0C-background { background-color: #52CBB0; }
-.base0D-background { background-color: #33B5E1; }
-.base0E-background { background-color: #A363D5; }
-.base0F-background { background-color: #D73C9A; }
-
-.base00 { color: #18262F; }
-.base01 { color: #222E38; }
-.base02 { color: #586875; }
-.base03 { color: #667581; }
-.base04 { color: #85939E; }
-.base05 { color: #A6AFB8; }
-.base06 { color: #E8E9ED; }
-.base07 { color: #F5F7FA; }
-.base08 { color: #EF5253; }
-.base09 { color: #E66B2B; }
-.base0A { color: #E4B51C; }
-.base0B { color: #7CC844; }
-.base0C { color: #52CBB0; }
-.base0D { color: #33B5E1; }
-.base0E { color: #A363D5; }
-.base0F { color: #D73C9A; }
diff --git a/static/css/base16-solarized-dark.css b/static/css/base16-solarized-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #002b36; }
-.base01-background { background-color: #073642; }
-.base02-background { background-color: #586e75; }
-.base03-background { background-color: #657b83; }
-.base04-background { background-color: #839496; }
-.base05-background { background-color: #93a1a1; }
-.base06-background { background-color: #eee8d5; }
-.base07-background { background-color: #fdf6e3; }
-.base08-background { background-color: #dc322f; }
-.base09-background { background-color: #cb4b16; }
-.base0A-background { background-color: #b58900; }
-.base0B-background { background-color: #859900; }
-.base0C-background { background-color: #2aa198; }
-.base0D-background { background-color: #268bd2; }
-.base0E-background { background-color: #6c71c4; }
-.base0F-background { background-color: #d33682; }
-
-.base00 { color: #002b36; }
-.base01 { color: #073642; }
-.base02 { color: #586e75; }
-.base03 { color: #657b83; }
-.base04 { color: #839496; }
-.base05 { color: #93a1a1; }
-.base06 { color: #eee8d5; }
-.base07 { color: #fdf6e3; }
-.base08 { color: #dc322f; }
-.base09 { color: #cb4b16; }
-.base0A { color: #b58900; }
-.base0B { color: #859900; }
-.base0C { color: #2aa198; }
-.base0D { color: #268bd2; }
-.base0E { color: #6c71c4; }
-.base0F { color: #d33682; }
diff --git a/static/css/base16-solarized-light.css b/static/css/base16-solarized-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #fdf6e3; }
-.base01-background { background-color: #eee8d5; }
-.base02-background { background-color: #93a1a1; }
-.base03-background { background-color: #839496; }
-.base04-background { background-color: #657b83; }
-.base05-background { background-color: #586e75; }
-.base06-background { background-color: #073642; }
-.base07-background { background-color: #002b36; }
-.base08-background { background-color: #dc322f; }
-.base09-background { background-color: #cb4b16; }
-.base0A-background { background-color: #b58900; }
-.base0B-background { background-color: #859900; }
-.base0C-background { background-color: #2aa198; }
-.base0D-background { background-color: #268bd2; }
-.base0E-background { background-color: #6c71c4; }
-.base0F-background { background-color: #d33682; }
-
-.base00 { color: #fdf6e3; }
-.base01 { color: #eee8d5; }
-.base02 { color: #93a1a1; }
-.base03 { color: #839496; }
-.base04 { color: #657b83; }
-.base05 { color: #586e75; }
-.base06 { color: #073642; }
-.base07 { color: #002b36; }
-.base08 { color: #dc322f; }
-.base09 { color: #cb4b16; }
-.base0A { color: #b58900; }
-.base0B { color: #859900; }
-.base0C { color: #2aa198; }
-.base0D { color: #268bd2; }
-.base0E { color: #6c71c4; }
-.base0F { color: #d33682; }
diff --git a/static/css/base16-spacemacs.css b/static/css/base16-spacemacs.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1f2022; }
-.base01-background { background-color: #282828; }
-.base02-background { background-color: #444155; }
-.base03-background { background-color: #585858; }
-.base04-background { background-color: #b8b8b8; }
-.base05-background { background-color: #a3a3a3; }
-.base06-background { background-color: #e8e8e8; }
-.base07-background { background-color: #f8f8f8; }
-.base08-background { background-color: #f2241f; }
-.base09-background { background-color: #ffa500; }
-.base0A-background { background-color: #b1951d; }
-.base0B-background { background-color: #67b11d; }
-.base0C-background { background-color: #2d9574; }
-.base0D-background { background-color: #4f97d7; }
-.base0E-background { background-color: #a31db1; }
-.base0F-background { background-color: #b03060; }
-
-.base00 { color: #1f2022; }
-.base01 { color: #282828; }
-.base02 { color: #444155; }
-.base03 { color: #585858; }
-.base04 { color: #b8b8b8; }
-.base05 { color: #a3a3a3; }
-.base06 { color: #e8e8e8; }
-.base07 { color: #f8f8f8; }
-.base08 { color: #f2241f; }
-.base09 { color: #ffa500; }
-.base0A { color: #b1951d; }
-.base0B { color: #67b11d; }
-.base0C { color: #2d9574; }
-.base0D { color: #4f97d7; }
-.base0E { color: #a31db1; }
-.base0F { color: #b03060; }
diff --git a/static/css/base16-summerfruit-dark.css b/static/css/base16-summerfruit-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #151515; }
-.base01-background { background-color: #202020; }
-.base02-background { background-color: #303030; }
-.base03-background { background-color: #505050; }
-.base04-background { background-color: #B0B0B0; }
-.base05-background { background-color: #D0D0D0; }
-.base06-background { background-color: #E0E0E0; }
-.base07-background { background-color: #FFFFFF; }
-.base08-background { background-color: #FF0086; }
-.base09-background { background-color: #FD8900; }
-.base0A-background { background-color: #ABA800; }
-.base0B-background { background-color: #00C918; }
-.base0C-background { background-color: #1FAAAA; }
-.base0D-background { background-color: #3777E6; }
-.base0E-background { background-color: #AD00A1; }
-.base0F-background { background-color: #CC6633; }
-
-.base00 { color: #151515; }
-.base01 { color: #202020; }
-.base02 { color: #303030; }
-.base03 { color: #505050; }
-.base04 { color: #B0B0B0; }
-.base05 { color: #D0D0D0; }
-.base06 { color: #E0E0E0; }
-.base07 { color: #FFFFFF; }
-.base08 { color: #FF0086; }
-.base09 { color: #FD8900; }
-.base0A { color: #ABA800; }
-.base0B { color: #00C918; }
-.base0C { color: #1FAAAA; }
-.base0D { color: #3777E6; }
-.base0E { color: #AD00A1; }
-.base0F { color: #CC6633; }
diff --git a/static/css/base16-summerfruit-light.css b/static/css/base16-summerfruit-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #FFFFFF; }
-.base01-background { background-color: #E0E0E0; }
-.base02-background { background-color: #D0D0D0; }
-.base03-background { background-color: #B0B0B0; }
-.base04-background { background-color: #000000; }
-.base05-background { background-color: #101010; }
-.base06-background { background-color: #151515; }
-.base07-background { background-color: #202020; }
-.base08-background { background-color: #FF0086; }
-.base09-background { background-color: #FD8900; }
-.base0A-background { background-color: #ABA800; }
-.base0B-background { background-color: #00C918; }
-.base0C-background { background-color: #1FAAAA; }
-.base0D-background { background-color: #3777E6; }
-.base0E-background { background-color: #AD00A1; }
-.base0F-background { background-color: #CC6633; }
-
-.base00 { color: #FFFFFF; }
-.base01 { color: #E0E0E0; }
-.base02 { color: #D0D0D0; }
-.base03 { color: #B0B0B0; }
-.base04 { color: #000000; }
-.base05 { color: #101010; }
-.base06 { color: #151515; }
-.base07 { color: #202020; }
-.base08 { color: #FF0086; }
-.base09 { color: #FD8900; }
-.base0A { color: #ABA800; }
-.base0B { color: #00C918; }
-.base0C { color: #1FAAAA; }
-.base0D { color: #3777E6; }
-.base0E { color: #AD00A1; }
-.base0F { color: #CC6633; }
diff --git a/static/css/base16-tomorrow-night.css b/static/css/base16-tomorrow-night.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1d1f21; }
-.base01-background { background-color: #282a2e; }
-.base02-background { background-color: #373b41; }
-.base03-background { background-color: #969896; }
-.base04-background { background-color: #b4b7b4; }
-.base05-background { background-color: #c5c8c6; }
-.base06-background { background-color: #e0e0e0; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #cc6666; }
-.base09-background { background-color: #de935f; }
-.base0A-background { background-color: #f0c674; }
-.base0B-background { background-color: #b5bd68; }
-.base0C-background { background-color: #8abeb7; }
-.base0D-background { background-color: #81a2be; }
-.base0E-background { background-color: #b294bb; }
-.base0F-background { background-color: #a3685a; }
-
-.base00 { color: #1d1f21; }
-.base01 { color: #282a2e; }
-.base02 { color: #373b41; }
-.base03 { color: #969896; }
-.base04 { color: #b4b7b4; }
-.base05 { color: #c5c8c6; }
-.base06 { color: #e0e0e0; }
-.base07 { color: #ffffff; }
-.base08 { color: #cc6666; }
-.base09 { color: #de935f; }
-.base0A { color: #f0c674; }
-.base0B { color: #b5bd68; }
-.base0C { color: #8abeb7; }
-.base0D { color: #81a2be; }
-.base0E { color: #b294bb; }
-.base0F { color: #a3685a; }
diff --git a/static/css/base16-tomorrow.css b/static/css/base16-tomorrow.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #ffffff; }
-.base01-background { background-color: #e0e0e0; }
-.base02-background { background-color: #d6d6d6; }
-.base03-background { background-color: #8e908c; }
-.base04-background { background-color: #969896; }
-.base05-background { background-color: #4d4d4c; }
-.base06-background { background-color: #282a2e; }
-.base07-background { background-color: #1d1f21; }
-.base08-background { background-color: #c82829; }
-.base09-background { background-color: #f5871f; }
-.base0A-background { background-color: #eab700; }
-.base0B-background { background-color: #718c00; }
-.base0C-background { background-color: #3e999f; }
-.base0D-background { background-color: #4271ae; }
-.base0E-background { background-color: #8959a8; }
-.base0F-background { background-color: #a3685a; }
-
-.base00 { color: #ffffff; }
-.base01 { color: #e0e0e0; }
-.base02 { color: #d6d6d6; }
-.base03 { color: #8e908c; }
-.base04 { color: #969896; }
-.base05 { color: #4d4d4c; }
-.base06 { color: #282a2e; }
-.base07 { color: #1d1f21; }
-.base08 { color: #c82829; }
-.base09 { color: #f5871f; }
-.base0A { color: #eab700; }
-.base0B { color: #718c00; }
-.base0C { color: #3e999f; }
-.base0D { color: #4271ae; }
-.base0E { color: #8959a8; }
-.base0F { color: #a3685a; }
diff --git a/static/css/base16-twilight.css b/static/css/base16-twilight.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #1e1e1e; }
-.base01-background { background-color: #323537; }
-.base02-background { background-color: #464b50; }
-.base03-background { background-color: #5f5a60; }
-.base04-background { background-color: #838184; }
-.base05-background { background-color: #a7a7a7; }
-.base06-background { background-color: #c3c3c3; }
-.base07-background { background-color: #ffffff; }
-.base08-background { background-color: #cf6a4c; }
-.base09-background { background-color: #cda869; }
-.base0A-background { background-color: #f9ee98; }
-.base0B-background { background-color: #8f9d6a; }
-.base0C-background { background-color: #afc4db; }
-.base0D-background { background-color: #7587a6; }
-.base0E-background { background-color: #9b859d; }
-.base0F-background { background-color: #9b703f; }
-
-.base00 { color: #1e1e1e; }
-.base01 { color: #323537; }
-.base02 { color: #464b50; }
-.base03 { color: #5f5a60; }
-.base04 { color: #838184; }
-.base05 { color: #a7a7a7; }
-.base06 { color: #c3c3c3; }
-.base07 { color: #ffffff; }
-.base08 { color: #cf6a4c; }
-.base09 { color: #cda869; }
-.base0A { color: #f9ee98; }
-.base0B { color: #8f9d6a; }
-.base0C { color: #afc4db; }
-.base0D { color: #7587a6; }
-.base0E { color: #9b859d; }
-.base0F { color: #9b703f; }
diff --git a/static/css/base16-unikitty-dark.css b/static/css/base16-unikitty-dark.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #2e2a31; }
-.base01-background { background-color: #4a464d; }
-.base02-background { background-color: #666369; }
-.base03-background { background-color: #838085; }
-.base04-background { background-color: #9f9da2; }
-.base05-background { background-color: #bcbabe; }
-.base06-background { background-color: #d8d7da; }
-.base07-background { background-color: #f5f4f7; }
-.base08-background { background-color: #d8137f; }
-.base09-background { background-color: #d65407; }
-.base0A-background { background-color: #dc8a0e; }
-.base0B-background { background-color: #17ad98; }
-.base0C-background { background-color: #149bda; }
-.base0D-background { background-color: #796af5; }
-.base0E-background { background-color: #bb60ea; }
-.base0F-background { background-color: #c720ca; }
-
-.base00 { color: #2e2a31; }
-.base01 { color: #4a464d; }
-.base02 { color: #666369; }
-.base03 { color: #838085; }
-.base04 { color: #9f9da2; }
-.base05 { color: #bcbabe; }
-.base06 { color: #d8d7da; }
-.base07 { color: #f5f4f7; }
-.base08 { color: #d8137f; }
-.base09 { color: #d65407; }
-.base0A { color: #dc8a0e; }
-.base0B { color: #17ad98; }
-.base0C { color: #149bda; }
-.base0D { color: #796af5; }
-.base0E { color: #bb60ea; }
-.base0F { color: #c720ca; }
diff --git a/static/css/base16-unikitty-light.css b/static/css/base16-unikitty-light.css
@@ -1,33 +0,0 @@
-.base00-background { background-color: #ffffff; }
-.base01-background { background-color: #e1e1e2; }
-.base02-background { background-color: #c4c3c5; }
-.base03-background { background-color: #a7a5a8; }
-.base04-background { background-color: #89878b; }
-.base05-background { background-color: #6c696e; }
-.base06-background { background-color: #4f4b51; }
-.base07-background { background-color: #322d34; }
-.base08-background { background-color: #d8137f; }
-.base09-background { background-color: #d65407; }
-.base0A-background { background-color: #dc8a0e; }
-.base0B-background { background-color: #17ad98; }
-.base0C-background { background-color: #149bda; }
-.base0D-background { background-color: #775dff; }
-.base0E-background { background-color: #aa17e6; }
-.base0F-background { background-color: #e013d0; }
-
-.base00 { color: #ffffff; }
-.base01 { color: #e1e1e2; }
-.base02 { color: #c4c3c5; }
-.base03 { color: #a7a5a8; }
-.base04 { color: #89878b; }
-.base05 { color: #6c696e; }
-.base06 { color: #4f4b51; }
-.base07 { color: #322d34; }
-.base08 { color: #d8137f; }
-.base09 { color: #d65407; }
-.base0A { color: #dc8a0e; }
-.base0B { color: #17ad98; }
-.base0C { color: #149bda; }
-.base0D { color: #775dff; }
-.base0E { color: #aa17e6; }
-.base0F { color: #e013d0; }
diff --git a/static/css/themes.json b/static/css/themes.json
@@ -1,66 +0,0 @@
-[
-"base16-pleroma-dark.css",
-"base16-pleroma-light.css",
-"base16-3024.css",
-"base16-apathy.css",
-"base16-ashes.css",
-"base16-atelier-cave.css",
-"base16-atelier-dune.css",
-"base16-atelier-estuary.css",
-"base16-atelier-forest.css",
-"base16-atelier-heath.css",
-"base16-atelier-lakeside.css",
-"base16-atelier-plateau.css",
-"base16-atelier-savanna.css",
-"base16-atelier-seaside.css",
-"base16-atelier-sulphurpool.css",
-"base16-bespin.css",
-"base16-brewer.css",
-"base16-bright.css",
-"base16-chalk.css",
-"base16-codeschool.css",
-"base16-darktooth.css",
-"base16-default-dark.css",
-"base16-default-light.css",
-"base16-eighties.css",
-"base16-embers.css",
-"base16-flat.css",
-"base16-github.css",
-"base16-google-dark.css",
-"base16-google-light.css",
-"base16-grayscale-dark.css",
-"base16-grayscale-light.css",
-"base16-green-screen.css",
-"base16-harmonic16-dark.css",
-"base16-harmonic16-light.css",
-"base16-hopscotch.css",
-"base16-ir-black.css",
-"base16-isotope.css",
-"base16-london-tube.css",
-"base16-macintosh.css",
-"base16-marrakesh.css",
-"base16-materia.css",
-"base16-mexico-light.css",
-"base16-mocha.css",
-"base16-monokai.css",
-"base16-ocean.css",
-"base16-oceanicnext.css",
-"base16-paraiso.css",
-"base16-phd.css",
-"base16-pico.css",
-"base16-pop.css",
-"base16-railscasts.css",
-"base16-seti-ui.css",
-"base16-shapeshifter.css",
-"base16-solar-flare.css",
-"base16-solarized-dark.css",
-"base16-solarized-light.css",
-"base16-spacemacs.css",
-"base16-summerfruit-dark.css",
-"base16-summerfruit-light.css",
-"base16-tomorrow-night.css",
-"base16-tomorrow.css",
-"base16-twilight.css",
-"base16-unikitty-dark.css",
-"base16-unikitty-light.css"
-]
diff --git a/static/fontello.json b/static/fontello.json
@@ -339,6 +339,12 @@
"css": "arrow-curved",
"code": 59426,
"src": "iconic"
+ },
+ {
+ "uid": "0ddd3e8201ccc7d41f7b7c9d27eca6c1",
+ "css": "link",
+ "code": 59427,
+ "src": "fontawesome"
}
]
-}
-\ No newline at end of file
+}
diff --git a/static/styles.json b/static/styles.json
@@ -1,6 +1,6 @@
{
- "pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
- "pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "pleroma-dark": "/static/themes/pleroma-dark.json",
+ "pleroma-light": "/static/themes/pleroma-light.json",
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
@@ -12,5 +12,6 @@
"redmond-xxi": "/static/themes/redmond-xxi.json",
"breezy-dark": "/static/themes/breezy-dark.json",
"breezy-light": "/static/themes/breezy-light.json",
- "mammal": "/static/themes/mammal.json"
+ "mammal": "/static/themes/mammal.json",
+ "paper": "/static/themes/paper.json"
}
diff --git a/static/terms-of-service.html b/static/terms-of-service.html
@@ -1,7 +1,4 @@
<h4>Terms of Service</h4>
-<p>This is a placeholder ToS.</p>
-
-<p>Edit <code>"/static/terms-of-service.html"</code> to make it fit the needs of your instance.</p>
-<br>
-<img src="/static/logo.png"/ style="display: block; margin: auto;">
+<p>This is a placeholder ToS. Edit <code>"/static/terms-of-service.html"</code> to make it fit the needs of your instance.</p>
+<img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" />
diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json
@@ -1,7 +1,9 @@
{
"_pleroma_theme_version": 2,
"name": "Breezy Dark (beta)",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
"shadows": {
"panel": [
{
@@ -19,7 +21,7 @@
"y": "0",
"blur": "0",
"spread": "1",
- "color": "#ffffff",
+ "color": "--btn,900",
"alpha": "0.15",
"inset": true
},
@@ -40,7 +42,7 @@
"blur": "40",
"spread": "-40",
"inset": true,
- "color": "#ffffff",
+ "color": "--panel,900",
"alpha": "0.1"
}
],
@@ -50,8 +52,8 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "--link",
- "alpha": "0.3",
+ "color": "--accent",
+ "alpha": "1",
"inset": true
},
{
@@ -67,19 +69,10 @@
"buttonPressed": [
{
"x": 0,
- "y": 0,
- "blur": "0",
- "spread": "50",
- "color": "--faint",
- "alpha": 1,
- "inset": true
- },
- {
- "x": 0,
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#ffffff",
+ "color": "--btn,900",
"alpha": 0.2,
"inset": true
},
@@ -99,31 +92,30 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#FFFFFF",
+ "color": "--input,900",
"alpha": "0.2",
"inset": true
}
]
},
- "fonts": {},
- "opacity": {
- "input": "1",
- "panel": "0"
- },
+ "opacity": {},
"colors": {
"bg": "#31363b",
"text": "#eff0f1",
"link": "#3daee9",
"fg": "#31363b",
- "panel": "#31363b",
- "input": "#232629",
- "topBarLink": "#eff0f1",
- "btn": "#31363b",
+ "panel": "transparent",
+ "input": "--bg,-6.47",
+ "topBarLink": "--topBarText",
+ "btn": "--bg",
"border": "#4c545b",
"cRed": "#da4453",
"cBlue": "#3daee9",
"cGreen": "#27ae60",
- "cOrange": "#f67400"
+ "cOrange": "#f67400",
+ "btnPressed": "--accent",
+ "selectedMenu": "--accent",
+ "selectedMenuPopover": "--accent"
},
"radii": {
"btn": "2",
diff --git a/static/themes/breezy-light.json b/static/themes/breezy-light.json
@@ -1,7 +1,9 @@
{
"_pleroma_theme_version": 2,
"name": "Breezy Light (beta)",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
"shadows": {
"panel": [
{
@@ -19,7 +21,7 @@
"y": "0",
"blur": "0",
"spread": "1",
- "color": "#000000",
+ "color": "--btn,900",
"alpha": "0.3",
"inset": true
},
@@ -40,7 +42,7 @@
"blur": "40",
"spread": "-40",
"inset": true,
- "color": "#ffffff",
+ "color": "--panel,900",
"alpha": "0.1"
}
],
@@ -50,8 +52,8 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "--link",
- "alpha": "0.3",
+ "color": "--accent",
+ "alpha": "1",
"inset": true
},
{
@@ -67,19 +69,10 @@
"buttonPressed": [
{
"x": 0,
- "y": 0,
- "blur": "0",
- "spread": "50",
- "color": "--faint",
- "alpha": 1,
- "inset": true
- },
- {
- "x": 0,
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#ffffff",
+ "color": "--btn,900",
"alpha": 0.2,
"inset": true
},
@@ -99,31 +92,30 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#000000",
+ "color": "--input,900",
"alpha": "0.2",
"inset": true
}
]
},
- "fonts": {},
"opacity": {
"input": "1"
},
"colors": {
"bg": "#eff0f1",
"text": "#232627",
- "link": "#2980b9",
- "fg": "#bcc2c7",
- "panel": "#475057",
- "panelText": "#fcfcfc",
- "input": "#fcfcfc",
- "topBar": "#475057",
- "topBarLink": "#eff0f1",
- "btn": "#eff0f1",
+ "fg": "#475057",
+ "accent": "#2980b9",
+ "input": "--bg,-6.47",
+ "topBarLink": "--topBarText",
+ "btn": "--bg",
"cRed": "#da4453",
"cBlue": "#2980b9",
"cGreen": "#27ae60",
- "cOrange": "#f67400"
+ "cOrange": "#f67400",
+ "btnPressed": "--accent",
+ "selectedMenu": "--accent",
+ "selectedMenuPopover": "--accent"
},
"radii": {
"btn": "2",
diff --git a/static/themes/paper.json b/static/themes/paper.json
@@ -0,0 +1,172 @@
+{
+ "_pleroma_theme_version": 2,
+ "name": "Paper",
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
+ "shadows": {
+ "panel": [
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "9",
+ "spread": 0,
+ "inset": false,
+ "color": "#668bb2",
+ "alpha": "0.1"
+ },
+ {
+ "x": "0",
+ "y": "1",
+ "blur": "2",
+ "spread": "-1",
+ "inset": false,
+ "color": "#668bb2",
+ "alpha": "0.1"
+ }
+ ],
+ "topBar": [
+ {
+ "x": 0,
+ "y": "3",
+ "blur": "8",
+ "spread": 0,
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ },
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "4",
+ "spread": 0,
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ }
+ ],
+ "button": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": 0,
+ "color": "#463f78",
+ "alpha": "0.1",
+ "inset": false
+ }
+ ],
+ "input": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "2",
+ "spread": 0,
+ "inset": true,
+ "color": "#6277b7",
+ "alpha": "0.1"
+ }
+ ],
+ "buttonHover": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": 0,
+ "color": "#494949",
+ "alpha": "0.1"
+ },
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "0",
+ "spread": "20",
+ "color": "#ffffff",
+ "alpha": "1",
+ "inset": true
+ }
+ ],
+ "buttonPressed": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "4",
+ "spread": "0",
+ "color": "#494949",
+ "alpha": "0.8",
+ "inset": false
+ }
+ ],
+ "avatarStatus": [
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "4",
+ "spread": "0",
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ }
+ ],
+ "avatar": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": "0",
+ "color": "#3e618e",
+ "alpha": "0.9"
+ }
+ ],
+ "popup": [
+ {
+ "x": "0",
+ "y": "3",
+ "blur": "11",
+ "spread": 0,
+ "color": "#668bb2",
+ "alpha": "0.2"
+ },
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "3",
+ "spread": "-1",
+ "color": "#668bb2",
+ "alpha": "0.2"
+ }
+ ]
+ },
+ "opacity": {
+ "underlay": "1",
+ "border": "0"
+ },
+ "colors": {
+ "bg": "#ffffff",
+ "fg": "#f6f6f6",
+ "text": "#494949",
+ "underlay": "#ffffff",
+ "link": "#788ca1",
+ "accent": "#97a0aa",
+ "cBlue": "#788ca1",
+ "cRed": "#eed7ce",
+ "cGreen": "#788ca1",
+ "cOrange": "#788ca1",
+ "postLink": "#788ca1",
+ "border": "#ffffff",
+ "icon": "#b6c9c4",
+ "panel": "#ffffff",
+ "topBarText": "#4b4b4b"
+ },
+ "radii": {
+ "btn": "0",
+ "input": "0",
+ "checkbox": "0",
+ "panel": "0",
+ "avatar": "2",
+ "avatarAlt": "2",
+ "tooltip": "0",
+ "attachment": "0"
+ }
+ }
+}
diff --git a/static/themes/pleroma-dark.json b/static/themes/pleroma-dark.json
@@ -0,0 +1,191 @@
+{
+ "_pleroma_theme_version": 2,
+ "name": "Pleroma Dark",
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
+ "shadows": {
+ "buttonHover": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "1",
+ "spread": "2",
+ "color": "#b9b9ba",
+ "alpha": "0.4",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "buttonPressed": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 4,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 1,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "2",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": 1
+ }
+ ],
+ "panelHeader": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "3",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.4"
+ },
+ {
+ "x": "0",
+ "y": "1",
+ "blur": "0",
+ "spread": 0,
+ "inset": true,
+ "color": "#ffffff",
+ "alpha": "0.2"
+ }
+ ],
+ "panel": [
+ {
+ "x": "0",
+ "y": "0",
+ "blur": "3",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.5"
+ },
+ {
+ "x": "0",
+ "y": "4",
+ "blur": "6",
+ "spread": "3",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.3"
+ }
+ ],
+ "button": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 2,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 1
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "topBar": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": 4,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.4"
+ },
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "7",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.3"
+ }
+ ]
+ },
+ "opacity": {
+ "underlay": "0.6"
+ },
+ "colors": {
+ "bg": "#0f161e",
+ "fg": "#151e2b",
+ "text": "#b9b9ba",
+ "underlay": "#090e14",
+ "accent": "#e2b188",
+ "cBlue": "#81beea",
+ "cRed": "#d31014",
+ "cGreen": "#5dc94a",
+ "cOrange": "#ffc459",
+ "border": "--fg,3",
+ "topBarText": "--text,-9.75",
+ "topBarLink": "--topBarText",
+ "btnToggled": "--accent,-24.2",
+ "alertErrorText": "--text,21.2",
+ "badgeNotification": "#e15932",
+ "badgeNotificationText": "#ffffff"
+ },
+ "radii": {
+ "btn": "3",
+ "input": "3",
+ "panel": "3",
+ "avatar": "3",
+ "attachment": "3"
+ }
+ }
+}
diff --git a/static/themes/pleroma-light.json b/static/themes/pleroma-light.json
@@ -0,0 +1,219 @@
+{
+ "_pleroma_theme_version": 2,
+ "name": "Pleroma Light",
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
+ "shadows": {
+ "button": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 2,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": "0.5",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "buttonHover": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "2",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": 0,
+ "y": "0",
+ "blur": "1",
+ "spread": "2",
+ "color": "#ffc39f",
+ "alpha": "1",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "input": [
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "2",
+ "inset": true,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.15"
+ }
+ ],
+ "panel": [
+ {
+ "x": "0",
+ "y": 1,
+ "blur": "3",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.5"
+ },
+ {
+ "x": "0",
+ "y": "3",
+ "blur": "6",
+ "spread": "1",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.2"
+ }
+ ],
+ "panelHeader": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": 0,
+ "spread": 0,
+ "inset": true,
+ "color": "#ffffff",
+ "alpha": "0.5"
+ },
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "3",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.3"
+ }
+ ],
+ "buttonPressed": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 4,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": "1",
+ "spread": "2",
+ "color": "#000000",
+ "alpha": "0.3",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "popup": [
+ {
+ "x": "1",
+ "y": "2",
+ "blur": "2",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": "1",
+ "y": "3",
+ "blur": "7",
+ "spread": "0",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.2"
+ }
+ ],
+ "avatarStatus": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "4",
+ "spread": "0",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.2"
+ }
+ ]
+ },
+ "opacity": {
+ "underlay": "0.4"
+ },
+ "colors": {
+ "bg": "#f2f6f9",
+ "fg": "#d6dfed",
+ "text": "#304055",
+ "underlay": "#5d6086",
+ "accent": "#f55b1b",
+ "cBlue": "#0095ff",
+ "cRed": "#d31014",
+ "cGreen": "#0fa00f",
+ "cOrange": "#ffa500",
+ "border": "#d8e6f9",
+ "topBarText": "#304055",
+ "topBarLink": "--topBarText",
+ "btnToggled": "--accent,-24.2",
+ "input": "#dee3ed",
+ "badgeNotification": "#e83802"
+ },
+ "radii": {
+ "btn": "3",
+ "input": "3",
+ "panel": "3",
+ "avatar": "3",
+ "attachment": "3"
+ }
+ }
+}
diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json
@@ -1,7 +1,8 @@
{
"_pleroma_theme_version": 2,
"name": "Redmond XX SE",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
"shadows": {
"panel": [
{
@@ -268,6 +269,7 @@
"bg": "#c0c0c0",
"text": "#000000",
"link": "#0000ff",
+ "accent": "#000080",
"fg": "#c0c0c0",
"panel": "#000080",
"panelFaint": "#c0c0c0",
@@ -275,13 +277,16 @@
"topBar": "#000080",
"topBarLink": "#ffffff",
"btn": "#c0c0c0",
+ "btnToggled": "--btn",
"faint": "#3f3f3f",
"faintLink": "#404080",
"border": "#808080",
"cRed": "#FF0000",
"cBlue": "#008080",
"cGreen": "#008000",
- "cOrange": "#808000"
+ "cOrange": "#808000",
+ "highlight": "--accent",
+ "selectedPost": "--bg,-10"
},
"radii": {
"btn": "0",
diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json
@@ -1,7 +1,8 @@
{
"_pleroma_theme_version": 2,
"name": "Redmond XX",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
"shadows": {
"panel": [
{
@@ -259,6 +260,7 @@
"bg": "#c0c0c0",
"text": "#000000",
"link": "#0000ff",
+ "accent": "#000080",
"fg": "#c0c0c0",
"panel": "#000080",
"panelFaint": "#c0c0c0",
@@ -266,13 +268,16 @@
"topBar": "#000080",
"topBarLink": "#ffffff",
"btn": "#c0c0c0",
+ "btnToggled": "--btn",
"faint": "#3f3f3f",
"faintLink": "#404080",
"border": "#808080",
"cRed": "#FF0000",
"cBlue": "#008080",
"cGreen": "#008000",
- "cOrange": "#808000"
+ "cOrange": "#808000",
+ "highlight": "--accent",
+ "selectedPost": "--bg,-10"
},
"radii": {
"btn": "0",
diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json
@@ -1,7 +1,8 @@
{
"_pleroma_theme_version": 2,
"name": "Redmond XXI",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
"shadows": {
"panel": [
{
@@ -241,6 +242,7 @@
"bg": "#d6d6ce",
"text": "#000000",
"link": "#0000ff",
+ "accent": "#0a246a",
"fg": "#d6d6ce",
"panel": "#042967",
"panelFaint": "#FFFFFF",
@@ -248,13 +250,16 @@
"topBar": "#042967",
"topBarLink": "#ffffff",
"btn": "#d6d6ce",
+ "btnToggled": "--btn",
"faint": "#3f3f3f",
"faintLink": "#404080",
"border": "#808080",
"cRed": "#c42726",
"cBlue": "#6699cc",
"cGreen": "#669966",
- "cOrange": "#cc6633"
+ "cOrange": "#cc6633",
+ "highlight": "--accent",
+ "selectedPost": "--bg,-10"
},
"radii": {
"btn": "0",
diff --git a/test/unit/specs/components/emoji_input.spec.js b/test/unit/specs/components/emoji_input.spec.js
@@ -36,7 +36,8 @@ describe('EmojiInput', () => {
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
- expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ')
+ const inputEvents = wrapper.emitted().input
+ expect(inputEvents[inputEvents.length - 1][0]).to.eql('Testing (test) ')
})
it('inserts string at the end with trailing space (source has a trailing space)', () => {
@@ -46,7 +47,8 @@ describe('EmojiInput', () => {
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
- expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ')
+ const inputEvents = wrapper.emitted().input
+ expect(inputEvents[inputEvents.length - 1][0]).to.eql('Testing (test) ')
})
it('inserts string at the begginning without leading space', () => {
@@ -56,7 +58,8 @@ describe('EmojiInput', () => {
input.setValue(initialString)
wrapper.setData({ caret: 0 })
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
- expect(wrapper.emitted().input[0][0]).to.eql('(test) Testing')
+ const inputEvents = wrapper.emitted().input
+ expect(inputEvents[inputEvents.length - 1][0]).to.eql('(test) Testing')
})
it('inserts string between words without creating extra spaces', () => {
@@ -66,7 +69,8 @@ describe('EmojiInput', () => {
input.setValue(initialString)
wrapper.setData({ caret: 6 })
wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
- expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde')
+ const inputEvents = wrapper.emitted().input
+ expect(inputEvents[inputEvents.length - 1][0]).to.eql('Spurdo :ebin: Sparde')
})
it('inserts string between words without creating extra spaces (other caret)', () => {
@@ -76,7 +80,8 @@ describe('EmojiInput', () => {
input.setValue(initialString)
wrapper.setData({ caret: 7 })
wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
- expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde')
+ const inputEvents = wrapper.emitted().input
+ expect(inputEvents[inputEvents.length - 1][0]).to.eql('Spurdo :ebin: Sparde')
})
it('inserts string without any padding if padEmoji setting is set to false', () => {
@@ -86,7 +91,8 @@ describe('EmojiInput', () => {
input.setValue(initialString)
wrapper.setData({ caret: initialString.length, keepOpen: false })
wrapper.vm.insert({ insertion: ':spam:' })
- expect(wrapper.emitted().input[0][0]).to.eql('Eat some spam!:spam:')
+ const inputEvents = wrapper.emitted().input
+ expect(inputEvents[inputEvents.length - 1][0]).to.eql('Eat some spam!:spam:')
})
it('correctly sets caret after insertion at beginning', (done) => {
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
@@ -241,6 +241,54 @@ describe('Statuses module', () => {
})
})
+ describe('emojiReactions', () => {
+ it('increments count in existing reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = [ { name: '😂', count: 1, accounts: [] } ]
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+ expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(2)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(true)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
+ })
+
+ it('adds a new reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = []
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+ expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(true)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
+ })
+
+ it('decreases count in existing reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = [ { name: '😂', count: 2, accounts: [{ id: 'me' }] } ]
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+ expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(false)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].accounts).to.eql([])
+ })
+
+ it('removes a reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = [{ name: '😂', count: 1, accounts: [{ id: 'me' }] }]
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+ expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0)
+ })
+ })
+
describe('showNewStatuses', () => {
it('resets the minId to the min of the visible statuses when adding new to visible statuses', () => {
const state = defaultState()
diff --git a/test/unit/specs/services/theme_data/sanity_checks.spec.js b/test/unit/specs/services/theme_data/sanity_checks.spec.js
@@ -0,0 +1,28 @@
+import { getColors } from 'src/services/theme_data/theme_data.service.js'
+
+const checkColors = (output) => {
+ expect(output).to.have.property('colors')
+ Object.entries(output.colors).forEach(([key, v]) => {
+ expect(v, key).to.be.an('object')
+ expect(v, key).to.include.all.keys('r', 'g', 'b')
+ 'rgba'.split('').forEach(k => {
+ if ((k === 'a' && v.hasOwnProperty('a')) || k !== 'a') {
+ expect(v[k], key + '.' + k).to.be.a('number')
+ expect(v[k], key + '.' + k).to.be.least(0)
+ expect(v[k], key + '.' + k).to.be.most(k === 'a' ? 1 : 255)
+ }
+ })
+ })
+}
+
+describe('Theme Data utility functions', () => {
+ const context = require.context('static/themes/', false, /\.json$/)
+ context.keys().forEach((key) => {
+ it(`Should render all colors for ${key} properly`, () => {
+ const { theme, source } = context(key)
+ const data = source || theme
+ const colors = getColors(data.colors, data.opacity, 1)
+ checkColors(colors)
+ })
+ })
+})
diff --git a/test/unit/specs/services/theme_data/theme_data.spec.js b/test/unit/specs/services/theme_data/theme_data.spec.js
@@ -0,0 +1,89 @@
+import { getLayersArray, topoSort } from 'src/services/theme_data/theme_data.service.js'
+
+describe('Theme Data utility functions', () => {
+ describe('getLayersArray', () => {
+ const fixture = {
+ layer1: null,
+ layer2: 'layer1',
+ layer3a: 'layer2',
+ layer3b: 'layer2'
+ }
+
+ it('should expand layers properly (3b)', () => {
+ const out = getLayersArray('layer3b', fixture)
+ expect(out).to.eql(['layer1', 'layer2', 'layer3b'])
+ })
+
+ it('should expand layers properly (3a)', () => {
+ const out = getLayersArray('layer3a', fixture)
+ expect(out).to.eql(['layer1', 'layer2', 'layer3a'])
+ })
+
+ it('should expand layers properly (2)', () => {
+ const out = getLayersArray('layer2', fixture)
+ expect(out).to.eql(['layer1', 'layer2'])
+ })
+
+ it('should expand layers properly (1)', () => {
+ const out = getLayersArray('layer1', fixture)
+ expect(out).to.eql(['layer1'])
+ })
+ })
+
+ describe('topoSort', () => {
+ const fixture1 = {
+ layerA: [],
+ layer1A: ['layerA'],
+ layer2A: ['layer1A'],
+ layerB: [],
+ layer1B: ['layerB'],
+ layer2B: ['layer1B'],
+ layer3AB: ['layer2B', 'layer2A']
+ }
+
+ // Same thing but messed up order
+ const fixture2 = {
+ layer1A: ['layerA'],
+ layer1B: ['layerB'],
+ layer2A: ['layer1A'],
+ layerB: [],
+ layer3AB: ['layer2B', 'layer2A'],
+ layer2B: ['layer1B'],
+ layerA: []
+ }
+
+ it('should make a topologically sorted array', () => {
+ const out = topoSort(fixture1, (node, inheritance) => inheritance[node])
+ // This basically checks all ordering that matters
+ expect(out.indexOf('layerA')).to.be.below(out.indexOf('layer1A'))
+ expect(out.indexOf('layer1A')).to.be.below(out.indexOf('layer2A'))
+ expect(out.indexOf('layerB')).to.be.below(out.indexOf('layer1B'))
+ expect(out.indexOf('layer1B')).to.be.below(out.indexOf('layer2B'))
+ expect(out.indexOf('layer2A')).to.be.below(out.indexOf('layer3AB'))
+ expect(out.indexOf('layer2B')).to.be.below(out.indexOf('layer3AB'))
+ })
+
+ it('order in object shouldn\'t matter', () => {
+ const out = topoSort(fixture2, (node, inheritance) => inheritance[node])
+ // This basically checks all ordering that matters
+ expect(out.indexOf('layerA')).to.be.below(out.indexOf('layer1A'))
+ expect(out.indexOf('layer1A')).to.be.below(out.indexOf('layer2A'))
+ expect(out.indexOf('layerB')).to.be.below(out.indexOf('layer1B'))
+ expect(out.indexOf('layer1B')).to.be.below(out.indexOf('layer2B'))
+ expect(out.indexOf('layer2A')).to.be.below(out.indexOf('layer3AB'))
+ expect(out.indexOf('layer2B')).to.be.below(out.indexOf('layer3AB'))
+ })
+
+ it('dependentless nodes should be first', () => {
+ const out = topoSort(fixture2, (node, inheritance) => inheritance[node])
+ // This basically checks all ordering that matters
+ expect(out.indexOf('layerA')).to.eql(0)
+ expect(out.indexOf('layerB')).to.eql(1)
+ })
+
+ it('ignores cyclic dependencies', () => {
+ const out = topoSort({ a: 'b', b: 'a', c: 'a' }, (node, inheritance) => [inheritance[node]])
+ expect(out.indexOf('a')).to.be.below(out.indexOf('c'))
+ })
+ })
+})
diff --git a/yarn.lock b/yarn.lock
@@ -710,6 +710,11 @@
dependencies:
qrcode "^1.3.0"
+"@ungap/event-target@^0.1.0":
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
+ integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA==
+
"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
@@ -2281,6 +2286,11 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
+custom-event-polyfill@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
+ integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -2747,9 +2757,10 @@ es6-promisify@^5.0.0:
dependencies:
es6-promise "^4.0.3"
-escape-html@~1.0.3:
+escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
@@ -5930,11 +5941,6 @@ pngjs@^3.3.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
-popper.js@^1.15.0:
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
- integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
-
portal-vue@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.4.tgz#1fc679d77e294dc8d026f1eb84aa467de11b392e"
@@ -7812,15 +7818,6 @@ v-click-outside@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7"
-v-tooltip@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.2.tgz#8610d9eece2cc44fd66c12ef2f12eec6435cab9b"
- integrity sha512-xQ+qzOFfywkLdjHknRPgMMupQNS8yJtf9Utd5Dxiu/0n4HtrxqsgDtN2MLZ0LKbburtSAQgyypuE/snM8bBZhw==
- dependencies:
- lodash "^4.17.11"
- popper.js "^1.15.0"
- vue-resize "^0.4.5"
-
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -7895,11 +7892,6 @@ vue-loader@^14.0.0:
vue-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0"
-vue-resize@^0.4.5:
- version "0.4.5"
- resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea"
- integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
-
vue-router@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"