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/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0] - 2020-02-28
|
||||
### Added
|
||||
- Tons of color slots including ones for hover/pressed/toggled buttons
|
||||
- Experimental `--variable[,mod]` syntax support for color slots in themes. the `mod` makes color brighter/darker depending on background color (makes darker color brighter/darker depending on background color)
|
||||
- Paper theme by Shpuld
|
||||
- Icons in nav panel
|
||||
- Private mode support
|
||||
- Support for 'Move' type notifications
|
||||
- Pleroma AMOLED dark theme
|
||||
- User level domain mutes, under User Settings -> Mutes
|
||||
- Emoji reactions for statuses
|
||||
- MRF keyword policy disclosure
|
||||
### Changed
|
||||
- Updated Pleroma default themes
|
||||
- theme engine update to 3 (themes v2.1 introduction)
|
||||
- massive internal changes in theme engine - slowly away from "generate things separately with spaghetti code" towards "feed all data into single 'generateTheme' function and declare slot inheritance and all in a separate file"
|
||||
- Breezy theme updates to make it closer to actual Breeze in some aspects
|
||||
- when using `--variable` in shadows it no longer uses the actual CSS3 variable, instead it generates color from other slots
|
||||
- theme doesn't get saved to local storage when opening FE anonymously
|
||||
- Captcha now resets on failed registrations
|
||||
- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
|
||||
- 403 messaging
|
||||
### Fixed
|
||||
- anon viewers won't get theme data saved to local storage, so admin changing default theme will have an effect for users coming back to instance.
|
||||
- Single notifications left unread when hitting read on another device/tab
|
||||
- Registration fixed
|
||||
- Deactivation of remote accounts from frontend
|
||||
- Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying
|
||||
- Improved performance of anything that uses popovers (most notably statuses)
|
||||
|
||||
## [1.1.7 and earlier] - 2019-12-14
|
||||
### Added
|
||||
|
|
|
@ -35,6 +35,7 @@ module.exports = {
|
|||
],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.runtime.common',
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components')
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"chromatism": "^3.0.0",
|
||||
"cropperjs": "^1.4.3",
|
||||
"diff": "^3.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"localforage": "^1.5.0",
|
||||
"object-path": "^0.11.3",
|
||||
|
@ -28,7 +29,6 @@
|
|||
"portal-vue": "^2.1.4",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
|
@ -43,6 +43,7 @@
|
|||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@ungap/event-target": "^0.1.0",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
||||
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
|
||||
"@vue/test-utils": "^1.0.0-beta.26",
|
||||
|
@ -56,6 +57,7 @@
|
|||
"connect-history-api-fallback": "^1.1.0",
|
||||
"cross-spawn": "^4.0.2",
|
||||
"css-loader": "^0.28.0",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
|
|
118
src/App.scss
118
src/App.scss
|
@ -31,9 +31,12 @@ h4 {
|
|||
margin: auto;
|
||||
min-height: 100vh;
|
||||
max-width: 980px;
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
align-content: flex-start;
|
||||
}
|
||||
.underlay {
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
background-color: var(--underlay, rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
|
@ -75,7 +78,7 @@ button {
|
|||
border-radius: $fallback--btnRadius;
|
||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
box-shadow: $fallback--buttonShadow;
|
||||
box-shadow: var(--buttonShadow);
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
|
@ -98,18 +101,39 @@ button {
|
|||
&:active {
|
||||
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressed, $fallback--fg);
|
||||
i {
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnDisabled, $fallback--fg);
|
||||
i {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg)
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
i {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
@ -121,12 +145,15 @@ button {
|
|||
}
|
||||
}
|
||||
|
||||
label.select {
|
||||
padding: 0;
|
||||
input, textarea, .select, .input {
|
||||
|
||||
}
|
||||
&.unstyled {
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
input, textarea, .select {
|
||||
border: none;
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
|
@ -140,13 +167,17 @@ input, textarea, .select {
|
|||
font-family: var(--inputFont, sans-serif);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 8px .5em;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 28px;
|
||||
line-height: 16px;
|
||||
hyphens: none;
|
||||
padding: 8px .5em;
|
||||
|
||||
&.select {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:disabled, &[disabled=disabled] {
|
||||
cursor: not-allowed;
|
||||
|
@ -160,7 +191,7 @@ input, textarea, .select {
|
|||
right: 5px;
|
||||
height: 100%;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
line-height: 28px;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
|
@ -198,7 +229,7 @@ input, textarea, .select {
|
|||
&:checked + label::before {
|
||||
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
|
||||
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
|
||||
background-color: var(--link, $fallback--link);
|
||||
background-color: var(--accent, $fallback--link);
|
||||
}
|
||||
&:disabled {
|
||||
&,
|
||||
|
@ -235,7 +266,7 @@ input, textarea, .select {
|
|||
display: none;
|
||||
&:checked + label::before {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
&:disabled {
|
||||
&,
|
||||
|
@ -353,6 +384,33 @@ i[class*=icon-] {
|
|||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
|
||||
button {
|
||||
&, i[class*=icon-] {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressedTopBar, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
@ -487,6 +545,10 @@ main-router {
|
|||
color: $fallback--faint;
|
||||
color: var(--panelFaint, $fallback--faint);
|
||||
}
|
||||
.faint-link {
|
||||
color: $fallback--faint;
|
||||
color: var(--faintLink, $fallback--faint);
|
||||
}
|
||||
|
||||
.alert {
|
||||
white-space: nowrap;
|
||||
|
@ -509,6 +571,30 @@ main-router {
|
|||
align-self: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
&, i[class*=icon-] {
|
||||
color: $fallback--text;
|
||||
color: var(--btnPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressedPanel, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledPanelText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--panelLink, $fallback--link)
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
</nav>
|
||||
<div
|
||||
id="content"
|
||||
class="container"
|
||||
class="container underlay"
|
||||
>
|
||||
<div class="sidebar-flexer mobile-hidden">
|
||||
<div class="sidebar-bounds">
|
||||
|
|
|
@ -27,3 +27,5 @@ $fallback--tooltipRadius: 5px;
|
|||
$fallback--avatarRadius: 4px;
|
||||
$fallback--avatarAltRadius: 10px;
|
||||
$fallback--attachmentRadius: 10px;
|
||||
|
||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
|
|
|
@ -5,6 +5,8 @@ import App from '../App.vue'
|
|||
import { windowWidth } from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
||||
|
||||
const getStatusnetConfig = async ({ store }) => {
|
||||
try {
|
||||
|
@ -185,12 +187,9 @@ const getAppSecret = async ({ store }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = async ({ store, accounts }) => {
|
||||
const backendInteractor = store.state.api.backendInteractor
|
||||
let nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
.map(id => backendInteractor.fetchUser({ id }))
|
||||
nicknames = await Promise.all(nicknames)
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
nicknames.map(nickname => store.dispatch('fetchUser', nickname))
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
}
|
||||
|
||||
|
@ -224,9 +223,16 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
|
@ -236,7 +242,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
})
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
await resolveStaffAccounts({ store, accounts })
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
|
@ -261,7 +267,7 @@ const checkOAuthToken = async ({ store }) => {
|
|||
try {
|
||||
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
|
@ -269,23 +275,29 @@ const checkOAuthToken = async ({ store }) => {
|
|||
}
|
||||
|
||||
const afterStoreSetup = async ({ store, i18n }) => {
|
||||
if (store.state.config.customTheme) {
|
||||
// This is a hack to deal with async loading of config.json and themes
|
||||
// See: style_setter.js, setPreset()
|
||||
window.themeLoaded = true
|
||||
store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: store.state.config.customTheme
|
||||
})
|
||||
}
|
||||
|
||||
const width = windowWidth()
|
||||
store.dispatch('setMobileLayout', width <= 800)
|
||||
await setConfig({ store })
|
||||
|
||||
const { customTheme, customThemeSource } = store.state.config
|
||||
const { theme } = store.state.instance
|
||||
const customThemePresent = customThemeSource || customTheme
|
||||
|
||||
if (customThemePresent) {
|
||||
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
|
||||
applyTheme(customThemeSource)
|
||||
} else {
|
||||
applyTheme(customTheme)
|
||||
}
|
||||
} else if (theme) {
|
||||
// do nothing, it will load asynchronously
|
||||
} else {
|
||||
console.error('Failed to load any theme!')
|
||||
}
|
||||
|
||||
// Now we can try getting the server settings and logging in
|
||||
await Promise.all([
|
||||
checkOAuthToken({ store }),
|
||||
setConfig({ store }),
|
||||
getTOS({ store }),
|
||||
getInstancePanel({ store }),
|
||||
getStickers({ store }),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
|
@ -8,7 +9,8 @@ const AccountActions = {
|
|||
return { }
|
||||
},
|
||||
components: {
|
||||
ProgressButton
|
||||
ProgressButton,
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
showRepeats () {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div class="account-actions">
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="click"
|
||||
class="account-tools-popover"
|
||||
:container="false"
|
||||
placement="bottom-end"
|
||||
:offset="5"
|
||||
placement="bottom"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="account-tools-popover"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="user.following">
|
||||
<button
|
||||
|
@ -51,10 +51,13 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-default ellipsis-button">
|
||||
<div
|
||||
slot="trigger"
|
||||
class="btn btn-default ellipsis-button"
|
||||
>
|
||||
<i class="icon-ellipsis trigger-button" />
|
||||
</div>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -62,7 +65,6 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
.account-actions {
|
||||
margin: 0 .8em;
|
||||
}
|
||||
|
@ -70,6 +72,7 @@
|
|||
.account-actions button.dropdown-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.account-actions .trigger-button {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
|
|
|
@ -2,6 +2,7 @@ import StillImage from '../still-image/still-image.vue'
|
|||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const Attachment = {
|
||||
props: [
|
||||
|
@ -49,7 +50,8 @@ const Attachment = {
|
|||
},
|
||||
fullwidth () {
|
||||
return this.type === 'html' || this.type === 'audio'
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
linkClicked ({ target }) {
|
||||
|
@ -58,7 +60,7 @@ const Attachment = {
|
|||
}
|
||||
},
|
||||
openModal (event) {
|
||||
const modalTypes = this.$store.getters.mergedConfig.playVideosInModal
|
||||
const modalTypes = this.mergedConfig.playVideosInModal
|
||||
? ['image', 'video']
|
||||
: ['image']
|
||||
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
|
||||
|
@ -71,7 +73,10 @@ const Attachment = {
|
|||
}
|
||||
},
|
||||
toggleHidden (event) {
|
||||
if (this.$store.getters.mergedConfig.useOneClickNsfw && !this.showHidden) {
|
||||
if (
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -130,6 +130,8 @@
|
|||
.placeholder {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
}
|
||||
|
||||
.nsfw-placeholder {
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
top: 100%;
|
||||
right: 0;
|
||||
max-height: 400px;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: $fallback--border;
|
||||
|
|
|
@ -87,13 +87,13 @@ export default {
|
|||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
&:indeterminate + .checkbox-indicator::before {
|
||||
content: '–';
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
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>
|
||||
<div
|
||||
class="color-control style-control"
|
||||
class="color-input style-control"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
>
|
||||
<label
|
||||
|
@ -9,46 +9,100 @@
|
|||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exlcude-disabled"
|
||||
type="checkbox"
|
||||
<Checkbox
|
||||
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
||||
>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
class="color-input"
|
||||
type="color"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<div class="input color-input-field">
|
||||
<input
|
||||
:id="name + '-t'"
|
||||
class="text-input"
|
||||
class="textColor unstyled"
|
||||
type="text"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<input
|
||||
v-if="validColor"
|
||||
:id="name"
|
||||
class="nativeColor unstyled"
|
||||
type="color"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<div
|
||||
v-if="transparentColor"
|
||||
class="transparentIndicator"
|
||||
/>
|
||||
<div
|
||||
v-if="computedColor"
|
||||
class="computedIndicator"
|
||||
:style="{backgroundColor: fallback}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
export default {
|
||||
props: [
|
||||
'name', 'label', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
// Name of color, used for identifying
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
// Readable label
|
||||
label: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
// Color value, should be required but vue cannot tell the difference
|
||||
// between "property missing" and "property set to undefined"
|
||||
value: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
// Color fallback to use when value is not defeind
|
||||
fallback: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
// Disable the control
|
||||
disabled: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Show "optional" tickbox, for when value might become mandatory
|
||||
showOptionalTickbox: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
present () {
|
||||
return typeof this.value !== 'undefined'
|
||||
},
|
||||
validColor () {
|
||||
return hex2rgb(this.value || this.fallback)
|
||||
},
|
||||
transparentColor () {
|
||||
return this.value === 'transparent'
|
||||
},
|
||||
computedColor () {
|
||||
return this.value && this.value.startsWith('--')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,9 +37,17 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'large', 'contrast'
|
||||
],
|
||||
props: {
|
||||
large: {
|
||||
required: false
|
||||
},
|
||||
// TODO: Make theme switcher compute theme initially so that contrast
|
||||
// component won't be called without contrast data
|
||||
contrast: {
|
||||
required: false,
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hint () {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
|
|
|
@ -150,6 +150,7 @@ const conversation = {
|
|||
if (!id) return
|
||||
this.highlight = id
|
||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
getHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
|
|
|
@ -75,18 +75,18 @@
|
|||
.dialog-modal-content {
|
||||
margin: 0;
|
||||
padding: 1rem 1rem;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
border-top: 1px solid $fallback--bg;
|
||||
border-top: 1px solid var(--bg, $fallback--bg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
border-top: 1px solid var(--border, $fallback--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
|
|
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('click', this.onClickInput)
|
||||
input.elm.addEventListener('transitionend', this.onTransition)
|
||||
input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
input.elm.addEventListener('input', this.onInput)
|
||||
},
|
||||
unmounted () {
|
||||
const { input } = this
|
||||
|
@ -159,7 +159,7 @@ const EmojiInput = {
|
|||
input.elm.removeEventListener('keydown', this.onKeyDown)
|
||||
input.elm.removeEventListener('click', this.onClickInput)
|
||||
input.elm.removeEventListener('transitionend', this.onTransition)
|
||||
input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
input.elm.removeEventListener('input', this.onInput)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -406,12 +406,6 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
this.$emit('input', e.target.value)
|
||||
},
|
||||
onCompositionUpdate (e) {
|
||||
this.showPicker = false
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
this.$emit('input', e.target.value)
|
||||
},
|
||||
onClickInput (e) {
|
||||
this.showPicker = false
|
||||
},
|
||||
|
|
|
@ -109,10 +109,16 @@
|
|||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
min-width: 75%;
|
||||
background: $fallback--bg;
|
||||
background: var(--bg, $fallback--bg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--postLink: var(--popoverPostLink, $fallback--link);
|
||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,7 +163,12 @@
|
|||
|
||||
&.highlighted {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--lightBg, $fallback--fg);
|
||||
background-color: var(--selectedMenuPopover, $fallback--fg);
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
left: 0;
|
||||
margin: 0 !important;
|
||||
z-index: 1;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--lightText: var(--popoverLightText, $fallback--faint);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
|
||||
.keep-open,
|
||||
.too-many-emoji {
|
||||
|
|
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: {
|
||||
exportData () {
|
||||
const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces
|
||||
const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces
|
||||
|
||||
// Create an invisible link with a data url and simulate a click
|
||||
const e = document.createElement('a')
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const ExtraButtons = {
|
||||
props: [ 'status' ],
|
||||
components: { Popover },
|
||||
methods: {
|
||||
deleteStatus () {
|
||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<v-popover
|
||||
<Popover
|
||||
v-if="canDelete || canMute || canPin"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div slot="content">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -47,17 +47,17 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-icon">
|
||||
<i class="icon-ellipsis" />
|
||||
</div>
|
||||
</v-popover>
|
||||
<i
|
||||
slot="trigger"
|
||||
class="icon-ellipsis button-icon"
|
||||
/>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./extra_buttons.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
|
||||
.icon-ellipsis {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
class="btn btn-default follow-button"
|
||||
:class="{ pressed: isPressed }"
|
||||
:class="{ toggled: isPressed }"
|
||||
:disabled="inProgress"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
|
|
|
@ -10,6 +10,7 @@ const tabModeDict = {
|
|||
const Interactions = {
|
||||
data () {
|
||||
return {
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
filterMode: tabModeDict['mentions']
|
||||
}
|
||||
},
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
:label="$t('interactions.follows')"
|
||||
/>
|
||||
<span
|
||||
v-if="!allowFollowingMove"
|
||||
key="moves"
|
||||
:label="$t('interactions.moves')"
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||
|
@ -14,7 +15,6 @@ const ModerationTools = {
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
showDropDown: false,
|
||||
tags: {
|
||||
FORCE_NSFW,
|
||||
STRIP_MEDIA,
|
||||
|
@ -24,11 +24,13 @@ const ModerationTools = {
|
|||
SANDBOX,
|
||||
QUARANTINE
|
||||
},
|
||||
showDeleteUserDialog: false
|
||||
showDeleteUserDialog: false,
|
||||
toggled: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal
|
||||
DialogModal,
|
||||
Popover
|
||||
},
|
||||
computed: {
|
||||
tagsSet () {
|
||||
|
@ -89,6 +91,9 @@ const ModerationTools = {
|
|||
window.history.back()
|
||||
}
|
||||
})
|
||||
},
|
||||
setToggled (value) {
|
||||
this.toggled = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="click"
|
||||
class="moderation-tools-popover"
|
||||
placement="bottom-end"
|
||||
@show="showDropDown = true"
|
||||
@hide="showDropDown = false"
|
||||
placement="bottom"
|
||||
:offset="{ y: 5 }"
|
||||
@show="setToggled(true)"
|
||||
@close="setToggled(false)"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div slot="content">
|
||||
<div class="dropdown-menu">
|
||||
<span v-if="user.is_local">
|
||||
<button
|
||||
|
@ -122,12 +123,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="btn btn-default btn-block"
|
||||
:class="{ pressed: showDropDown }"
|
||||
:class="{ toggled }"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.moderation') }}
|
||||
</button>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
<portal to="modal">
|
||||
<DialogModal
|
||||
v-if="showDeleteUserDialog"
|
||||
|
@ -160,7 +162,6 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
|
||||
.menu-checkbox {
|
||||
float: right;
|
||||
|
|
|
@ -11,7 +11,10 @@ const MRFTransparencyPanel = {
|
|||
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
|
||||
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
|
||||
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
|
||||
mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', [])
|
||||
mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
|
||||
keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
|
||||
keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
|
||||
keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])
|
||||
}),
|
||||
hasInstanceSpecificPolicies () {
|
||||
return this.quarantineInstances.length ||
|
||||
|
@ -20,6 +23,11 @@ const MRFTransparencyPanel = {
|
|||
this.ftlRemovalInstances.length ||
|
||||
this.mediaNsfwInstances.length ||
|
||||
this.mediaRemovalInstances.length
|
||||
},
|
||||
hasKeywordPolicies () {
|
||||
return this.keywordsFtlRemoval.length ||
|
||||
this.keywordsReject.length ||
|
||||
this.keywordsReplace.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
<div class="panel panel-default base01-background">
|
||||
<div class="panel-heading timeline-heading base02-background">
|
||||
<div class="title">
|
||||
{{ $t("about.federation") }}
|
||||
{{ $t("about.mrf.federation") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="mrf-section">
|
||||
<h2>{{ $t("about.mrf_policies") }}</h2>
|
||||
<p>{{ $t("about.mrf_policies_desc") }}</p>
|
||||
<h2>{{ $t("about.mrf.mrf_policies") }}</h2>
|
||||
<p>{{ $t("about.mrf.mrf_policies_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -23,13 +23,13 @@
|
|||
</ul>
|
||||
|
||||
<h2 v-if="hasInstanceSpecificPolicies">
|
||||
{{ $t("about.mrf_policy_simple") }}
|
||||
{{ $t("about.mrf.simple.simple_policies") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="acceptInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.accept") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_accept_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.accept_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -41,9 +41,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="rejectInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_reject") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.reject") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_reject_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.reject_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -55,9 +55,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="quarantineInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_quarantine") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.quarantine") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_quarantine_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -69,9 +69,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="ftlRemovalInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_ftl_removal") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.ftl_removal") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_ftl_removal_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -83,9 +83,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="mediaNsfwInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_media_nsfw") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.media_nsfw") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_media_nsfw_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -97,9 +97,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="mediaRemovalInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_media_removal") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.media_removal") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_media_removal_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -109,6 +109,49 @@
|
|||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 v-if="hasKeywordPolicies">
|
||||
{{ $t("about.mrf.keyword.keyword_policies") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="keywordsFtlRemoval.length">
|
||||
<h4>{{ $t("about.mrf.keyword.ftl_removal") }}</h4>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
v-for="keyword in keywordsFtlRemoval"
|
||||
:key="keyword"
|
||||
v-text="keyword"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="keywordsReject.length">
|
||||
<h4>{{ $t("about.mrf.keyword.reject") }}</h4>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
v-for="keyword in keywordsReject"
|
||||
:key="keyword"
|
||||
v-text="keyword"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="keywordsReplace.length">
|
||||
<h4>{{ $t("about.mrf.keyword.replace") }}</h4>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
v-for="keyword in keywordsReplace"
|
||||
:key="keyword"
|
||||
>
|
||||
{{ keyword.pattern }}
|
||||
{{ $t("about.mrf.keyword.is_replaced_by") }}
|
||||
{{ keyword.replacement }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { mapState } from 'vuex'
|
|||
const NavPanel = {
|
||||
created () {
|
||||
if (this.currentUser && this.currentUser.locked) {
|
||||
this.$store.dispatch('startFetchingFollowRequest')
|
||||
this.$store.dispatch('startFetchingFollowRequests')
|
||||
}
|
||||
},
|
||||
computed: mapState({
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="federating && !privateMode">
|
||||
<li v-if="federating && (currentUser || !privateMode)">
|
||||
<router-link :to="{ name: 'public-external-timeline' }">
|
||||
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
|
||||
</router-link>
|
||||
|
@ -100,13 +100,25 @@
|
|||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--link;
|
||||
color: var(--selectedMenuText, $fallback--link);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
font-weight: bolder;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
|
|
@ -78,6 +78,13 @@
|
|||
<i class="fa icon-arrow-curved lit" />
|
||||
<small>{{ $t('notifications.migrated_to') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'pleroma:emoji_reaction'">
|
||||
<small>
|
||||
<i18n path="notifications.reacted_with">
|
||||
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
|
||||
</i18n>
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'follow' || notification.type === 'move'"
|
||||
|
|
|
@ -68,6 +68,9 @@
|
|||
a {
|
||||
color: var(--faintLink);
|
||||
}
|
||||
.status-content a {
|
||||
color: var(--postFaintLink);
|
||||
}
|
||||
}
|
||||
padding: 0;
|
||||
.media-body {
|
||||
|
@ -94,6 +97,10 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.emoji-reaction-emoji {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.notification-details {
|
||||
min-width: 0px;
|
||||
word-wrap: break-word;
|
||||
|
|
|
@ -9,18 +9,12 @@
|
|||
>
|
||||
{{ $t('settings.style.common.opacity') }}
|
||||
</label>
|
||||
<input
|
||||
<Checkbox
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exclude-disabled"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', !present ? fallback : undefined)"
|
||||
>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@change="$emit('input', !present ? fallback : undefined)"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
|
@ -37,7 +31,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
export default {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: [
|
||||
'name', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
|
|
|
@ -104,8 +104,10 @@
|
|||
.result-fill {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
color: $fallback--text;
|
||||
color: var(--pollText, $fallback--text);
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--linkBg, $fallback--lightBg);
|
||||
background-color: var(--poll, $fallback--lightBg);
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
top: 0;
|
||||
|
|
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
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exclude-disabled"
|
||||
class="opt"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@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 {
|
||||
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 {
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
<li>
|
||||
<Checkbox v-model="useStreamingApi">
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br/>
|
||||
<br>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
|
@ -92,6 +92,11 @@
|
|||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -328,6 +333,11 @@
|
|||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -3,6 +3,17 @@ import OpacityInput from '../opacity_input/opacity_input.vue'
|
|||
import { getCssShadow } from '../../services/style_setter/style_setter.js'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
|
||||
const toModel = (object = {}) => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
inset: false,
|
||||
color: '#000000',
|
||||
alpha: 1,
|
||||
...object
|
||||
})
|
||||
|
||||
export default {
|
||||
// 'Value' and 'Fallback' can be undefined, but if they are
|
||||
// initially vue won't detect it when they become something else
|
||||
|
@ -15,7 +26,7 @@ export default {
|
|||
return {
|
||||
selectedId: 0,
|
||||
// TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason)
|
||||
cValue: this.value || this.fallback || []
|
||||
cValue: (this.value || this.fallback || []).map(toModel)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -24,12 +35,12 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
add () {
|
||||
this.cValue.push(Object.assign({}, this.selected))
|
||||
this.cValue.push(toModel(this.selected))
|
||||
this.selectedId = this.cValue.length - 1
|
||||
},
|
||||
del () {
|
||||
this.cValue.splice(this.selectedId, 1)
|
||||
this.selectedId = this.cValue.length === 0 ? undefined : this.selectedId - 1
|
||||
this.selectedId = this.cValue.length === 0 ? undefined : Math.max(this.selectedId - 1, 0)
|
||||
},
|
||||
moveUp () {
|
||||
const movable = this.cValue.splice(this.selectedId, 1)[0]
|
||||
|
@ -46,19 +57,24 @@ export default {
|
|||
this.cValue = this.value || this.fallback
|
||||
},
|
||||
computed: {
|
||||
anyShadows () {
|
||||
return this.cValue.length > 0
|
||||
},
|
||||
anyShadowsFallback () {
|
||||
return this.fallback.length > 0
|
||||
},
|
||||
selected () {
|
||||
if (this.ready && this.cValue.length > 0) {
|
||||
if (this.ready && this.anyShadows) {
|
||||
return this.cValue[this.selectedId]
|
||||
} else {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
inset: false,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
return toModel({})
|
||||
}
|
||||
},
|
||||
currentFallback () {
|
||||
if (this.ready && this.anyShadowsFallback) {
|
||||
return this.fallback[this.selectedId]
|
||||
} else {
|
||||
return toModel({})
|
||||
}
|
||||
},
|
||||
moveUpValid () {
|
||||
|
@ -80,7 +96,7 @@ export default {
|
|||
},
|
||||
style () {
|
||||
return this.ready ? {
|
||||
boxShadow: getCssShadow(this.cValue)
|
||||
boxShadow: getCssShadow(this.fallback)
|
||||
} : {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -191,15 +191,20 @@
|
|||
v-model="selected.color"
|
||||
:disabled="!present"
|
||||
:label="$t('settings.style.common.color')"
|
||||
:fallback="currentFallback.color"
|
||||
:show-optional-tickbox="false"
|
||||
name="shadow"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="selected.alpha"
|
||||
:disabled="!present"
|
||||
/>
|
||||
<p>
|
||||
{{ $t('settings.style.shadows.hint') }}
|
||||
</p>
|
||||
<i18n
|
||||
path="settings.style.shadows.hintV3"
|
||||
tag="p"
|
||||
>
|
||||
<code>--variable,mod</code>
|
||||
</i18n>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -12,7 +12,7 @@ const SideDrawer = {
|
|||
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
|
||||
|
||||
if (this.currentUser && this.currentUser.locked) {
|
||||
this.$store.dispatch('startFetchingFollowRequest')
|
||||
this.$store.dispatch('startFetchingFollowRequests')
|
||||
}
|
||||
},
|
||||
components: { UserCard },
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="federating && !privateMode"
|
||||
v-if="federating && (currentUser || !privateMode)"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<router-link to="/main/all">
|
||||
|
@ -223,7 +223,13 @@
|
|||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
|
||||
.button-icon:before {
|
||||
width: 1.1em;
|
||||
|
@ -289,7 +295,13 @@
|
|||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import map from 'lodash/map'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
const StaffPanel = {
|
||||
|
@ -6,7 +7,7 @@ const StaffPanel = {
|
|||
},
|
||||
computed: {
|
||||
staffAccounts () {
|
||||
return this.$store.state.instance.staffAccounts
|
||||
return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
||||
import ReactButton from '../react_button/react_button.vue'
|
||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||
import Poll from '../poll/poll.vue'
|
||||
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||
|
@ -11,6 +12,7 @@ import LinkPreview from '../link-preview/link-preview.vue'
|
|||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import StatusPopover from '../status_popover/status_popover.vue'
|
||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
|
@ -254,6 +256,16 @@ const Status = {
|
|||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
hasImageAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'image'
|
||||
)
|
||||
},
|
||||
hasVideoAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'video'
|
||||
)
|
||||
},
|
||||
maxThumbnails () {
|
||||
return this.mergedConfig.maxThumbnails
|
||||
},
|
||||
|
@ -319,6 +331,7 @@ const Status = {
|
|||
components: {
|
||||
Attachment,
|
||||
FavoriteButton,
|
||||
ReactButton,
|
||||
RetweetButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
|
@ -329,7 +342,8 @@ const Status = {
|
|||
LinkPreview,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
StatusPopover
|
||||
StatusPopover,
|
||||
EmojiReactions
|
||||
},
|
||||
methods: {
|
||||
visibilityIcon (visibility) {
|
||||
|
|
|
@ -177,6 +177,8 @@
|
|||
<StatusPopover
|
||||
v-if="!isPreview"
|
||||
:status-id="status.in_reply_to_status_id"
|
||||
class="reply-to-popover"
|
||||
style="min-width: 0"
|
||||
>
|
||||
<a
|
||||
class="reply-to"
|
||||
|
@ -277,7 +279,21 @@
|
|||
href="#"
|
||||
class="cw-status-hider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_more") }}</a>
|
||||
>
|
||||
{{ $t("general.show_more") }}
|
||||
<span
|
||||
v-if="hasImageAttachments"
|
||||
class="icon-picture"
|
||||
/>
|
||||
<span
|
||||
v-if="hasVideoAttachments"
|
||||
class="icon-video"
|
||||
/>
|
||||
<span
|
||||
v-if="status.card"
|
||||
class="icon-link"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="showingMore"
|
||||
href="#"
|
||||
|
@ -354,6 +370,11 @@
|
|||
</div>
|
||||
</transition>
|
||||
|
||||
<EmojiReactions
|
||||
v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
|
||||
:status="status"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!noHeading && !isPreview"
|
||||
class="status-actions media-body"
|
||||
|
@ -382,6 +403,10 @@
|
|||
:logged-in="loggedIn"
|
||||
:status="status"
|
||||
/>
|
||||
<ReactButton
|
||||
:logged-in="loggedIn"
|
||||
:status="status"
|
||||
/>
|
||||
<extra-buttons
|
||||
:status="status"
|
||||
@onError="showError"
|
||||
|
@ -445,7 +470,15 @@ $status-margin: 0.75em;
|
|||
|
||||
&_focused {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedPost, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedPostText, $fallback--text);
|
||||
--lightText: var(--selectedPostLightText, $fallback--light);
|
||||
--faint: var(--selectedPostFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedPostFaintLink, $fallback--faint);
|
||||
--postLink: var(--selectedPostPostLink, $fallback--faint);
|
||||
--postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
|
||||
--icon: var(--selectedPostIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
.timeline & {
|
||||
|
@ -541,11 +574,10 @@ $status-margin: 0.75em;
|
|||
align-items: stretch;
|
||||
|
||||
> .reply-to-and-accountname > a {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
@ -554,7 +586,6 @@ $status-margin: 0.75em;
|
|||
display: flex;
|
||||
height: 18px;
|
||||
margin-right: 0.5em;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
.icon-reply {
|
||||
transform: scaleX(-1);
|
||||
|
@ -565,6 +596,10 @@ $status-margin: 0.75em;
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.reply-to-popover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -572,9 +607,8 @@ $status-margin: 0.75em;
|
|||
.reply-to-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0 0.4em 0 0.2em;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
|
||||
.replies-separator {
|
||||
|
@ -636,6 +670,11 @@ $status-margin: 0.75em;
|
|||
line-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
}
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
|
|
@ -5,22 +5,14 @@ const StatusPopover = {
|
|||
props: [
|
||||
'statusId'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
popperOptions: {
|
||||
modifiers: {
|
||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
status () {
|
||||
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Status: () => import('../status/status.vue')
|
||||
Status: () => import('../status/status.vue'),
|
||||
Popover: () => import('../popover/popover.vue')
|
||||
},
|
||||
methods: {
|
||||
enter () {
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<template>
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="hover"
|
||||
popover-class="status-popover"
|
||||
placement="top-start"
|
||||
:popper-options="popperOptions"
|
||||
@show="enter()"
|
||||
:bound-to="{ x: 'container' }"
|
||||
@show="enter"
|
||||
>
|
||||
<template slot="trigger">
|
||||
<slot />
|
||||
</template>
|
||||
<div
|
||||
slot="content"
|
||||
>
|
||||
<template slot="popover">
|
||||
<Status
|
||||
v-if="status"
|
||||
:is-preview="true"
|
||||
|
@ -18,10 +23,8 @@
|
|||
>
|
||||
<i class="icon-spin4 animate-spin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</v-popover>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./status_popover.js" ></script>
|
||||
|
@ -29,13 +32,11 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.tooltip.popover.status-popover {
|
||||
.status-popover {
|
||||
font-size: 1rem;
|
||||
min-width: 15em;
|
||||
max-width: 95%;
|
||||
margin-left: 0.5em;
|
||||
|
||||
.popover-inner {
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
border-style: solid;
|
||||
|
@ -44,29 +45,6 @@
|
|||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
}
|
||||
|
||||
.popover-arrow::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
left: -7px;
|
||||
border: solid 7px transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&[x-placement^="bottom-start"] .popover-arrow::before {
|
||||
top: -2px;
|
||||
border-top-width: 0;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
&[x-placement^="top-start"] .popover-arrow::before {
|
||||
bottom: -2px;
|
||||
border-bottom-width: 0;
|
||||
border-top-color: $fallback--border;
|
||||
border-top-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.status-el.status-el {
|
||||
border: none;
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
img {
|
||||
height: 100%;
|
||||
&:hover {
|
||||
filter: drop-shadow(0 0 5px var(--link, $fallback--link));
|
||||
filter: drop-shadow(0 0 5px var(--accent, $fallback--link));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<template>
|
||||
<div class="preview-container">
|
||||
<div class="underlay underlay-preview" />
|
||||
<div class="panel dummy">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
|
@ -19,7 +21,7 @@
|
|||
</div>
|
||||
<div class="panel-body theme-preview-content">
|
||||
<div class="post">
|
||||
<div class="avatar">
|
||||
<div class="avatar still-image">
|
||||
( ͡° ͜ʖ ͡°)
|
||||
</div>
|
||||
<div class="content">
|
||||
|
@ -98,4 +100,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.preview-container {
|
||||
position: relative;
|
||||
}
|
||||
.underlay-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,29 @@
|
|||
import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
|
||||
import { set, delete as del } from 'vue'
|
||||
import { generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
|
||||
import {
|
||||
rgb2hex,
|
||||
hex2rgb,
|
||||
getContrastRatioLayers
|
||||
} from '../../services/color_convert/color_convert.js'
|
||||
import {
|
||||
DEFAULT_SHADOWS,
|
||||
generateColors,
|
||||
generateShadows,
|
||||
generateRadii,
|
||||
generateFonts,
|
||||
composePreset,
|
||||
getThemes,
|
||||
shadows2to3,
|
||||
colors2to3
|
||||
} from '../../services/style_setter/style_setter.js'
|
||||
import {
|
||||
SLOT_INHERITANCE
|
||||
} from '../../services/theme_data/pleromafe.js'
|
||||
import {
|
||||
CURRENT_VERSION,
|
||||
OPACITIES,
|
||||
getLayers,
|
||||
getOpacitySlot
|
||||
} from '../../services/theme_data/theme_data.service.js'
|
||||
import ColorInput from '../color_input/color_input.vue'
|
||||
import RangeInput from '../range_input/range_input.vue'
|
||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||
|
@ -24,11 +47,22 @@ const v1OnlyNames = [
|
|||
'cOrange'
|
||||
].map(_ => _ + 'ColorLocal')
|
||||
|
||||
const colorConvert = (color) => {
|
||||
if (color.startsWith('--') || color === 'transparent') {
|
||||
return color
|
||||
} else {
|
||||
return hex2rgb(color)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
availableStyles: [],
|
||||
selected: this.$store.getters.mergedConfig.theme,
|
||||
themeWarning: undefined,
|
||||
tempImportFile: undefined,
|
||||
engineVersion: 0,
|
||||
|
||||
previewShadows: {},
|
||||
previewColors: {},
|
||||
|
@ -45,51 +79,13 @@ export default {
|
|||
keepRoundness: false,
|
||||
keepFonts: false,
|
||||
|
||||
textColorLocal: '',
|
||||
linkColorLocal: '',
|
||||
...Object.keys(SLOT_INHERITANCE)
|
||||
.map(key => [key, ''])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
|
||||
|
||||
bgColorLocal: '',
|
||||
bgOpacityLocal: undefined,
|
||||
|
||||
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: '',
|
||||
...Object.keys(OPACITIES)
|
||||
.map(key => [key, ''])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
|
||||
|
||||
shadowSelected: undefined,
|
||||
shadowsLocal: {},
|
||||
|
@ -108,69 +104,105 @@ export default {
|
|||
created () {
|
||||
const self = this
|
||||
|
||||
getThemes().then((themesComplete) => {
|
||||
getThemes()
|
||||
.then((promises) => {
|
||||
return Promise.all(
|
||||
Object.entries(promises)
|
||||
.map(([k, v]) => v.then(res => [k, res]))
|
||||
)
|
||||
})
|
||||
.then(themes => themes.reduce((acc, [k, v]) => {
|
||||
if (v) {
|
||||
return {
|
||||
...acc,
|
||||
[k]: v
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {}))
|
||||
.then((themesComplete) => {
|
||||
self.availableStyles = themesComplete
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
|
||||
this.loadThemeFromLocalStorage()
|
||||
if (typeof this.shadowSelected === 'undefined') {
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
themeWarningHelp () {
|
||||
if (!this.themeWarning) return
|
||||
const t = this.$t
|
||||
const pre = 'settings.style.switcher.help.'
|
||||
const {
|
||||
origin,
|
||||
themeEngineVersion,
|
||||
type,
|
||||
noActionsPossible
|
||||
} = this.themeWarning
|
||||
if (origin === 'file') {
|
||||
// Loaded v2 theme from file
|
||||
if (themeEngineVersion === 2 && type === 'wrong_version') {
|
||||
return t(pre + 'v2_imported')
|
||||
}
|
||||
if (themeEngineVersion > CURRENT_VERSION) {
|
||||
return t(pre + 'future_version_imported') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'snapshot_missing')
|
||||
: t(pre + 'snapshot_present')
|
||||
)
|
||||
}
|
||||
if (themeEngineVersion < CURRENT_VERSION) {
|
||||
return t(pre + 'future_version_imported') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'snapshot_missing')
|
||||
: t(pre + 'snapshot_present')
|
||||
)
|
||||
}
|
||||
} else if (origin === 'localStorage') {
|
||||
if (type === 'snapshot_source_mismatch') {
|
||||
return t(pre + 'snapshot_source_mismatch')
|
||||
}
|
||||
// FE upgraded from v2
|
||||
if (themeEngineVersion === 2) {
|
||||
return t(pre + 'upgraded_from_v2')
|
||||
}
|
||||
// Admin downgraded FE
|
||||
if (themeEngineVersion > CURRENT_VERSION) {
|
||||
return t(pre + 'fe_downgraded') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'migration_snapshot_ok')
|
||||
: t(pre + 'migration_snapshot_gone')
|
||||
)
|
||||
}
|
||||
// Admin upgraded FE
|
||||
if (themeEngineVersion < CURRENT_VERSION) {
|
||||
return t(pre + 'fe_upgraded') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'migration_snapshot_ok')
|
||||
: t(pre + 'migration_snapshot_gone')
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedVersion () {
|
||||
return Array.isArray(this.selected) ? 1 : 2
|
||||
},
|
||||
currentColors () {
|
||||
return {
|
||||
bg: this.bgColorLocal,
|
||||
text: this.textColorLocal,
|
||||
link: this.linkColorLocal,
|
||||
|
||||
fg: this.fgColorLocal,
|
||||
fgText: this.fgTextColorLocal,
|
||||
fgLink: this.fgLinkColorLocal,
|
||||
|
||||
panel: this.panelColorLocal,
|
||||
panelText: this.panelTextColorLocal,
|
||||
panelLink: this.panelLinkColorLocal,
|
||||
panelFaint: this.panelFaintColorLocal,
|
||||
|
||||
input: this.inputColorLocal,
|
||||
inputText: this.inputTextColorLocal,
|
||||
|
||||
topBar: this.topBarColorLocal,
|
||||
topBarText: this.topBarTextColorLocal,
|
||||
topBarLink: this.topBarLinkColorLocal,
|
||||
|
||||
btn: this.btnColorLocal,
|
||||
btnText: this.btnTextColorLocal,
|
||||
|
||||
alertError: this.alertErrorColorLocal,
|
||||
alertWarning: this.alertWarningColorLocal,
|
||||
badgeNotification: this.badgeNotificationColorLocal,
|
||||
|
||||
faint: this.faintColorLocal,
|
||||
faintLink: this.faintLinkColorLocal,
|
||||
border: this.borderColorLocal,
|
||||
|
||||
cRed: this.cRedColorLocal,
|
||||
cBlue: this.cBlueColorLocal,
|
||||
cGreen: this.cGreenColorLocal,
|
||||
cOrange: this.cOrangeColorLocal
|
||||
}
|
||||
return Object.keys(SLOT_INHERITANCE)
|
||||
.map(key => [key, this[key + 'ColorLocal']])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||
},
|
||||
currentOpacity () {
|
||||
return {
|
||||
bg: this.bgOpacityLocal,
|
||||
btn: this.btnOpacityLocal,
|
||||
input: this.inputOpacityLocal,
|
||||
panel: this.panelOpacityLocal,
|
||||
topBar: this.topBarOpacityLocal,
|
||||
border: this.borderOpacityLocal,
|
||||
faint: this.faintOpacityLocal
|
||||
}
|
||||
return Object.keys(OPACITIES)
|
||||
.map(key => [key, this[key + 'OpacityLocal']])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||
},
|
||||
currentRadii () {
|
||||
return {
|
||||
|
@ -193,6 +225,7 @@ export default {
|
|||
},
|
||||
// This needs optimization maybe
|
||||
previewContrast () {
|
||||
try {
|
||||
if (!this.previewTheme.colors.bg) return {}
|
||||
const colors = this.previewTheme.colors
|
||||
const opacity = this.previewTheme.opacity
|
||||
|
@ -206,62 +239,52 @@ export default {
|
|||
laa: ratio >= 3,
|
||||
laaa: ratio >= 4.5
|
||||
})
|
||||
const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
|
||||
|
||||
// fgsfds :DDDD
|
||||
const fgs = {
|
||||
text: hex2rgb(colors.text),
|
||||
panelText: hex2rgb(colors.panelText),
|
||||
panelLink: hex2rgb(colors.panelLink),
|
||||
btnText: hex2rgb(colors.btnText),
|
||||
topBarText: hex2rgb(colors.topBarText),
|
||||
inputText: hex2rgb(colors.inputText),
|
||||
const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
|
||||
const slotIsBaseText = key === 'text' || key === 'link'
|
||||
const slotIsText = slotIsBaseText || (
|
||||
typeof value === 'object' && value !== null && value.textColor
|
||||
)
|
||||
if (!slotIsText) return acc
|
||||
const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
|
||||
const background = variant || layer
|
||||
const opacitySlot = getOpacitySlot(background)
|
||||
const textColors = [
|
||||
key,
|
||||
...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
|
||||
]
|
||||
|
||||
link: hex2rgb(colors.link),
|
||||
topBarLink: hex2rgb(colors.topBarLink),
|
||||
const layers = getLayers(
|
||||
layer,
|
||||
variant || layer,
|
||||
opacitySlot,
|
||||
colorsConverted,
|
||||
opacity
|
||||
)
|
||||
|
||||
red: hex2rgb(colors.cRed),
|
||||
green: hex2rgb(colors.cGreen),
|
||||
blue: hex2rgb(colors.cBlue),
|
||||
orange: hex2rgb(colors.cOrange)
|
||||
return {
|
||||
...acc,
|
||||
...textColors.reduce((acc, textColorKey) => {
|
||||
const newKey = slotIsBaseText
|
||||
? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
|
||||
: textColorKey
|
||||
return {
|
||||
...acc,
|
||||
[newKey]: getContrastRatioLayers(
|
||||
colorsConverted[textColorKey],
|
||||
layers,
|
||||
colorsConverted[textColorKey]
|
||||
)
|
||||
}
|
||||
|
||||
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 }, {})
|
||||
} catch (e) {
|
||||
console.warn('Failure computing contrasts', e)
|
||||
}
|
||||
},
|
||||
previewRules () {
|
||||
if (!this.preview.rules) return ''
|
||||
|
@ -272,7 +295,7 @@ export default {
|
|||
].join(';')
|
||||
},
|
||||
shadowsAvailable () {
|
||||
return Object.keys(this.previewTheme.shadows).sort()
|
||||
return Object.keys(DEFAULT_SHADOWS).sort()
|
||||
},
|
||||
currentShadowOverriden: {
|
||||
get () {
|
||||
|
@ -287,7 +310,7 @@ export default {
|
|||
}
|
||||
},
|
||||
currentShadowFallback () {
|
||||
return this.previewTheme.shadows[this.shadowSelected]
|
||||
return (this.previewTheme.shadows || {})[this.shadowSelected]
|
||||
},
|
||||
currentShadow: {
|
||||
get () {
|
||||
|
@ -309,27 +332,34 @@ export default {
|
|||
!this.keepColor
|
||||
)
|
||||
|
||||
const theme = {}
|
||||
const source = {
|
||||
themeEngineVersion: CURRENT_VERSION
|
||||
}
|
||||
|
||||
if (this.keepFonts || saveEverything) {
|
||||
theme.fonts = this.fontsLocal
|
||||
source.fonts = this.fontsLocal
|
||||
}
|
||||
if (this.keepShadows || saveEverything) {
|
||||
theme.shadows = this.shadowsLocal
|
||||
source.shadows = this.shadowsLocal
|
||||
}
|
||||
if (this.keepOpacity || saveEverything) {
|
||||
theme.opacity = this.currentOpacity
|
||||
source.opacity = this.currentOpacity
|
||||
}
|
||||
if (this.keepColor || saveEverything) {
|
||||
theme.colors = this.currentColors
|
||||
source.colors = this.currentColors
|
||||
}
|
||||
if (this.keepRoundness || saveEverything) {
|
||||
theme.radii = this.currentRadii
|
||||
source.radii = this.currentRadii
|
||||
}
|
||||
|
||||
const theme = {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
...this.previewTheme
|
||||
}
|
||||
|
||||
return {
|
||||
// To separate from other random JSON files and possible future theme formats
|
||||
_pleroma_theme_version: 2, theme
|
||||
// To separate from other random JSON files and possible future source formats
|
||||
_pleroma_theme_version: 2, theme, source
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -346,10 +376,128 @@ export default {
|
|||
Checkbox
|
||||
},
|
||||
methods: {
|
||||
loadTheme (
|
||||
{
|
||||
theme,
|
||||
source,
|
||||
_pleroma_theme_version: fileVersion
|
||||
},
|
||||
origin,
|
||||
forceUseSource = false
|
||||
) {
|
||||
this.dismissWarning()
|
||||
if (!source && !theme) {
|
||||
throw new Error('Can\'t load theme: empty')
|
||||
}
|
||||
const version = (origin === 'localStorage' && !theme.colors)
|
||||
? 'l1'
|
||||
: fileVersion
|
||||
const snapshotEngineVersion = (theme || {}).themeEngineVersion
|
||||
const themeEngineVersion = (source || {}).themeEngineVersion || 2
|
||||
const versionsMatch = themeEngineVersion === CURRENT_VERSION
|
||||
const sourceSnapshotMismatch = (
|
||||
theme !== undefined &&
|
||||
source !== undefined &&
|
||||
themeEngineVersion !== snapshotEngineVersion
|
||||
)
|
||||
// Force loading of source if user requested it or if snapshot
|
||||
// is unavailable
|
||||
const forcedSourceLoad = (source && forceUseSource) || !theme
|
||||
if (!(versionsMatch && !sourceSnapshotMismatch) &&
|
||||
!forcedSourceLoad &&
|
||||
version !== 'l1' &&
|
||||
origin !== 'defaults'
|
||||
) {
|
||||
if (sourceSnapshotMismatch && origin === 'localStorage') {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
themeEngineVersion,
|
||||
type: 'snapshot_source_mismatch'
|
||||
}
|
||||
} else if (!theme) {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
noActionsPossible: true,
|
||||
themeEngineVersion,
|
||||
type: 'no_snapshot_old_version'
|
||||
}
|
||||
} else if (!versionsMatch) {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
noActionsPossible: !source,
|
||||
themeEngineVersion,
|
||||
type: 'wrong_version'
|
||||
}
|
||||
}
|
||||
}
|
||||
this.normalizeLocalState(theme, version, source, forcedSourceLoad)
|
||||
},
|
||||
forceLoadLocalStorage () {
|
||||
this.loadThemeFromLocalStorage(true)
|
||||
},
|
||||
dismissWarning () {
|
||||
this.themeWarning = undefined
|
||||
this.tempImportFile = undefined
|
||||
},
|
||||
forceLoad () {
|
||||
const { origin } = this.themeWarning
|
||||
switch (origin) {
|
||||
case 'localStorage':
|
||||
this.loadThemeFromLocalStorage(true)
|
||||
break
|
||||
case 'file':
|
||||
this.onImport(this.tempImportFile, true)
|
||||
break
|
||||
}
|
||||
this.dismissWarning()
|
||||
},
|
||||
forceSnapshot () {
|
||||
const { origin } = this.themeWarning
|
||||
switch (origin) {
|
||||
case 'localStorage':
|
||||
this.loadThemeFromLocalStorage(false, true)
|
||||
break
|
||||
case 'file':
|
||||
console.err('Forcing snapshout from file is not supported yet')
|
||||
break
|
||||
}
|
||||
this.dismissWarning()
|
||||
},
|
||||
loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
|
||||
const {
|
||||
customTheme: theme,
|
||||
customThemeSource: source
|
||||
} = this.$store.getters.mergedConfig
|
||||
if (!theme && !source) {
|
||||
// Anon user or never touched themes
|
||||
this.loadTheme(
|
||||
this.$store.state.instance.themeData,
|
||||
'defaults',
|
||||
confirmLoadSource
|
||||
)
|
||||
} else {
|
||||
this.loadTheme(
|
||||
{
|
||||
theme,
|
||||
source: forceSnapshot ? theme : source
|
||||
},
|
||||
'localStorage',
|
||||
confirmLoadSource
|
||||
)
|
||||
}
|
||||
},
|
||||
setCustomTheme () {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
...this.previewTheme
|
||||
}
|
||||
})
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customThemeSource',
|
||||
value: {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
shadows: this.shadowsLocal,
|
||||
fonts: this.fontsLocal,
|
||||
opacity: this.currentOpacity,
|
||||
|
@ -358,21 +506,27 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
onImport (parsed) {
|
||||
if (parsed._pleroma_theme_version === 1) {
|
||||
this.normalizeLocalState(parsed, 1)
|
||||
} else if (parsed._pleroma_theme_version === 2) {
|
||||
this.normalizeLocalState(parsed.theme, 2)
|
||||
}
|
||||
updatePreviewColorsAndShadows () {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.previewShadows = generateShadows(
|
||||
{ shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
|
||||
this.previewColors.theme.colors,
|
||||
this.previewColors.mod
|
||||
)
|
||||
},
|
||||
onImport (parsed, forceSource = false) {
|
||||
this.tempImportFile = parsed
|
||||
this.loadTheme(parsed, 'file', forceSource)
|
||||
},
|
||||
importValidator (parsed) {
|
||||
const version = parsed._pleroma_theme_version
|
||||
return version >= 1 || version <= 2
|
||||
},
|
||||
clearAll () {
|
||||
const state = this.$store.getters.mergedConfig.customTheme
|
||||
const version = state.colors ? 2 : 'l1'
|
||||
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
|
||||
this.loadThemeFromLocalStorage()
|
||||
},
|
||||
|
||||
// Clears all the extra stuff when loading V1 theme
|
||||
|
@ -411,19 +565,37 @@ export default {
|
|||
|
||||
/**
|
||||
* This applies stored theme data onto form. Supports three versions of data:
|
||||
* v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
|
||||
* v2 (version = 2) - newer version of themes.
|
||||
* v1 (version = 1) - older version of themes (import from file)
|
||||
* v1l (version = l1) - older version of theme (load from local storage)
|
||||
* v1 and v1l differ because of way themes were stored/exported.
|
||||
* @param {Object} input - input data
|
||||
* @param {Object} theme - theme data (snapshot)
|
||||
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
|
||||
* @param {Object} source - theme source - this will be used if compatible
|
||||
* @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
|
||||
* this allows importing source anyway
|
||||
*/
|
||||
normalizeLocalState (input, version = 0) {
|
||||
const colors = input.colors || input
|
||||
normalizeLocalState (theme, version = 0, source, forceSource = false) {
|
||||
let input
|
||||
if (typeof source !== 'undefined') {
|
||||
if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
|
||||
input = source
|
||||
version = source.themeEngineVersion
|
||||
} else {
|
||||
input = theme
|
||||
}
|
||||
} else {
|
||||
input = theme
|
||||
}
|
||||
|
||||
const radii = input.radii || input
|
||||
const opacity = input.opacity
|
||||
const shadows = input.shadows || {}
|
||||
const fonts = input.fonts || {}
|
||||
const colors = !input.themeEngineVersion
|
||||
? colors2to3(input.colors || input)
|
||||
: input.colors || input
|
||||
|
||||
if (version === 0) {
|
||||
if (input.version) version = input.version
|
||||
|
@ -437,6 +609,8 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
this.engineVersion = version
|
||||
|
||||
// Stuff that differs between V1 and V2
|
||||
if (version === 1) {
|
||||
this.fgColorLocal = rgb2hex(colors.btn)
|
||||
|
@ -445,7 +619,7 @@ export default {
|
|||
|
||||
if (!this.keepColor) {
|
||||
this.clearV1()
|
||||
const keys = new Set(version !== 1 ? Object.keys(colors) : [])
|
||||
const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
|
||||
if (version === 1 || version === 'l1') {
|
||||
keys
|
||||
.add('bg')
|
||||
|
@ -457,7 +631,17 @@ export default {
|
|||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
this[key + 'ColorLocal'] = rgb2hex(colors[key])
|
||||
const color = colors[key]
|
||||
const hex = rgb2hex(colors[key])
|
||||
this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
|
||||
})
|
||||
}
|
||||
|
||||
if (opacity && !this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
Object.entries(opacity).forEach(([k, v]) => {
|
||||
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
|
||||
this[k + 'OpacityLocal'] = v
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -472,7 +656,11 @@ export default {
|
|||
|
||||
if (!this.keepShadows) {
|
||||
this.clearShadows()
|
||||
if (version === 2) {
|
||||
this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
|
||||
} else {
|
||||
this.shadowsLocal = shadows
|
||||
}
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
|
||||
|
@ -480,14 +668,6 @@ export default {
|
|||
this.clearFonts()
|
||||
this.fontsLocal = fonts
|
||||
}
|
||||
|
||||
if (opacity && !this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
Object.entries(opacity).forEach(([k, v]) => {
|
||||
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
|
||||
this[k + 'OpacityLocal'] = v
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -502,8 +682,9 @@ export default {
|
|||
},
|
||||
shadowsLocal: {
|
||||
handler () {
|
||||
if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
|
||||
try {
|
||||
this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
|
||||
this.updatePreviewColorsAndShadows()
|
||||
this.shadowsInvalid = false
|
||||
} catch (e) {
|
||||
this.shadowsInvalid = true
|
||||
|
@ -526,27 +707,24 @@ export default {
|
|||
},
|
||||
currentColors () {
|
||||
try {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.updatePreviewColorsAndShadows()
|
||||
this.colorsInvalid = false
|
||||
this.shadowsInvalid = false
|
||||
} catch (e) {
|
||||
this.colorsInvalid = true
|
||||
this.shadowsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
currentOpacity () {
|
||||
try {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.updatePreviewColorsAndShadows()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
selected () {
|
||||
this.dismissWarning()
|
||||
if (this.selectedVersion === 1) {
|
||||
if (!this.keepRoundness) {
|
||||
this.clearRoundness()
|
||||
|
@ -573,7 +751,7 @@ export default {
|
|||
this.cOrangeColorLocal = this.selected[8]
|
||||
}
|
||||
} else if (this.selectedVersion >= 2) {
|
||||
this.normalizeLocalState(this.selected.theme, 2)
|
||||
this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
@import '../../_variables.scss';
|
||||
.style-switcher {
|
||||
.theme-warning {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: .5em;
|
||||
.buttons {
|
||||
.btn {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.preset-switcher {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
@ -15,10 +25,16 @@
|
|||
|
||||
&.disabled {
|
||||
input, select {
|
||||
&:not(.exclude-disabled) {
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
margin: .5em;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
input, select {
|
||||
|
@ -26,15 +42,6 @@
|
|||
margin: 0;
|
||||
flex: 0;
|
||||
|
||||
&[type=color] {
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
height: 29px;
|
||||
min-width: 2em;
|
||||
border: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
&[type=number] {
|
||||
min-width: 5em;
|
||||
}
|
||||
|
@ -42,13 +49,6 @@
|
|||
&[type=range] {
|
||||
flex: 1;
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
&[type=checkbox] + label {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
&:not([type=number]):not([type=text]) {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,53 @@
|
|||
<div class="style-switcher">
|
||||
<div class="presets-container">
|
||||
<div class="save-load">
|
||||
<export-import
|
||||
<div
|
||||
v-if="themeWarning"
|
||||
class="theme-warning"
|
||||
>
|
||||
<div class="alert warning">
|
||||
{{ themeWarningHelp }}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<template v-if="themeWarning.type === 'snapshot_source_mismatch'">
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceLoad"
|
||||
>
|
||||
{{ $t('settings.style.switcher.use_source') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceSnapshot"
|
||||
>
|
||||
{{ $t('settings.style.switcher.use_snapshot') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="themeWarning.noActionsPossible">
|
||||
<button
|
||||
class="btn"
|
||||
@click="dismissWarning"
|
||||
>
|
||||
{{ $t('general.dismiss') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceLoad"
|
||||
>
|
||||
{{ $t('settings.style.switcher.load_theme') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="dismissWarning"
|
||||
>
|
||||
{{ $t('settings.style.switcher.keep_as_is') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ExportImport
|
||||
:export-object="exportedTheme"
|
||||
:export-label="$t("settings.export_theme")"
|
||||
:import-label="$t("settings.import_theme")"
|
||||
|
@ -27,8 +73,8 @@
|
|||
:key="style.name"
|
||||
:value="style"
|
||||
:style="{
|
||||
backgroundColor: style[1] || style.theme.colors.bg,
|
||||
color: style[3] || style.theme.colors.text
|
||||
backgroundColor: style[1] || (style.theme || style.source).colors.bg,
|
||||
color: style[3] || (style.theme || style.source).colors.text
|
||||
}"
|
||||
>
|
||||
{{ style[0] || style.name }}
|
||||
|
@ -38,7 +84,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</export-import>
|
||||
</ExportImport>
|
||||
</div>
|
||||
<div class="save-load-options">
|
||||
<span class="keep-option">
|
||||
|
@ -70,9 +116,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-container">
|
||||
<preview :style="previewRules" />
|
||||
</div>
|
||||
|
||||
<keep-alive>
|
||||
<tab-switcher key="style-tweak">
|
||||
|
@ -106,7 +150,7 @@
|
|||
<OpacityInput
|
||||
v-model="bgOpacityLocal"
|
||||
name="bgOpacity"
|
||||
:fallback="previewTheme.opacity.bg || 1"
|
||||
:fallback="previewTheme.opacity.bg"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="textColorLocal"
|
||||
|
@ -114,10 +158,19 @@
|
|||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgText" />
|
||||
<ColorInput
|
||||
v-model="accentColorLocal"
|
||||
name="accentColor"
|
||||
:fallback="previewTheme.colors.link"
|
||||
:label="$t('settings.accent')"
|
||||
:show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="linkColorLocal"
|
||||
name="linkColor"
|
||||
:fallback="previewTheme.colors.accent"
|
||||
:label="$t('settings.links')"
|
||||
:show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgLink" />
|
||||
</div>
|
||||
|
@ -148,13 +201,13 @@
|
|||
name="cRedColor"
|
||||
:label="$t('settings.cRed')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgRed" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCRed" />
|
||||
<ColorInput
|
||||
v-model="cBlueColorLocal"
|
||||
name="cBlueColor"
|
||||
:label="$t('settings.cBlue')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgBlue" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCBlue" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<ColorInput
|
||||
|
@ -162,13 +215,13 @@
|
|||
name="cGreenColor"
|
||||
:label="$t('settings.cGreen')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgGreen" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCGreen" />
|
||||
<ColorInput
|
||||
v-model="cOrangeColorLocal"
|
||||
name="cOrangeColor"
|
||||
:label="$t('settings.cOrange')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgOrange" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCOrange" />
|
||||
</div>
|
||||
<p>{{ $t('settings.theme_help_v2_2') }}</p>
|
||||
</div>
|
||||
|
@ -193,6 +246,14 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
|
||||
<ColorInput
|
||||
v-model="postLinkColorLocal"
|
||||
name="postLinkColor"
|
||||
:fallback="previewTheme.colors.accent"
|
||||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.postLink" />
|
||||
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
|
||||
<ColorInput
|
||||
v-model="alertErrorColorLocal"
|
||||
|
@ -200,14 +261,53 @@
|
|||
:label="$t('settings.style.advanced_colors.alert_error')"
|
||||
:fallback="previewTheme.colors.alertError"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.alertError" />
|
||||
<ColorInput
|
||||
v-model="alertErrorTextColorLocal"
|
||||
name="alertErrorText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertErrorText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertErrorText"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertWarningColorLocal"
|
||||
name="alertWarning"
|
||||
:label="$t('settings.style.advanced_colors.alert_warning')"
|
||||
:fallback="previewTheme.colors.alertWarning"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.alertWarning" />
|
||||
<ColorInput
|
||||
v-model="alertWarningTextColorLocal"
|
||||
name="alertWarningText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertWarningText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertWarningText"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertNeutralColorLocal"
|
||||
name="alertNeutral"
|
||||
:label="$t('settings.style.advanced_colors.alert_neutral')"
|
||||
:fallback="previewTheme.colors.alertNeutral"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertNeutralTextColorLocal"
|
||||
name="alertNeutralText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertNeutralText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertNeutralText"
|
||||
large="true"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="alertOpacityLocal"
|
||||
name="alertOpacity"
|
||||
:fallback="previewTheme.opacity.alert"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
|
||||
|
@ -217,19 +317,30 @@
|
|||
:label="$t('settings.style.advanced_colors.badge_notification')"
|
||||
:fallback="previewTheme.colors.badgeNotification"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="badgeNotificationTextColorLocal"
|
||||
name="badgeNotificationText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.badgeNotificationText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.badgeNotificationText"
|
||||
large="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
|
||||
<ColorInput
|
||||
v-model="panelColorLocal"
|
||||
name="panelColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.panel"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="panelOpacityLocal"
|
||||
name="panelOpacity"
|
||||
:fallback="previewTheme.opacity.panel || 1"
|
||||
:fallback="previewTheme.opacity.panel"
|
||||
:disabled="panelColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="panelTextColorLocal"
|
||||
|
@ -239,7 +350,7 @@
|
|||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.panelText"
|
||||
large="1"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="panelLinkColorLocal"
|
||||
|
@ -249,7 +360,7 @@
|
|||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.panelLink"
|
||||
large="1"
|
||||
large="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
|
@ -257,7 +368,7 @@
|
|||
<ColorInput
|
||||
v-model="topBarColorLocal"
|
||||
name="topBarColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.topBar"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
|
@ -280,13 +391,14 @@
|
|||
<ColorInput
|
||||
v-model="inputColorLocal"
|
||||
name="inputColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.input"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="inputOpacityLocal"
|
||||
name="inputOpacity"
|
||||
:fallback="previewTheme.opacity.input || 1"
|
||||
:fallback="previewTheme.opacity.input"
|
||||
:disabled="inputColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="inputTextColorLocal"
|
||||
|
@ -301,13 +413,14 @@
|
|||
<ColorInput
|
||||
v-model="btnColorLocal"
|
||||
name="btnColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.btn"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="btnOpacityLocal"
|
||||
name="btnOpacity"
|
||||
:fallback="previewTheme.opacity.btn || 1"
|
||||
:fallback="previewTheme.opacity.btn"
|
||||
:disabled="btnColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnTextColorLocal"
|
||||
|
@ -316,6 +429,124 @@
|
|||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnText" />
|
||||
<ColorInput
|
||||
v-model="btnPanelTextColorLocal"
|
||||
name="btnPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnTopBarTextColorLocal"
|
||||
name="btnTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnTopBarText" />
|
||||
<h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
|
||||
<ColorInput
|
||||
v-model="btnPressedColorLocal"
|
||||
name="btnPressedColor"
|
||||
:fallback="previewTheme.colors.btnPressed"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnPressedTextColorLocal"
|
||||
name="btnPressedTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedText" />
|
||||
<ColorInput
|
||||
v-model="btnPressedPanelTextColorLocal"
|
||||
name="btnPressedPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnPressedTopBarTextColorLocal"
|
||||
name="btnPressedTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
|
||||
<h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
|
||||
<ColorInput
|
||||
v-model="btnDisabledColorLocal"
|
||||
name="btnDisabledColor"
|
||||
:fallback="previewTheme.colors.btnDisabled"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledTextColorLocal"
|
||||
name="btnDisabledTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledPanelTextColorLocal"
|
||||
name="btnDisabledPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledTopBarTextColorLocal"
|
||||
name="btnDisabledTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
|
||||
<ColorInput
|
||||
v-model="btnToggledColorLocal"
|
||||
name="btnToggledColor"
|
||||
:fallback="previewTheme.colors.btnToggled"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnToggledTextColorLocal"
|
||||
name="btnToggledTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledText" />
|
||||
<ColorInput
|
||||
v-model="btnToggledPanelTextColorLocal"
|
||||
name="btnToggledPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnToggledTopBarTextColorLocal"
|
||||
name="btnToggledTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
|
||||
<ColorInput
|
||||
v-model="tabColorLocal"
|
||||
name="tabColor"
|
||||
:fallback="previewTheme.colors.tab"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="tabTextColorLocal"
|
||||
name="tabTextColor"
|
||||
:fallback="previewTheme.colors.tabText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.tabText" />
|
||||
<ColorInput
|
||||
v-model="tabActiveTextColorLocal"
|
||||
name="tabActiveTextColor"
|
||||
:fallback="previewTheme.colors.tabActiveText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.tabActiveText" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
|
||||
|
@ -328,7 +559,8 @@
|
|||
<OpacityInput
|
||||
v-model="borderOpacityLocal"
|
||||
name="borderOpacity"
|
||||
:fallback="previewTheme.opacity.border || 1"
|
||||
:fallback="previewTheme.opacity.border"
|
||||
:disabled="borderColorLocal === 'transparent'"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
|
@ -336,7 +568,7 @@
|
|||
<ColorInput
|
||||
v-model="faintColorLocal"
|
||||
name="faintColor"
|
||||
:fallback="previewTheme.colors.faint || 1"
|
||||
:fallback="previewTheme.colors.faint"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ColorInput
|
||||
|
@ -354,9 +586,146 @@
|
|||
<OpacityInput
|
||||
v-model="faintOpacityLocal"
|
||||
name="faintOpacity"
|
||||
:fallback="previewTheme.opacity.faint || 0.5"
|
||||
:fallback="previewTheme.opacity.faint"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4>
|
||||
<ColorInput
|
||||
v-model="underlayColorLocal"
|
||||
name="underlay"
|
||||
:label="$t('settings.style.advanced_colors.underlay')"
|
||||
:fallback="previewTheme.colors.underlay"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="underlayOpacityLocal"
|
||||
name="underlayOpacity"
|
||||
:fallback="previewTheme.opacity.underlay"
|
||||
:disabled="underlayOpacityLocal === 'transparent'"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
|
||||
<ColorInput
|
||||
v-model="pollColorLocal"
|
||||
name="poll"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.poll"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="pollTextColorLocal"
|
||||
name="pollText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.pollText"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
|
||||
<ColorInput
|
||||
v-model="iconColorLocal"
|
||||
name="icon"
|
||||
:label="$t('settings.style.advanced_colors.icons')"
|
||||
:fallback="previewTheme.colors.icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
|
||||
<ColorInput
|
||||
v-model="highlightColorLocal"
|
||||
name="highlight"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.highlight"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="highlightTextColorLocal"
|
||||
name="highlightText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.highlightText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.highlightText" />
|
||||
<ColorInput
|
||||
v-model="highlightLinkColorLocal"
|
||||
name="highlightLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.highlightLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.highlightLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.popover') }}</h4>
|
||||
<ColorInput
|
||||
v-model="popoverColorLocal"
|
||||
name="popover"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.popover"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="popoverOpacityLocal"
|
||||
name="popoverOpacity"
|
||||
:fallback="previewTheme.opacity.popover"
|
||||
:disabled="popoverOpacityLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="popoverTextColorLocal"
|
||||
name="popoverText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.popoverText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.popoverText" />
|
||||
<ColorInput
|
||||
v-model="popoverLinkColorLocal"
|
||||
name="popoverLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.popoverLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.popoverLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
|
||||
<ColorInput
|
||||
v-model="selectedPostColorLocal"
|
||||
name="selectedPost"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.selectedPost"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="selectedPostTextColorLocal"
|
||||
name="selectedPostText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.selectedPostText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedPostText" />
|
||||
<ColorInput
|
||||
v-model="selectedPostLinkColorLocal"
|
||||
name="selectedPostLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.selectedPostLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedPostLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
|
||||
<ColorInput
|
||||
v-model="selectedMenuColorLocal"
|
||||
name="selectedMenu"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.selectedMenu"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="selectedMenuTextColorLocal"
|
||||
name="selectedMenuText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.selectedMenuText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedMenuText" />
|
||||
<ColorInput
|
||||
v-model="selectedMenuLinkColorLocal"
|
||||
name="selectedMenuLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.selectedMenuLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -491,7 +860,7 @@
|
|||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
<shadow-control
|
||||
<ShadowControl
|
||||
v-model="currentShadow"
|
||||
:ready="!!currentShadowFallback"
|
||||
:fallback="currentShadowFallback"
|
||||
|
|
|
@ -52,6 +52,11 @@
|
|||
margin-bottom: 6px - 99px;
|
||||
white-space: nowrap;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--tabText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--tab, $fallback--fg);
|
||||
|
||||
&:not(.active) {
|
||||
z-index: 4;
|
||||
|
||||
|
@ -63,6 +68,8 @@
|
|||
&.active {
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
color: $fallback--text;
|
||||
color: var(--tabActiveText, $fallback--text);
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
@ -4,7 +4,6 @@ import ProgressButton from '../progress_button/progress_button.vue'
|
|||
import FollowButton from '../follow_button/follow_button.vue'
|
||||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||
import AccountActions from '../account_actions/account_actions.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
|
@ -30,22 +29,12 @@ export default {
|
|||
}]
|
||||
},
|
||||
style () {
|
||||
const color = this.$store.getters.mergedConfig.customTheme.colors
|
||||
? this.$store.getters.mergedConfig.customTheme.colors.bg // v2
|
||||
: this.$store.getters.mergedConfig.colors.bg // v1
|
||||
|
||||
if (color) {
|
||||
const rgb = (typeof color === 'string') ? hex2rgb(color) : color
|
||||
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
|
||||
|
||||
return {
|
||||
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
|
||||
backgroundImage: [
|
||||
`linear-gradient(to bottom, ${tintColor}, ${tintColor})`,
|
||||
`linear-gradient(to bottom, var(--profileTint), var(--profileTint))`,
|
||||
`url(${this.user.cover_photo})`
|
||||
].join(', ')
|
||||
}
|
||||
}
|
||||
},
|
||||
isOtherUser () {
|
||||
return this.user.id !== this.$store.state.users.currentUser.id
|
||||
|
|
|
@ -151,7 +151,7 @@
|
|||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-else
|
||||
class="btn btn-default pressed"
|
||||
class="btn btn-default toggled"
|
||||
:click="unsubscribeUser"
|
||||
:title="$t('user_card.unsubscribe')"
|
||||
>
|
||||
|
@ -162,7 +162,7 @@
|
|||
<div>
|
||||
<button
|
||||
v-if="user.muted"
|
||||
class="btn btn-default btn-block pressed"
|
||||
class="btn btn-default btn-block toggled"
|
||||
@click="unmuteUser"
|
||||
>
|
||||
{{ $t('user_card.muted') }}
|
||||
|
@ -286,6 +286,7 @@
|
|||
mask-size: 100% 60%;
|
||||
border-top-left-radius: calc(var(--panelRadius) - 1px);
|
||||
border-top-right-radius: calc(var(--panelRadius) - 1px);
|
||||
background-color: var(--profileBg);
|
||||
|
||||
&.hide-bio {
|
||||
mask-size: 100% 40px;
|
||||
|
@ -299,6 +300,11 @@
|
|||
&-bio {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
|
@ -460,14 +466,13 @@
|
|||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
// TODO use proper colors
|
||||
.staff {
|
||||
flex: none;
|
||||
text-transform: capitalize;
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
color: var(--alertNeutralText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
background-color: var(--alertNeutral, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -538,12 +543,6 @@
|
|||
|
||||
button {
|
||||
margin: 0;
|
||||
|
||||
&.pressed {
|
||||
// TODO: This should be themed.
|
||||
border-bottom-color: rgba(255, 255, 255, 0.2);
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
|
|||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
import BlockCard from '../block_card/block_card.vue'
|
||||
import MuteCard from '../mute_card/mute_card.vue'
|
||||
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
|
||||
import SelectableList from '../selectable_list/selectable_list.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import EmojiInput from '../emoji_input/emoji_input.vue'
|
||||
|
@ -32,6 +33,12 @@ const MuteList = withSubscription({
|
|||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const DomainMuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const UserSettings = {
|
||||
data () {
|
||||
return {
|
||||
|
@ -48,6 +55,7 @@ const UserSettings = {
|
|||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
pickAvatarBtnVisible: true,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
|
@ -67,7 +75,8 @@ const UserSettings = {
|
|||
changedPassword: false,
|
||||
changePasswordError: false,
|
||||
activeTab: 'profile',
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -80,10 +89,12 @@ const UserSettings = {
|
|||
ImageCropper,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
ProgressButton,
|
||||
Importer,
|
||||
Exporter,
|
||||
|
@ -152,6 +163,7 @@ const UserSettings = {
|
|||
hide_follows: this.hideFollows,
|
||||
hide_followers: this.hideFollowers,
|
||||
discoverable: this.discoverable,
|
||||
allow_following_move: this.allowFollowingMove,
|
||||
hide_follows_count: this.hideFollowsCount,
|
||||
hide_followers_count: this.hideFollowersCount,
|
||||
show_role: this.showRole
|
||||
|
@ -297,7 +309,7 @@ const UserSettings = {
|
|||
newPassword: this.changePasswordInputs[1],
|
||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changePassword({ params })
|
||||
this.$store.state.api.backendInteractor.changePassword(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedPassword = true
|
||||
|
@ -314,7 +326,7 @@ const UserSettings = {
|
|||
email: this.newEmail,
|
||||
password: this.changeEmailPassword
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changeEmail({ params })
|
||||
this.$store.state.api.backendInteractor.changeEmail(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedEmail = true
|
||||
|
@ -365,6 +377,13 @@ const UserSettings = {
|
|||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
unmuteDomains (domains) {
|
||||
return this.$store.dispatch('unmuteDomains', domains)
|
||||
},
|
||||
muteDomain () {
|
||||
return this.$store.dispatch('muteDomain', this.newDomainToMute)
|
||||
.then(() => { this.newDomainToMute = '' })
|
||||
},
|
||||
identity (value) {
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -90,9 +90,7 @@
|
|||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox
|
||||
v-model="hideFollowers"
|
||||
>
|
||||
<Checkbox v-model="hideFollowers">
|
||||
{{ $t('settings.hide_followers_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
|
@ -104,6 +102,11 @@
|
|||
{{ $t('settings.hide_followers_count_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="allowFollowingMove">
|
||||
{{ $t('settings.allow_following_move') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p v-if="role === 'admin' || role === 'moderator'">
|
||||
<Checkbox v-model="showRole">
|
||||
<template v-if="role === 'admin'">
|
||||
|
@ -509,6 +512,8 @@
|
|||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<tab-switcher>
|
||||
<div label="Users">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
|
@ -563,6 +568,59 @@
|
|||
</template>
|
||||
</MuteList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.domain_mutes')">
|
||||
<div class="profile-edit-domain-mute-form">
|
||||
<input
|
||||
v-model="newDomainToMute"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
type="text"
|
||||
@keyup.enter="muteDomain"
|
||||
>
|
||||
<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>
|
||||
</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 {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,43 @@
|
|||
{
|
||||
"about": {
|
||||
"staff": "Staff",
|
||||
"mrf": {
|
||||
"federation": "Federation",
|
||||
"keyword": {
|
||||
"keyword_policies": "Keyword Policies",
|
||||
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
|
||||
"reject": "Reject",
|
||||
"replace": "Replace",
|
||||
"is_replaced_by": "→"
|
||||
},
|
||||
"mrf_policies": "Enabled MRF Policies",
|
||||
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
|
||||
"mrf_policy_simple": "Instance-specific Policies",
|
||||
"mrf_policy_simple_accept": "Accept",
|
||||
"mrf_policy_simple_accept_desc": "This instance only accepts messages from the following instances:",
|
||||
"mrf_policy_simple_reject": "Reject",
|
||||
"mrf_policy_simple_reject_desc": "This instance will not accept messages from the following instances:",
|
||||
"mrf_policy_simple_quarantine": "Quarantine",
|
||||
"mrf_policy_simple_quarantine_desc": "This instance will send only public posts to the following instances:",
|
||||
"mrf_policy_simple_ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
|
||||
"mrf_policy_simple_ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
|
||||
"mrf_policy_simple_media_removal": "Media Removal",
|
||||
"mrf_policy_simple_media_removal_desc": "This instance removes media from posts on the following instances:",
|
||||
"mrf_policy_simple_media_nsfw": "Media Force-set As Sensitive",
|
||||
"mrf_policy_simple_media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
|
||||
"simple": {
|
||||
"simple_policies": "Instance-specific Policies",
|
||||
"accept": "Accept",
|
||||
"accept_desc": "This instance only accepts messages from the following instances:",
|
||||
"reject": "Reject",
|
||||
"reject_desc": "This instance will not accept messages from the following instances:",
|
||||
"quarantine": "Quarantine",
|
||||
"quarantine_desc": "This instance will send only public posts to the following instances:",
|
||||
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
|
||||
"ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
|
||||
"media_removal": "Media Removal",
|
||||
"media_removal_desc": "This instance removes media from posts on the following instances:",
|
||||
"media_nsfw": "Media Force-set As Sensitive",
|
||||
"media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
|
||||
}
|
||||
},
|
||||
"staff": "Staff"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat"
|
||||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Mute",
|
||||
"mute_progress": "Muting...",
|
||||
"unmute": "Unmute",
|
||||
"unmute_progress": "Unmuting..."
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Export",
|
||||
"processing": "Processing, you'll soon be asked to download your file"
|
||||
|
@ -46,6 +63,7 @@
|
|||
"optional": "optional",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
"dismiss": "Dismiss",
|
||||
"cancel": "Cancel",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
|
@ -111,7 +129,8 @@
|
|||
"read": "Read!",
|
||||
"repeated_you": "repeated your status",
|
||||
"no_more_notifications": "No more notifications",
|
||||
"migrated_to": "migrated to"
|
||||
"migrated_to": "migrated to",
|
||||
"reacted_with": "reacted with {0}"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Add Poll",
|
||||
|
@ -226,6 +245,7 @@
|
|||
"desc": "To enable two-factor authentication, enter the code from your two-factor app:"
|
||||
}
|
||||
},
|
||||
"allow_following_move": "Allow auto-follow when following account moves",
|
||||
"attachmentRadius": "Attachments",
|
||||
"attachments": "Attachments",
|
||||
"autoload": "Enable automatic loading when scrolled to the bottom",
|
||||
|
@ -264,8 +284,10 @@
|
|||
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
|
||||
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
|
||||
"discoverable": "Allow discovery of this account in search results and other services",
|
||||
"domain_mutes": "Domains",
|
||||
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
|
||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
|
||||
"export_theme": "Save preset",
|
||||
"filtering": "Filtering",
|
||||
"filtering_explanation": "All statuses containing these words will be muted, one per line",
|
||||
|
@ -274,6 +296,7 @@
|
|||
"follow_import": "Follow import",
|
||||
"follow_import_error": "Error importing followers",
|
||||
"follows_imported": "Follows imported! Processing them will take a while.",
|
||||
"accent": "Accent",
|
||||
"foreground": "Foreground",
|
||||
"general": "General",
|
||||
"hide_attachments_in_convo": "Hide attachments in conversations",
|
||||
|
@ -314,6 +337,7 @@
|
|||
"notification_visibility_mentions": "Mentions",
|
||||
"notification_visibility_repeats": "Repeats",
|
||||
"notification_visibility_moves": "User Migrates",
|
||||
"notification_visibility_emoji_reactions": "Reactions",
|
||||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||
"no_blocks": "No blocks",
|
||||
"no_mutes": "No mutes",
|
||||
|
@ -361,6 +385,7 @@
|
|||
"post_status_content_type": "Post status content type",
|
||||
"stop_gifs": "Play-on-hover GIFs",
|
||||
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
|
||||
"user_mutes": "Users",
|
||||
"useStreamingApi": "Receive posts and notifications real-time",
|
||||
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
|
||||
"text": "Text",
|
||||
|
@ -369,6 +394,7 @@
|
|||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||
"tooltipRadius": "Tooltips/alerts",
|
||||
"type_domains_to_mute": "Type in domains to mute",
|
||||
"upload_a_photo": "Upload a photo",
|
||||
"user_settings": "User Settings",
|
||||
"values": {
|
||||
|
@ -396,7 +422,24 @@
|
|||
"save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.",
|
||||
"reset": "Reset",
|
||||
"clear_all": "Clear all",
|
||||
"clear_opacity": "Clear opacity"
|
||||
"clear_opacity": "Clear opacity",
|
||||
"load_theme": "Load theme",
|
||||
"keep_as_is": "Keep as is",
|
||||
"use_snapshot": "Old version",
|
||||
"use_source": "New version",
|
||||
"help": {
|
||||
"upgraded_from_v2": "PleromaFE has been upgraded, theme could look a little bit different than you remember.",
|
||||
"v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.",
|
||||
"future_version_imported": "File you imported was made in newer version of FE.",
|
||||
"older_version_imported": "File you imported was made in older version of FE.",
|
||||
"snapshot_present": "Theme snapshot is loaded, so all values are overriden. You can load theme's actual data instead.",
|
||||
"snapshot_missing": "No theme snapshot was in the file so it could look different than originally envisioned.",
|
||||
"fe_upgraded": "PleromaFE's theme engine upgraded after version update.",
|
||||
"fe_downgraded": "PleromaFE's version rolled back.",
|
||||
"migration_snapshot_ok": "Just to be safe, theme snapshot loaded. You can try loading theme data.",
|
||||
"migration_napshot_gone": "For whatever reason snapshot was missing, some stuff could look different than you remember.",
|
||||
"snapshot_source_mismatch": "Versions conflict: most likely FE was rolled back and updated again, if you changed theme using older version of FE you most likely want to use old version, otherwise use new version."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"color": "Color",
|
||||
|
@ -425,14 +468,27 @@
|
|||
"alert": "Alert background",
|
||||
"alert_error": "Error",
|
||||
"alert_warning": "Warning",
|
||||
"alert_neutral": "Neutral",
|
||||
"post": "Posts/User bios",
|
||||
"badge": "Badge background",
|
||||
"popover": "Tooltips, menus, popovers",
|
||||
"badge_notification": "Notification",
|
||||
"panel_header": "Panel header",
|
||||
"top_bar": "Top bar",
|
||||
"borders": "Borders",
|
||||
"buttons": "Buttons",
|
||||
"inputs": "Input fields",
|
||||
"faint_text": "Faded text"
|
||||
"faint_text": "Faded text",
|
||||
"underlay": "Underlay",
|
||||
"poll": "Poll graph",
|
||||
"icons": "Icons",
|
||||
"highlight": "Highlighted elements",
|
||||
"pressed": "Pressed",
|
||||
"selectedPost": "Selected post",
|
||||
"selectedMenu": "Selected menu item",
|
||||
"disabled": "Disabled",
|
||||
"toggled": "Toggled",
|
||||
"tabs": "Tabs"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Roundness"
|
||||
|
@ -445,7 +501,7 @@
|
|||
"blur": "Blur",
|
||||
"spread": "Spread",
|
||||
"inset": "Inset",
|
||||
"hint": "For shadows you can also use --variable as a color value to use CSS3 variables. Please note that setting opacity won't work in this case.",
|
||||
"hintV3": "For shadows you can also use the {0} notation to use other color slot.",
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
|
||||
"drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.",
|
||||
|
@ -639,6 +695,7 @@
|
|||
"repeat": "Repeat",
|
||||
"reply": "Reply",
|
||||
"favorite": "Favorite",
|
||||
"add_reaction": "Add Reaction",
|
||||
"user_settings": "User Settings"
|
||||
},
|
||||
"upload":{
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
"notifications": "Ilmoitukset",
|
||||
"read": "Lue!",
|
||||
"repeated_you": "toisti viestisi",
|
||||
"no_more_notifications": "Ei enempää ilmoituksia"
|
||||
"no_more_notifications": "Ei enempää ilmoituksia",
|
||||
"reacted_with": "lisäsi reaktion {0}"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Lisää äänestys",
|
||||
|
@ -140,6 +141,7 @@
|
|||
"delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
|
||||
"delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
|
||||
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
|
||||
"emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
|
||||
"export_theme": "Tallenna teema",
|
||||
"filtering": "Suodatus",
|
||||
"filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.",
|
||||
|
@ -183,6 +185,7 @@
|
|||
"notification_visibility_likes": "Tykkäykset",
|
||||
"notification_visibility_mentions": "Maininnat",
|
||||
"notification_visibility_repeats": "Toistot",
|
||||
"notification_visibility_emoji_reactions": "Reaktiot",
|
||||
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
|
||||
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
|
||||
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
{
|
||||
"about": {
|
||||
"staff": "スタッフ",
|
||||
"mrf": {
|
||||
"federation": "フェデレーション",
|
||||
"mrf_policies": "ゆうこうなMRFポリシー",
|
||||
"mrf_policies_desc": "MRFポリシーは、このインスタンスのフェデレーションのふるまいを、いじります。これらのMRFポリシーがゆうこうになっています:",
|
||||
"mrf_policy_simple": "インスタンスのポリシー",
|
||||
"mrf_policy_simple_accept": "うけいれ",
|
||||
"mrf_policy_simple_accept_desc": "このインスンスは、これらのインスタンスからのメッセージのみをうけいれます:",
|
||||
"mrf_policy_simple_reject": "おことわり",
|
||||
"mrf_policy_simple_reject_desc": "このインスタンスは、これらのインスタンスからのメッセージをうけいれません:",
|
||||
"mrf_policy_simple_quarantine": "けんえき",
|
||||
"mrf_policy_simple_quarantine_desc": "このインスタンスは、これらのインスタンスに、パブリックなとうこうのみを、おくります:",
|
||||
"mrf_policy_simple_ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
|
||||
"mrf_policy_simple_ftl_removal_desc": "このインスタンスは、つながっているすべてのネットワーク」タイムラインから、これらのインスタンスを、とりのぞきます:",
|
||||
"mrf_policy_simple_media_removal": "メディアをのぞく",
|
||||
"mrf_policy_simple_media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
|
||||
"mrf_policy_simple_media_nsfw": "メディアをすべてセンシティブにする",
|
||||
"mrf_policy_simple_media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
|
||||
"simple": {
|
||||
"simple_policies": "インスタンスのポリシー",
|
||||
"accept": "うけいれ",
|
||||
"accept_desc": "このインスンスは、これらのインスタンスからのメッセージのみをうけいれます:",
|
||||
"reject": "おことわり",
|
||||
"reject_desc": "このインスタンスは、これらのインスタンスからのメッセージをうけいれません:",
|
||||
"quarantine": "けんえき",
|
||||
"quarantine_desc": "このインスタンスは、これらのインスタンスに、パブリックなとうこうのみを、おくります:",
|
||||
"ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
|
||||
"ftl_removal_desc": "このインスタンスは、つながっているすべてのネットワーク」タイムラインから、これらのインスタンスを、とりのぞきます:",
|
||||
"media_removal": "メディアをのぞく",
|
||||
"media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
|
||||
"media_nsfw": "メディアをすべてセンシティブにする",
|
||||
"media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
|
||||
}
|
||||
},
|
||||
"staff": "スタッフ"
|
||||
},
|
||||
"chat": {
|
||||
"title": "チャット"
|
||||
|
|
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 Vuex from 'vuex'
|
||||
|
||||
import 'custom-event-polyfill'
|
||||
import './lib/event_target_polyfill.js'
|
||||
|
||||
import interfaceModule from './modules/interface.js'
|
||||
import instanceModule from './modules/instance.js'
|
||||
import statusesModule from './modules/statuses.js'
|
||||
|
@ -28,7 +31,6 @@ import VueChatScroll from 'vue-chat-scroll'
|
|||
import VueClickOutside from 'v-click-outside'
|
||||
import PortalVue from 'portal-vue'
|
||||
import VBodyScrollLock from './directives/body_scroll_lock'
|
||||
import VTooltip from 'v-tooltip'
|
||||
|
||||
import afterStoreSetup from './boot/after_store.js'
|
||||
|
||||
|
@ -41,13 +43,6 @@ Vue.use(VueChatScroll)
|
|||
Vue.use(VueClickOutside)
|
||||
Vue.use(PortalVue)
|
||||
Vue.use(VBodyScrollLock)
|
||||
Vue.use(VTooltip, {
|
||||
popover: {
|
||||
defaultTrigger: 'hover click',
|
||||
defaultContainer: false,
|
||||
defaultOffset: 5
|
||||
}
|
||||
})
|
||||
|
||||
const i18n = new VueI18n({
|
||||
// By default, use the browser locale, we will update it if neccessary
|
||||
|
|
|
@ -146,6 +146,7 @@ const api = {
|
|||
startFetchingFollowRequests (store) {
|
||||
if (store.state.fetchers['followRequests']) return
|
||||
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
|
||||
|
||||
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
|
||||
},
|
||||
stopFetchingFollowRequests (store) {
|
||||
|
|
|
@ -5,6 +5,9 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
|||
|
||||
export const defaultState = {
|
||||
colors: {},
|
||||
theme: undefined,
|
||||
customTheme: undefined,
|
||||
customThemeSource: undefined,
|
||||
hideISP: false,
|
||||
// bad name: actually hides posts of muted USERS
|
||||
hideMutedPosts: undefined, // instance default
|
||||
|
@ -20,6 +23,7 @@ export const defaultState = {
|
|||
autoLoad: true,
|
||||
streaming: false,
|
||||
hoverPreview: true,
|
||||
emojiReactionsOnTimeline: true,
|
||||
autohideFloatingPostButton: false,
|
||||
pauseOnUnfocused: true,
|
||||
stopGifs: false,
|
||||
|
@ -29,7 +33,8 @@ export const defaultState = {
|
|||
mentions: true,
|
||||
likes: true,
|
||||
repeats: true,
|
||||
moves: true
|
||||
moves: true,
|
||||
emojiReactions: false
|
||||
},
|
||||
webPushNotifications: false,
|
||||
muteWords: [],
|
||||
|
@ -94,10 +99,10 @@ const config = {
|
|||
commit('setOption', { name, value })
|
||||
switch (name) {
|
||||
case 'theme':
|
||||
setPreset(value, commit)
|
||||
setPreset(value)
|
||||
break
|
||||
case 'customTheme':
|
||||
applyTheme(value, commit)
|
||||
applyTheme(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { set } from 'vue'
|
||||
import { setPreset } from '../services/style_setter/style_setter.js'
|
||||
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
import { instanceDefaultProperties } from './config.js'
|
||||
|
||||
const defaultState = {
|
||||
|
@ -10,6 +11,7 @@ const defaultState = {
|
|||
textlimit: 5000,
|
||||
server: 'http://localhost:4040/',
|
||||
theme: 'pleroma-dark',
|
||||
themeData: undefined,
|
||||
background: '/static/aurora_borealis.jpg',
|
||||
logo: '/static/logo.png',
|
||||
logoMask: true,
|
||||
|
@ -96,6 +98,9 @@ const instance = {
|
|||
dispatch('initializeSocket')
|
||||
}
|
||||
break
|
||||
case 'theme':
|
||||
dispatch('setTheme', value)
|
||||
break
|
||||
}
|
||||
},
|
||||
async getStaticEmoji ({ commit }) {
|
||||
|
@ -147,9 +152,23 @@ const instance = {
|
|||
}
|
||||
},
|
||||
|
||||
setTheme ({ commit }, themeName) {
|
||||
setTheme ({ commit, rootState }, themeName) {
|
||||
commit('setInstanceOption', { name: 'theme', value: themeName })
|
||||
return setPreset(themeName, commit)
|
||||
getPreset(themeName)
|
||||
.then(themeData => {
|
||||
commit('setInstanceOption', { name: 'themeData', value: themeData })
|
||||
// No need to apply theme if there's user theme already
|
||||
const { customTheme } = rootState.config
|
||||
if (customTheme) return
|
||||
|
||||
// New theme presets don't have 'theme' property, they use 'source'
|
||||
const themeSource = themeData.source
|
||||
if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
|
||||
applyTheme(themeSource)
|
||||
} else {
|
||||
applyTheme(themeData.theme)
|
||||
}
|
||||
})
|
||||
},
|
||||
fetchEmoji ({ dispatch, state }) {
|
||||
if (!state.customEmojiFetched) {
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
|
||||
import {
|
||||
remove,
|
||||
slice,
|
||||
each,
|
||||
findIndex,
|
||||
find,
|
||||
maxBy,
|
||||
minBy,
|
||||
merge,
|
||||
first,
|
||||
last,
|
||||
isArray,
|
||||
omitBy
|
||||
} from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import apiService from '../services/api/api.service.js'
|
||||
// import parse from '../services/status_parser/status_parser.js'
|
||||
|
@ -68,7 +81,8 @@ const visibleNotificationTypes = (rootState) => {
|
|||
rootState.config.notificationVisibility.mentions && 'mention',
|
||||
rootState.config.notificationVisibility.repeats && 'repeat',
|
||||
rootState.config.notificationVisibility.follows && 'follow',
|
||||
rootState.config.notificationVisibility.moves && 'move'
|
||||
rootState.config.notificationVisibility.moves && 'move',
|
||||
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
|
||||
].filter(_ => _)
|
||||
}
|
||||
|
||||
|
@ -312,6 +326,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
|
||||
}
|
||||
|
||||
if (notification.type === 'pleroma:emoji_reaction') {
|
||||
dispatch('fetchEmojiReactionsBy', notification.status.id)
|
||||
}
|
||||
|
||||
// Only add a new notification if we don't have one for the same action
|
||||
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
|
||||
state.notifications.maxId = notification.id > state.notifications.maxId
|
||||
|
@ -345,7 +363,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
break
|
||||
}
|
||||
|
||||
if (i18nString) {
|
||||
if (notification.type === 'pleroma:emoji_reaction') {
|
||||
notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
|
||||
} else if (i18nString) {
|
||||
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
|
||||
} else {
|
||||
notifObj.body = notification.status.text
|
||||
|
@ -358,10 +378,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
}
|
||||
|
||||
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
|
||||
let notification = new window.Notification(title, notifObj)
|
||||
let desktopNotification = new window.Notification(title, notifObj)
|
||||
// Chrome is known for not closing notifications automatically
|
||||
// according to MDN, anyway.
|
||||
setTimeout(notification.close.bind(notification), 5000)
|
||||
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
|
||||
}
|
||||
}
|
||||
} else if (notification.seen) {
|
||||
|
@ -518,6 +538,53 @@ export const mutations = {
|
|||
newStatus.fave_num = newStatus.favoritedBy.length
|
||||
newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
|
||||
},
|
||||
addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
set(status, 'emoji_reactions', emojiReactions)
|
||||
},
|
||||
addOwnReaction (state, { id, emoji, currentUser }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
|
||||
const reaction = status.emoji_reactions[reactionIndex] || { name: emoji, count: 0, accounts: [] }
|
||||
|
||||
const newReaction = {
|
||||
...reaction,
|
||||
count: reaction.count + 1,
|
||||
me: true,
|
||||
accounts: [
|
||||
...reaction.accounts,
|
||||
currentUser
|
||||
]
|
||||
}
|
||||
|
||||
// Update count of existing reaction if it exists, otherwise append at the end
|
||||
if (reactionIndex >= 0) {
|
||||
set(status.emoji_reactions, reactionIndex, newReaction)
|
||||
} else {
|
||||
set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction])
|
||||
}
|
||||
},
|
||||
removeOwnReaction (state, { id, emoji, currentUser }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
|
||||
if (reactionIndex < 0) return
|
||||
|
||||
const reaction = status.emoji_reactions[reactionIndex]
|
||||
const accounts = reaction.accounts || []
|
||||
|
||||
const newReaction = {
|
||||
...reaction,
|
||||
count: reaction.count - 1,
|
||||
me: false,
|
||||
accounts: accounts.filter(acc => acc.id !== currentUser.id)
|
||||
}
|
||||
|
||||
if (newReaction.count > 0) {
|
||||
set(status.emoji_reactions, reactionIndex, newReaction)
|
||||
} else {
|
||||
set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji))
|
||||
}
|
||||
},
|
||||
updateStatusWithPoll (state, { id, poll }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
status.poll = poll
|
||||
|
@ -622,6 +689,35 @@ const statuses = {
|
|||
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
|
||||
})
|
||||
},
|
||||
reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
|
||||
const currentUser = rootState.users.currentUser
|
||||
if (!currentUser) return
|
||||
|
||||
commit('addOwnReaction', { id, emoji, currentUser })
|
||||
rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
|
||||
ok => {
|
||||
dispatch('fetchEmojiReactionsBy', id)
|
||||
}
|
||||
)
|
||||
},
|
||||
unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
|
||||
const currentUser = rootState.users.currentUser
|
||||
if (!currentUser) return
|
||||
|
||||
commit('removeOwnReaction', { id, emoji, currentUser })
|
||||
rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
|
||||
ok => {
|
||||
dispatch('fetchEmojiReactionsBy', id)
|
||||
}
|
||||
)
|
||||
},
|
||||
fetchEmojiReactionsBy ({ rootState, commit }, id) {
|
||||
rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
|
||||
emojiReactions => {
|
||||
commit('addEmojiReactionsBy', { id, emojiReactions, currentUser: rootState.users.currentUser })
|
||||
}
|
||||
)
|
||||
},
|
||||
fetchFavs ({ rootState, commit }, id) {
|
||||
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
|
||||
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
|
||||
|
|
|
@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
|
|||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
}
|
||||
|
||||
const muteDomain = (store, domain) => {
|
||||
return store.rootState.api.backendInteractor.muteDomain({ domain })
|
||||
.then(() => store.commit('addDomainMute', domain))
|
||||
}
|
||||
|
||||
const unmuteDomain = (store, domain) => {
|
||||
return store.rootState.api.backendInteractor.unmuteDomain({ domain })
|
||||
.then(() => store.commit('removeDomainMute', domain))
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setMuted (state, { user: { id }, muted }) {
|
||||
const user = state.usersObject[id]
|
||||
|
@ -177,6 +187,20 @@ export const mutations = {
|
|||
state.currentUser.muteIds.push(muteId)
|
||||
}
|
||||
},
|
||||
saveDomainMutes (state, domainMutes) {
|
||||
state.currentUser.domainMutes = domainMutes
|
||||
},
|
||||
addDomainMute (state, domain) {
|
||||
if (state.currentUser.domainMutes.indexOf(domain) === -1) {
|
||||
state.currentUser.domainMutes.push(domain)
|
||||
}
|
||||
},
|
||||
removeDomainMute (state, domain) {
|
||||
const index = state.currentUser.domainMutes.indexOf(domain)
|
||||
if (index !== -1) {
|
||||
state.currentUser.domainMutes.splice(index, 1)
|
||||
}
|
||||
},
|
||||
setPinnedToUser (state, status) {
|
||||
const user = state.usersObject[status.user.id]
|
||||
const index = user.pinnedStatusIds.indexOf(status.id)
|
||||
|
@ -297,6 +321,25 @@ const users = {
|
|||
unmuteUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => unmuteUser(store, id)))
|
||||
},
|
||||
fetchDomainMutes (store) {
|
||||
return store.rootState.api.backendInteractor.fetchDomainMutes()
|
||||
.then((domainMutes) => {
|
||||
store.commit('saveDomainMutes', domainMutes)
|
||||
return domainMutes
|
||||
})
|
||||
},
|
||||
muteDomain (store, domain) {
|
||||
return muteDomain(store, domain)
|
||||
},
|
||||
unmuteDomain (store, domain) {
|
||||
return unmuteDomain(store, domain)
|
||||
},
|
||||
muteDomains (store, domains = []) {
|
||||
return Promise.all(domains.map(domain => muteDomain(store, domain)))
|
||||
},
|
||||
unmuteDomains (store, domain = []) {
|
||||
return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
|
||||
},
|
||||
fetchFriends ({ rootState, commit }, id) {
|
||||
const user = rootState.users.usersObject[id]
|
||||
const maxId = last(user.friendIds)
|
||||
|
@ -331,9 +374,9 @@ const users = {
|
|||
return rootState.api.backendInteractor.unsubscribeUser({ id })
|
||||
.then((relationship) => commit('updateUserRelationship', [relationship]))
|
||||
},
|
||||
toggleActivationStatus ({ rootState, commit }, user) {
|
||||
toggleActivationStatus ({ rootState, commit }, { user }) {
|
||||
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
|
||||
api(user)
|
||||
api({ user })
|
||||
.then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
|
||||
},
|
||||
registerPushNotifications (store) {
|
||||
|
@ -460,6 +503,7 @@ const users = {
|
|||
user.credentials = accessToken
|
||||
user.blockIds = []
|
||||
user.muteIds = []
|
||||
user.domainMutes = []
|
||||
commit('setCurrentUser', user)
|
||||
commit('addNewUsers', [user])
|
||||
|
||||
|
|
|
@ -72,7 +72,11 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
|
|||
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
|
||||
const MASTODON_SEARCH_2 = `/api/v2/search`
|
||||
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
|
||||
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
|
||||
const MASTODON_STREAMING = '/api/v1/streaming'
|
||||
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
||||
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
||||
|
@ -398,8 +402,8 @@ const fetchStatus = ({ id, credentials }) => {
|
|||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const tagUser = ({ tag, credentials, ...options }) => {
|
||||
const screenName = options.screen_name
|
||||
const tagUser = ({ tag, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
const form = {
|
||||
nicknames: [screenName],
|
||||
tags: [tag]
|
||||
|
@ -415,8 +419,8 @@ const tagUser = ({ tag, credentials, ...options }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const untagUser = ({ tag, credentials, ...options }) => {
|
||||
const screenName = options.screen_name
|
||||
const untagUser = ({ tag, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
const body = {
|
||||
nicknames: [screenName],
|
||||
tags: [tag]
|
||||
|
@ -432,7 +436,7 @@ const untagUser = ({ tag, credentials, ...options }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const addRight = ({ right, credentials, ...user }) => {
|
||||
const addRight = ({ right, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
|
||||
return fetch(PERMISSION_GROUP_URL(screenName, right), {
|
||||
|
@ -442,7 +446,7 @@ const addRight = ({ right, credentials, ...user }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const deleteRight = ({ right, credentials, ...user }) => {
|
||||
const deleteRight = ({ right, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
|
||||
return fetch(PERMISSION_GROUP_URL(screenName, right), {
|
||||
|
@ -474,7 +478,7 @@ const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
|
|||
}).then(response => get(response, 'users.0'))
|
||||
}
|
||||
|
||||
const deleteUser = ({ credentials, ...user }) => {
|
||||
const deleteUser = ({ credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
const headers = authHeaders(credentials)
|
||||
|
||||
|
@ -491,7 +495,8 @@ const fetchTimeline = ({
|
|||
until = false,
|
||||
userId = false,
|
||||
tag = false,
|
||||
withMuted = false
|
||||
withMuted = false,
|
||||
withMove = false
|
||||
}) => {
|
||||
const timelineUrls = {
|
||||
public: MASTODON_PUBLIC_TIMELINE,
|
||||
|
@ -531,6 +536,9 @@ const fetchTimeline = ({
|
|||
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
||||
params.push(['only_media', false])
|
||||
}
|
||||
if (timeline === 'notifications') {
|
||||
params.push(['with_move', withMove])
|
||||
}
|
||||
|
||||
params.push(['count', 20])
|
||||
params.push(['with_muted', withMuted])
|
||||
|
@ -880,6 +888,30 @@ const fetchRebloggedByUsers = ({ id }) => {
|
|||
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
|
||||
}
|
||||
|
||||
const fetchEmojiReactions = ({ id, credentials }) => {
|
||||
return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials })
|
||||
.then((reactions) => reactions.map(r => {
|
||||
r.accounts = r.accounts.map(parseUser)
|
||||
return r
|
||||
}))
|
||||
}
|
||||
|
||||
const reactWithEmoji = ({ id, emoji, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_EMOJI_REACT_URL(id, emoji),
|
||||
method: 'PUT',
|
||||
credentials
|
||||
}).then(parseStatus)
|
||||
}
|
||||
|
||||
const unreactWithEmoji = ({ id, emoji, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
|
||||
method: 'DELETE',
|
||||
credentials
|
||||
}).then(parseStatus)
|
||||
}
|
||||
|
||||
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_REPORT_USER_URL,
|
||||
|
@ -948,6 +980,28 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const fetchDomainMutes = ({ credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
|
||||
}
|
||||
|
||||
const muteDomain = ({ domain, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||
method: 'POST',
|
||||
payload: { domain },
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
const unmuteDomain = ({ domain, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||
method: 'DELETE',
|
||||
payload: { domain },
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
|
||||
return Object.entries({
|
||||
...(credentials
|
||||
|
@ -1107,10 +1161,16 @@ const apiService = {
|
|||
fetchPoll,
|
||||
fetchFavoritedByUsers,
|
||||
fetchRebloggedByUsers,
|
||||
fetchEmojiReactions,
|
||||
reactWithEmoji,
|
||||
unreactWithEmoji,
|
||||
reportUser,
|
||||
updateNotificationSettings,
|
||||
search2,
|
||||
searchUsers
|
||||
searchUsers,
|
||||
fetchDomainMutes,
|
||||
muteDomain,
|
||||
unmuteDomain
|
||||
}
|
||||
|
||||
export default apiService
|
||||
|
|
|
@ -16,7 +16,7 @@ const backendInteractorService = credentials => ({
|
|||
return notificationsFetcher.fetchAndUpdate({ store, credentials })
|
||||
},
|
||||
|
||||
startFetchingFollowRequest ({ store }) {
|
||||
startFetchingFollowRequests ({ store }) {
|
||||
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') {
|
||||
return undefined
|
||||
}
|
||||
if (r[0] === '#') {
|
||||
// TODO: clean up this mess
|
||||
if (r[0] === '#' || r === 'transparent') {
|
||||
return r
|
||||
}
|
||||
if (typeof r === 'object') {
|
||||
({ r, g, b } = r)
|
||||
}
|
||||
[r, g, b] = map([r, g, b], (val) => {
|
||||
[r, g, b] = [r, g, b].map(val => {
|
||||
val = Math.ceil(val)
|
||||
val = val < 0 ? 0 : val
|
||||
val = val > 255 ? 255 : val
|
||||
|
@ -58,7 +69,7 @@ const srgbToLinear = (srgb) => {
|
|||
* @param {Object} srgb - sRGB color
|
||||
* @returns {Number} relative luminance
|
||||
*/
|
||||
const relativeLuminance = (srgb) => {
|
||||
export const relativeLuminance = (srgb) => {
|
||||
const { r, g, b } = srgbToLinear(srgb)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
|
@ -71,7 +82,7 @@ const relativeLuminance = (srgb) => {
|
|||
* @param {Object} b - sRGB color
|
||||
* @returns {Number} color ratio
|
||||
*/
|
||||
const getContrastRatio = (a, b) => {
|
||||
export const getContrastRatio = (a, b) => {
|
||||
const la = relativeLuminance(a)
|
||||
const lb = relativeLuminance(b)
|
||||
const [l1, l2] = la > lb ? [la, lb] : [lb, la]
|
||||
|
@ -79,6 +90,17 @@ const getContrastRatio = (a, b) => {
|
|||
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
|
||||
*
|
||||
|
@ -87,7 +109,7 @@ const getContrastRatio = (a, b) => {
|
|||
* @param {Object} bg - bottom layer color
|
||||
* @returns {Object} sRGB of resulting color
|
||||
*/
|
||||
const alphaBlend = (fg, fga, bg) => {
|
||||
export const alphaBlend = (fg, fga, bg) => {
|
||||
if (fga === 1 || typeof fga === 'undefined') return fg
|
||||
return 'rgb'.split('').reduce((acc, c) => {
|
||||
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
|
||||
|
@ -97,14 +119,30 @@ const alphaBlend = (fg, fga, bg) => {
|
|||
}, {})
|
||||
}
|
||||
|
||||
const invert = (rgb) => {
|
||||
/**
|
||||
* Same as `alphaBlend` but for multiple layers in-between
|
||||
*
|
||||
* @param {Object} bedrock - layer at the very bottom
|
||||
* @param {[Object, Number]} layers[] - layers between text and bedrock
|
||||
*/
|
||||
export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color, opacity]) => {
|
||||
return alphaBlend(color, opacity, acc)
|
||||
}, bedrock)
|
||||
|
||||
export const invert = (rgb) => {
|
||||
return 'rgb'.split('').reduce((acc, c) => {
|
||||
acc[c] = 255 - rgb[c]
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const hex2rgb = (hex) => {
|
||||
/**
|
||||
* Converts #rrggbb hex notation into an {r, g, b} object
|
||||
*
|
||||
* @param {String} hex - #rrggbb string
|
||||
* @returns {Object} rgb representation of the color, values are 0-255
|
||||
*/
|
||||
export const hex2rgb = (hex) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
|
@ -113,18 +151,72 @@ const hex2rgb = (hex) => {
|
|||
} : null
|
||||
}
|
||||
|
||||
const mixrgb = (a, b) => {
|
||||
return Object.keys(a).reduce((acc, k) => {
|
||||
/**
|
||||
* Old somewhat weird function for mixing two colors together
|
||||
*
|
||||
* @param {Object} a - one color (rgb)
|
||||
* @param {Object} b - other color (rgb)
|
||||
* @returns {Object} result
|
||||
*/
|
||||
export const mixrgb = (a, b) => {
|
||||
return 'rgb'.split('').reduce((acc, k) => {
|
||||
acc[k] = (a[k] + b[k]) / 2
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export {
|
||||
rgb2hex,
|
||||
hex2rgb,
|
||||
mixrgb,
|
||||
invert,
|
||||
getContrastRatio,
|
||||
alphaBlend
|
||||
/**
|
||||
* Converts rgb object into a CSS rgba() color
|
||||
*
|
||||
* @param {Object} color - rgb
|
||||
* @returns {String} CSS rgba() color
|
||||
*/
|
||||
export const rgba2css = function (rgba) {
|
||||
return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})`
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
if (status.is_post_verb) {
|
||||
return 'status'
|
||||
|
@ -41,7 +43,7 @@ export const parseUser = (data) => {
|
|||
}
|
||||
|
||||
output.name = data.display_name
|
||||
output.name_html = addEmojis(data.display_name, data.emojis)
|
||||
output.name_html = addEmojis(escape(data.display_name), data.emojis)
|
||||
|
||||
output.description = data.note
|
||||
output.description_html = addEmojis(data.note, data.emojis)
|
||||
|
@ -81,6 +83,8 @@ export const parseUser = (data) => {
|
|||
output.subscribed = relationship.subscribing
|
||||
}
|
||||
|
||||
output.allow_following_move = data.pleroma.allow_following_move
|
||||
|
||||
output.hide_follows = data.pleroma.hide_follows
|
||||
output.hide_followers = data.pleroma.hide_followers
|
||||
output.hide_follows_count = data.pleroma.hide_follows_count
|
||||
|
@ -242,6 +246,7 @@ export const parseStatus = (data) => {
|
|||
output.is_local = pleroma.local
|
||||
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
|
||||
output.thread_muted = pleroma.thread_muted
|
||||
output.emoji_reactions = pleroma.emoji_reactions
|
||||
} else {
|
||||
output.text = data.content
|
||||
output.summary = data.spoiler_text
|
||||
|
@ -255,7 +260,7 @@ export const parseStatus = (data) => {
|
|||
output.retweeted_status = parseStatus(data.reblog)
|
||||
}
|
||||
|
||||
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
|
||||
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
|
||||
output.external_url = data.url
|
||||
output.poll = data.poll
|
||||
output.pinned = data.pinned
|
||||
|
@ -349,6 +354,7 @@ export const parseNotification = (data) => {
|
|||
? null
|
||||
: parseUser(data.target)
|
||||
output.from_profile = parseUser(data.account)
|
||||
output.emoji = data.emoji
|
||||
} else {
|
||||
const parsedNotice = parseStatus(data.notice)
|
||||
output.type = data.ntype
|
||||
|
|
|
@ -7,7 +7,8 @@ export const visibleTypes = store => ([
|
|||
store.state.config.notificationVisibility.mentions && 'mention',
|
||||
store.state.config.notificationVisibility.repeats && 'repeat',
|
||||
store.state.config.notificationVisibility.follows && 'follow',
|
||||
store.state.config.notificationVisibility.moves && 'move'
|
||||
store.state.config.notificationVisibility.moves && 'move',
|
||||
store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
|
||||
].filter(_ => _))
|
||||
|
||||
const sortById = (a, b) => {
|
||||
|
|
|
@ -11,9 +11,12 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
|||
const rootState = store.rootState || store.state
|
||||
const timelineData = rootState.statuses.notifications
|
||||
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
|
||||
const allowFollowingMove = rootState.users.currentUser.allow_following_move
|
||||
|
||||
args['withMuted'] = !hideMutedPosts
|
||||
|
||||
args['withMove'] = !allowFollowingMove
|
||||
|
||||
args['timeline'] = 'notifications'
|
||||
if (older) {
|
||||
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
|
||||
|
|
|
@ -1,78 +1,9 @@
|
|||
import { times } from 'lodash'
|
||||
import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
|
||||
import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.js'
|
||||
import { convert } from 'chromatism'
|
||||
import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js'
|
||||
import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js'
|
||||
|
||||
// While this is not used anymore right now, I left it in if we want to do custom
|
||||
// styles that aren't just colors, so user can pick from a few different distinct
|
||||
// styles as well as set their own colors in the future.
|
||||
|
||||
const setStyle = (href, commit) => {
|
||||
/***
|
||||
What's going on here?
|
||||
I want to make it easy for admins to style this application. To have
|
||||
a good set of default themes, I chose the system from base16
|
||||
(https://chriskempson.github.io/base16/) to style all elements. They
|
||||
all have the base00..0F classes. So the only thing an admin needs to
|
||||
do to style Pleroma is to change these colors in that one css file.
|
||||
Some default things (body text color, link color) need to be set dy-
|
||||
namically, so this is done here by waiting for the stylesheet to be
|
||||
loaded and then creating an element with the respective classes.
|
||||
|
||||
It is a bit weird, but should make life for admins somewhat easier.
|
||||
***/
|
||||
const head = document.head
|
||||
const body = document.body
|
||||
body.classList.add('hidden')
|
||||
const cssEl = document.createElement('link')
|
||||
cssEl.setAttribute('rel', 'stylesheet')
|
||||
cssEl.setAttribute('href', href)
|
||||
head.appendChild(cssEl)
|
||||
|
||||
const setDynamic = () => {
|
||||
const baseEl = document.createElement('div')
|
||||
body.appendChild(baseEl)
|
||||
|
||||
let colors = {}
|
||||
times(16, (n) => {
|
||||
const name = `base0${n.toString(16).toUpperCase()}`
|
||||
baseEl.setAttribute('class', name)
|
||||
const color = window.getComputedStyle(baseEl).getPropertyValue('color')
|
||||
colors[name] = color
|
||||
})
|
||||
|
||||
body.removeChild(baseEl)
|
||||
|
||||
const styleEl = document.createElement('style')
|
||||
head.appendChild(styleEl)
|
||||
// const styleSheet = styleEl.sheet
|
||||
|
||||
body.classList.remove('hidden')
|
||||
}
|
||||
|
||||
cssEl.addEventListener('load', setDynamic)
|
||||
}
|
||||
|
||||
const rgb2rgba = function (rgba) {
|
||||
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
|
||||
}
|
||||
|
||||
const getTextColor = function (bg, text, preserve) {
|
||||
const bgIsLight = convert(bg).hsl.l > 50
|
||||
const textIsLight = convert(text).hsl.l > 50
|
||||
|
||||
if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
|
||||
const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
|
||||
const result = Object.assign(base, invertLightness(text).rgb)
|
||||
if (!preserve && getContrastRatio(bg, result) < 4.5) {
|
||||
return contrastRatio(bg, text).rgb
|
||||
}
|
||||
return result
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const applyTheme = (input, commit) => {
|
||||
const { rules, theme } = generatePreset(input)
|
||||
export const applyTheme = (input) => {
|
||||
const { rules } = generatePreset(input)
|
||||
const head = document.head
|
||||
const body = document.body
|
||||
body.classList.add('hidden')
|
||||
|
@ -87,14 +18,9 @@ const applyTheme = (input, commit) => {
|
|||
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
|
||||
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
|
||||
body.classList.remove('hidden')
|
||||
|
||||
// commit('setOption', { name: 'colors', value: htmlColors })
|
||||
// commit('setOption', { name: 'radii', value: radii })
|
||||
commit('setOption', { name: 'customTheme', value: input })
|
||||
commit('setOption', { name: 'colors', value: theme.colors })
|
||||
}
|
||||
|
||||
const getCssShadow = (input, usesDropShadow) => {
|
||||
export const getCssShadow = (input, usesDropShadow) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
@ -132,122 +58,18 @@ const getCssShadowFilter = (input) => {
|
|||
.join(' ')
|
||||
}
|
||||
|
||||
const getCssColor = (input, a) => {
|
||||
let rgb = {}
|
||||
if (typeof input === 'object') {
|
||||
rgb = input
|
||||
} else if (typeof input === 'string') {
|
||||
if (input.startsWith('#')) {
|
||||
rgb = hex2rgb(input)
|
||||
} else if (input.startsWith('--')) {
|
||||
return `var(${input})`
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
return rgb2rgba({ ...rgb, a })
|
||||
}
|
||||
export const generateColors = (themeData) => {
|
||||
const sourceColors = !themeData.themeEngineVersion
|
||||
? colors2to3(themeData.colors || themeData)
|
||||
: themeData.colors || themeData
|
||||
|
||||
const generateColors = (input) => {
|
||||
const colors = {}
|
||||
const opacity = Object.assign({
|
||||
alert: 0.5,
|
||||
input: 0.5,
|
||||
faint: 0.5
|
||||
}, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => {
|
||||
if (typeof v !== 'undefined') {
|
||||
acc[k] = v
|
||||
}
|
||||
return acc
|
||||
}, {}))
|
||||
const col = Object.entries(input.colors || input).reduce((acc, [k, v]) => {
|
||||
if (typeof v === 'object') {
|
||||
acc[k] = v
|
||||
} else {
|
||||
acc[k] = hex2rgb(v)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const isLightOnDark = convert(col.bg).hsl.l < convert(col.text).hsl.l
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
|
||||
colors.text = col.text
|
||||
colors.lightText = brightness(20 * mod, colors.text).rgb
|
||||
colors.link = col.link
|
||||
colors.faint = col.faint || Object.assign({}, col.text)
|
||||
|
||||
colors.bg = col.bg
|
||||
colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb
|
||||
|
||||
colors.fg = col.fg
|
||||
colors.fgText = col.fgText || getTextColor(colors.fg, colors.text)
|
||||
colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true)
|
||||
|
||||
colors.border = col.border || brightness(2 * mod, colors.fg).rgb
|
||||
|
||||
colors.btn = col.btn || Object.assign({}, col.fg)
|
||||
colors.btnText = col.btnText || getTextColor(colors.btn, colors.fgText)
|
||||
|
||||
colors.input = col.input || Object.assign({}, col.fg)
|
||||
colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText)
|
||||
|
||||
colors.panel = col.panel || Object.assign({}, col.fg)
|
||||
colors.panelText = col.panelText || getTextColor(colors.panel, colors.fgText)
|
||||
colors.panelLink = col.panelLink || getTextColor(colors.panel, colors.fgLink)
|
||||
colors.panelFaint = col.panelFaint || getTextColor(colors.panel, colors.faint)
|
||||
|
||||
colors.topBar = col.topBar || Object.assign({}, col.fg)
|
||||
colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText)
|
||||
colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink)
|
||||
|
||||
colors.faintLink = col.faintLink || Object.assign({}, col.link)
|
||||
colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
|
||||
|
||||
colors.icon = mixrgb(colors.bg, colors.text)
|
||||
|
||||
colors.cBlue = col.cBlue || hex2rgb('#0000FF')
|
||||
colors.cRed = col.cRed || hex2rgb('#FF0000')
|
||||
colors.cGreen = col.cGreen || hex2rgb('#00FF00')
|
||||
colors.cOrange = col.cOrange || hex2rgb('#E3FF00')
|
||||
|
||||
colors.alertError = col.alertError || Object.assign({}, colors.cRed)
|
||||
colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text)
|
||||
colors.alertErrorPanelText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.panel), colors.panelText)
|
||||
|
||||
colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange)
|
||||
colors.alertWarningText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.bg), colors.text)
|
||||
colors.alertWarningPanelText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.panel), colors.panelText)
|
||||
|
||||
colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed)
|
||||
colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb
|
||||
|
||||
Object.entries(opacity).forEach(([ k, v ]) => {
|
||||
if (typeof v === 'undefined') return
|
||||
if (k === 'alert') {
|
||||
colors.alertError.a = v
|
||||
colors.alertWarning.a = v
|
||||
return
|
||||
}
|
||||
if (k === 'faint') {
|
||||
colors[k + 'Link'].a = v
|
||||
colors['panelFaint'].a = v
|
||||
}
|
||||
if (k === 'bg') {
|
||||
colors['lightBg'].a = v
|
||||
}
|
||||
if (colors[k]) {
|
||||
colors[k].a = v
|
||||
} else {
|
||||
console.error('Wrong key ' + k)
|
||||
}
|
||||
})
|
||||
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
|
||||
|
||||
const htmlColors = Object.entries(colors)
|
||||
.reduce((acc, [k, v]) => {
|
||||
if (!v) return acc
|
||||
acc.solid[k] = rgb2hex(v)
|
||||
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v)
|
||||
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
|
||||
return acc
|
||||
}, { complete: {}, solid: {} })
|
||||
return {
|
||||
|
@ -264,7 +86,7 @@ const generateColors = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generateRadii = (input) => {
|
||||
export const generateRadii = (input) => {
|
||||
let inputRadii = input.radii || {}
|
||||
// v1 -> v2
|
||||
if (typeof input.btnRadius !== 'undefined') {
|
||||
|
@ -297,7 +119,7 @@ const generateRadii = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generateFonts = (input) => {
|
||||
export const generateFonts = (input) => {
|
||||
const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
|
@ -332,8 +154,7 @@ const generateFonts = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generateShadows = (input) => {
|
||||
const border = (top, shadow) => ({
|
||||
const border = (top, shadow) => ({
|
||||
x: 0,
|
||||
y: top ? 1 : -1,
|
||||
blur: 0,
|
||||
|
@ -341,19 +162,19 @@ const generateShadows = (input) => {
|
|||
color: shadow ? '#000000' : '#FFFFFF',
|
||||
alpha: 0.2,
|
||||
inset: true
|
||||
})
|
||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||
const hoverGlow = {
|
||||
})
|
||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||
const hoverGlow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '--faint',
|
||||
alpha: 1
|
||||
}
|
||||
}
|
||||
|
||||
const shadows = {
|
||||
export const DEFAULT_SHADOWS = {
|
||||
panel: [{
|
||||
x: 1,
|
||||
y: 1,
|
||||
|
@ -406,15 +227,50 @@ const generateShadows = (input) => {
|
|||
spread: 0,
|
||||
color: '#000000',
|
||||
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 {
|
||||
rules: {
|
||||
shadows: Object
|
||||
.entries(shadows)
|
||||
// TODO for v2.1: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
||||
.map(([k, v]) => [
|
||||
`--${k}Shadow: ${getCssShadow(v)}`,
|
||||
|
@ -429,7 +285,7 @@ const generateShadows = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const composePreset = (colors, radii, shadows, fonts) => {
|
||||
export const composePreset = (colors, radii, shadows, fonts) => {
|
||||
return {
|
||||
rules: {
|
||||
...shadows.rules,
|
||||
|
@ -446,98 +302,110 @@ const composePreset = (colors, radii, shadows, fonts) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generatePreset = (input) => {
|
||||
const shadows = generateShadows(input)
|
||||
export const generatePreset = (input) => {
|
||||
const colors = generateColors(input)
|
||||
const radii = generateRadii(input)
|
||||
const fonts = generateFonts(input)
|
||||
|
||||
return composePreset(colors, radii, shadows, fonts)
|
||||
return composePreset(
|
||||
colors,
|
||||
generateRadii(input),
|
||||
generateShadows(input, colors.theme.colors, colors.mod),
|
||||
generateFonts(input)
|
||||
)
|
||||
}
|
||||
|
||||
const getThemes = () => {
|
||||
return window.fetch('/static/styles.json')
|
||||
export const getThemes = () => {
|
||||
const cache = 'no-store'
|
||||
|
||||
return window.fetch('/static/styles.json', { cache })
|
||||
.then((data) => data.json())
|
||||
.then((themes) => {
|
||||
return Promise.all(Object.entries(themes).map(([k, v]) => {
|
||||
return Object.entries(themes).map(([k, v]) => {
|
||||
let promise = null
|
||||
if (typeof v === 'object') {
|
||||
return Promise.resolve([k, v])
|
||||
promise = Promise.resolve(v)
|
||||
} else if (typeof v === 'string') {
|
||||
return window.fetch(v)
|
||||
promise = window.fetch(v, { cache })
|
||||
.then((data) => data.json())
|
||||
.then((theme) => {
|
||||
return [k, theme]
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
return []
|
||||
return null
|
||||
})
|
||||
}
|
||||
}))
|
||||
return [k, promise]
|
||||
})
|
||||
})
|
||||
.then((promises) => {
|
||||
return promises
|
||||
.filter(([k, v]) => v)
|
||||
.reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
}
|
||||
export const colors2to3 = (colors) => {
|
||||
return Object.entries(colors).reduce((acc, [slotName, color]) => {
|
||||
const btnPositions = ['', 'Panel', 'TopBar']
|
||||
switch (slotName) {
|
||||
case 'lightBg':
|
||||
return { ...acc, highlight: color }
|
||||
case 'btnText':
|
||||
return {
|
||||
...acc,
|
||||
...btnPositions
|
||||
.reduce(
|
||||
(statePositionAcc, position) =>
|
||||
({ ...statePositionAcc, ['btn' + position + 'Text']: color })
|
||||
, {}
|
||||
)
|
||||
}
|
||||
default:
|
||||
return { ...acc, [slotName]: color }
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
const setPreset = (val, commit) => {
|
||||
return getThemes().then((themes) => {
|
||||
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
||||
/**
|
||||
* This handles compatibility issues when importing v2 theme's shadows to current format
|
||||
*
|
||||
* Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
|
||||
*/
|
||||
export const shadows2to3 = (shadows, opacity) => {
|
||||
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const isDynamic = ({ color }) => color.startsWith('--')
|
||||
const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
|
||||
}
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const getPreset = (val) => {
|
||||
return getThemes()
|
||||
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
|
||||
.then((theme) => {
|
||||
const isV1 = Array.isArray(theme)
|
||||
const data = isV1 ? {} : theme.theme
|
||||
|
||||
if (isV1) {
|
||||
const bgRgb = hex2rgb(theme[1])
|
||||
const fgRgb = hex2rgb(theme[2])
|
||||
const textRgb = hex2rgb(theme[3])
|
||||
const linkRgb = hex2rgb(theme[4])
|
||||
const bg = hex2rgb(theme[1])
|
||||
const fg = hex2rgb(theme[2])
|
||||
const text = hex2rgb(theme[3])
|
||||
const link = hex2rgb(theme[4])
|
||||
|
||||
const cRedRgb = hex2rgb(theme[5] || '#FF0000')
|
||||
const cGreenRgb = hex2rgb(theme[6] || '#00FF00')
|
||||
const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
|
||||
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
|
||||
const cRed = hex2rgb(theme[5] || '#FF0000')
|
||||
const cGreen = hex2rgb(theme[6] || '#00FF00')
|
||||
const cBlue = hex2rgb(theme[7] || '#0000FF')
|
||||
const cOrange = hex2rgb(theme[8] || '#E3FF00')
|
||||
|
||||
data.colors = {
|
||||
bg: bgRgb,
|
||||
fg: fgRgb,
|
||||
text: textRgb,
|
||||
link: linkRgb,
|
||||
cRed: cRedRgb,
|
||||
cBlue: cBlueRgb,
|
||||
cGreen: cGreenRgb,
|
||||
cOrange: cOrangeRgb
|
||||
}
|
||||
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
|
||||
}
|
||||
|
||||
// This is a hack, this function is only called during initial load.
|
||||
// We want to cancel loading the theme from config.json if we're already
|
||||
// loading a theme from the persisted state.
|
||||
// Needed some way of dealing with the async way of things.
|
||||
// load config -> set preset -> wait for styles.json to load ->
|
||||
// load persisted state -> set colors -> styles.json loaded -> set colors
|
||||
if (!window.themeLoaded) {
|
||||
applyTheme(data, commit)
|
||||
}
|
||||
return { theme: data, source: theme.source }
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
setStyle,
|
||||
setPreset,
|
||||
applyTheme,
|
||||
getTextColor,
|
||||
generateColors,
|
||||
generateRadii,
|
||||
generateShadows,
|
||||
generateFonts,
|
||||
generatePreset,
|
||||
getThemes,
|
||||
composePreset,
|
||||
getCssShadow,
|
||||
getCssShadowFilter
|
||||
}
|
||||
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
|
||||
|
|
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