forked from AkkomaGang/akkoma-fe
Merge branch 'develop' into 'master'
Update master with 2.0.0 See merge request pleroma/pleroma-fe!1074
This commit is contained in:
commit
1b9805b550
168 changed files with 4975 additions and 3513 deletions
17
CHANGELOG.md
17
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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.0] - 2020-02-28
|
||||||
### Added
|
### 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
|
- Icons in nav panel
|
||||||
- Private mode support
|
- Private mode support
|
||||||
- Support for 'Move' type notifications
|
- Support for 'Move' type notifications
|
||||||
- Pleroma AMOLED dark theme
|
- Pleroma AMOLED dark theme
|
||||||
|
- User level domain mutes, under User Settings -> Mutes
|
||||||
|
- Emoji reactions for statuses
|
||||||
|
- MRF keyword policy disclosure
|
||||||
### Changed
|
### 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
|
- Captcha now resets on failed registrations
|
||||||
- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
|
- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
|
||||||
- 403 messaging
|
- 403 messaging
|
||||||
### Fixed
|
### 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
|
- Single notifications left unread when hitting read on another device/tab
|
||||||
- Registration fixed
|
- Registration fixed
|
||||||
- Deactivation of remote accounts from frontend
|
- 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
|
## [1.1.7 and earlier] - 2019-12-14
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -35,6 +35,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
alias: {
|
alias: {
|
||||||
'vue$': 'vue/dist/vue.runtime.common',
|
'vue$': 'vue/dist/vue.runtime.common',
|
||||||
|
'static': path.resolve(__dirname, '../static'),
|
||||||
'src': path.resolve(__dirname, '../src'),
|
'src': path.resolve(__dirname, '../src'),
|
||||||
'assets': path.resolve(__dirname, '../src/assets'),
|
'assets': path.resolve(__dirname, '../src/assets'),
|
||||||
'components': path.resolve(__dirname, '../src/components')
|
'components': path.resolve(__dirname, '../src/components')
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"chromatism": "^3.0.0",
|
"chromatism": "^3.0.0",
|
||||||
"cropperjs": "^1.4.3",
|
"cropperjs": "^1.4.3",
|
||||||
"diff": "^3.0.1",
|
"diff": "^3.0.1",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-mocha-reporter": "^2.2.1",
|
||||||
"localforage": "^1.5.0",
|
"localforage": "^1.5.0",
|
||||||
"object-path": "^0.11.3",
|
"object-path": "^0.11.3",
|
||||||
|
@ -28,7 +29,6 @@
|
||||||
"portal-vue": "^2.1.4",
|
"portal-vue": "^2.1.4",
|
||||||
"sanitize-html": "^1.13.0",
|
"sanitize-html": "^1.13.0",
|
||||||
"v-click-outside": "^2.1.1",
|
"v-click-outside": "^2.1.1",
|
||||||
"v-tooltip": "^2.0.2",
|
|
||||||
"vue": "^2.5.13",
|
"vue": "^2.5.13",
|
||||||
"vue-chat-scroll": "^1.2.1",
|
"vue-chat-scroll": "^1.2.1",
|
||||||
"vue-i18n": "^7.3.2",
|
"vue-i18n": "^7.3.2",
|
||||||
|
@ -43,6 +43,7 @@
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||||
"@babel/preset-env": "^7.7.6",
|
"@babel/preset-env": "^7.7.6",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "^7.7.4",
|
||||||
|
"@ungap/event-target": "^0.1.0",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
||||||
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
|
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
|
||||||
"@vue/test-utils": "^1.0.0-beta.26",
|
"@vue/test-utils": "^1.0.0-beta.26",
|
||||||
|
@ -56,6 +57,7 @@
|
||||||
"connect-history-api-fallback": "^1.1.0",
|
"connect-history-api-fallback": "^1.1.0",
|
||||||
"cross-spawn": "^4.0.2",
|
"cross-spawn": "^4.0.2",
|
||||||
"css-loader": "^0.28.0",
|
"css-loader": "^0.28.0",
|
||||||
|
"custom-event-polyfill": "^1.0.7",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"eslint-config-standard": "^12.0.0",
|
||||||
"eslint-friendly-formatter": "^2.0.5",
|
"eslint-friendly-formatter": "^2.0.5",
|
||||||
|
|
118
src/App.scss
118
src/App.scss
|
@ -31,9 +31,12 @@ h4 {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
max-width: 980px;
|
max-width: 980px;
|
||||||
background-color: rgba(0,0,0,0.15);
|
|
||||||
align-content: flex-start;
|
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-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -75,7 +78,7 @@ button {
|
||||||
border-radius: $fallback--btnRadius;
|
border-radius: $fallback--btnRadius;
|
||||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||||
cursor: pointer;
|
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);
|
box-shadow: var(--buttonShadow);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
@ -98,18 +101,39 @@ button {
|
||||||
&:active {
|
&: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: 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);
|
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 {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
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 {
|
&.toggled {
|
||||||
color: $fallback--faint;
|
color: $fallback--text;
|
||||||
color: var(--faint, $fallback--faint);
|
color: var(--btnToggledText, $fallback--text);
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--bg, $fallback--bg)
|
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 {
|
&.danger {
|
||||||
|
@ -121,12 +145,15 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label.select {
|
input, textarea, .select, .input {
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
}
|
&.unstyled {
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
input, textarea, .select {
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: $fallback--inputRadius;
|
border-radius: $fallback--inputRadius;
|
||||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||||
|
@ -140,13 +167,17 @@ input, textarea, .select {
|
||||||
font-family: var(--inputFont, sans-serif);
|
font-family: var(--inputFont, sans-serif);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px .5em;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
hyphens: none;
|
hyphens: none;
|
||||||
|
padding: 8px .5em;
|
||||||
|
|
||||||
|
&.select {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled, &[disabled=disabled] {
|
&:disabled, &[disabled=disabled] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
@ -160,7 +191,7 @@ input, textarea, .select {
|
||||||
right: 5px;
|
right: 5px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -198,7 +229,7 @@ input, textarea, .select {
|
||||||
&:checked + label::before {
|
&:checked + label::before {
|
||||||
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
|
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;
|
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 {
|
&:disabled {
|
||||||
&,
|
&,
|
||||||
|
@ -235,7 +266,7 @@ input, textarea, .select {
|
||||||
display: none;
|
display: none;
|
||||||
&:checked + label::before {
|
&:checked + label::before {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
}
|
}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
&,
|
&,
|
||||||
|
@ -353,6 +384,33 @@ i[class*=icon-] {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
box-sizing: border-box;
|
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 {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -487,6 +545,10 @@ main-router {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
color: var(--panelFaint, $fallback--faint);
|
color: var(--panelFaint, $fallback--faint);
|
||||||
}
|
}
|
||||||
|
.faint-link {
|
||||||
|
color: $fallback--faint;
|
||||||
|
color: var(--faintLink, $fallback--faint);
|
||||||
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -509,6 +571,30 @@ main-router {
|
||||||
align-self: stretch;
|
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 {
|
a {
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
color: var(--panelLink, $fallback--link)
|
color: var(--panelLink, $fallback--link)
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
<div
|
<div
|
||||||
id="content"
|
id="content"
|
||||||
class="container"
|
class="container underlay"
|
||||||
>
|
>
|
||||||
<div class="sidebar-flexer mobile-hidden">
|
<div class="sidebar-flexer mobile-hidden">
|
||||||
<div class="sidebar-bounds">
|
<div class="sidebar-bounds">
|
||||||
|
|
|
@ -27,3 +27,5 @@ $fallback--tooltipRadius: 5px;
|
||||||
$fallback--avatarRadius: 4px;
|
$fallback--avatarRadius: 4px;
|
||||||
$fallback--avatarAltRadius: 10px;
|
$fallback--avatarAltRadius: 10px;
|
||||||
$fallback--attachmentRadius: 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;
|
||||||
|
|
|
@ -5,6 +5,8 @@ import App from '../App.vue'
|
||||||
import { windowWidth } from '../services/window_utils/window_utils'
|
import { windowWidth } from '../services/window_utils/window_utils'
|
||||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.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 }) => {
|
const getStatusnetConfig = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
|
@ -185,12 +187,9 @@ const getAppSecret = async ({ store }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveStaffAccounts = async ({ store, accounts }) => {
|
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||||
const backendInteractor = store.state.api.backendInteractor
|
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||||
let nicknames = accounts.map(uri => uri.split('/').pop())
|
nicknames.map(nickname => store.dispatch('fetchUser', nickname))
|
||||||
.map(id => backendInteractor.fetchUser({ id }))
|
|
||||||
nicknames = await Promise.all(nicknames)
|
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,9 +223,16 @@ const getNodeInfo = async ({ store }) => {
|
||||||
|
|
||||||
const frontendVersion = window.___pleromafe_commit_hash
|
const frontendVersion = window.___pleromafe_commit_hash
|
||||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||||
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
|
|
||||||
|
|
||||||
const federation = metadata.federation
|
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: 'federationPolicy', value: federation })
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
name: 'federating',
|
name: 'federating',
|
||||||
|
@ -236,7 +242,7 @@ const getNodeInfo = async ({ store }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const accounts = metadata.staffAccounts
|
const accounts = metadata.staffAccounts
|
||||||
await resolveStaffAccounts({ store, accounts })
|
resolveStaffAccounts({ store, accounts })
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw (res)
|
||||||
}
|
}
|
||||||
|
@ -261,7 +267,7 @@ const checkOAuthToken = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('loginUser', store.getters.getUserToken())
|
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
|
@ -269,23 +275,29 @@ const checkOAuthToken = async ({ store }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const afterStoreSetup = async ({ store, i18n }) => {
|
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()
|
const width = windowWidth()
|
||||||
store.dispatch('setMobileLayout', width <= 800)
|
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
|
// Now we can try getting the server settings and logging in
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
checkOAuthToken({ store }),
|
checkOAuthToken({ store }),
|
||||||
setConfig({ store }),
|
|
||||||
getTOS({ store }),
|
getTOS({ store }),
|
||||||
getInstancePanel({ store }),
|
getInstancePanel({ store }),
|
||||||
getStickers({ store }),
|
getStickers({ store }),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
|
||||||
const AccountActions = {
|
const AccountActions = {
|
||||||
props: [
|
props: [
|
||||||
|
@ -8,7 +9,8 @@ const AccountActions = {
|
||||||
return { }
|
return { }
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ProgressButton
|
ProgressButton,
|
||||||
|
Popover
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showRepeats () {
|
showRepeats () {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="account-actions">
|
<div class="account-actions">
|
||||||
<v-popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
class="account-tools-popover"
|
placement="bottom"
|
||||||
:container="false"
|
>
|
||||||
placement="bottom-end"
|
<div
|
||||||
:offset="5"
|
slot="content"
|
||||||
|
class="account-tools-popover"
|
||||||
>
|
>
|
||||||
<div slot="popover">
|
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<template v-if="user.following">
|
<template v-if="user.following">
|
||||||
<button
|
<button
|
||||||
|
@ -51,10 +51,13 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn btn-default ellipsis-button">
|
<div
|
||||||
|
slot="trigger"
|
||||||
|
class="btn btn-default ellipsis-button"
|
||||||
|
>
|
||||||
<i class="icon-ellipsis trigger-button" />
|
<i class="icon-ellipsis trigger-button" />
|
||||||
</div>
|
</div>
|
||||||
</v-popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -62,7 +65,6 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
@import '../popper/popper.scss';
|
|
||||||
.account-actions {
|
.account-actions {
|
||||||
margin: 0 .8em;
|
margin: 0 .8em;
|
||||||
}
|
}
|
||||||
|
@ -70,6 +72,7 @@
|
||||||
.account-actions button.dropdown-item {
|
.account-actions button.dropdown-item {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-actions .trigger-button {
|
.account-actions .trigger-button {
|
||||||
color: $fallback--lightText;
|
color: $fallback--lightText;
|
||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import StillImage from '../still-image/still-image.vue'
|
||||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||||
import nsfwImage from '../../assets/nsfw.png'
|
import nsfwImage from '../../assets/nsfw.png'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
const Attachment = {
|
const Attachment = {
|
||||||
props: [
|
props: [
|
||||||
|
@ -49,7 +50,8 @@ const Attachment = {
|
||||||
},
|
},
|
||||||
fullwidth () {
|
fullwidth () {
|
||||||
return this.type === 'html' || this.type === 'audio'
|
return this.type === 'html' || this.type === 'audio'
|
||||||
}
|
},
|
||||||
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
linkClicked ({ target }) {
|
linkClicked ({ target }) {
|
||||||
|
@ -58,7 +60,7 @@ const Attachment = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModal (event) {
|
openModal (event) {
|
||||||
const modalTypes = this.$store.getters.mergedConfig.playVideosInModal
|
const modalTypes = this.mergedConfig.playVideosInModal
|
||||||
? ['image', 'video']
|
? ['image', 'video']
|
||||||
: ['image']
|
: ['image']
|
||||||
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
|
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
|
||||||
|
@ -71,7 +73,10 @@ const Attachment = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleHidden (event) {
|
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)
|
this.openModal(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,6 +130,8 @@
|
||||||
.placeholder {
|
.placeholder {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--postLink, $fallback--link);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nsfw-placeholder {
|
.nsfw-placeholder {
|
||||||
|
|
|
@ -40,8 +40,8 @@
|
||||||
top: 100%;
|
top: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--lightBg, $fallback--lightBg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
|
|
|
@ -87,13 +87,13 @@ export default {
|
||||||
|
|
||||||
&:checked + .checkbox-indicator::before {
|
&:checked + .checkbox-indicator::before {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:indeterminate + .checkbox-indicator::before {
|
&:indeterminate + .checkbox-indicator::before {
|
||||||
content: '–';
|
content: '–';
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
68
src/components/color_input/color_input.scss
Normal file
68
src/components/color_input/color_input.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="color-control style-control"
|
class="color-input style-control"
|
||||||
:class="{ disabled: !present || disabled }"
|
:class="{ disabled: !present || disabled }"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
@ -9,46 +9,100 @@
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Checkbox
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
||||||
:id="name + '-o'"
|
|
||||||
class="opt exlcude-disabled"
|
|
||||||
type="checkbox"
|
|
||||||
:checked="present"
|
:checked="present"
|
||||||
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
:disabled="disabled"
|
||||||
>
|
class="opt"
|
||||||
<label
|
@change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
||||||
v-if="typeof fallback !== 'undefined'"
|
|
||||||
class="opt-l"
|
|
||||||
:for="name + '-o'"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<div class="input color-input-field">
|
||||||
:id="name"
|
|
||||||
class="color-input"
|
|
||||||
type="color"
|
|
||||||
:value="value || fallback"
|
|
||||||
:disabled="!present || disabled"
|
|
||||||
@input="$emit('input', $event.target.value)"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
:id="name + '-t'"
|
:id="name + '-t'"
|
||||||
class="text-input"
|
class="textColor unstyled"
|
||||||
type="text"
|
type="text"
|
||||||
:value="value || fallback"
|
:value="value || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
@input="$emit('input', $event.target.value)"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<style lang="scss" src="./color_input.scss"></style>
|
||||||
<script>
|
<script>
|
||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||||
export default {
|
export default {
|
||||||
props: [
|
components: {
|
||||||
'name', 'label', 'value', 'fallback', 'disabled'
|
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: {
|
computed: {
|
||||||
present () {
|
present () {
|
||||||
return typeof this.value !== 'undefined'
|
return typeof this.value !== 'undefined'
|
||||||
|
},
|
||||||
|
validColor () {
|
||||||
|
return hex2rgb(this.value || this.fallback)
|
||||||
|
},
|
||||||
|
transparentColor () {
|
||||||
|
return this.value === 'transparent'
|
||||||
|
},
|
||||||
|
computedColor () {
|
||||||
|
return this.value && this.value.startsWith('--')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,17 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: {
|
||||||
'large', 'contrast'
|
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: {
|
computed: {
|
||||||
hint () {
|
hint () {
|
||||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||||
|
|
|
@ -150,6 +150,7 @@ const conversation = {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
getHighlight () {
|
getHighlight () {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.highlight : null
|
||||||
|
|
|
@ -75,18 +75,18 @@
|
||||||
.dialog-modal-content {
|
.dialog-modal-content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1rem 1rem;
|
padding: 1rem 1rem;
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--lightBg, $fallback--lightBg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-modal-footer {
|
.dialog-modal-footer {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: .5em .5em;
|
padding: .5em .5em;
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--lightBg, $fallback--lightBg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
border-top: 1px solid $fallback--bg;
|
border-top: 1px solid $fallback--border;
|
||||||
border-top: 1px solid var(--bg, $fallback--bg);
|
border-top: 1px solid var(--border, $fallback--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
|
15
src/components/domain_mute_card/domain_mute_card.js
Normal file
15
src/components/domain_mute_card/domain_mute_card.js
Normal file
|
@ -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
|
38
src/components/domain_mute_card/domain_mute_card.vue
Normal file
38
src/components/domain_mute_card/domain_mute_card.vue
Normal file
|
@ -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>
|
|
@ -147,7 +147,7 @@ const EmojiInput = {
|
||||||
input.elm.addEventListener('keydown', this.onKeyDown)
|
input.elm.addEventListener('keydown', this.onKeyDown)
|
||||||
input.elm.addEventListener('click', this.onClickInput)
|
input.elm.addEventListener('click', this.onClickInput)
|
||||||
input.elm.addEventListener('transitionend', this.onTransition)
|
input.elm.addEventListener('transitionend', this.onTransition)
|
||||||
input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
|
input.elm.addEventListener('input', this.onInput)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
const { input } = this
|
const { input } = this
|
||||||
|
@ -159,7 +159,7 @@ const EmojiInput = {
|
||||||
input.elm.removeEventListener('keydown', this.onKeyDown)
|
input.elm.removeEventListener('keydown', this.onKeyDown)
|
||||||
input.elm.removeEventListener('click', this.onClickInput)
|
input.elm.removeEventListener('click', this.onClickInput)
|
||||||
input.elm.removeEventListener('transitionend', this.onTransition)
|
input.elm.removeEventListener('transitionend', this.onTransition)
|
||||||
input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
|
input.elm.removeEventListener('input', this.onInput)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -406,12 +406,6 @@ const EmojiInput = {
|
||||||
this.resize()
|
this.resize()
|
||||||
this.$emit('input', e.target.value)
|
this.$emit('input', e.target.value)
|
||||||
},
|
},
|
||||||
onCompositionUpdate (e) {
|
|
||||||
this.showPicker = false
|
|
||||||
this.setCaret(e)
|
|
||||||
this.resize()
|
|
||||||
this.$emit('input', e.target.value)
|
|
||||||
},
|
|
||||||
onClickInput (e) {
|
onClickInput (e) {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
},
|
},
|
||||||
|
|
|
@ -109,10 +109,16 @@
|
||||||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
box-shadow: var(--popupShadow);
|
box-shadow: var(--popupShadow);
|
||||||
min-width: 75%;
|
min-width: 75%;
|
||||||
background: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
background: var(--bg, $fallback--bg);
|
background-color: var(--popover, $fallback--bg);
|
||||||
color: $fallback--lightText;
|
color: $fallback--link;
|
||||||
color: var(--lightText, $fallback--lightText);
|
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 {
|
&.highlighted {
|
||||||
background-color: $fallback--fg;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,15 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
z-index: 1;
|
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,
|
.keep-open,
|
||||||
.too-many-emoji {
|
.too-many-emoji {
|
||||||
|
|
69
src/components/emoji_reactions/emoji_reactions.js
Normal file
69
src/components/emoji_reactions/emoji_reactions.js
Normal file
|
@ -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
|
141
src/components/emoji_reactions/emoji_reactions.vue
Normal file
141
src/components/emoji_reactions/emoji_reactions.vue
Normal file
|
@ -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>
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
exportData () {
|
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
|
// Create an invisible link with a data url and simulate a click
|
||||||
const e = document.createElement('a')
|
const e = document.createElement('a')
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
|
||||||
const ExtraButtons = {
|
const ExtraButtons = {
|
||||||
props: [ 'status' ],
|
props: [ 'status' ],
|
||||||
|
components: { Popover },
|
||||||
methods: {
|
methods: {
|
||||||
deleteStatus () {
|
deleteStatus () {
|
||||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<v-popover
|
<Popover
|
||||||
v-if="canDelete || canMute || canPin"
|
v-if="canDelete || canMute || canPin"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
class="extra-button-popover"
|
class="extra-button-popover"
|
||||||
>
|
>
|
||||||
<div slot="popover">
|
<div slot="content">
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button
|
<button
|
||||||
v-if="canMute && !status.thread_muted"
|
v-if="canMute && !status.thread_muted"
|
||||||
|
@ -47,17 +47,17 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-icon">
|
<i
|
||||||
<i class="icon-ellipsis" />
|
slot="trigger"
|
||||||
</div>
|
class="icon-ellipsis button-icon"
|
||||||
</v-popover>
|
/>
|
||||||
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./extra_buttons.js" ></script>
|
<script src="./extra_buttons.js" ></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
@import '../popper/popper.scss';
|
|
||||||
|
|
||||||
.icon-ellipsis {
|
.icon-ellipsis {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="btn btn-default follow-button"
|
class="btn btn-default follow-button"
|
||||||
:class="{ pressed: isPressed }"
|
:class="{ toggled: isPressed }"
|
||||||
:disabled="inProgress"
|
:disabled="inProgress"
|
||||||
:title="title"
|
:title="title"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
|
|
@ -10,6 +10,7 @@ const tabModeDict = {
|
||||||
const Interactions = {
|
const Interactions = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||||
filterMode: tabModeDict['mentions']
|
filterMode: tabModeDict['mentions']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
:label="$t('interactions.follows')"
|
:label="$t('interactions.follows')"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
v-if="!allowFollowingMove"
|
||||||
key="moves"
|
key="moves"
|
||||||
:label="$t('interactions.moves')"
|
:label="$t('interactions.moves')"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
|
||||||
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||||
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||||
|
@ -14,7 +15,6 @@ const ModerationTools = {
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showDropDown: false,
|
|
||||||
tags: {
|
tags: {
|
||||||
FORCE_NSFW,
|
FORCE_NSFW,
|
||||||
STRIP_MEDIA,
|
STRIP_MEDIA,
|
||||||
|
@ -24,11 +24,13 @@ const ModerationTools = {
|
||||||
SANDBOX,
|
SANDBOX,
|
||||||
QUARANTINE
|
QUARANTINE
|
||||||
},
|
},
|
||||||
showDeleteUserDialog: false
|
showDeleteUserDialog: false,
|
||||||
|
toggled: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
DialogModal
|
DialogModal,
|
||||||
|
Popover
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tagsSet () {
|
tagsSet () {
|
||||||
|
@ -89,6 +91,9 @@ const ModerationTools = {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
setToggled (value) {
|
||||||
|
this.toggled = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
class="moderation-tools-popover"
|
class="moderation-tools-popover"
|
||||||
placement="bottom-end"
|
placement="bottom"
|
||||||
@show="showDropDown = true"
|
:offset="{ y: 5 }"
|
||||||
@hide="showDropDown = false"
|
@show="setToggled(true)"
|
||||||
|
@close="setToggled(false)"
|
||||||
>
|
>
|
||||||
<div slot="popover">
|
<div slot="content">
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<span v-if="user.is_local">
|
<span v-if="user.is_local">
|
||||||
<button
|
<button
|
||||||
|
@ -122,12 +123,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
slot="trigger"
|
||||||
class="btn btn-default btn-block"
|
class="btn btn-default btn-block"
|
||||||
:class="{ pressed: showDropDown }"
|
:class="{ toggled }"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.admin_menu.moderation') }}
|
{{ $t('user_card.admin_menu.moderation') }}
|
||||||
</button>
|
</button>
|
||||||
</v-popover>
|
</Popover>
|
||||||
<portal to="modal">
|
<portal to="modal">
|
||||||
<DialogModal
|
<DialogModal
|
||||||
v-if="showDeleteUserDialog"
|
v-if="showDeleteUserDialog"
|
||||||
|
@ -160,7 +162,6 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
@import '../popper/popper.scss';
|
|
||||||
|
|
||||||
.menu-checkbox {
|
.menu-checkbox {
|
||||||
float: right;
|
float: right;
|
||||||
|
|
|
@ -11,7 +11,10 @@ const MRFTransparencyPanel = {
|
||||||
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
|
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
|
||||||
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
|
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
|
||||||
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
|
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 () {
|
hasInstanceSpecificPolicies () {
|
||||||
return this.quarantineInstances.length ||
|
return this.quarantineInstances.length ||
|
||||||
|
@ -20,6 +23,11 @@ const MRFTransparencyPanel = {
|
||||||
this.ftlRemovalInstances.length ||
|
this.ftlRemovalInstances.length ||
|
||||||
this.mediaNsfwInstances.length ||
|
this.mediaNsfwInstances.length ||
|
||||||
this.mediaRemovalInstances.length
|
this.mediaRemovalInstances.length
|
||||||
|
},
|
||||||
|
hasKeywordPolicies () {
|
||||||
|
return this.keywordsFtlRemoval.length ||
|
||||||
|
this.keywordsReject.length ||
|
||||||
|
this.keywordsReplace.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
<div class="panel panel-default base01-background">
|
<div class="panel panel-default base01-background">
|
||||||
<div class="panel-heading timeline-heading base02-background">
|
<div class="panel-heading timeline-heading base02-background">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{ $t("about.federation") }}
|
{{ $t("about.mrf.federation") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="mrf-section">
|
<div class="mrf-section">
|
||||||
<h2>{{ $t("about.mrf_policies") }}</h2>
|
<h2>{{ $t("about.mrf.mrf_policies") }}</h2>
|
||||||
<p>{{ $t("about.mrf_policies_desc") }}</p>
|
<p>{{ $t("about.mrf.mrf_policies_desc") }}</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
@ -23,13 +23,13 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 v-if="hasInstanceSpecificPolicies">
|
<h2 v-if="hasInstanceSpecificPolicies">
|
||||||
{{ $t("about.mrf_policy_simple") }}
|
{{ $t("about.mrf.simple.simple_policies") }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div v-if="acceptInstances.length">
|
<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>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
@ -41,9 +41,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="rejectInstances.length">
|
<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>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
@ -55,9 +55,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="quarantineInstances.length">
|
<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>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
@ -69,9 +69,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="ftlRemovalInstances.length">
|
<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>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
@ -83,9 +83,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="mediaNsfwInstances.length">
|
<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>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
@ -97,9 +97,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="mediaRemovalInstances.length">
|
<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>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
@ -109,6 +109,49 @@
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { mapState } from 'vuex'
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
created () {
|
created () {
|
||||||
if (this.currentUser && this.currentUser.locked) {
|
if (this.currentUser && this.currentUser.locked) {
|
||||||
this.$store.dispatch('startFetchingFollowRequest')
|
this.$store.dispatch('startFetchingFollowRequests')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: mapState({
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
|
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="federating && !privateMode">
|
<li v-if="federating && (currentUser || !privateMode)">
|
||||||
<router-link :to="{ name: 'public-external-timeline' }">
|
<router-link :to="{ name: 'public-external-timeline' }">
|
||||||
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
|
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -100,13 +100,25 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $fallback--lightBg;
|
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 {
|
&.router-link-active {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
background-color: $fallback--lightBg;
|
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 {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
|
@ -78,6 +78,13 @@
|
||||||
<i class="fa icon-arrow-curved lit" />
|
<i class="fa icon-arrow-curved lit" />
|
||||||
<small>{{ $t('notifications.migrated_to') }}</small>
|
<small>{{ $t('notifications.migrated_to') }}</small>
|
||||||
</span>
|
</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>
|
||||||
<div
|
<div
|
||||||
v-if="notification.type === 'follow' || notification.type === 'move'"
|
v-if="notification.type === 'follow' || notification.type === 'move'"
|
||||||
|
|
|
@ -68,6 +68,9 @@
|
||||||
a {
|
a {
|
||||||
color: var(--faintLink);
|
color: var(--faintLink);
|
||||||
}
|
}
|
||||||
|
.status-content a {
|
||||||
|
color: var(--postFaintLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
padding: 0;
|
padding: 0;
|
||||||
.media-body {
|
.media-body {
|
||||||
|
@ -94,6 +97,10 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-reaction-emoji {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-details {
|
.notification-details {
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
|
@ -9,18 +9,12 @@
|
||||||
>
|
>
|
||||||
{{ $t('settings.style.common.opacity') }}
|
{{ $t('settings.style.common.opacity') }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Checkbox
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
:id="name + '-o'"
|
|
||||||
class="opt exclude-disabled"
|
|
||||||
type="checkbox"
|
|
||||||
:checked="present"
|
:checked="present"
|
||||||
@input="$emit('input', !present ? fallback : undefined)"
|
:disabled="disabled"
|
||||||
>
|
class="opt"
|
||||||
<label
|
@change="$emit('input', !present ? fallback : undefined)"
|
||||||
v-if="typeof fallback !== 'undefined'"
|
|
||||||
class="opt-l"
|
|
||||||
:for="name + '-o'"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
:id="name"
|
:id="name"
|
||||||
|
@ -37,7 +31,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
props: [
|
props: [
|
||||||
'name', 'value', 'fallback', 'disabled'
|
'name', 'value', 'fallback', 'disabled'
|
||||||
],
|
],
|
||||||
|
|
|
@ -104,8 +104,10 @@
|
||||||
.result-fill {
|
.result-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--pollText, $fallback--text);
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--lightBg;
|
||||||
background-color: var(--linkBg, $fallback--lightBg);
|
background-color: var(--poll, $fallback--lightBg);
|
||||||
border-radius: $fallback--panelRadius;
|
border-radius: $fallback--panelRadius;
|
||||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
156
src/components/popover/popover.js
Normal file
156
src/components/popover/popover.js
Normal file
|
@ -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
|
118
src/components/popover/popover.vue
Normal file
118
src/components/popover/popover.vue
Normal file
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@
|
||||||
<input
|
<input
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
:id="name + '-o'"
|
:id="name + '-o'"
|
||||||
class="opt exclude-disabled"
|
class="opt"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="present"
|
:checked="present"
|
||||||
@input="$emit('input', !present ? fallback : undefined)"
|
@input="$emit('input', !present ? fallback : undefined)"
|
||||||
|
|
39
src/components/react_button/react_button.js
Normal file
39
src/components/react_button/react_button.js
Normal file
|
@ -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
|
111
src/components/react_button/react_button.vue
Normal file
111
src/components/react_button/react_button.vue
Normal file
|
@ -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>
|
|
@ -68,7 +68,12 @@
|
||||||
|
|
||||||
&-item-selected-inner {
|
&-item-selected-inner {
|
||||||
background-color: $fallback--lightBg;
|
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 {
|
&-header {
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
<li>
|
<li>
|
||||||
<Checkbox v-model="useStreamingApi">
|
<Checkbox v-model="useStreamingApi">
|
||||||
{{ $t('settings.useStreamingApi') }}
|
{{ $t('settings.useStreamingApi') }}
|
||||||
<br/>
|
<br>
|
||||||
<small>
|
<small>
|
||||||
{{ $t('settings.useStreamingApiWarning') }}
|
{{ $t('settings.useStreamingApiWarning') }}
|
||||||
</small>
|
</small>
|
||||||
|
@ -92,6 +92,11 @@
|
||||||
{{ $t('settings.reply_link_preview') }}
|
{{ $t('settings.reply_link_preview') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||||
|
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -328,6 +333,11 @@
|
||||||
{{ $t('settings.notification_visibility_moves') }}
|
{{ $t('settings.notification_visibility_moves') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||||
|
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -3,6 +3,17 @@ import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||||
import { getCssShadow } from '../../services/style_setter/style_setter.js'
|
import { getCssShadow } from '../../services/style_setter/style_setter.js'
|
||||||
import { hex2rgb } from '../../services/color_convert/color_convert.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 {
|
export default {
|
||||||
// 'Value' and 'Fallback' can be undefined, but if they are
|
// 'Value' and 'Fallback' can be undefined, but if they are
|
||||||
// initially vue won't detect it when they become something else
|
// initially vue won't detect it when they become something else
|
||||||
|
@ -15,7 +26,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
selectedId: 0,
|
selectedId: 0,
|
||||||
// TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason)
|
// 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: {
|
components: {
|
||||||
|
@ -24,12 +35,12 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
add () {
|
add () {
|
||||||
this.cValue.push(Object.assign({}, this.selected))
|
this.cValue.push(toModel(this.selected))
|
||||||
this.selectedId = this.cValue.length - 1
|
this.selectedId = this.cValue.length - 1
|
||||||
},
|
},
|
||||||
del () {
|
del () {
|
||||||
this.cValue.splice(this.selectedId, 1)
|
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 () {
|
moveUp () {
|
||||||
const movable = this.cValue.splice(this.selectedId, 1)[0]
|
const movable = this.cValue.splice(this.selectedId, 1)[0]
|
||||||
|
@ -46,19 +57,24 @@ export default {
|
||||||
this.cValue = this.value || this.fallback
|
this.cValue = this.value || this.fallback
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
anyShadows () {
|
||||||
|
return this.cValue.length > 0
|
||||||
|
},
|
||||||
|
anyShadowsFallback () {
|
||||||
|
return this.fallback.length > 0
|
||||||
|
},
|
||||||
selected () {
|
selected () {
|
||||||
if (this.ready && this.cValue.length > 0) {
|
if (this.ready && this.anyShadows) {
|
||||||
return this.cValue[this.selectedId]
|
return this.cValue[this.selectedId]
|
||||||
} else {
|
} else {
|
||||||
return {
|
return toModel({})
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
blur: 0,
|
|
||||||
spread: 0,
|
|
||||||
inset: false,
|
|
||||||
color: '#000000',
|
|
||||||
alpha: 1
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
currentFallback () {
|
||||||
|
if (this.ready && this.anyShadowsFallback) {
|
||||||
|
return this.fallback[this.selectedId]
|
||||||
|
} else {
|
||||||
|
return toModel({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moveUpValid () {
|
moveUpValid () {
|
||||||
|
@ -80,7 +96,7 @@ export default {
|
||||||
},
|
},
|
||||||
style () {
|
style () {
|
||||||
return this.ready ? {
|
return this.ready ? {
|
||||||
boxShadow: getCssShadow(this.cValue)
|
boxShadow: getCssShadow(this.fallback)
|
||||||
} : {}
|
} : {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,15 +191,20 @@
|
||||||
v-model="selected.color"
|
v-model="selected.color"
|
||||||
:disabled="!present"
|
:disabled="!present"
|
||||||
:label="$t('settings.style.common.color')"
|
:label="$t('settings.style.common.color')"
|
||||||
|
:fallback="currentFallback.color"
|
||||||
|
:show-optional-tickbox="false"
|
||||||
name="shadow"
|
name="shadow"
|
||||||
/>
|
/>
|
||||||
<OpacityInput
|
<OpacityInput
|
||||||
v-model="selected.alpha"
|
v-model="selected.alpha"
|
||||||
:disabled="!present"
|
:disabled="!present"
|
||||||
/>
|
/>
|
||||||
<p>
|
<i18n
|
||||||
{{ $t('settings.style.shadows.hint') }}
|
path="settings.style.shadows.hintV3"
|
||||||
</p>
|
tag="p"
|
||||||
|
>
|
||||||
|
<code>--variable,mod</code>
|
||||||
|
</i18n>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -12,7 +12,7 @@ const SideDrawer = {
|
||||||
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
|
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
|
||||||
|
|
||||||
if (this.currentUser && this.currentUser.locked) {
|
if (this.currentUser && this.currentUser.locked) {
|
||||||
this.$store.dispatch('startFetchingFollowRequest')
|
this.$store.dispatch('startFetchingFollowRequests')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { UserCard },
|
components: { UserCard },
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="federating && !privateMode"
|
v-if="federating && (currentUser || !privateMode)"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<router-link to="/main/all">
|
<router-link to="/main/all">
|
||||||
|
@ -223,7 +223,13 @@
|
||||||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||||
box-shadow: var(--panelShadow);
|
box-shadow: var(--panelShadow);
|
||||||
background-color: $fallback--bg;
|
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 {
|
.button-icon:before {
|
||||||
width: 1.1em;
|
width: 1.1em;
|
||||||
|
@ -289,7 +295,13 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $fallback--lightBg;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import map from 'lodash/map'
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
|
||||||
const StaffPanel = {
|
const StaffPanel = {
|
||||||
|
@ -6,7 +7,7 @@ const StaffPanel = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
staffAccounts () {
|
staffAccounts () {
|
||||||
return this.$store.state.instance.staffAccounts
|
return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Attachment from '../attachment/attachment.vue'
|
import Attachment from '../attachment/attachment.vue'
|
||||||
import FavoriteButton from '../favorite_button/favorite_button.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 RetweetButton from '../retweet_button/retweet_button.vue'
|
||||||
import Poll from '../poll/poll.vue'
|
import Poll from '../poll/poll.vue'
|
||||||
import ExtraButtons from '../extra_buttons/extra_buttons.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 AvatarList from '../avatar_list/avatar_list.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
import StatusPopover from '../status_popover/status_popover.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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
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'
|
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)
|
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 () {
|
maxThumbnails () {
|
||||||
return this.mergedConfig.maxThumbnails
|
return this.mergedConfig.maxThumbnails
|
||||||
},
|
},
|
||||||
|
@ -319,6 +331,7 @@ const Status = {
|
||||||
components: {
|
components: {
|
||||||
Attachment,
|
Attachment,
|
||||||
FavoriteButton,
|
FavoriteButton,
|
||||||
|
ReactButton,
|
||||||
RetweetButton,
|
RetweetButton,
|
||||||
ExtraButtons,
|
ExtraButtons,
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
|
@ -329,7 +342,8 @@ const Status = {
|
||||||
LinkPreview,
|
LinkPreview,
|
||||||
AvatarList,
|
AvatarList,
|
||||||
Timeago,
|
Timeago,
|
||||||
StatusPopover
|
StatusPopover,
|
||||||
|
EmojiReactions
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
visibilityIcon (visibility) {
|
visibilityIcon (visibility) {
|
||||||
|
|
|
@ -177,6 +177,8 @@
|
||||||
<StatusPopover
|
<StatusPopover
|
||||||
v-if="!isPreview"
|
v-if="!isPreview"
|
||||||
:status-id="status.in_reply_to_status_id"
|
:status-id="status.in_reply_to_status_id"
|
||||||
|
class="reply-to-popover"
|
||||||
|
style="min-width: 0"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="reply-to"
|
class="reply-to"
|
||||||
|
@ -277,7 +279,21 @@
|
||||||
href="#"
|
href="#"
|
||||||
class="cw-status-hider"
|
class="cw-status-hider"
|
||||||
@click.prevent="toggleShowMore"
|
@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
|
<a
|
||||||
v-if="showingMore"
|
v-if="showingMore"
|
||||||
href="#"
|
href="#"
|
||||||
|
@ -354,6 +370,11 @@
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
|
<EmojiReactions
|
||||||
|
v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
|
||||||
|
:status="status"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!noHeading && !isPreview"
|
v-if="!noHeading && !isPreview"
|
||||||
class="status-actions media-body"
|
class="status-actions media-body"
|
||||||
|
@ -382,6 +403,10 @@
|
||||||
:logged-in="loggedIn"
|
:logged-in="loggedIn"
|
||||||
:status="status"
|
:status="status"
|
||||||
/>
|
/>
|
||||||
|
<ReactButton
|
||||||
|
:logged-in="loggedIn"
|
||||||
|
:status="status"
|
||||||
|
/>
|
||||||
<extra-buttons
|
<extra-buttons
|
||||||
:status="status"
|
:status="status"
|
||||||
@onError="showError"
|
@onError="showError"
|
||||||
|
@ -445,7 +470,15 @@ $status-margin: 0.75em;
|
||||||
|
|
||||||
&_focused {
|
&_focused {
|
||||||
background-color: $fallback--lightBg;
|
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 & {
|
.timeline & {
|
||||||
|
@ -541,11 +574,10 @@ $status-margin: 0.75em;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
> .reply-to-and-accountname > a {
|
> .reply-to-and-accountname > a {
|
||||||
|
overflow: hidden;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -554,7 +586,6 @@ $status-margin: 0.75em;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
overflow: hidden;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
.icon-reply {
|
.icon-reply {
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
|
@ -565,6 +596,10 @@ $status-margin: 0.75em;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reply-to-popover {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.reply-to {
|
.reply-to {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -572,9 +607,8 @@ $status-margin: 0.75em;
|
||||||
.reply-to-text {
|
.reply-to-text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
margin: 0 0.4em 0 0.2em;
|
margin: 0 0.4em 0 0.2em;
|
||||||
color: $fallback--faint;
|
|
||||||
color: var(--faint, $fallback--faint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.replies-separator {
|
.replies-separator {
|
||||||
|
@ -636,6 +670,11 @@ $status-margin: 0.75em;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--postLink, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
img, video {
|
img, video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
|
|
@ -5,22 +5,14 @@ const StatusPopover = {
|
||||||
props: [
|
props: [
|
||||||
'statusId'
|
'statusId'
|
||||||
],
|
],
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
popperOptions: {
|
|
||||||
modifiers: {
|
|
||||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
status () {
|
status () {
|
||||||
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
|
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status: () => import('../status/status.vue')
|
Status: () => import('../status/status.vue'),
|
||||||
|
Popover: () => import('../popover/popover.vue')
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
enter () {
|
enter () {
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<v-popover
|
<Popover
|
||||||
|
trigger="hover"
|
||||||
popover-class="status-popover"
|
popover-class="status-popover"
|
||||||
placement="top-start"
|
:bound-to="{ x: 'container' }"
|
||||||
:popper-options="popperOptions"
|
@show="enter"
|
||||||
@show="enter()"
|
>
|
||||||
|
<template slot="trigger">
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
slot="content"
|
||||||
>
|
>
|
||||||
<template slot="popover">
|
|
||||||
<Status
|
<Status
|
||||||
v-if="status"
|
v-if="status"
|
||||||
:is-preview="true"
|
:is-preview="true"
|
||||||
|
@ -18,10 +23,8 @@
|
||||||
>
|
>
|
||||||
<i class="icon-spin4 animate-spin" />
|
<i class="icon-spin4 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</Popover>
|
||||||
<slot />
|
|
||||||
</v-popover>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./status_popover.js" ></script>
|
<script src="./status_popover.js" ></script>
|
||||||
|
@ -29,13 +32,11 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.tooltip.popover.status-popover {
|
.status-popover {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
min-width: 15em;
|
min-width: 15em;
|
||||||
max-width: 95%;
|
max-width: 95%;
|
||||||
margin-left: 0.5em;
|
|
||||||
|
|
||||||
.popover-inner {
|
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
border-color: var(--border, $fallback--border);
|
border-color: var(--border, $fallback--border);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
@ -44,29 +45,6 @@
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||||
box-shadow: var(--popupShadow);
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-el.status-el {
|
.status-el.status-el {
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: drop-shadow(0 0 5px var(--link, $fallback--link));
|
filter: drop-shadow(0 0 5px var(--accent, $fallback--link));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div class="preview-container">
|
||||||
|
<div class="underlay underlay-preview" />
|
||||||
<div class="panel dummy">
|
<div class="panel dummy">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -19,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body theme-preview-content">
|
<div class="panel-body theme-preview-content">
|
||||||
<div class="post">
|
<div class="post">
|
||||||
<div class="avatar">
|
<div class="avatar still-image">
|
||||||
( ͡° ͜ʖ ͡°)
|
( ͡° ͜ʖ ͡°)
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -98,4 +100,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.preview-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.underlay-preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,6 +1,29 @@
|
||||||
import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
|
|
||||||
import { set, delete as del } from 'vue'
|
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 ColorInput from '../color_input/color_input.vue'
|
||||||
import RangeInput from '../range_input/range_input.vue'
|
import RangeInput from '../range_input/range_input.vue'
|
||||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||||
|
@ -24,11 +47,22 @@ const v1OnlyNames = [
|
||||||
'cOrange'
|
'cOrange'
|
||||||
].map(_ => _ + 'ColorLocal')
|
].map(_ => _ + 'ColorLocal')
|
||||||
|
|
||||||
|
const colorConvert = (color) => {
|
||||||
|
if (color.startsWith('--') || color === 'transparent') {
|
||||||
|
return color
|
||||||
|
} else {
|
||||||
|
return hex2rgb(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
availableStyles: [],
|
availableStyles: [],
|
||||||
selected: this.$store.getters.mergedConfig.theme,
|
selected: this.$store.getters.mergedConfig.theme,
|
||||||
|
themeWarning: undefined,
|
||||||
|
tempImportFile: undefined,
|
||||||
|
engineVersion: 0,
|
||||||
|
|
||||||
previewShadows: {},
|
previewShadows: {},
|
||||||
previewColors: {},
|
previewColors: {},
|
||||||
|
@ -45,51 +79,13 @@ export default {
|
||||||
keepRoundness: false,
|
keepRoundness: false,
|
||||||
keepFonts: false,
|
keepFonts: false,
|
||||||
|
|
||||||
textColorLocal: '',
|
...Object.keys(SLOT_INHERITANCE)
|
||||||
linkColorLocal: '',
|
.map(key => [key, ''])
|
||||||
|
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
|
||||||
|
|
||||||
bgColorLocal: '',
|
...Object.keys(OPACITIES)
|
||||||
bgOpacityLocal: undefined,
|
.map(key => [key, ''])
|
||||||
|
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
|
||||||
fgColorLocal: '',
|
|
||||||
fgTextColorLocal: undefined,
|
|
||||||
fgLinkColorLocal: undefined,
|
|
||||||
|
|
||||||
btnColorLocal: undefined,
|
|
||||||
btnTextColorLocal: undefined,
|
|
||||||
btnOpacityLocal: undefined,
|
|
||||||
|
|
||||||
inputColorLocal: undefined,
|
|
||||||
inputTextColorLocal: undefined,
|
|
||||||
inputOpacityLocal: undefined,
|
|
||||||
|
|
||||||
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: '',
|
|
||||||
|
|
||||||
shadowSelected: undefined,
|
shadowSelected: undefined,
|
||||||
shadowsLocal: {},
|
shadowsLocal: {},
|
||||||
|
@ -108,69 +104,105 @@ export default {
|
||||||
created () {
|
created () {
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
getThemes().then((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
|
self.availableStyles = themesComplete
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
|
this.loadThemeFromLocalStorage()
|
||||||
if (typeof this.shadowSelected === 'undefined') {
|
if (typeof this.shadowSelected === 'undefined') {
|
||||||
this.shadowSelected = this.shadowsAvailable[0]
|
this.shadowSelected = this.shadowsAvailable[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
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 () {
|
selectedVersion () {
|
||||||
return Array.isArray(this.selected) ? 1 : 2
|
return Array.isArray(this.selected) ? 1 : 2
|
||||||
},
|
},
|
||||||
currentColors () {
|
currentColors () {
|
||||||
return {
|
return Object.keys(SLOT_INHERITANCE)
|
||||||
bg: this.bgColorLocal,
|
.map(key => [key, this[key + 'ColorLocal']])
|
||||||
text: this.textColorLocal,
|
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
currentOpacity () {
|
currentOpacity () {
|
||||||
return {
|
return Object.keys(OPACITIES)
|
||||||
bg: this.bgOpacityLocal,
|
.map(key => [key, this[key + 'OpacityLocal']])
|
||||||
btn: this.btnOpacityLocal,
|
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||||
input: this.inputOpacityLocal,
|
|
||||||
panel: this.panelOpacityLocal,
|
|
||||||
topBar: this.topBarOpacityLocal,
|
|
||||||
border: this.borderOpacityLocal,
|
|
||||||
faint: this.faintOpacityLocal
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
currentRadii () {
|
currentRadii () {
|
||||||
return {
|
return {
|
||||||
|
@ -193,6 +225,7 @@ export default {
|
||||||
},
|
},
|
||||||
// This needs optimization maybe
|
// This needs optimization maybe
|
||||||
previewContrast () {
|
previewContrast () {
|
||||||
|
try {
|
||||||
if (!this.previewTheme.colors.bg) return {}
|
if (!this.previewTheme.colors.bg) return {}
|
||||||
const colors = this.previewTheme.colors
|
const colors = this.previewTheme.colors
|
||||||
const opacity = this.previewTheme.opacity
|
const opacity = this.previewTheme.opacity
|
||||||
|
@ -206,62 +239,52 @@ export default {
|
||||||
laa: ratio >= 3,
|
laa: ratio >= 3,
|
||||||
laaa: ratio >= 4.5
|
laaa: ratio >= 4.5
|
||||||
})
|
})
|
||||||
|
const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
|
||||||
|
|
||||||
// fgsfds :DDDD
|
const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
|
||||||
const fgs = {
|
const slotIsBaseText = key === 'text' || key === 'link'
|
||||||
text: hex2rgb(colors.text),
|
const slotIsText = slotIsBaseText || (
|
||||||
panelText: hex2rgb(colors.panelText),
|
typeof value === 'object' && value !== null && value.textColor
|
||||||
panelLink: hex2rgb(colors.panelLink),
|
)
|
||||||
btnText: hex2rgb(colors.btnText),
|
if (!slotIsText) return acc
|
||||||
topBarText: hex2rgb(colors.topBarText),
|
const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
|
||||||
inputText: hex2rgb(colors.inputText),
|
const background = variant || layer
|
||||||
|
const opacitySlot = getOpacitySlot(background)
|
||||||
|
const textColors = [
|
||||||
|
key,
|
||||||
|
...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
|
||||||
|
]
|
||||||
|
|
||||||
link: hex2rgb(colors.link),
|
const layers = getLayers(
|
||||||
topBarLink: hex2rgb(colors.topBarLink),
|
layer,
|
||||||
|
variant || layer,
|
||||||
|
opacitySlot,
|
||||||
|
colorsConverted,
|
||||||
|
opacity
|
||||||
|
)
|
||||||
|
|
||||||
red: hex2rgb(colors.cRed),
|
return {
|
||||||
green: hex2rgb(colors.cGreen),
|
...acc,
|
||||||
blue: hex2rgb(colors.cBlue),
|
...textColors.reduce((acc, textColorKey) => {
|
||||||
orange: hex2rgb(colors.cOrange)
|
const newKey = slotIsBaseText
|
||||||
|
? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
|
||||||
|
: textColorKey
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[newKey]: getContrastRatioLayers(
|
||||||
|
colorsConverted[textColorKey],
|
||||||
|
layers,
|
||||||
|
colorsConverted[textColorKey]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}, {})
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
}, {})
|
||||||
|
|
||||||
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
|
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failure computing contrasts', e)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
previewRules () {
|
previewRules () {
|
||||||
if (!this.preview.rules) return ''
|
if (!this.preview.rules) return ''
|
||||||
|
@ -272,7 +295,7 @@ export default {
|
||||||
].join(';')
|
].join(';')
|
||||||
},
|
},
|
||||||
shadowsAvailable () {
|
shadowsAvailable () {
|
||||||
return Object.keys(this.previewTheme.shadows).sort()
|
return Object.keys(DEFAULT_SHADOWS).sort()
|
||||||
},
|
},
|
||||||
currentShadowOverriden: {
|
currentShadowOverriden: {
|
||||||
get () {
|
get () {
|
||||||
|
@ -287,7 +310,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentShadowFallback () {
|
currentShadowFallback () {
|
||||||
return this.previewTheme.shadows[this.shadowSelected]
|
return (this.previewTheme.shadows || {})[this.shadowSelected]
|
||||||
},
|
},
|
||||||
currentShadow: {
|
currentShadow: {
|
||||||
get () {
|
get () {
|
||||||
|
@ -309,27 +332,34 @@ export default {
|
||||||
!this.keepColor
|
!this.keepColor
|
||||||
)
|
)
|
||||||
|
|
||||||
const theme = {}
|
const source = {
|
||||||
|
themeEngineVersion: CURRENT_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
if (this.keepFonts || saveEverything) {
|
if (this.keepFonts || saveEverything) {
|
||||||
theme.fonts = this.fontsLocal
|
source.fonts = this.fontsLocal
|
||||||
}
|
}
|
||||||
if (this.keepShadows || saveEverything) {
|
if (this.keepShadows || saveEverything) {
|
||||||
theme.shadows = this.shadowsLocal
|
source.shadows = this.shadowsLocal
|
||||||
}
|
}
|
||||||
if (this.keepOpacity || saveEverything) {
|
if (this.keepOpacity || saveEverything) {
|
||||||
theme.opacity = this.currentOpacity
|
source.opacity = this.currentOpacity
|
||||||
}
|
}
|
||||||
if (this.keepColor || saveEverything) {
|
if (this.keepColor || saveEverything) {
|
||||||
theme.colors = this.currentColors
|
source.colors = this.currentColors
|
||||||
}
|
}
|
||||||
if (this.keepRoundness || saveEverything) {
|
if (this.keepRoundness || saveEverything) {
|
||||||
theme.radii = this.currentRadii
|
source.radii = this.currentRadii
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
themeEngineVersion: CURRENT_VERSION,
|
||||||
|
...this.previewTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// To separate from other random JSON files and possible future theme formats
|
// To separate from other random JSON files and possible future source formats
|
||||||
_pleroma_theme_version: 2, theme
|
_pleroma_theme_version: 2, theme, source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -346,10 +376,128 @@ export default {
|
||||||
Checkbox
|
Checkbox
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
setCustomTheme () {
|
||||||
this.$store.dispatch('setOption', {
|
this.$store.dispatch('setOption', {
|
||||||
name: 'customTheme',
|
name: 'customTheme',
|
||||||
value: {
|
value: {
|
||||||
|
themeEngineVersion: CURRENT_VERSION,
|
||||||
|
...this.previewTheme
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.$store.dispatch('setOption', {
|
||||||
|
name: 'customThemeSource',
|
||||||
|
value: {
|
||||||
|
themeEngineVersion: CURRENT_VERSION,
|
||||||
shadows: this.shadowsLocal,
|
shadows: this.shadowsLocal,
|
||||||
fonts: this.fontsLocal,
|
fonts: this.fontsLocal,
|
||||||
opacity: this.currentOpacity,
|
opacity: this.currentOpacity,
|
||||||
|
@ -358,21 +506,27 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onImport (parsed) {
|
updatePreviewColorsAndShadows () {
|
||||||
if (parsed._pleroma_theme_version === 1) {
|
this.previewColors = generateColors({
|
||||||
this.normalizeLocalState(parsed, 1)
|
opacity: this.currentOpacity,
|
||||||
} else if (parsed._pleroma_theme_version === 2) {
|
colors: this.currentColors
|
||||||
this.normalizeLocalState(parsed.theme, 2)
|
})
|
||||||
}
|
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) {
|
importValidator (parsed) {
|
||||||
const version = parsed._pleroma_theme_version
|
const version = parsed._pleroma_theme_version
|
||||||
return version >= 1 || version <= 2
|
return version >= 1 || version <= 2
|
||||||
},
|
},
|
||||||
clearAll () {
|
clearAll () {
|
||||||
const state = this.$store.getters.mergedConfig.customTheme
|
this.loadThemeFromLocalStorage()
|
||||||
const version = state.colors ? 2 : 'l1'
|
|
||||||
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Clears all the extra stuff when loading V1 theme
|
// 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:
|
* 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.
|
* v2 (version = 2) - newer version of themes.
|
||||||
* v1 (version = 1) - older version of themes (import from file)
|
* v1 (version = 1) - older version of themes (import from file)
|
||||||
* v1l (version = l1) - older version of theme (load from local storage)
|
* v1l (version = l1) - older version of theme (load from local storage)
|
||||||
* v1 and v1l differ because of way themes were stored/exported.
|
* 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 {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) {
|
normalizeLocalState (theme, version = 0, source, forceSource = false) {
|
||||||
const colors = input.colors || input
|
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 radii = input.radii || input
|
||||||
const opacity = input.opacity
|
const opacity = input.opacity
|
||||||
const shadows = input.shadows || {}
|
const shadows = input.shadows || {}
|
||||||
const fonts = input.fonts || {}
|
const fonts = input.fonts || {}
|
||||||
|
const colors = !input.themeEngineVersion
|
||||||
|
? colors2to3(input.colors || input)
|
||||||
|
: input.colors || input
|
||||||
|
|
||||||
if (version === 0) {
|
if (version === 0) {
|
||||||
if (input.version) version = input.version
|
if (input.version) version = input.version
|
||||||
|
@ -437,6 +609,8 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.engineVersion = version
|
||||||
|
|
||||||
// Stuff that differs between V1 and V2
|
// Stuff that differs between V1 and V2
|
||||||
if (version === 1) {
|
if (version === 1) {
|
||||||
this.fgColorLocal = rgb2hex(colors.btn)
|
this.fgColorLocal = rgb2hex(colors.btn)
|
||||||
|
@ -445,7 +619,7 @@ export default {
|
||||||
|
|
||||||
if (!this.keepColor) {
|
if (!this.keepColor) {
|
||||||
this.clearV1()
|
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') {
|
if (version === 1 || version === 'l1') {
|
||||||
keys
|
keys
|
||||||
.add('bg')
|
.add('bg')
|
||||||
|
@ -457,7 +631,17 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
keys.forEach(key => {
|
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) {
|
if (!this.keepShadows) {
|
||||||
this.clearShadows()
|
this.clearShadows()
|
||||||
|
if (version === 2) {
|
||||||
|
this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
|
||||||
|
} else {
|
||||||
this.shadowsLocal = shadows
|
this.shadowsLocal = shadows
|
||||||
|
}
|
||||||
this.shadowSelected = this.shadowsAvailable[0]
|
this.shadowSelected = this.shadowsAvailable[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,14 +668,6 @@ export default {
|
||||||
this.clearFonts()
|
this.clearFonts()
|
||||||
this.fontsLocal = fonts
|
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: {
|
watch: {
|
||||||
|
@ -502,8 +682,9 @@ export default {
|
||||||
},
|
},
|
||||||
shadowsLocal: {
|
shadowsLocal: {
|
||||||
handler () {
|
handler () {
|
||||||
|
if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
|
||||||
try {
|
try {
|
||||||
this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
|
this.updatePreviewColorsAndShadows()
|
||||||
this.shadowsInvalid = false
|
this.shadowsInvalid = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.shadowsInvalid = true
|
this.shadowsInvalid = true
|
||||||
|
@ -526,27 +707,24 @@ export default {
|
||||||
},
|
},
|
||||||
currentColors () {
|
currentColors () {
|
||||||
try {
|
try {
|
||||||
this.previewColors = generateColors({
|
this.updatePreviewColorsAndShadows()
|
||||||
opacity: this.currentOpacity,
|
|
||||||
colors: this.currentColors
|
|
||||||
})
|
|
||||||
this.colorsInvalid = false
|
this.colorsInvalid = false
|
||||||
|
this.shadowsInvalid = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.colorsInvalid = true
|
this.colorsInvalid = true
|
||||||
|
this.shadowsInvalid = true
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentOpacity () {
|
currentOpacity () {
|
||||||
try {
|
try {
|
||||||
this.previewColors = generateColors({
|
this.updatePreviewColorsAndShadows()
|
||||||
opacity: this.currentOpacity,
|
|
||||||
colors: this.currentColors
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selected () {
|
selected () {
|
||||||
|
this.dismissWarning()
|
||||||
if (this.selectedVersion === 1) {
|
if (this.selectedVersion === 1) {
|
||||||
if (!this.keepRoundness) {
|
if (!this.keepRoundness) {
|
||||||
this.clearRoundness()
|
this.clearRoundness()
|
||||||
|
@ -573,7 +751,7 @@ export default {
|
||||||
this.cOrangeColorLocal = this.selected[8]
|
this.cOrangeColorLocal = this.selected[8]
|
||||||
}
|
}
|
||||||
} else if (this.selectedVersion >= 2) {
|
} else if (this.selectedVersion >= 2) {
|
||||||
this.normalizeLocalState(this.selected.theme, 2)
|
this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
.style-switcher {
|
.style-switcher {
|
||||||
|
.theme-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
.buttons {
|
||||||
|
.btn {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.preset-switcher {
|
.preset-switcher {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
@ -15,10 +25,16 @@
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
input, select {
|
input, select {
|
||||||
&:not(.exclude-disabled) {
|
|
||||||
opacity: .5
|
opacity: .5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opt {
|
||||||
|
margin: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-input {
|
||||||
|
flex: 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, select {
|
input, select {
|
||||||
|
@ -26,15 +42,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 0;
|
flex: 0;
|
||||||
|
|
||||||
&[type=color] {
|
|
||||||
padding: 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 29px;
|
|
||||||
min-width: 2em;
|
|
||||||
border: none;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[type=number] {
|
&[type=number] {
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
}
|
}
|
||||||
|
@ -42,13 +49,6 @@
|
||||||
&[type=range] {
|
&[type=range] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 3em;
|
min-width: 3em;
|
||||||
}
|
|
||||||
|
|
||||||
&[type=checkbox] + label {
|
|
||||||
margin: 6px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not([type=number]):not([type=text]) {
|
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,53 @@
|
||||||
<div class="style-switcher">
|
<div class="style-switcher">
|
||||||
<div class="presets-container">
|
<div class="presets-container">
|
||||||
<div class="save-load">
|
<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-object="exportedTheme"
|
||||||
:export-label="$t("settings.export_theme")"
|
:export-label="$t("settings.export_theme")"
|
||||||
:import-label="$t("settings.import_theme")"
|
:import-label="$t("settings.import_theme")"
|
||||||
|
@ -27,8 +73,8 @@
|
||||||
:key="style.name"
|
:key="style.name"
|
||||||
:value="style"
|
:value="style"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: style[1] || style.theme.colors.bg,
|
backgroundColor: style[1] || (style.theme || style.source).colors.bg,
|
||||||
color: style[3] || style.theme.colors.text
|
color: style[3] || (style.theme || style.source).colors.text
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ style[0] || style.name }}
|
{{ style[0] || style.name }}
|
||||||
|
@ -38,7 +84,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</export-import>
|
</ExportImport>
|
||||||
</div>
|
</div>
|
||||||
<div class="save-load-options">
|
<div class="save-load-options">
|
||||||
<span class="keep-option">
|
<span class="keep-option">
|
||||||
|
@ -70,9 +116,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preview-container">
|
|
||||||
<preview :style="previewRules" />
|
<preview :style="previewRules" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<tab-switcher key="style-tweak">
|
<tab-switcher key="style-tweak">
|
||||||
|
@ -106,7 +150,7 @@
|
||||||
<OpacityInput
|
<OpacityInput
|
||||||
v-model="bgOpacityLocal"
|
v-model="bgOpacityLocal"
|
||||||
name="bgOpacity"
|
name="bgOpacity"
|
||||||
:fallback="previewTheme.opacity.bg || 1"
|
:fallback="previewTheme.opacity.bg"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="textColorLocal"
|
v-model="textColorLocal"
|
||||||
|
@ -114,10 +158,19 @@
|
||||||
:label="$t('settings.text')"
|
:label="$t('settings.text')"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.bgText" />
|
<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
|
<ColorInput
|
||||||
v-model="linkColorLocal"
|
v-model="linkColorLocal"
|
||||||
name="linkColor"
|
name="linkColor"
|
||||||
|
:fallback="previewTheme.colors.accent"
|
||||||
:label="$t('settings.links')"
|
:label="$t('settings.links')"
|
||||||
|
:show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.bgLink" />
|
<ContrastRatio :contrast="previewContrast.bgLink" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -148,13 +201,13 @@
|
||||||
name="cRedColor"
|
name="cRedColor"
|
||||||
:label="$t('settings.cRed')"
|
:label="$t('settings.cRed')"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.bgRed" />
|
<ContrastRatio :contrast="previewContrast.bgCRed" />
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="cBlueColorLocal"
|
v-model="cBlueColorLocal"
|
||||||
name="cBlueColor"
|
name="cBlueColor"
|
||||||
:label="$t('settings.cBlue')"
|
:label="$t('settings.cBlue')"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.bgBlue" />
|
<ContrastRatio :contrast="previewContrast.bgCBlue" />
|
||||||
</div>
|
</div>
|
||||||
<div class="color-item">
|
<div class="color-item">
|
||||||
<ColorInput
|
<ColorInput
|
||||||
|
@ -162,13 +215,13 @@
|
||||||
name="cGreenColor"
|
name="cGreenColor"
|
||||||
:label="$t('settings.cGreen')"
|
:label="$t('settings.cGreen')"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.bgGreen" />
|
<ContrastRatio :contrast="previewContrast.bgCGreen" />
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="cOrangeColorLocal"
|
v-model="cOrangeColorLocal"
|
||||||
name="cOrangeColor"
|
name="cOrangeColor"
|
||||||
:label="$t('settings.cOrange')"
|
:label="$t('settings.cOrange')"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.bgOrange" />
|
<ContrastRatio :contrast="previewContrast.bgCOrange" />
|
||||||
</div>
|
</div>
|
||||||
<p>{{ $t('settings.theme_help_v2_2') }}</p>
|
<p>{{ $t('settings.theme_help_v2_2') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -193,6 +246,14 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-item">
|
<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>
|
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="alertErrorColorLocal"
|
v-model="alertErrorColorLocal"
|
||||||
|
@ -200,14 +261,53 @@
|
||||||
:label="$t('settings.style.advanced_colors.alert_error')"
|
:label="$t('settings.style.advanced_colors.alert_error')"
|
||||||
:fallback="previewTheme.colors.alertError"
|
: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
|
<ColorInput
|
||||||
v-model="alertWarningColorLocal"
|
v-model="alertWarningColorLocal"
|
||||||
name="alertWarning"
|
name="alertWarning"
|
||||||
:label="$t('settings.style.advanced_colors.alert_warning')"
|
:label="$t('settings.style.advanced_colors.alert_warning')"
|
||||||
:fallback="previewTheme.colors.alertWarning"
|
: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>
|
||||||
<div class="color-item">
|
<div class="color-item">
|
||||||
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
|
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
|
||||||
|
@ -217,19 +317,30 @@
|
||||||
:label="$t('settings.style.advanced_colors.badge_notification')"
|
:label="$t('settings.style.advanced_colors.badge_notification')"
|
||||||
:fallback="previewTheme.colors.badgeNotification"
|
: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>
|
||||||
<div class="color-item">
|
<div class="color-item">
|
||||||
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
|
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="panelColorLocal"
|
v-model="panelColorLocal"
|
||||||
name="panelColor"
|
name="panelColor"
|
||||||
:fallback="fgColorLocal"
|
:fallback="previewTheme.colors.panel"
|
||||||
:label="$t('settings.background')"
|
:label="$t('settings.background')"
|
||||||
/>
|
/>
|
||||||
<OpacityInput
|
<OpacityInput
|
||||||
v-model="panelOpacityLocal"
|
v-model="panelOpacityLocal"
|
||||||
name="panelOpacity"
|
name="panelOpacity"
|
||||||
:fallback="previewTheme.opacity.panel || 1"
|
:fallback="previewTheme.opacity.panel"
|
||||||
|
:disabled="panelColorLocal === 'transparent'"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="panelTextColorLocal"
|
v-model="panelTextColorLocal"
|
||||||
|
@ -239,7 +350,7 @@
|
||||||
/>
|
/>
|
||||||
<ContrastRatio
|
<ContrastRatio
|
||||||
:contrast="previewContrast.panelText"
|
:contrast="previewContrast.panelText"
|
||||||
large="1"
|
large="true"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="panelLinkColorLocal"
|
v-model="panelLinkColorLocal"
|
||||||
|
@ -249,7 +360,7 @@
|
||||||
/>
|
/>
|
||||||
<ContrastRatio
|
<ContrastRatio
|
||||||
:contrast="previewContrast.panelLink"
|
:contrast="previewContrast.panelLink"
|
||||||
large="1"
|
large="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-item">
|
<div class="color-item">
|
||||||
|
@ -257,7 +368,7 @@
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="topBarColorLocal"
|
v-model="topBarColorLocal"
|
||||||
name="topBarColor"
|
name="topBarColor"
|
||||||
:fallback="fgColorLocal"
|
:fallback="previewTheme.colors.topBar"
|
||||||
:label="$t('settings.background')"
|
:label="$t('settings.background')"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
|
@ -280,13 +391,14 @@
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="inputColorLocal"
|
v-model="inputColorLocal"
|
||||||
name="inputColor"
|
name="inputColor"
|
||||||
:fallback="fgColorLocal"
|
:fallback="previewTheme.colors.input"
|
||||||
:label="$t('settings.background')"
|
:label="$t('settings.background')"
|
||||||
/>
|
/>
|
||||||
<OpacityInput
|
<OpacityInput
|
||||||
v-model="inputOpacityLocal"
|
v-model="inputOpacityLocal"
|
||||||
name="inputOpacity"
|
name="inputOpacity"
|
||||||
:fallback="previewTheme.opacity.input || 1"
|
:fallback="previewTheme.opacity.input"
|
||||||
|
:disabled="inputColorLocal === 'transparent'"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="inputTextColorLocal"
|
v-model="inputTextColorLocal"
|
||||||
|
@ -301,13 +413,14 @@
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="btnColorLocal"
|
v-model="btnColorLocal"
|
||||||
name="btnColor"
|
name="btnColor"
|
||||||
:fallback="fgColorLocal"
|
:fallback="previewTheme.colors.btn"
|
||||||
:label="$t('settings.background')"
|
:label="$t('settings.background')"
|
||||||
/>
|
/>
|
||||||
<OpacityInput
|
<OpacityInput
|
||||||
v-model="btnOpacityLocal"
|
v-model="btnOpacityLocal"
|
||||||
name="btnOpacity"
|
name="btnOpacity"
|
||||||
:fallback="previewTheme.opacity.btn || 1"
|
:fallback="previewTheme.opacity.btn"
|
||||||
|
:disabled="btnColorLocal === 'transparent'"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="btnTextColorLocal"
|
v-model="btnTextColorLocal"
|
||||||
|
@ -316,6 +429,124 @@
|
||||||
:label="$t('settings.text')"
|
:label="$t('settings.text')"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.btnText" />
|
<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>
|
||||||
<div class="color-item">
|
<div class="color-item">
|
||||||
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
|
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
|
||||||
|
@ -328,7 +559,8 @@
|
||||||
<OpacityInput
|
<OpacityInput
|
||||||
v-model="borderOpacityLocal"
|
v-model="borderOpacityLocal"
|
||||||
name="borderOpacity"
|
name="borderOpacity"
|
||||||
:fallback="previewTheme.opacity.border || 1"
|
:fallback="previewTheme.opacity.border"
|
||||||
|
:disabled="borderColorLocal === 'transparent'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-item">
|
<div class="color-item">
|
||||||
|
@ -336,7 +568,7 @@
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="faintColorLocal"
|
v-model="faintColorLocal"
|
||||||
name="faintColor"
|
name="faintColor"
|
||||||
:fallback="previewTheme.colors.faint || 1"
|
:fallback="previewTheme.colors.faint"
|
||||||
:label="$t('settings.text')"
|
:label="$t('settings.text')"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
|
@ -354,9 +586,146 @@
|
||||||
<OpacityInput
|
<OpacityInput
|
||||||
v-model="faintOpacityLocal"
|
v-model="faintOpacityLocal"
|
||||||
name="faintOpacity"
|
name="faintOpacity"
|
||||||
:fallback="previewTheme.opacity.faint || 0.5"
|
:fallback="previewTheme.opacity.faint"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -491,7 +860,7 @@
|
||||||
{{ $t('settings.style.switcher.clear_all') }}
|
{{ $t('settings.style.switcher.clear_all') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<shadow-control
|
<ShadowControl
|
||||||
v-model="currentShadow"
|
v-model="currentShadow"
|
||||||
:ready="!!currentShadowFallback"
|
:ready="!!currentShadowFallback"
|
||||||
:fallback="currentShadowFallback"
|
:fallback="currentShadowFallback"
|
||||||
|
|
|
@ -52,6 +52,11 @@
|
||||||
margin-bottom: 6px - 99px;
|
margin-bottom: 6px - 99px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--tabText, $fallback--text);
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--tab, $fallback--fg);
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
|
||||||
|
@ -63,6 +68,8 @@
|
||||||
&.active {
|
&.active {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--tabActiveText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import FollowButton from '../follow_button/follow_button.vue'
|
import FollowButton from '../follow_button/follow_button.vue'
|
||||||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||||
import AccountActions from '../account_actions/account_actions.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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
@ -30,22 +29,12 @@ export default {
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
style () {
|
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 {
|
return {
|
||||||
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
|
|
||||||
backgroundImage: [
|
backgroundImage: [
|
||||||
`linear-gradient(to bottom, ${tintColor}, ${tintColor})`,
|
`linear-gradient(to bottom, var(--profileTint), var(--profileTint))`,
|
||||||
`url(${this.user.cover_photo})`
|
`url(${this.user.cover_photo})`
|
||||||
].join(', ')
|
].join(', ')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isOtherUser () {
|
isOtherUser () {
|
||||||
return this.user.id !== this.$store.state.users.currentUser.id
|
return this.user.id !== this.$store.state.users.currentUser.id
|
||||||
|
|
|
@ -151,7 +151,7 @@
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-default pressed"
|
class="btn btn-default toggled"
|
||||||
:click="unsubscribeUser"
|
:click="unsubscribeUser"
|
||||||
:title="$t('user_card.unsubscribe')"
|
:title="$t('user_card.unsubscribe')"
|
||||||
>
|
>
|
||||||
|
@ -162,7 +162,7 @@
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
v-if="user.muted"
|
v-if="user.muted"
|
||||||
class="btn btn-default btn-block pressed"
|
class="btn btn-default btn-block toggled"
|
||||||
@click="unmuteUser"
|
@click="unmuteUser"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.muted') }}
|
{{ $t('user_card.muted') }}
|
||||||
|
@ -286,6 +286,7 @@
|
||||||
mask-size: 100% 60%;
|
mask-size: 100% 60%;
|
||||||
border-top-left-radius: calc(var(--panelRadius) - 1px);
|
border-top-left-radius: calc(var(--panelRadius) - 1px);
|
||||||
border-top-right-radius: calc(var(--panelRadius) - 1px);
|
border-top-right-radius: calc(var(--panelRadius) - 1px);
|
||||||
|
background-color: var(--profileBg);
|
||||||
|
|
||||||
&.hide-bio {
|
&.hide-bio {
|
||||||
mask-size: 100% 40px;
|
mask-size: 100% 40px;
|
||||||
|
@ -299,6 +300,11 @@
|
||||||
&-bio {
|
&-bio {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--postLink, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -460,14 +466,13 @@
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO use proper colors
|
|
||||||
.staff {
|
.staff {
|
||||||
flex: none;
|
flex: none;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnText, $fallback--text);
|
color: var(--alertNeutralText, $fallback--text);
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--btn, $fallback--fg);
|
background-color: var(--alertNeutral, $fallback--fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -538,12 +543,6 @@
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 0;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
import BlockCard from '../block_card/block_card.vue'
|
import BlockCard from '../block_card/block_card.vue'
|
||||||
import MuteCard from '../mute_card/mute_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 SelectableList from '../selectable_list/selectable_list.vue'
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import EmojiInput from '../emoji_input/emoji_input.vue'
|
import EmojiInput from '../emoji_input/emoji_input.vue'
|
||||||
|
@ -32,6 +33,12 @@ const MuteList = withSubscription({
|
||||||
childPropName: 'items'
|
childPropName: 'items'
|
||||||
})(SelectableList)
|
})(SelectableList)
|
||||||
|
|
||||||
|
const DomainMuteList = withSubscription({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||||
|
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
||||||
|
childPropName: 'items'
|
||||||
|
})(SelectableList)
|
||||||
|
|
||||||
const UserSettings = {
|
const UserSettings = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -48,6 +55,7 @@ const UserSettings = {
|
||||||
showRole: this.$store.state.users.currentUser.show_role,
|
showRole: this.$store.state.users.currentUser.show_role,
|
||||||
role: this.$store.state.users.currentUser.role,
|
role: this.$store.state.users.currentUser.role,
|
||||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
discoverable: this.$store.state.users.currentUser.discoverable,
|
||||||
|
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||||
pickAvatarBtnVisible: true,
|
pickAvatarBtnVisible: true,
|
||||||
bannerUploading: false,
|
bannerUploading: false,
|
||||||
backgroundUploading: false,
|
backgroundUploading: false,
|
||||||
|
@ -67,7 +75,8 @@ const UserSettings = {
|
||||||
changedPassword: false,
|
changedPassword: false,
|
||||||
changePasswordError: false,
|
changePasswordError: false,
|
||||||
activeTab: 'profile',
|
activeTab: 'profile',
|
||||||
notificationSettings: this.$store.state.users.currentUser.notification_settings
|
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
||||||
|
newDomainToMute: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -80,10 +89,12 @@ const UserSettings = {
|
||||||
ImageCropper,
|
ImageCropper,
|
||||||
BlockList,
|
BlockList,
|
||||||
MuteList,
|
MuteList,
|
||||||
|
DomainMuteList,
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
Autosuggest,
|
Autosuggest,
|
||||||
BlockCard,
|
BlockCard,
|
||||||
MuteCard,
|
MuteCard,
|
||||||
|
DomainMuteCard,
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Importer,
|
Importer,
|
||||||
Exporter,
|
Exporter,
|
||||||
|
@ -152,6 +163,7 @@ const UserSettings = {
|
||||||
hide_follows: this.hideFollows,
|
hide_follows: this.hideFollows,
|
||||||
hide_followers: this.hideFollowers,
|
hide_followers: this.hideFollowers,
|
||||||
discoverable: this.discoverable,
|
discoverable: this.discoverable,
|
||||||
|
allow_following_move: this.allowFollowingMove,
|
||||||
hide_follows_count: this.hideFollowsCount,
|
hide_follows_count: this.hideFollowsCount,
|
||||||
hide_followers_count: this.hideFollowersCount,
|
hide_followers_count: this.hideFollowersCount,
|
||||||
show_role: this.showRole
|
show_role: this.showRole
|
||||||
|
@ -297,7 +309,7 @@ const UserSettings = {
|
||||||
newPassword: this.changePasswordInputs[1],
|
newPassword: this.changePasswordInputs[1],
|
||||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
newPasswordConfirmation: this.changePasswordInputs[2]
|
||||||
}
|
}
|
||||||
this.$store.state.api.backendInteractor.changePassword({ params })
|
this.$store.state.api.backendInteractor.changePassword(params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 'success') {
|
if (res.status === 'success') {
|
||||||
this.changedPassword = true
|
this.changedPassword = true
|
||||||
|
@ -314,7 +326,7 @@ const UserSettings = {
|
||||||
email: this.newEmail,
|
email: this.newEmail,
|
||||||
password: this.changeEmailPassword
|
password: this.changeEmailPassword
|
||||||
}
|
}
|
||||||
this.$store.state.api.backendInteractor.changeEmail({ params })
|
this.$store.state.api.backendInteractor.changeEmail(params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 'success') {
|
if (res.status === 'success') {
|
||||||
this.changedEmail = true
|
this.changedEmail = true
|
||||||
|
@ -365,6 +377,13 @@ const UserSettings = {
|
||||||
unmuteUsers (ids) {
|
unmuteUsers (ids) {
|
||||||
return this.$store.dispatch('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) {
|
identity (value) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,9 +90,7 @@
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<Checkbox
|
<Checkbox v-model="hideFollowers">
|
||||||
v-model="hideFollowers"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_followers_description') }}
|
{{ $t('settings.hide_followers_description') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</p>
|
</p>
|
||||||
|
@ -104,6 +102,11 @@
|
||||||
{{ $t('settings.hide_followers_count_description') }}
|
{{ $t('settings.hide_followers_count_description') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="allowFollowingMove">
|
||||||
|
{{ $t('settings.allow_following_move') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
<p v-if="role === 'admin' || role === 'moderator'">
|
<p v-if="role === 'admin' || role === 'moderator'">
|
||||||
<Checkbox v-model="showRole">
|
<Checkbox v-model="showRole">
|
||||||
<template v-if="role === 'admin'">
|
<template v-if="role === 'admin'">
|
||||||
|
@ -509,6 +512,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.mutes_tab')">
|
<div :label="$t('settings.mutes_tab')">
|
||||||
|
<tab-switcher>
|
||||||
|
<div label="Users">
|
||||||
<div class="profile-edit-usersearch-wrapper">
|
<div class="profile-edit-usersearch-wrapper">
|
||||||
<Autosuggest
|
<Autosuggest
|
||||||
:filter="filterUnMutedUsers"
|
:filter="filterUnMutedUsers"
|
||||||
|
@ -563,6 +568,59 @@
|
||||||
</template>
|
</template>
|
||||||
</MuteList>
|
</MuteList>
|
||||||
</div>
|
</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"
|
||||||
|
>
|
||||||
|
<ProgressButton
|
||||||
|
class="btn btn-default"
|
||||||
|
:click="muteDomain"
|
||||||
|
>
|
||||||
|
{{ $t('domain_mute_card.mute') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('domain_mute_card.mute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
</div>
|
||||||
|
<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>
|
</tab-switcher>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 {
|
.setting-subitem {
|
||||||
margin-left: 1.75em;
|
margin-left: 1.75em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,43 @@
|
||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"staff": "Staff",
|
"mrf": {
|
||||||
"federation": "Federation",
|
"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": "Enabled MRF Policies",
|
||||||
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
|
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
|
||||||
"mrf_policy_simple": "Instance-specific Policies",
|
"simple": {
|
||||||
"mrf_policy_simple_accept": "Accept",
|
"simple_policies": "Instance-specific Policies",
|
||||||
"mrf_policy_simple_accept_desc": "This instance only accepts messages from the following instances:",
|
"accept": "Accept",
|
||||||
"mrf_policy_simple_reject": "Reject",
|
"accept_desc": "This instance only accepts messages from the following instances:",
|
||||||
"mrf_policy_simple_reject_desc": "This instance will not accept messages from the following instances:",
|
"reject": "Reject",
|
||||||
"mrf_policy_simple_quarantine": "Quarantine",
|
"reject_desc": "This instance will not accept messages from the following instances:",
|
||||||
"mrf_policy_simple_quarantine_desc": "This instance will send only public posts to the following instances:",
|
"quarantine": "Quarantine",
|
||||||
"mrf_policy_simple_ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
|
"quarantine_desc": "This instance will send only public posts to the following instances:",
|
||||||
"mrf_policy_simple_ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
|
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
|
||||||
"mrf_policy_simple_media_removal": "Media Removal",
|
"ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
|
||||||
"mrf_policy_simple_media_removal_desc": "This instance removes media from posts on the following instances:",
|
"media_removal": "Media Removal",
|
||||||
"mrf_policy_simple_media_nsfw": "Media Force-set As Sensitive",
|
"media_removal_desc": "This instance removes media from posts on the following instances:",
|
||||||
"mrf_policy_simple_media_nsfw_desc": "This instance forces media to be set sensitive in 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": {
|
"chat": {
|
||||||
"title": "Chat"
|
"title": "Chat"
|
||||||
},
|
},
|
||||||
|
"domain_mute_card": {
|
||||||
|
"mute": "Mute",
|
||||||
|
"mute_progress": "Muting...",
|
||||||
|
"unmute": "Unmute",
|
||||||
|
"unmute_progress": "Unmuting..."
|
||||||
|
},
|
||||||
"exporter": {
|
"exporter": {
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"processing": "Processing, you'll soon be asked to download your file"
|
"processing": "Processing, you'll soon be asked to download your file"
|
||||||
|
@ -46,6 +63,7 @@
|
||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"show_more": "Show more",
|
"show_more": "Show more",
|
||||||
"show_less": "Show less",
|
"show_less": "Show less",
|
||||||
|
"dismiss": "Dismiss",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
|
@ -111,7 +129,8 @@
|
||||||
"read": "Read!",
|
"read": "Read!",
|
||||||
"repeated_you": "repeated your status",
|
"repeated_you": "repeated your status",
|
||||||
"no_more_notifications": "No more notifications",
|
"no_more_notifications": "No more notifications",
|
||||||
"migrated_to": "migrated to"
|
"migrated_to": "migrated to",
|
||||||
|
"reacted_with": "reacted with {0}"
|
||||||
},
|
},
|
||||||
"polls": {
|
"polls": {
|
||||||
"add_poll": "Add Poll",
|
"add_poll": "Add Poll",
|
||||||
|
@ -226,6 +245,7 @@
|
||||||
"desc": "To enable two-factor authentication, enter the code from your two-factor app:"
|
"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",
|
"attachmentRadius": "Attachments",
|
||||||
"attachments": "Attachments",
|
"attachments": "Attachments",
|
||||||
"autoload": "Enable automatic loading when scrolled to the bottom",
|
"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_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.",
|
"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",
|
"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.",
|
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
|
||||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||||
|
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
|
||||||
"export_theme": "Save preset",
|
"export_theme": "Save preset",
|
||||||
"filtering": "Filtering",
|
"filtering": "Filtering",
|
||||||
"filtering_explanation": "All statuses containing these words will be muted, one per line",
|
"filtering_explanation": "All statuses containing these words will be muted, one per line",
|
||||||
|
@ -274,6 +296,7 @@
|
||||||
"follow_import": "Follow import",
|
"follow_import": "Follow import",
|
||||||
"follow_import_error": "Error importing followers",
|
"follow_import_error": "Error importing followers",
|
||||||
"follows_imported": "Follows imported! Processing them will take a while.",
|
"follows_imported": "Follows imported! Processing them will take a while.",
|
||||||
|
"accent": "Accent",
|
||||||
"foreground": "Foreground",
|
"foreground": "Foreground",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"hide_attachments_in_convo": "Hide attachments in conversations",
|
"hide_attachments_in_convo": "Hide attachments in conversations",
|
||||||
|
@ -314,6 +337,7 @@
|
||||||
"notification_visibility_mentions": "Mentions",
|
"notification_visibility_mentions": "Mentions",
|
||||||
"notification_visibility_repeats": "Repeats",
|
"notification_visibility_repeats": "Repeats",
|
||||||
"notification_visibility_moves": "User Migrates",
|
"notification_visibility_moves": "User Migrates",
|
||||||
|
"notification_visibility_emoji_reactions": "Reactions",
|
||||||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||||
"no_blocks": "No blocks",
|
"no_blocks": "No blocks",
|
||||||
"no_mutes": "No mutes",
|
"no_mutes": "No mutes",
|
||||||
|
@ -361,6 +385,7 @@
|
||||||
"post_status_content_type": "Post status content type",
|
"post_status_content_type": "Post status content type",
|
||||||
"stop_gifs": "Play-on-hover GIFs",
|
"stop_gifs": "Play-on-hover GIFs",
|
||||||
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
|
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
|
||||||
|
"user_mutes": "Users",
|
||||||
"useStreamingApi": "Receive posts and notifications real-time",
|
"useStreamingApi": "Receive posts and notifications real-time",
|
||||||
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
|
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
|
||||||
"text": "Text",
|
"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_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.",
|
"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",
|
"tooltipRadius": "Tooltips/alerts",
|
||||||
|
"type_domains_to_mute": "Type in domains to mute",
|
||||||
"upload_a_photo": "Upload a photo",
|
"upload_a_photo": "Upload a photo",
|
||||||
"user_settings": "User Settings",
|
"user_settings": "User Settings",
|
||||||
"values": {
|
"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.",
|
"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",
|
"reset": "Reset",
|
||||||
"clear_all": "Clear all",
|
"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": {
|
"common": {
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
|
@ -425,14 +468,27 @@
|
||||||
"alert": "Alert background",
|
"alert": "Alert background",
|
||||||
"alert_error": "Error",
|
"alert_error": "Error",
|
||||||
"alert_warning": "Warning",
|
"alert_warning": "Warning",
|
||||||
|
"alert_neutral": "Neutral",
|
||||||
|
"post": "Posts/User bios",
|
||||||
"badge": "Badge background",
|
"badge": "Badge background",
|
||||||
|
"popover": "Tooltips, menus, popovers",
|
||||||
"badge_notification": "Notification",
|
"badge_notification": "Notification",
|
||||||
"panel_header": "Panel header",
|
"panel_header": "Panel header",
|
||||||
"top_bar": "Top bar",
|
"top_bar": "Top bar",
|
||||||
"borders": "Borders",
|
"borders": "Borders",
|
||||||
"buttons": "Buttons",
|
"buttons": "Buttons",
|
||||||
"inputs": "Input fields",
|
"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": {
|
"radii": {
|
||||||
"_tab_label": "Roundness"
|
"_tab_label": "Roundness"
|
||||||
|
@ -445,7 +501,7 @@
|
||||||
"blur": "Blur",
|
"blur": "Blur",
|
||||||
"spread": "Spread",
|
"spread": "Spread",
|
||||||
"inset": "Inset",
|
"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": {
|
"filter_hint": {
|
||||||
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
|
"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.",
|
"drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.",
|
||||||
|
@ -639,6 +695,7 @@
|
||||||
"repeat": "Repeat",
|
"repeat": "Repeat",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
|
"add_reaction": "Add Reaction",
|
||||||
"user_settings": "User Settings"
|
"user_settings": "User Settings"
|
||||||
},
|
},
|
||||||
"upload":{
|
"upload":{
|
||||||
|
|
|
@ -53,7 +53,8 @@
|
||||||
"notifications": "Ilmoitukset",
|
"notifications": "Ilmoitukset",
|
||||||
"read": "Lue!",
|
"read": "Lue!",
|
||||||
"repeated_you": "toisti viestisi",
|
"repeated_you": "toisti viestisi",
|
||||||
"no_more_notifications": "Ei enempää ilmoituksia"
|
"no_more_notifications": "Ei enempää ilmoituksia",
|
||||||
|
"reacted_with": "lisäsi reaktion {0}"
|
||||||
},
|
},
|
||||||
"polls": {
|
"polls": {
|
||||||
"add_poll": "Lisää äänestys",
|
"add_poll": "Lisää äänestys",
|
||||||
|
@ -140,6 +141,7 @@
|
||||||
"delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
|
"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_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
|
||||||
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
|
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
|
||||||
|
"emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
|
||||||
"export_theme": "Tallenna teema",
|
"export_theme": "Tallenna teema",
|
||||||
"filtering": "Suodatus",
|
"filtering": "Suodatus",
|
||||||
"filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.",
|
"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_likes": "Tykkäykset",
|
||||||
"notification_visibility_mentions": "Maininnat",
|
"notification_visibility_mentions": "Maininnat",
|
||||||
"notification_visibility_repeats": "Toistot",
|
"notification_visibility_repeats": "Toistot",
|
||||||
|
"notification_visibility_emoji_reactions": "Reaktiot",
|
||||||
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
|
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
|
||||||
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
|
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
|
||||||
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
|
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"staff": "スタッフ",
|
"mrf": {
|
||||||
"federation": "フェデレーション",
|
"federation": "フェデレーション",
|
||||||
"mrf_policies": "ゆうこうなMRFポリシー",
|
"mrf_policies": "ゆうこうなMRFポリシー",
|
||||||
"mrf_policies_desc": "MRFポリシーは、このインスタンスのフェデレーションのふるまいを、いじります。これらのMRFポリシーがゆうこうになっています:",
|
"mrf_policies_desc": "MRFポリシーは、このインスタンスのフェデレーションのふるまいを、いじります。これらのMRFポリシーがゆうこうになっています:",
|
||||||
"mrf_policy_simple": "インスタンスのポリシー",
|
"simple": {
|
||||||
"mrf_policy_simple_accept": "うけいれ",
|
"simple_policies": "インスタンスのポリシー",
|
||||||
"mrf_policy_simple_accept_desc": "このインスンスは、これらのインスタンスからのメッセージのみをうけいれます:",
|
"accept": "うけいれ",
|
||||||
"mrf_policy_simple_reject": "おことわり",
|
"accept_desc": "このインスンスは、これらのインスタンスからのメッセージのみをうけいれます:",
|
||||||
"mrf_policy_simple_reject_desc": "このインスタンスは、これらのインスタンスからのメッセージをうけいれません:",
|
"reject": "おことわり",
|
||||||
"mrf_policy_simple_quarantine": "けんえき",
|
"reject_desc": "このインスタンスは、これらのインスタンスからのメッセージをうけいれません:",
|
||||||
"mrf_policy_simple_quarantine_desc": "このインスタンスは、これらのインスタンスに、パブリックなとうこうのみを、おくります:",
|
"quarantine": "けんえき",
|
||||||
"mrf_policy_simple_ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
|
"quarantine_desc": "このインスタンスは、これらのインスタンスに、パブリックなとうこうのみを、おくります:",
|
||||||
"mrf_policy_simple_ftl_removal_desc": "このインスタンスは、つながっているすべてのネットワーク」タイムラインから、これらのインスタンスを、とりのぞきます:",
|
"ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
|
||||||
"mrf_policy_simple_media_removal": "メディアをのぞく",
|
"ftl_removal_desc": "このインスタンスは、つながっているすべてのネットワーク」タイムラインから、これらのインスタンスを、とりのぞきます:",
|
||||||
"mrf_policy_simple_media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
|
"media_removal": "メディアをのぞく",
|
||||||
"mrf_policy_simple_media_nsfw": "メディアをすべてセンシティブにする",
|
"media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
|
||||||
"mrf_policy_simple_media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
|
"media_nsfw": "メディアをすべてセンシティブにする",
|
||||||
|
"media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"staff": "スタッフ"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"title": "チャット"
|
"title": "チャット"
|
||||||
|
|
9
src/lib/event_target_polyfill.js
Normal file
9
src/lib/event_target_polyfill.js
Normal file
|
@ -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
|
||||||
|
}
|
11
src/main.js
11
src/main.js
|
@ -2,6 +2,9 @@ import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
|
import 'custom-event-polyfill'
|
||||||
|
import './lib/event_target_polyfill.js'
|
||||||
|
|
||||||
import interfaceModule from './modules/interface.js'
|
import interfaceModule from './modules/interface.js'
|
||||||
import instanceModule from './modules/instance.js'
|
import instanceModule from './modules/instance.js'
|
||||||
import statusesModule from './modules/statuses.js'
|
import statusesModule from './modules/statuses.js'
|
||||||
|
@ -28,7 +31,6 @@ import VueChatScroll from 'vue-chat-scroll'
|
||||||
import VueClickOutside from 'v-click-outside'
|
import VueClickOutside from 'v-click-outside'
|
||||||
import PortalVue from 'portal-vue'
|
import PortalVue from 'portal-vue'
|
||||||
import VBodyScrollLock from './directives/body_scroll_lock'
|
import VBodyScrollLock from './directives/body_scroll_lock'
|
||||||
import VTooltip from 'v-tooltip'
|
|
||||||
|
|
||||||
import afterStoreSetup from './boot/after_store.js'
|
import afterStoreSetup from './boot/after_store.js'
|
||||||
|
|
||||||
|
@ -41,13 +43,6 @@ Vue.use(VueChatScroll)
|
||||||
Vue.use(VueClickOutside)
|
Vue.use(VueClickOutside)
|
||||||
Vue.use(PortalVue)
|
Vue.use(PortalVue)
|
||||||
Vue.use(VBodyScrollLock)
|
Vue.use(VBodyScrollLock)
|
||||||
Vue.use(VTooltip, {
|
|
||||||
popover: {
|
|
||||||
defaultTrigger: 'hover click',
|
|
||||||
defaultContainer: false,
|
|
||||||
defaultOffset: 5
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const i18n = new VueI18n({
|
const i18n = new VueI18n({
|
||||||
// By default, use the browser locale, we will update it if neccessary
|
// By default, use the browser locale, we will update it if neccessary
|
||||||
|
|
|
@ -146,6 +146,7 @@ const api = {
|
||||||
startFetchingFollowRequests (store) {
|
startFetchingFollowRequests (store) {
|
||||||
if (store.state.fetchers['followRequests']) return
|
if (store.state.fetchers['followRequests']) return
|
||||||
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
|
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
|
||||||
|
|
||||||
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
|
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
|
||||||
},
|
},
|
||||||
stopFetchingFollowRequests (store) {
|
stopFetchingFollowRequests (store) {
|
||||||
|
|
|
@ -5,6 +5,9 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
colors: {},
|
colors: {},
|
||||||
|
theme: undefined,
|
||||||
|
customTheme: undefined,
|
||||||
|
customThemeSource: undefined,
|
||||||
hideISP: false,
|
hideISP: false,
|
||||||
// bad name: actually hides posts of muted USERS
|
// bad name: actually hides posts of muted USERS
|
||||||
hideMutedPosts: undefined, // instance default
|
hideMutedPosts: undefined, // instance default
|
||||||
|
@ -20,6 +23,7 @@ export const defaultState = {
|
||||||
autoLoad: true,
|
autoLoad: true,
|
||||||
streaming: false,
|
streaming: false,
|
||||||
hoverPreview: true,
|
hoverPreview: true,
|
||||||
|
emojiReactionsOnTimeline: true,
|
||||||
autohideFloatingPostButton: false,
|
autohideFloatingPostButton: false,
|
||||||
pauseOnUnfocused: true,
|
pauseOnUnfocused: true,
|
||||||
stopGifs: false,
|
stopGifs: false,
|
||||||
|
@ -29,7 +33,8 @@ export const defaultState = {
|
||||||
mentions: true,
|
mentions: true,
|
||||||
likes: true,
|
likes: true,
|
||||||
repeats: true,
|
repeats: true,
|
||||||
moves: true
|
moves: true,
|
||||||
|
emojiReactions: false
|
||||||
},
|
},
|
||||||
webPushNotifications: false,
|
webPushNotifications: false,
|
||||||
muteWords: [],
|
muteWords: [],
|
||||||
|
@ -94,10 +99,10 @@ const config = {
|
||||||
commit('setOption', { name, value })
|
commit('setOption', { name, value })
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'theme':
|
case 'theme':
|
||||||
setPreset(value, commit)
|
setPreset(value)
|
||||||
break
|
break
|
||||||
case 'customTheme':
|
case 'customTheme':
|
||||||
applyTheme(value, commit)
|
applyTheme(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { set } from 'vue'
|
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'
|
import { instanceDefaultProperties } from './config.js'
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
@ -10,6 +11,7 @@ const defaultState = {
|
||||||
textlimit: 5000,
|
textlimit: 5000,
|
||||||
server: 'http://localhost:4040/',
|
server: 'http://localhost:4040/',
|
||||||
theme: 'pleroma-dark',
|
theme: 'pleroma-dark',
|
||||||
|
themeData: undefined,
|
||||||
background: '/static/aurora_borealis.jpg',
|
background: '/static/aurora_borealis.jpg',
|
||||||
logo: '/static/logo.png',
|
logo: '/static/logo.png',
|
||||||
logoMask: true,
|
logoMask: true,
|
||||||
|
@ -96,6 +98,9 @@ const instance = {
|
||||||
dispatch('initializeSocket')
|
dispatch('initializeSocket')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case 'theme':
|
||||||
|
dispatch('setTheme', value)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getStaticEmoji ({ commit }) {
|
async getStaticEmoji ({ commit }) {
|
||||||
|
@ -147,9 +152,23 @@ const instance = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setTheme ({ commit }, themeName) {
|
setTheme ({ commit, rootState }, themeName) {
|
||||||
commit('setInstanceOption', { name: 'theme', value: 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 }) {
|
fetchEmoji ({ dispatch, state }) {
|
||||||
if (!state.customEmojiFetched) {
|
if (!state.customEmojiFetched) {
|
||||||
|
|
|
@ -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 { set } from 'vue'
|
||||||
import apiService from '../services/api/api.service.js'
|
import apiService from '../services/api/api.service.js'
|
||||||
// import parse from '../services/status_parser/status_parser.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.mentions && 'mention',
|
||||||
rootState.config.notificationVisibility.repeats && 'repeat',
|
rootState.config.notificationVisibility.repeats && 'repeat',
|
||||||
rootState.config.notificationVisibility.follows && 'follow',
|
rootState.config.notificationVisibility.follows && 'follow',
|
||||||
rootState.config.notificationVisibility.moves && 'move'
|
rootState.config.notificationVisibility.moves && 'move',
|
||||||
|
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
|
||||||
].filter(_ => _)
|
].filter(_ => _)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,6 +326,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
||||||
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
|
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
|
// Only add a new notification if we don't have one for the same action
|
||||||
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
|
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
|
||||||
state.notifications.maxId = notification.id > state.notifications.maxId
|
state.notifications.maxId = notification.id > state.notifications.maxId
|
||||||
|
@ -345,7 +363,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
||||||
break
|
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)
|
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
|
||||||
} else {
|
} else {
|
||||||
notifObj.body = notification.status.text
|
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)) {
|
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
|
// Chrome is known for not closing notifications automatically
|
||||||
// according to MDN, anyway.
|
// according to MDN, anyway.
|
||||||
setTimeout(notification.close.bind(notification), 5000)
|
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (notification.seen) {
|
} else if (notification.seen) {
|
||||||
|
@ -518,6 +538,53 @@ export const mutations = {
|
||||||
newStatus.fave_num = newStatus.favoritedBy.length
|
newStatus.fave_num = newStatus.favoritedBy.length
|
||||||
newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
|
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 }) {
|
updateStatusWithPoll (state, { id, poll }) {
|
||||||
const status = state.allStatusesObject[id]
|
const status = state.allStatusesObject[id]
|
||||||
status.poll = poll
|
status.poll = poll
|
||||||
|
@ -622,6 +689,35 @@ const statuses = {
|
||||||
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
|
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) {
|
fetchFavs ({ rootState, commit }, id) {
|
||||||
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
|
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
|
||||||
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
|
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
|
||||||
|
|
|
@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
|
||||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
.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 = {
|
export const mutations = {
|
||||||
setMuted (state, { user: { id }, muted }) {
|
setMuted (state, { user: { id }, muted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
|
@ -177,6 +187,20 @@ export const mutations = {
|
||||||
state.currentUser.muteIds.push(muteId)
|
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) {
|
setPinnedToUser (state, status) {
|
||||||
const user = state.usersObject[status.user.id]
|
const user = state.usersObject[status.user.id]
|
||||||
const index = user.pinnedStatusIds.indexOf(status.id)
|
const index = user.pinnedStatusIds.indexOf(status.id)
|
||||||
|
@ -297,6 +321,25 @@ const users = {
|
||||||
unmuteUsers (store, ids = []) {
|
unmuteUsers (store, ids = []) {
|
||||||
return Promise.all(ids.map(id => unmuteUser(store, id)))
|
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) {
|
fetchFriends ({ rootState, commit }, id) {
|
||||||
const user = rootState.users.usersObject[id]
|
const user = rootState.users.usersObject[id]
|
||||||
const maxId = last(user.friendIds)
|
const maxId = last(user.friendIds)
|
||||||
|
@ -331,9 +374,9 @@ const users = {
|
||||||
return rootState.api.backendInteractor.unsubscribeUser({ id })
|
return rootState.api.backendInteractor.unsubscribeUser({ id })
|
||||||
.then((relationship) => commit('updateUserRelationship', [relationship]))
|
.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
|
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
|
||||||
api(user)
|
api({ user })
|
||||||
.then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
|
.then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
|
||||||
},
|
},
|
||||||
registerPushNotifications (store) {
|
registerPushNotifications (store) {
|
||||||
|
@ -460,6 +503,7 @@ const users = {
|
||||||
user.credentials = accessToken
|
user.credentials = accessToken
|
||||||
user.blockIds = []
|
user.blockIds = []
|
||||||
user.muteIds = []
|
user.muteIds = []
|
||||||
|
user.domainMutes = []
|
||||||
commit('setCurrentUser', user)
|
commit('setCurrentUser', user)
|
||||||
commit('addNewUsers', [user])
|
commit('addNewUsers', [user])
|
||||||
|
|
||||||
|
|
|
@ -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_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
|
||||||
const MASTODON_SEARCH_2 = `/api/v2/search`
|
const MASTODON_SEARCH_2 = `/api/v2/search`
|
||||||
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/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 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
|
const oldfetch = window.fetch
|
||||||
|
|
||||||
|
@ -398,8 +402,8 @@ const fetchStatus = ({ id, credentials }) => {
|
||||||
.then((data) => parseStatus(data))
|
.then((data) => parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagUser = ({ tag, credentials, ...options }) => {
|
const tagUser = ({ tag, credentials, user }) => {
|
||||||
const screenName = options.screen_name
|
const screenName = user.screen_name
|
||||||
const form = {
|
const form = {
|
||||||
nicknames: [screenName],
|
nicknames: [screenName],
|
||||||
tags: [tag]
|
tags: [tag]
|
||||||
|
@ -415,8 +419,8 @@ const tagUser = ({ tag, credentials, ...options }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const untagUser = ({ tag, credentials, ...options }) => {
|
const untagUser = ({ tag, credentials, user }) => {
|
||||||
const screenName = options.screen_name
|
const screenName = user.screen_name
|
||||||
const body = {
|
const body = {
|
||||||
nicknames: [screenName],
|
nicknames: [screenName],
|
||||||
tags: [tag]
|
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
|
const screenName = user.screen_name
|
||||||
|
|
||||||
return fetch(PERMISSION_GROUP_URL(screenName, right), {
|
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
|
const screenName = user.screen_name
|
||||||
|
|
||||||
return fetch(PERMISSION_GROUP_URL(screenName, right), {
|
return fetch(PERMISSION_GROUP_URL(screenName, right), {
|
||||||
|
@ -474,7 +478,7 @@ const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
|
||||||
}).then(response => get(response, 'users.0'))
|
}).then(response => get(response, 'users.0'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteUser = ({ credentials, ...user }) => {
|
const deleteUser = ({ credentials, user }) => {
|
||||||
const screenName = user.screen_name
|
const screenName = user.screen_name
|
||||||
const headers = authHeaders(credentials)
|
const headers = authHeaders(credentials)
|
||||||
|
|
||||||
|
@ -491,7 +495,8 @@ const fetchTimeline = ({
|
||||||
until = false,
|
until = false,
|
||||||
userId = false,
|
userId = false,
|
||||||
tag = false,
|
tag = false,
|
||||||
withMuted = false
|
withMuted = false,
|
||||||
|
withMove = false
|
||||||
}) => {
|
}) => {
|
||||||
const timelineUrls = {
|
const timelineUrls = {
|
||||||
public: MASTODON_PUBLIC_TIMELINE,
|
public: MASTODON_PUBLIC_TIMELINE,
|
||||||
|
@ -531,6 +536,9 @@ const fetchTimeline = ({
|
||||||
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
||||||
params.push(['only_media', false])
|
params.push(['only_media', false])
|
||||||
}
|
}
|
||||||
|
if (timeline === 'notifications') {
|
||||||
|
params.push(['with_move', withMove])
|
||||||
|
}
|
||||||
|
|
||||||
params.push(['count', 20])
|
params.push(['count', 20])
|
||||||
params.push(['with_muted', withMuted])
|
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))
|
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 }) => {
|
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: MASTODON_REPORT_USER_URL,
|
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 = {} }) => {
|
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
|
||||||
return Object.entries({
|
return Object.entries({
|
||||||
...(credentials
|
...(credentials
|
||||||
|
@ -1107,10 +1161,16 @@ const apiService = {
|
||||||
fetchPoll,
|
fetchPoll,
|
||||||
fetchFavoritedByUsers,
|
fetchFavoritedByUsers,
|
||||||
fetchRebloggedByUsers,
|
fetchRebloggedByUsers,
|
||||||
|
fetchEmojiReactions,
|
||||||
|
reactWithEmoji,
|
||||||
|
unreactWithEmoji,
|
||||||
reportUser,
|
reportUser,
|
||||||
updateNotificationSettings,
|
updateNotificationSettings,
|
||||||
search2,
|
search2,
|
||||||
searchUsers
|
searchUsers,
|
||||||
|
fetchDomainMutes,
|
||||||
|
muteDomain,
|
||||||
|
unmuteDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -16,7 +16,7 @@ const backendInteractorService = credentials => ({
|
||||||
return notificationsFetcher.fetchAndUpdate({ store, credentials })
|
return notificationsFetcher.fetchAndUpdate({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
startFetchingFollowRequest ({ store }) {
|
startFetchingFollowRequests ({ store }) {
|
||||||
return followRequestFetcher.startFetching({ store, credentials })
|
return followRequestFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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') {
|
if (r === null || typeof r === 'undefined') {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if (r[0] === '#') {
|
// TODO: clean up this mess
|
||||||
|
if (r[0] === '#' || r === 'transparent') {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
if (typeof r === 'object') {
|
if (typeof r === 'object') {
|
||||||
({ r, g, b } = r)
|
({ 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 = Math.ceil(val)
|
||||||
val = val < 0 ? 0 : val
|
val = val < 0 ? 0 : val
|
||||||
val = val > 255 ? 255 : val
|
val = val > 255 ? 255 : val
|
||||||
|
@ -58,7 +69,7 @@ const srgbToLinear = (srgb) => {
|
||||||
* @param {Object} srgb - sRGB color
|
* @param {Object} srgb - sRGB color
|
||||||
* @returns {Number} relative luminance
|
* @returns {Number} relative luminance
|
||||||
*/
|
*/
|
||||||
const relativeLuminance = (srgb) => {
|
export const relativeLuminance = (srgb) => {
|
||||||
const { r, g, b } = srgbToLinear(srgb)
|
const { r, g, b } = srgbToLinear(srgb)
|
||||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
}
|
}
|
||||||
|
@ -71,7 +82,7 @@ const relativeLuminance = (srgb) => {
|
||||||
* @param {Object} b - sRGB color
|
* @param {Object} b - sRGB color
|
||||||
* @returns {Number} color ratio
|
* @returns {Number} color ratio
|
||||||
*/
|
*/
|
||||||
const getContrastRatio = (a, b) => {
|
export const getContrastRatio = (a, b) => {
|
||||||
const la = relativeLuminance(a)
|
const la = relativeLuminance(a)
|
||||||
const lb = relativeLuminance(b)
|
const lb = relativeLuminance(b)
|
||||||
const [l1, l2] = la > lb ? [la, lb] : [lb, la]
|
const [l1, l2] = la > lb ? [la, lb] : [lb, la]
|
||||||
|
@ -79,6 +90,17 @@ const getContrastRatio = (a, b) => {
|
||||||
return (l1 + 0.05) / (l2 + 0.05)
|
return (l1 + 0.05) / (l2 + 0.05)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* This performs alpha blending between solid background and semi-transparent foreground
|
||||||
*
|
*
|
||||||
|
@ -87,7 +109,7 @@ const getContrastRatio = (a, b) => {
|
||||||
* @param {Object} bg - bottom layer color
|
* @param {Object} bg - bottom layer color
|
||||||
* @returns {Object} sRGB of resulting 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
|
if (fga === 1 || typeof fga === 'undefined') return fg
|
||||||
return 'rgb'.split('').reduce((acc, c) => {
|
return 'rgb'.split('').reduce((acc, c) => {
|
||||||
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
|
// 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) => {
|
return 'rgb'.split('').reduce((acc, c) => {
|
||||||
acc[c] = 255 - rgb[c]
|
acc[c] = 255 - rgb[c]
|
||||||
return acc
|
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)
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
return result ? {
|
return result ? {
|
||||||
r: parseInt(result[1], 16),
|
r: parseInt(result[1], 16),
|
||||||
|
@ -113,18 +151,72 @@ const hex2rgb = (hex) => {
|
||||||
} : null
|
} : 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
|
acc[k] = (a[k] + b[k]) / 2
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
export {
|
* Converts rgb object into a CSS rgba() color
|
||||||
rgb2hex,
|
*
|
||||||
hex2rgb,
|
* @param {Object} color - rgb
|
||||||
mixrgb,
|
* @returns {String} CSS rgba() color
|
||||||
invert,
|
*/
|
||||||
getContrastRatio,
|
export const rgba2css = function (rgba) {
|
||||||
alphaBlend
|
return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import escape from 'escape-html'
|
||||||
|
|
||||||
const qvitterStatusType = (status) => {
|
const qvitterStatusType = (status) => {
|
||||||
if (status.is_post_verb) {
|
if (status.is_post_verb) {
|
||||||
return 'status'
|
return 'status'
|
||||||
|
@ -41,7 +43,7 @@ export const parseUser = (data) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
output.name = data.display_name
|
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 = data.note
|
||||||
output.description_html = addEmojis(data.note, data.emojis)
|
output.description_html = addEmojis(data.note, data.emojis)
|
||||||
|
@ -81,6 +83,8 @@ export const parseUser = (data) => {
|
||||||
output.subscribed = relationship.subscribing
|
output.subscribed = relationship.subscribing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output.allow_following_move = data.pleroma.allow_following_move
|
||||||
|
|
||||||
output.hide_follows = data.pleroma.hide_follows
|
output.hide_follows = data.pleroma.hide_follows
|
||||||
output.hide_followers = data.pleroma.hide_followers
|
output.hide_followers = data.pleroma.hide_followers
|
||||||
output.hide_follows_count = data.pleroma.hide_follows_count
|
output.hide_follows_count = data.pleroma.hide_follows_count
|
||||||
|
@ -242,6 +246,7 @@ export const parseStatus = (data) => {
|
||||||
output.is_local = pleroma.local
|
output.is_local = pleroma.local
|
||||||
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
|
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
|
||||||
output.thread_muted = pleroma.thread_muted
|
output.thread_muted = pleroma.thread_muted
|
||||||
|
output.emoji_reactions = pleroma.emoji_reactions
|
||||||
} else {
|
} else {
|
||||||
output.text = data.content
|
output.text = data.content
|
||||||
output.summary = data.spoiler_text
|
output.summary = data.spoiler_text
|
||||||
|
@ -255,7 +260,7 @@ export const parseStatus = (data) => {
|
||||||
output.retweeted_status = parseStatus(data.reblog)
|
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.external_url = data.url
|
||||||
output.poll = data.poll
|
output.poll = data.poll
|
||||||
output.pinned = data.pinned
|
output.pinned = data.pinned
|
||||||
|
@ -349,6 +354,7 @@ export const parseNotification = (data) => {
|
||||||
? null
|
? null
|
||||||
: parseUser(data.target)
|
: parseUser(data.target)
|
||||||
output.from_profile = parseUser(data.account)
|
output.from_profile = parseUser(data.account)
|
||||||
|
output.emoji = data.emoji
|
||||||
} else {
|
} else {
|
||||||
const parsedNotice = parseStatus(data.notice)
|
const parsedNotice = parseStatus(data.notice)
|
||||||
output.type = data.ntype
|
output.type = data.ntype
|
||||||
|
|
|
@ -7,7 +7,8 @@ export const visibleTypes = store => ([
|
||||||
store.state.config.notificationVisibility.mentions && 'mention',
|
store.state.config.notificationVisibility.mentions && 'mention',
|
||||||
store.state.config.notificationVisibility.repeats && 'repeat',
|
store.state.config.notificationVisibility.repeats && 'repeat',
|
||||||
store.state.config.notificationVisibility.follows && 'follow',
|
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(_ => _))
|
].filter(_ => _))
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
const sortById = (a, b) => {
|
||||||
|
|
|
@ -11,9 +11,12 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
||||||
const rootState = store.rootState || store.state
|
const rootState = store.rootState || store.state
|
||||||
const timelineData = rootState.statuses.notifications
|
const timelineData = rootState.statuses.notifications
|
||||||
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
|
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
|
||||||
|
const allowFollowingMove = rootState.users.currentUser.allow_following_move
|
||||||
|
|
||||||
args['withMuted'] = !hideMutedPosts
|
args['withMuted'] = !hideMutedPosts
|
||||||
|
|
||||||
|
args['withMove'] = !allowFollowingMove
|
||||||
|
|
||||||
args['timeline'] = 'notifications'
|
args['timeline'] = 'notifications'
|
||||||
if (older) {
|
if (older) {
|
||||||
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
|
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
|
||||||
|
|
|
@ -1,78 +1,9 @@
|
||||||
import { times } from 'lodash'
|
import { convert } from 'chromatism'
|
||||||
import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
|
import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js'
|
||||||
import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } 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
|
export const applyTheme = (input) => {
|
||||||
// styles that aren't just colors, so user can pick from a few different distinct
|
const { rules } = generatePreset(input)
|
||||||
// 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)
|
|
||||||
const head = document.head
|
const head = document.head
|
||||||
const body = document.body
|
const body = document.body
|
||||||
body.classList.add('hidden')
|
body.classList.add('hidden')
|
||||||
|
@ -87,14 +18,9 @@ const applyTheme = (input, commit) => {
|
||||||
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
|
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
|
||||||
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
|
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
|
||||||
body.classList.remove('hidden')
|
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) {
|
if (input.length === 0) {
|
||||||
return 'none'
|
return 'none'
|
||||||
}
|
}
|
||||||
|
@ -132,122 +58,18 @@ const getCssShadowFilter = (input) => {
|
||||||
.join(' ')
|
.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCssColor = (input, a) => {
|
export const generateColors = (themeData) => {
|
||||||
let rgb = {}
|
const sourceColors = !themeData.themeEngineVersion
|
||||||
if (typeof input === 'object') {
|
? colors2to3(themeData.colors || themeData)
|
||||||
rgb = input
|
: themeData.colors || themeData
|
||||||
} 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, opacity } = getColors(sourceColors, themeData.opacity || {})
|
||||||
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')
|
|
||||||
|
|
||||||
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 htmlColors = Object.entries(colors)
|
const htmlColors = Object.entries(colors)
|
||||||
.reduce((acc, [k, v]) => {
|
.reduce((acc, [k, v]) => {
|
||||||
if (!v) return acc
|
if (!v) return acc
|
||||||
acc.solid[k] = rgb2hex(v)
|
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
|
return acc
|
||||||
}, { complete: {}, solid: {} })
|
}, { complete: {}, solid: {} })
|
||||||
return {
|
return {
|
||||||
|
@ -264,7 +86,7 @@ const generateColors = (input) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateRadii = (input) => {
|
export const generateRadii = (input) => {
|
||||||
let inputRadii = input.radii || {}
|
let inputRadii = input.radii || {}
|
||||||
// v1 -> v2
|
// v1 -> v2
|
||||||
if (typeof input.btnRadius !== 'undefined') {
|
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]) => {
|
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] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||||
acc[k] = v
|
acc[k] = v
|
||||||
|
@ -332,8 +154,7 @@ const generateFonts = (input) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateShadows = (input) => {
|
const border = (top, shadow) => ({
|
||||||
const border = (top, shadow) => ({
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: top ? 1 : -1,
|
y: top ? 1 : -1,
|
||||||
blur: 0,
|
blur: 0,
|
||||||
|
@ -341,19 +162,19 @@ const generateShadows = (input) => {
|
||||||
color: shadow ? '#000000' : '#FFFFFF',
|
color: shadow ? '#000000' : '#FFFFFF',
|
||||||
alpha: 0.2,
|
alpha: 0.2,
|
||||||
inset: true
|
inset: true
|
||||||
})
|
})
|
||||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||||
const hoverGlow = {
|
const hoverGlow = {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
blur: 4,
|
blur: 4,
|
||||||
spread: 0,
|
spread: 0,
|
||||||
color: '--faint',
|
color: '--faint',
|
||||||
alpha: 1
|
alpha: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const shadows = {
|
export const DEFAULT_SHADOWS = {
|
||||||
panel: [{
|
panel: [{
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 1,
|
y: 1,
|
||||||
|
@ -406,15 +227,50 @@ const generateShadows = (input) => {
|
||||||
spread: 0,
|
spread: 0,
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
alpha: 1
|
alpha: 1
|
||||||
}],
|
}]
|
||||||
...(input.shadows || {})
|
}
|
||||||
|
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 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 {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
shadows: Object
|
shadows: Object
|
||||||
.entries(shadows)
|
.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
|
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
||||||
.map(([k, v]) => [
|
.map(([k, v]) => [
|
||||||
`--${k}Shadow: ${getCssShadow(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 {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
...shadows.rules,
|
...shadows.rules,
|
||||||
|
@ -446,98 +302,110 @@ const composePreset = (colors, radii, shadows, fonts) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatePreset = (input) => {
|
export const generatePreset = (input) => {
|
||||||
const shadows = generateShadows(input)
|
|
||||||
const colors = generateColors(input)
|
const colors = generateColors(input)
|
||||||
const radii = generateRadii(input)
|
return composePreset(
|
||||||
const fonts = generateFonts(input)
|
colors,
|
||||||
|
generateRadii(input),
|
||||||
return composePreset(colors, radii, shadows, fonts)
|
generateShadows(input, colors.theme.colors, colors.mod),
|
||||||
|
generateFonts(input)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getThemes = () => {
|
export const getThemes = () => {
|
||||||
return window.fetch('/static/styles.json')
|
const cache = 'no-store'
|
||||||
|
|
||||||
|
return window.fetch('/static/styles.json', { cache })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((themes) => {
|
.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') {
|
if (typeof v === 'object') {
|
||||||
return Promise.resolve([k, v])
|
promise = Promise.resolve(v)
|
||||||
} else if (typeof v === 'string') {
|
} else if (typeof v === 'string') {
|
||||||
return window.fetch(v)
|
promise = window.fetch(v, { cache })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((theme) => {
|
|
||||||
return [k, theme]
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
return []
|
return null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}))
|
return [k, promise]
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.then((promises) => {
|
.then((promises) => {
|
||||||
return promises
|
return promises
|
||||||
.filter(([k, v]) => v)
|
|
||||||
.reduce((acc, [k, v]) => {
|
.reduce((acc, [k, v]) => {
|
||||||
acc[k] = v
|
acc[k] = v
|
||||||
return acc
|
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) => {
|
* This handles compatibility issues when importing v2 theme's shadows to current format
|
||||||
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
*
|
||||||
|
* 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 }
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPreset = (val) => {
|
||||||
|
return getThemes()
|
||||||
|
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
|
||||||
|
.then((theme) => {
|
||||||
const isV1 = Array.isArray(theme)
|
const isV1 = Array.isArray(theme)
|
||||||
const data = isV1 ? {} : theme.theme
|
const data = isV1 ? {} : theme.theme
|
||||||
|
|
||||||
if (isV1) {
|
if (isV1) {
|
||||||
const bgRgb = hex2rgb(theme[1])
|
const bg = hex2rgb(theme[1])
|
||||||
const fgRgb = hex2rgb(theme[2])
|
const fg = hex2rgb(theme[2])
|
||||||
const textRgb = hex2rgb(theme[3])
|
const text = hex2rgb(theme[3])
|
||||||
const linkRgb = hex2rgb(theme[4])
|
const link = hex2rgb(theme[4])
|
||||||
|
|
||||||
const cRedRgb = hex2rgb(theme[5] || '#FF0000')
|
const cRed = hex2rgb(theme[5] || '#FF0000')
|
||||||
const cGreenRgb = hex2rgb(theme[6] || '#00FF00')
|
const cGreen = hex2rgb(theme[6] || '#00FF00')
|
||||||
const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
|
const cBlue = hex2rgb(theme[7] || '#0000FF')
|
||||||
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
|
const cOrange = hex2rgb(theme[8] || '#E3FF00')
|
||||||
|
|
||||||
data.colors = {
|
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
|
||||||
bg: bgRgb,
|
|
||||||
fg: fgRgb,
|
|
||||||
text: textRgb,
|
|
||||||
link: linkRgb,
|
|
||||||
cRed: cRedRgb,
|
|
||||||
cBlue: cBlueRgb,
|
|
||||||
cGreen: cGreenRgb,
|
|
||||||
cOrange: cOrangeRgb
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a hack, this function is only called during initial load.
|
return { theme: data, source: theme.source }
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
|
||||||
setStyle,
|
|
||||||
setPreset,
|
|
||||||
applyTheme,
|
|
||||||
getTextColor,
|
|
||||||
generateColors,
|
|
||||||
generateRadii,
|
|
||||||
generateShadows,
|
|
||||||
generateFonts,
|
|
||||||
generatePreset,
|
|
||||||
getThemes,
|
|
||||||
composePreset,
|
|
||||||
getCssShadow,
|
|
||||||
getCssShadowFilter
|
|
||||||
}
|
|
||||||
|
|
631
src/services/theme_data/pleromafe.js
Normal file
631
src/services/theme_data/pleromafe.js
Normal file
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
374
src/services/theme_data/theme_data.service.js
Normal file
374
src/services/theme_data/theme_data.service.js
Normal file
|
@ -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: {} })
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
|
@ -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; }
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue