logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
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:

MCHANGELOG.md17+++++++++++++++++
Mbuild/webpack.base.conf.js1+
Mpackage.json4+++-
Msrc/App.scss118++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/App.vue2+-
Msrc/_variables.scss2++
Msrc/boot/after_store.js52++++++++++++++++++++++++++++++++--------------------
Msrc/components/account_actions/account_actions.js4+++-
Msrc/components/account_actions/account_actions.vue21++++++++++++---------
Msrc/components/attachment/attachment.js11++++++++---
Msrc/components/attachment/attachment.vue2++
Msrc/components/autosuggest/autosuggest.vue4++--
Msrc/components/checkbox/checkbox.vue4++--
Asrc/components/color_input/color_input.scss68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/color_input/color_input.vue118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/components/contrast_ratio/contrast_ratio.vue14+++++++++++---
Msrc/components/conversation/conversation.js1+
Msrc/components/dialog_modal/dialog_modal.vue12++++++------
Asrc/components/domain_mute_card/domain_mute_card.js15+++++++++++++++
Asrc/components/domain_mute_card/domain_mute_card.vue38++++++++++++++++++++++++++++++++++++++
Msrc/components/emoji_input/emoji_input.js10++--------
Msrc/components/emoji_input/emoji_input.vue21++++++++++++++++-----
Msrc/components/emoji_picker/emoji_picker.scss9+++++++++
Asrc/components/emoji_reactions/emoji_reactions.js69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/emoji_reactions/emoji_reactions.vue141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/export_import/export_import.vue2+-
Msrc/components/extra_buttons/extra_buttons.js3+++
Msrc/components/extra_buttons/extra_buttons.vue14+++++++-------
Msrc/components/follow_button/follow_button.vue2+-
Msrc/components/interactions/interactions.js1+
Msrc/components/interactions/interactions.vue1+
Msrc/components/moderation_tools/moderation_tools.js11++++++++---
Msrc/components/moderation_tools/moderation_tools.vue17+++++++++--------
Msrc/components/mrf_transparency_panel/mrf_transparency_panel.js10+++++++++-
Msrc/components/mrf_transparency_panel/mrf_transparency_panel.vue75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/components/nav_panel/nav_panel.js2+-
Msrc/components/nav_panel/nav_panel.vue18+++++++++++++++---
Msrc/components/notification/notification.vue7+++++++
Msrc/components/notifications/notifications.scss7+++++++
Msrc/components/opacity_input/opacity_input.vue18++++++++----------
Msrc/components/poll/poll.vue4+++-
Asrc/components/popover/popover.js156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/popover/popover.vue118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/components/popper/popper.scss147-------------------------------------------------------------------------------
Msrc/components/range_input/range_input.vue2+-
Asrc/components/react_button/react_button.js39+++++++++++++++++++++++++++++++++++++++
Asrc/components/react_button/react_button.vue111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/selectable_list/selectable_list.vue7++++++-
Msrc/components/settings/settings.vue14++++++++++++--
Msrc/components/shadow_control/shadow_control.js44++++++++++++++++++++++++++++++--------------
Msrc/components/shadow_control/shadow_control.vue11++++++++---
Msrc/components/side_drawer/side_drawer.js2+-
Msrc/components/side_drawer/side_drawer.vue18+++++++++++++++---
Msrc/components/staff_panel/staff_panel.js3++-
Msrc/components/status/status.js16+++++++++++++++-
Msrc/components/status/status.vue53++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/components/status_popover/status_popover.js12++----------
Msrc/components/status_popover/status_popover.vue64+++++++++++++++++++++-------------------------------------------
Msrc/components/sticker_picker/sticker_picker.vue2+-
Msrc/components/style_switcher/preview.vue188+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/components/style_switcher/style_switcher.js592+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/components/style_switcher/style_switcher.scss38+++++++++++++++++++-------------------
Msrc/components/style_switcher/style_switcher.vue423++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/components/tab_switcher/tab_switcher.scss7+++++++
Msrc/components/user_card/user_card.js21+++++----------------
Msrc/components/user_card/user_card.vue21++++++++++-----------
Msrc/components/user_settings/user_settings.js25++++++++++++++++++++++---
Msrc/components/user_settings/user_settings.vue166++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/i18n/en.json99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/i18n/fi.json5++++-
Msrc/i18n/ja_easy.json38+++++++++++++++++++++-----------------
Asrc/lib/event_target_polyfill.js9+++++++++
Msrc/main.js11+++--------
Msrc/modules/api.js1+
Msrc/modules/config.js11++++++++---
Msrc/modules/instance.js25++++++++++++++++++++++---
Msrc/modules/statuses.js106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/modules/users.js48++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/services/api/api.service.js78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/services/backend_interactor_service/backend_interactor_service.js2+-
Msrc/services/color_convert/color_convert.js128++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/services/entity_normalizer/entity_normalizer.service.js10++++++++--
Msrc/services/notification_utils/notification_utils.js3++-
Msrc/services/notifications_fetcher/notifications_fetcher.service.js3+++
Msrc/services/style_setter/style_setter.js532++++++++++++++++++++++++++++++-------------------------------------------------
Asrc/services/theme_data/pleromafe.js631+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/theme_data.service.js374+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dstatic/css/base16-3024.css33---------------------------------
Dstatic/css/base16-apathy.css33---------------------------------
Dstatic/css/base16-ashes.css33---------------------------------
Dstatic/css/base16-atelier-cave.css33---------------------------------
Dstatic/css/base16-atelier-dune.css33---------------------------------
Dstatic/css/base16-atelier-estuary.css33---------------------------------
Dstatic/css/base16-atelier-forest.css33---------------------------------
Dstatic/css/base16-atelier-heath.css33---------------------------------
Dstatic/css/base16-atelier-lakeside.css33---------------------------------
Dstatic/css/base16-atelier-plateau.css33---------------------------------
Dstatic/css/base16-atelier-savanna.css33---------------------------------
Dstatic/css/base16-atelier-seaside.css33---------------------------------
Dstatic/css/base16-atelier-sulphurpool.css33---------------------------------
Dstatic/css/base16-bespin.css33---------------------------------
Dstatic/css/base16-brewer.css33---------------------------------
Dstatic/css/base16-bright.css33---------------------------------
Dstatic/css/base16-chalk.css33---------------------------------
Dstatic/css/base16-codeschool.css33---------------------------------
Dstatic/css/base16-darktooth.css33---------------------------------
Dstatic/css/base16-default-dark.css33---------------------------------
Dstatic/css/base16-default-light.css33---------------------------------
Dstatic/css/base16-eighties.css33---------------------------------
Dstatic/css/base16-embers.css33---------------------------------
Dstatic/css/base16-flat.css33---------------------------------
Dstatic/css/base16-github.css33---------------------------------
Dstatic/css/base16-google-dark.css33---------------------------------
Dstatic/css/base16-google-light.css33---------------------------------
Dstatic/css/base16-grayscale-dark.css33---------------------------------
Dstatic/css/base16-grayscale-light.css33---------------------------------
Dstatic/css/base16-green-screen.css33---------------------------------
Dstatic/css/base16-harmonic16-dark.css33---------------------------------
Dstatic/css/base16-harmonic16-light.css33---------------------------------
Dstatic/css/base16-hopscotch.css33---------------------------------
Dstatic/css/base16-ir-black.css33---------------------------------
Dstatic/css/base16-isotope.css33---------------------------------
Dstatic/css/base16-london-tube.css33---------------------------------
Dstatic/css/base16-macintosh.css33---------------------------------
Dstatic/css/base16-marrakesh.css33---------------------------------
Dstatic/css/base16-materia.css33---------------------------------
Dstatic/css/base16-mexico-light.css33---------------------------------
Dstatic/css/base16-mocha.css33---------------------------------
Dstatic/css/base16-monokai.css33---------------------------------
Dstatic/css/base16-ocean.css33---------------------------------
Dstatic/css/base16-oceanicnext.css33---------------------------------
Dstatic/css/base16-paraiso.css33---------------------------------
Dstatic/css/base16-phd.css33---------------------------------
Dstatic/css/base16-pico.css33---------------------------------
Dstatic/css/base16-pleroma-dark.css33---------------------------------
Dstatic/css/base16-pleroma-light.css33---------------------------------
Dstatic/css/base16-pop.css33---------------------------------
Dstatic/css/base16-railscasts.css33---------------------------------
Dstatic/css/base16-seti-ui.css33---------------------------------
Dstatic/css/base16-shapeshifter.css33---------------------------------
Dstatic/css/base16-solar-flare.css33---------------------------------
Dstatic/css/base16-solarized-dark.css33---------------------------------
Dstatic/css/base16-solarized-light.css33---------------------------------
Dstatic/css/base16-spacemacs.css33---------------------------------
Dstatic/css/base16-summerfruit-dark.css33---------------------------------
Dstatic/css/base16-summerfruit-light.css33---------------------------------
Dstatic/css/base16-tomorrow-night.css33---------------------------------
Dstatic/css/base16-tomorrow.css33---------------------------------
Dstatic/css/base16-twilight.css33---------------------------------
Dstatic/css/base16-unikitty-dark.css33---------------------------------
Dstatic/css/base16-unikitty-light.css33---------------------------------
Dstatic/css/themes.json66------------------------------------------------------------------
Mstatic/fontello.json9+++++++--
Mstatic/styles.json7++++---
Mstatic/terms-of-service.html7++-----
Mstatic/themes/breezy-dark.json44++++++++++++++++++--------------------------
Mstatic/themes/breezy-light.json44++++++++++++++++++--------------------------
Astatic/themes/paper.json172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatic/themes/pleroma-dark.json191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatic/themes/pleroma-light.json219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstatic/themes/redmond-xx-se.json9+++++++--
Mstatic/themes/redmond-xx.json9+++++++--
Mstatic/themes/redmond-xxi.json9+++++++--
Mtest/unit/specs/components/emoji_input.spec.js18++++++++++++------
Mtest/unit/specs/modules/statuses.spec.js48++++++++++++++++++++++++++++++++++++++++++++++++
Atest/unit/specs/services/theme_data/sanity_checks.spec.js28++++++++++++++++++++++++++++
Atest/unit/specs/services/theme_data/theme_data.spec.js89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Myarn.lock32++++++++++++--------------------
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(&quot;settings.export_theme&quot;)" :import-label="$t(&quot;settings.import_theme&quot;)" @@ -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"