Merge remote-tracking branch 'upstream/develop' into search-mobile-fixes
* upstream/develop: (176 commits) fix chrome Prevent html-minifier to remove placeholder comment in index.html template Add placeholder to insert server generated metatags. Related to #430 added condition to check for logined user fix gradients and minor artifacts keep track of new instance options fix old MR oof get rid of slots fix timeago font added hide_network option, fixed properties naming Fix fetching new users, add storing local users in usersObjects with their screen_name as well as id, so that they could be fetched zero-state with screen-name link. improve notification subscription Fix typo that prevented scope copy from working. Refactor arrays to individual options Reset enableFollowsExport to true after 2 sec when an export file is available to download added check for activatePanel is function or not addressed PR comments activate panel on user screen click added not preload check so hidden toggles asap ...
This commit is contained in:
		
						commit
						bd745543b6
					
				
					 97 changed files with 5233 additions and 1934 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -6,3 +6,4 @@ test/unit/coverage
 | 
			
		|||
test/e2e/reports
 | 
			
		||||
selenium-debug.log
 | 
			
		||||
.idea/
 | 
			
		||||
config/local.json
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,15 @@ npm run build
 | 
			
		|||
npm run unit
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# For Contributors:
 | 
			
		||||
 | 
			
		||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
 | 
			
		||||
 | 
			
		||||
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
 | 
			
		||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
 | 
			
		||||
 | 
			
		||||
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.
 | 
			
		||||
 | 
			
		||||
# Configuration
 | 
			
		||||
 | 
			
		||||
Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ var path = require('path')
 | 
			
		|||
var config = require('../config')
 | 
			
		||||
var utils = require('./utils')
 | 
			
		||||
var projectRoot = path.resolve(__dirname, '../')
 | 
			
		||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
 | 
			
		||||
 | 
			
		||||
var env = process.env.NODE_ENV
 | 
			
		||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
 | 
			
		||||
| 
						 | 
				
			
			@ -91,5 +92,10 @@ module.exports = {
 | 
			
		|||
        browsers: ['last 2 versions']
 | 
			
		||||
      })
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    new ServiceWorkerWebpackPlugin({
 | 
			
		||||
      entry: path.join(__dirname, '..', 'src/sw.js')
 | 
			
		||||
    })
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,9 @@ module.exports = merge(baseWebpackConfig, {
 | 
			
		|||
  devtool: '#eval-source-map',
 | 
			
		||||
  plugins: [
 | 
			
		||||
    new webpack.DefinePlugin({
 | 
			
		||||
      'process.env': config.dev.env
 | 
			
		||||
      'process.env': config.dev.env,
 | 
			
		||||
      'COMMIT_HASH': JSON.stringify('DEV'),
 | 
			
		||||
      'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
 | 
			
		||||
    }),
 | 
			
		||||
    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
 | 
			
		||||
    new webpack.optimize.OccurenceOrderPlugin(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,8 +7,13 @@ var baseWebpackConfig = require('./webpack.base.conf')
 | 
			
		|||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
 | 
			
		||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
 | 
			
		||||
var env = process.env.NODE_ENV === 'testing'
 | 
			
		||||
  ? require('../config/test.env')
 | 
			
		||||
  : config.build.env
 | 
			
		||||
    ? require('../config/test.env')
 | 
			
		||||
    : config.build.env
 | 
			
		||||
 | 
			
		||||
let commitHash = require('child_process')
 | 
			
		||||
    .execSync('git rev-parse --short HEAD')
 | 
			
		||||
    .toString();
 | 
			
		||||
console.log(commitHash)
 | 
			
		||||
 | 
			
		||||
var webpackConfig = merge(baseWebpackConfig, {
 | 
			
		||||
  module: {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +34,9 @@ var webpackConfig = merge(baseWebpackConfig, {
 | 
			
		|||
  plugins: [
 | 
			
		||||
    // http://vuejs.github.io/vue-loader/workflow/production.html
 | 
			
		||||
    new webpack.DefinePlugin({
 | 
			
		||||
      'process.env': env
 | 
			
		||||
      'process.env': env,
 | 
			
		||||
      'COMMIT_HASH': JSON.stringify(commitHash),
 | 
			
		||||
      'DEV_OVERRIDES': JSON.stringify(undefined)
 | 
			
		||||
    }),
 | 
			
		||||
    new webpack.optimize.UglifyJsPlugin({
 | 
			
		||||
      compress: {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, {
 | 
			
		|||
      minify: {
 | 
			
		||||
        removeComments: true,
 | 
			
		||||
        collapseWhitespace: true,
 | 
			
		||||
        removeAttributeQuotes: true
 | 
			
		||||
        removeAttributeQuotes: true,
 | 
			
		||||
        ignoreCustomComments: [/server-generated-meta/]
 | 
			
		||||
        // more options:
 | 
			
		||||
        // https://github.com/kangax/html-minifier#options-quick-reference
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,15 @@
 | 
			
		|||
// see http://vuejs-templates.github.io/webpack for documentation.
 | 
			
		||||
var path = require('path')
 | 
			
		||||
const path = require('path')
 | 
			
		||||
let settings = {}
 | 
			
		||||
try {
 | 
			
		||||
  settings = require('./local.json')
 | 
			
		||||
  console.log('Using local dev server settings (/config/local.json):')
 | 
			
		||||
  console.log(JSON.stringify(settings, null, 2))
 | 
			
		||||
} catch (e) {
 | 
			
		||||
  console.log('Local dev server settings not found (/config/local.json)')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const target = settings.target || 'http://localhost:4000/'
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  build: {
 | 
			
		||||
| 
						 | 
				
			
			@ -19,21 +29,22 @@ module.exports = {
 | 
			
		|||
  dev: {
 | 
			
		||||
    env: require('./dev.env'),
 | 
			
		||||
    port: 8080,
 | 
			
		||||
    settings,
 | 
			
		||||
    assetsSubDirectory: 'static',
 | 
			
		||||
    assetsPublicPath: '/',
 | 
			
		||||
    proxyTable: {
 | 
			
		||||
      '/api': {
 | 
			
		||||
        target: 'http://localhost:4000/',
 | 
			
		||||
        target,
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
        cookieDomainRewrite: 'localhost'
 | 
			
		||||
      },
 | 
			
		||||
      '/nodeinfo': {
 | 
			
		||||
        target: 'http://localhost:4000/',
 | 
			
		||||
        target,
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
        cookieDomainRewrite: 'localhost'
 | 
			
		||||
      },
 | 
			
		||||
      '/socket': {
 | 
			
		||||
        target: 'http://localhost:4000/',
 | 
			
		||||
        target,
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
        cookieDomainRewrite: 'localhost',
 | 
			
		||||
        ws: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								config/local.example.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								config/local.example.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
{
 | 
			
		||||
  "target": "https://pleroma.soykaf.com/",
 | 
			
		||||
  "staticConfigPreference": false
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
    <meta charset="utf-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
    <title>Pleroma</title>
 | 
			
		||||
    <!--server-generated-meta-->
 | 
			
		||||
    <link rel="icon" type="image/png" href="/favicon.png">
 | 
			
		||||
    <link rel="stylesheet" href="/static/font/css/fontello.css">
 | 
			
		||||
    <link rel="stylesheet" href="/static/font/css/animation.css">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@
 | 
			
		|||
  "dependencies": {
 | 
			
		||||
    "babel-plugin-add-module-exports": "^0.2.1",
 | 
			
		||||
    "babel-plugin-lodash": "^3.2.11",
 | 
			
		||||
    "chromatism": "^3.0.0",
 | 
			
		||||
    "diff": "^3.0.1",
 | 
			
		||||
    "karma-mocha-reporter": "^2.2.1",
 | 
			
		||||
    "localforage": "^1.5.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +31,7 @@
 | 
			
		|||
    "vue-router": "^3.0.1",
 | 
			
		||||
    "vue-template-compiler": "^2.3.4",
 | 
			
		||||
    "vue-timeago": "^3.1.2",
 | 
			
		||||
    "vuelidate": "^0.7.4",
 | 
			
		||||
    "vuex": "^3.0.1",
 | 
			
		||||
    "whatwg-fetch": "^2.0.3"
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -88,6 +90,7 @@
 | 
			
		|||
    "raw-loader": "^0.5.1",
 | 
			
		||||
    "selenium-server": "2.53.1",
 | 
			
		||||
    "semver": "^5.3.0",
 | 
			
		||||
    "serviceworker-webpack-plugin": "0.2.3",
 | 
			
		||||
    "shelljs": "^0.7.4",
 | 
			
		||||
    "sinon": "^1.17.3",
 | 
			
		||||
    "sinon-chai": "^2.8.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,7 +61,12 @@ export default {
 | 
			
		|||
      })
 | 
			
		||||
    },
 | 
			
		||||
    logo () { return this.$store.state.instance.logo },
 | 
			
		||||
    style () { return { 'background-image': `url(${this.background})` } },
 | 
			
		||||
    style () {
 | 
			
		||||
      return {
 | 
			
		||||
        '--body-background-image': `url(${this.background})`,
 | 
			
		||||
        'background-image': `url(${this.background})`
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    sitename () { return this.$store.state.instance.name },
 | 
			
		||||
    chat () { return this.$store.state.chat.channel.state === 'joined' },
 | 
			
		||||
    suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										240
									
								
								src/App.scss
									
									
									
									
									
								
							
							
						
						
									
										240
									
								
								src/App.scss
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -34,10 +34,11 @@ h4 {
 | 
			
		|||
 | 
			
		||||
body {
 | 
			
		||||
  font-family: sans-serif;
 | 
			
		||||
  font-family: var(--interfaceFont, sans-serif);
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  color: $fallback--fg;
 | 
			
		||||
  color: var(--fg, $fallback--fg);
 | 
			
		||||
  color: $fallback--text;
 | 
			
		||||
  color: var(--text, $fallback--text);
 | 
			
		||||
  max-width: 100vw;
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -50,19 +51,24 @@ a {
 | 
			
		|||
 | 
			
		||||
button {
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  color: $fallback--fg;
 | 
			
		||||
  color: var(--fg, $fallback--fg);
 | 
			
		||||
  background-color: $fallback--btn;
 | 
			
		||||
  background-color: var(--btn, $fallback--btn);
 | 
			
		||||
  color: $fallback--text;
 | 
			
		||||
  color: var(--btnText, $fallback--text);
 | 
			
		||||
  background-color: $fallback--fg;
 | 
			
		||||
  background-color: var(--btn, $fallback--fg);
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-radius: $fallback--btnRadius;
 | 
			
		||||
  border-radius: var(--btnRadius, $fallback--btnRadius);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  border-top: 1px solid rgba(255, 255, 255, 0.2);
 | 
			
		||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
 | 
			
		||||
  box-shadow: 0px 0px 2px black;
 | 
			
		||||
  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: var(--buttonShadow);
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  font-family: sans-serif;
 | 
			
		||||
  font-family: var(--interfaceFont, sans-serif);
 | 
			
		||||
 | 
			
		||||
  i[class*=icon-] {
 | 
			
		||||
    color: $fallback--text;
 | 
			
		||||
    color: var(--btnText, $fallback--text);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &::-moz-focus-inner {
 | 
			
		||||
    border: none;
 | 
			
		||||
| 
						 | 
				
			
			@ -70,11 +76,12 @@ button {
 | 
			
		|||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
 | 
			
		||||
    box-shadow: var(--buttonHoverShadow);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:active {
 | 
			
		||||
    border-bottom: 1px solid rgba(255, 255, 255, 0.2);
 | 
			
		||||
    border-top: 1px solid rgba(0, 0, 0, 0.2);
 | 
			
		||||
    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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:disabled {
 | 
			
		||||
| 
						 | 
				
			
			@ -99,32 +106,37 @@ input, textarea, .select {
 | 
			
		|||
  border: none;
 | 
			
		||||
  border-radius: $fallback--inputRadius;
 | 
			
		||||
  border-radius: var(--inputRadius, $fallback--inputRadius);
 | 
			
		||||
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
 | 
			
		||||
  border-top: 1px solid rgba(0, 0, 0, 0.2);
 | 
			
		||||
  box-shadow: 0px 0px 2px black inset;
 | 
			
		||||
  background-color: $fallback--input;
 | 
			
		||||
  background-color: var(--input, $fallback--input);
 | 
			
		||||
  color: $fallback--lightFg;
 | 
			
		||||
  color: var(--lightFg, $fallback--lightFg);
 | 
			
		||||
  box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
 | 
			
		||||
  box-shadow: var(--inputShadow);
 | 
			
		||||
  background-color: $fallback--fg;
 | 
			
		||||
  background-color: var(--input, $fallback--fg);
 | 
			
		||||
  color: $fallback--lightText;
 | 
			
		||||
  color: var(--inputText, $fallback--lightText);
 | 
			
		||||
  font-family: sans-serif;
 | 
			
		||||
  font-family: var(--inputFont, sans-serif);
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  padding: 8px 7px;
 | 
			
		||||
  padding: 8px .5em;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  height: 29px;
 | 
			
		||||
  height: 28px;
 | 
			
		||||
  line-height: 16px;
 | 
			
		||||
  hyphens: none;
 | 
			
		||||
 | 
			
		||||
  &:disabled, &[disabled=disabled] {
 | 
			
		||||
    cursor: not-allowed;
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .icon-down-open {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    right: 5px;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    color: $fallback--fg;
 | 
			
		||||
    color: var(--fg, $fallback--fg);
 | 
			
		||||
    line-height: 29px;
 | 
			
		||||
    color: $fallback--text;
 | 
			
		||||
    color: var(--text, $fallback--text);
 | 
			
		||||
    line-height: 28px;
 | 
			
		||||
    z-index: 0;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -135,22 +147,33 @@ input, textarea, .select {
 | 
			
		|||
    appearance: none;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: $fallback--text;
 | 
			
		||||
    color: var(--text, $fallback--text);
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    color: $fallback--fg;
 | 
			
		||||
    color: var(--fg, $fallback--fg);
 | 
			
		||||
    padding: 4px 2em 3px 3px;
 | 
			
		||||
    padding: 0 2em 0 .2em;
 | 
			
		||||
    font-family: sans-serif;
 | 
			
		||||
    font-family: var(--inputFont, sans-serif);
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    height: 29px;
 | 
			
		||||
    height: 28px;
 | 
			
		||||
    line-height: 16px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &[type=range] {
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &[type=radio],
 | 
			
		||||
  &[type=checkbox] {
 | 
			
		||||
    display: none;
 | 
			
		||||
    &:checked + label::before {
 | 
			
		||||
      color: $fallback--fg;
 | 
			
		||||
      color: var(--fg, $fallback--fg);
 | 
			
		||||
      color: $fallback--text;
 | 
			
		||||
      color: var(--text, $fallback--text);
 | 
			
		||||
    }
 | 
			
		||||
    &:disabled,
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -166,14 +189,13 @@ input, textarea, .select {
 | 
			
		|||
      transition: color 200ms;
 | 
			
		||||
      width: 1.1em;
 | 
			
		||||
      height: 1.1em;
 | 
			
		||||
      border-radius: $fallback--checkBoxRadius;
 | 
			
		||||
      border-radius: var(--checkBoxRadius, $fallback--checkBoxRadius);
 | 
			
		||||
      border-bottom: 1px solid rgba(255, 255, 255, 0.2);
 | 
			
		||||
      border-top: 1px solid rgba(0, 0, 0, 0.2);
 | 
			
		||||
      border-radius: $fallback--checkboxRadius;
 | 
			
		||||
      border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
 | 
			
		||||
      box-shadow: 0px 0px 2px black inset;
 | 
			
		||||
      box-shadow: var(--inputShadow);
 | 
			
		||||
      margin-right: .5em;
 | 
			
		||||
      background-color: $fallback--input;
 | 
			
		||||
      background-color: var(--input, $fallback--input);
 | 
			
		||||
      background-color: $fallback--fg;
 | 
			
		||||
      background-color: var(--input, $fallback--fg);
 | 
			
		||||
      vertical-align: top;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      line-height: 1.1em;
 | 
			
		||||
| 
						 | 
				
			
			@ -187,8 +209,8 @@ input, textarea, .select {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
option {
 | 
			
		||||
  color: $fallback--fg;
 | 
			
		||||
  color: var(--fg, $fallback--fg);
 | 
			
		||||
  color: $fallback--text;
 | 
			
		||||
  color: var(--text, $fallback--text);
 | 
			
		||||
  background-color: $fallback--bg;
 | 
			
		||||
  background-color: var(--bg, $fallback--bg);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -206,24 +228,23 @@ i[class*=icon-] {
 | 
			
		|||
  padding: 0 10px 0 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gaps {
 | 
			
		||||
  margin: -1em 0 0 -1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  line-height: 50px;
 | 
			
		||||
  height: 50px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
  .nav-icon {
 | 
			
		||||
    font-size: 1.1em;
 | 
			
		||||
    margin-left: 0.4em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gaps > .item {
 | 
			
		||||
  padding: 1em 0 0 1em;
 | 
			
		||||
  &.right {
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    padding-right: 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.auto-size {
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +278,7 @@ nav {
 | 
			
		|||
      mask-position: center;
 | 
			
		||||
      mask-size: contain;
 | 
			
		||||
      background-color: $fallback--fg;
 | 
			
		||||
      background-color: var(--fg, $fallback--fg);
 | 
			
		||||
      background-color: var(--topBarText, $fallback--fg);
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -274,17 +295,15 @@ nav {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  .inner-nav {
 | 
			
		||||
    padding-left: 20px;
 | 
			
		||||
    padding-right: 20px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    flex-basis: 970px;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    height: 50px;
 | 
			
		||||
 | 
			
		||||
    a i {
 | 
			
		||||
    a, a i {
 | 
			
		||||
      color: $fallback--link;
 | 
			
		||||
      color: var(--link, $fallback--link);
 | 
			
		||||
      color: var(--topBarLink, $fallback--link);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -307,15 +326,33 @@ main-router {
 | 
			
		|||
 | 
			
		||||
.panel {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  margin: 0.5em;
 | 
			
		||||
 | 
			
		||||
  background-color: $fallback--bg;
 | 
			
		||||
  background-color: var(--bg, $fallback--bg);
 | 
			
		||||
 | 
			
		||||
  border-radius: $fallback--panelRadius;
 | 
			
		||||
  border-radius: var(--panelRadius, $fallback--panelRadius);
 | 
			
		||||
  box-shadow: 1px 1px 4px rgba(0,0,0,.6);
 | 
			
		||||
  &::after, & {
 | 
			
		||||
    border-radius: $fallback--panelRadius;
 | 
			
		||||
    border-radius: var(--panelRadius, $fallback--panelRadius);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &::after {
 | 
			
		||||
    content: '';
 | 
			
		||||
    position: absolute;
 | 
			
		||||
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
 | 
			
		||||
    box-shadow: 1px 1px 4px rgba(0,0,0,.6);
 | 
			
		||||
    box-shadow: var(--panelShadow);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.panel-body:empty::before {
 | 
			
		||||
| 
						 | 
				
			
			@ -333,15 +370,23 @@ main-router {
 | 
			
		|||
  padding: .6em .6em;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  line-height: 28px;
 | 
			
		||||
  background-color: $fallback--btn;
 | 
			
		||||
  background-color: var(--btn, $fallback--btn);
 | 
			
		||||
  color: var(--panelText);
 | 
			
		||||
  background-color: $fallback--fg;
 | 
			
		||||
  background-color: var(--panel, $fallback--fg);
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  box-shadow: var(--panelHeaderShadow);
 | 
			
		||||
 | 
			
		||||
  .title {
 | 
			
		||||
    flex: 1 0 auto;
 | 
			
		||||
    font-size: 1.3em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .faint {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    color: $fallback--faint;
 | 
			
		||||
    color: var(--panelFaint, $fallback--faint);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .alert {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
| 
						 | 
				
			
			@ -362,6 +407,11 @@ main-router {
 | 
			
		|||
    min-width: 1px;
 | 
			
		||||
    align-self: stretch;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    color: $fallback--link;
 | 
			
		||||
    color: var(--panelLink, $fallback--link)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.panel-heading.stub {
 | 
			
		||||
| 
						 | 
				
			
			@ -372,6 +422,11 @@ main-router {
 | 
			
		|||
.panel-footer {
 | 
			
		||||
  border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
 | 
			
		||||
  border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    color: $fallback--link;
 | 
			
		||||
    color: var(--panelLink, $fallback--link)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.panel-body > p {
 | 
			
		||||
| 
						 | 
				
			
			@ -390,11 +445,30 @@ main-router {
 | 
			
		|||
 | 
			
		||||
nav {
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
  background-color: $fallback--btn;
 | 
			
		||||
  background-color: var(--btn, $fallback--btn);
 | 
			
		||||
  color: var(--topBarText);
 | 
			
		||||
  background-color: $fallback--fg;
 | 
			
		||||
  background-color: var(--topBar, $fallback--fg);
 | 
			
		||||
  color: $fallback--faint;
 | 
			
		||||
  color: var(--faint, $fallback--faint);
 | 
			
		||||
  box-shadow: 0px 0px 4px rgba(0,0,0,.6);
 | 
			
		||||
  box-shadow: var(--topBarShadow);
 | 
			
		||||
 | 
			
		||||
  .back-button {
 | 
			
		||||
    display: block;
 | 
			
		||||
    max-width: 99px;
 | 
			
		||||
    transition-property: opacity, max-width;
 | 
			
		||||
    transition-duration: 300ms;
 | 
			
		||||
    transition-timing-function: ease-out;
 | 
			
		||||
 | 
			
		||||
    i {
 | 
			
		||||
      margin: 0 1em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.hidden {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      max-width: 20px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-enter-active, .fade-leave-active {
 | 
			
		||||
| 
						 | 
				
			
			@ -429,6 +503,7 @@ nav {
 | 
			
		|||
  display: none;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 46px;
 | 
			
		||||
 | 
			
		||||
  button {
 | 
			
		||||
    display: block;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -442,6 +517,16 @@ nav {
 | 
			
		|||
  body {
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  nav {
 | 
			
		||||
    .back-button {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    .site-name {
 | 
			
		||||
      padding-left: 20px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sidebar-bounds {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    max-height: 100vh;
 | 
			
		||||
| 
						 | 
				
			
			@ -468,20 +553,46 @@ nav {
 | 
			
		|||
    flex-grow: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.badge {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  border-radius: 99px;
 | 
			
		||||
  min-width: 22px;
 | 
			
		||||
  max-width: 22px;
 | 
			
		||||
  min-height: 22px;
 | 
			
		||||
  max-height: 22px;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
  line-height: 22px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
 | 
			
		||||
  &.badge-notification {
 | 
			
		||||
    background-color: $fallback--cRed;
 | 
			
		||||
    background-color: var(--badgeNotification, $fallback--cRed);
 | 
			
		||||
    color: white;
 | 
			
		||||
    color: var(--badgeNotificationText, white);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.alert {
 | 
			
		||||
  margin: 0.35em;
 | 
			
		||||
  padding: 0.25em;
 | 
			
		||||
  border-radius: $fallback--tooltipRadius;
 | 
			
		||||
  border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
 | 
			
		||||
  color: $fallback--faint;
 | 
			
		||||
  color: var(--faint, $fallback--faint);
 | 
			
		||||
  min-height: 28px;
 | 
			
		||||
  line-height: 28px;
 | 
			
		||||
 | 
			
		||||
  &.error {
 | 
			
		||||
    background-color: $fallback--cAlertRed;
 | 
			
		||||
    background-color: var(--cAlertRed, $fallback--cAlertRed);
 | 
			
		||||
    background-color: $fallback--alertError;
 | 
			
		||||
    background-color: var(--alertError, $fallback--alertError);
 | 
			
		||||
    color: $fallback--text;
 | 
			
		||||
    color: var(--alertErrorText, $fallback--text);
 | 
			
		||||
 | 
			
		||||
    .panel-heading & {
 | 
			
		||||
      color: $fallback--text;
 | 
			
		||||
      color: var(--alertErrorPanelText, $fallback--text);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -513,19 +624,14 @@ nav {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item.right {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
  padding-right: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.visibility-tray {
 | 
			
		||||
  font-size: 1.2em;
 | 
			
		||||
  padding: 3px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  .selected {
 | 
			
		||||
    color: $fallback--lightFg;
 | 
			
		||||
    color: var(--lightFg, $fallback--lightFg);
 | 
			
		||||
    color: $fallback--lightText;
 | 
			
		||||
    color: var(--lightText, $fallback--lightText);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .text-format {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,10 @@
 | 
			
		|||
      </div>
 | 
			
		||||
      <div class='inner-nav'>
 | 
			
		||||
        <div class='item'>
 | 
			
		||||
          <router-link :to="{ name: 'root'}">{{sitename}}</router-link>
 | 
			
		||||
          <router-link class="back-button" @click.native="activatePanel('timeline')" :to="{ name: 'root' }" active-class="hidden">
 | 
			
		||||
            <i class="icon-left-open" :title="$t('nav.back')"></i>
 | 
			
		||||
          </router-link>
 | 
			
		||||
          <router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class='item right'>
 | 
			
		||||
          <user-finder class="nav-icon" @toggled="onFinderToggled"></user-finder>
 | 
			
		||||
| 
						 | 
				
			
			@ -25,12 +28,12 @@
 | 
			
		|||
        <div class="sidebar-bounds">
 | 
			
		||||
          <div class="sidebar-scroller">
 | 
			
		||||
            <div class="sidebar">
 | 
			
		||||
              <user-panel></user-panel>
 | 
			
		||||
              <user-panel :activatePanel="activatePanel"></user-panel>
 | 
			
		||||
              <nav-panel :activatePanel="activatePanel"></nav-panel>
 | 
			
		||||
              <instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
 | 
			
		||||
              <features-panel v-if="!currentUser"></features-panel>
 | 
			
		||||
              <who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
 | 
			
		||||
              <notifications v-if="currentUser"></notifications>
 | 
			
		||||
              <notifications :activatePanel="activatePanel" v-if="currentUser"></notifications>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,24 +3,23 @@ $main-background: white;
 | 
			
		|||
$darkened-background: whitesmoke;
 | 
			
		||||
 | 
			
		||||
$fallback--bg: #121a24;
 | 
			
		||||
$fallback--btn: #182230;
 | 
			
		||||
$fallback--input: #182230;
 | 
			
		||||
$fallback--fg: #182230;
 | 
			
		||||
$fallback--faint: rgba(185, 185, 186, .5);
 | 
			
		||||
$fallback--fg: #b9b9ba;
 | 
			
		||||
$fallback--text: #b9b9ba;
 | 
			
		||||
$fallback--link: #d8a070;
 | 
			
		||||
$fallback--icon: #666;
 | 
			
		||||
$fallback--lightBg: rgb(21, 30, 42);
 | 
			
		||||
$fallback--lightFg: #b9b9ba;
 | 
			
		||||
$fallback--lightText: #b9b9ba;
 | 
			
		||||
$fallback--border: #222;
 | 
			
		||||
$fallback--cRed: #ff0000;
 | 
			
		||||
$fallback--cBlue: #0095ff;
 | 
			
		||||
$fallback--cGreen: #0fa00f;
 | 
			
		||||
$fallback--cOrange: orange;
 | 
			
		||||
 | 
			
		||||
$fallback--cAlertRed: rgba(211,16,20,.5);
 | 
			
		||||
$fallback--alertError: rgba(211,16,20,.5);
 | 
			
		||||
 | 
			
		||||
$fallback--panelRadius: 10px;
 | 
			
		||||
$fallback--checkBoxRadius: 2px;
 | 
			
		||||
$fallback--checkboxRadius: 2px;
 | 
			
		||||
$fallback--btnRadius: 4px;
 | 
			
		||||
$fallback--inputRadius: 4px;
 | 
			
		||||
$fallback--tooltipRadius: 5px;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,17 +17,29 @@ import FollowRequests from '../components/follow_requests/follow_requests.vue'
 | 
			
		|||
import OAuthCallback from '../components/oauth_callback/oauth_callback.vue'
 | 
			
		||||
import UserSearch from '../components/user_search/user_search.vue'
 | 
			
		||||
 | 
			
		||||
const afterStoreSetup = ({store, i18n}) => {
 | 
			
		||||
const afterStoreSetup = ({ store, i18n }) => {
 | 
			
		||||
  window.fetch('/api/statusnet/config.json')
 | 
			
		||||
    .then((res) => res.json())
 | 
			
		||||
    .then((data) => {
 | 
			
		||||
      const {name, closed: registrationClosed, textlimit, server} = data.site
 | 
			
		||||
      const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site
 | 
			
		||||
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'name', value: name })
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
 | 
			
		||||
      store.dispatch('setInstanceOption', { name: 'server', value: server })
 | 
			
		||||
 | 
			
		||||
      if (data.nsfwCensorImage) {
 | 
			
		||||
        store.dispatch('setInstanceOption', { name: 'nsfwCensorImage', value: data.nsfwCensorImage })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (vapidPublicKey) {
 | 
			
		||||
        store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var apiConfig = data.site.pleromafe
 | 
			
		||||
 | 
			
		||||
      window.fetch('/static/config.json')
 | 
			
		||||
| 
						 | 
				
			
			@ -38,8 +50,17 @@ const afterStoreSetup = ({store, i18n}) => {
 | 
			
		|||
          return {}
 | 
			
		||||
        })
 | 
			
		||||
        .then((staticConfig) => {
 | 
			
		||||
          const overrides = window.___pleromafe_dev_overrides || {}
 | 
			
		||||
          const env = window.___pleromafe_mode.NODE_ENV
 | 
			
		||||
 | 
			
		||||
          // This takes static config and overrides properties that are present in apiConfig
 | 
			
		||||
          var config = Object.assign({}, staticConfig, apiConfig)
 | 
			
		||||
          let config = {}
 | 
			
		||||
          if (overrides.staticConfigPreference && env === 'development') {
 | 
			
		||||
            console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
 | 
			
		||||
            config = Object.assign({}, apiConfig, staticConfig)
 | 
			
		||||
          } else {
 | 
			
		||||
            config = Object.assign({}, staticConfig, apiConfig)
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          var theme = (config.theme)
 | 
			
		||||
          var background = (config.background)
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +79,7 @@ const afterStoreSetup = ({store, i18n}) => {
 | 
			
		|||
          var loginMethod = (config.loginMethod)
 | 
			
		||||
          var scopeCopy = (config.scopeCopy)
 | 
			
		||||
          var subjectLineBehavior = (config.subjectLineBehavior)
 | 
			
		||||
          var alwaysShowSubjectInput = (config.alwaysShowSubjectInput)
 | 
			
		||||
 | 
			
		||||
          store.dispatch('setInstanceOption', { name: 'theme', value: theme })
 | 
			
		||||
          store.dispatch('setInstanceOption', { name: 'background', value: background })
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +97,7 @@ const afterStoreSetup = ({store, i18n}) => {
 | 
			
		|||
          store.dispatch('setInstanceOption', { name: 'loginMethod', value: loginMethod })
 | 
			
		||||
          store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy })
 | 
			
		||||
          store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior })
 | 
			
		||||
          store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput })
 | 
			
		||||
          if (chatDisabled) {
 | 
			
		||||
            store.dispatch('disableChat')
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,8 +11,9 @@ const Attachment = {
 | 
			
		|||
  ],
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      nsfwImage,
 | 
			
		||||
      nsfwImage: this.$store.state.config.nsfwCensorImage || nsfwImage,
 | 
			
		||||
      hideNsfwLocal: this.$store.state.config.hideNsfw,
 | 
			
		||||
      preloadImage: this.$store.state.config.preloadImage,
 | 
			
		||||
      loopVideo: this.$store.state.config.loopVideo,
 | 
			
		||||
      showHidden: false,
 | 
			
		||||
      loading: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +47,7 @@ const Attachment = {
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    toggleHidden () {
 | 
			
		||||
      if (this.img) {
 | 
			
		||||
      if (this.img && !this.preloadImage) {
 | 
			
		||||
        if (this.img.onload) {
 | 
			
		||||
          this.img.onload()
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,12 +9,11 @@
 | 
			
		|||
    <div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
 | 
			
		||||
      <a href="#" @click.prevent="toggleHidden()">Hide</a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank" :title="attachment.description">
 | 
			
		||||
    <a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :href="attachment.url" target="_blank" :title="attachment.description">
 | 
			
		||||
      <StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
 | 
			
		||||
    </a>
 | 
			
		||||
 | 
			
		||||
    <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo"></video>
 | 
			
		||||
    <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo" playsinline></video>
 | 
			
		||||
 | 
			
		||||
    <audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -161,6 +160,10 @@
 | 
			
		|||
    display: flex;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
 | 
			
		||||
    &.hidden {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .still-image {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,8 +55,8 @@
 | 
			
		|||
.chat-heading {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  .icon-comment-empty {
 | 
			
		||||
    color: $fallback--fg;
 | 
			
		||||
    color: var(--fg, $fallback--fg);
 | 
			
		||||
    color: $fallback--text;
 | 
			
		||||
    color: var(--text, $fallback--text);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										53
									
								
								src/components/color_input/color_input.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/components/color_input/color_input.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="color-control style-control" :class="{ disabled: !present || disabled }">
 | 
			
		||||
  <label :for="name" class="label">
 | 
			
		||||
    {{label}}
 | 
			
		||||
  </label>
 | 
			
		||||
  <input
 | 
			
		||||
    v-if="typeof fallback !== 'undefined'"
 | 
			
		||||
    class="opt exlcude-disabled"
 | 
			
		||||
    :id="name + '-o'"
 | 
			
		||||
    type="checkbox"
 | 
			
		||||
    :checked="present"
 | 
			
		||||
    @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)">
 | 
			
		||||
  <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
 | 
			
		||||
  <input
 | 
			
		||||
    :id="name"
 | 
			
		||||
    class="color-input"
 | 
			
		||||
    type="color"
 | 
			
		||||
    :value="value || fallback"
 | 
			
		||||
    :disabled="!present || disabled"
 | 
			
		||||
    @input="$emit('input', $event.target.value)"
 | 
			
		||||
    >
 | 
			
		||||
  <input
 | 
			
		||||
    :id="name + '-t'"
 | 
			
		||||
    class="text-input"
 | 
			
		||||
    type="text"
 | 
			
		||||
    :value="value || fallback"
 | 
			
		||||
    :disabled="!present || disabled"
 | 
			
		||||
    @input="$emit('input', $event.target.value)"
 | 
			
		||||
    >
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: [
 | 
			
		||||
    'name', 'label', 'value', 'fallback', 'disabled'
 | 
			
		||||
  ],
 | 
			
		||||
  computed: {
 | 
			
		||||
    present () {
 | 
			
		||||
      return typeof this.value !== 'undefined'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.color-control {
 | 
			
		||||
  input.text-input {
 | 
			
		||||
    max-width: 7em;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										69
									
								
								src/components/contrast_ratio/contrast_ratio.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/components/contrast_ratio/contrast_ratio.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
<template>
 | 
			
		||||
<span  v-if="contrast" class="contrast-ratio">
 | 
			
		||||
  <span :title="hint" class="rating">
 | 
			
		||||
    <span v-if="contrast.aaa">
 | 
			
		||||
      <i class="icon-thumbs-up-alt"/>
 | 
			
		||||
    </span>
 | 
			
		||||
    <span v-if="!contrast.aaa && contrast.aa">
 | 
			
		||||
      <i class="icon-adjust"/>
 | 
			
		||||
    </span>
 | 
			
		||||
    <span v-if="!contrast.aaa && !contrast.aa">
 | 
			
		||||
      <i class="icon-attention"/>
 | 
			
		||||
    </span>
 | 
			
		||||
  </span>
 | 
			
		||||
  <span class="rating" v-if="contrast && large" :title="hint_18pt">
 | 
			
		||||
    <span v-if="contrast.laaa">
 | 
			
		||||
      <i class="icon-thumbs-up-alt"/>
 | 
			
		||||
    </span>
 | 
			
		||||
    <span v-if="!contrast.laaa && contrast.laa">
 | 
			
		||||
      <i class="icon-adjust"/>
 | 
			
		||||
    </span>
 | 
			
		||||
    <span v-if="!contrast.laaa && !contrast.laa">
 | 
			
		||||
      <i class="icon-attention"/>
 | 
			
		||||
    </span>
 | 
			
		||||
  </span>
 | 
			
		||||
</span>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: [
 | 
			
		||||
    'large', 'contrast'
 | 
			
		||||
  ],
 | 
			
		||||
  computed: {
 | 
			
		||||
    hint () {
 | 
			
		||||
      const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
 | 
			
		||||
      const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
 | 
			
		||||
      const context = this.$t('settings.style.common.contrast.context.text')
 | 
			
		||||
      const ratio = this.contrast.text
 | 
			
		||||
      return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
 | 
			
		||||
    },
 | 
			
		||||
    hint_18pt () {
 | 
			
		||||
      const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad')
 | 
			
		||||
      const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
 | 
			
		||||
      const context = this.$t('settings.style.common.contrast.context.18pt')
 | 
			
		||||
      const ratio = this.contrast.text
 | 
			
		||||
      return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.contrast-ratio {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
 | 
			
		||||
  margin-top: -4px;
 | 
			
		||||
  margin-bottom: 5px;
 | 
			
		||||
 | 
			
		||||
  .label {
 | 
			
		||||
    margin-right: 1em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .rating {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -14,8 +14,8 @@
 | 
			
		|||
.icon-cancel,.delete-status {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: var(--cRed, $fallback--cRed);
 | 
			
		||||
    color: $fallback--cRed;
 | 
			
		||||
    color: var(--cRed, $fallback--cRed);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										87
									
								
								src/components/export_import/export_import.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/components/export_import/export_import.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="import-export-container">
 | 
			
		||||
  <slot name="before"/>
 | 
			
		||||
  <button class="btn" @click="exportData">{{ exportLabel }}</button>
 | 
			
		||||
  <button class="btn" @click="importData">{{ importLabel }}</button>
 | 
			
		||||
  <slot name="afterButtons"/>
 | 
			
		||||
  <p v-if="importFailed" class="alert error">{{ importFailedText }}</p>
 | 
			
		||||
  <slot name="afterError"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: [
 | 
			
		||||
    'exportObject',
 | 
			
		||||
    'importLabel',
 | 
			
		||||
    'exportLabel',
 | 
			
		||||
    'importFailedText',
 | 
			
		||||
    'validator',
 | 
			
		||||
    'onImport',
 | 
			
		||||
    'onImportFailure'
 | 
			
		||||
  ],
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      importFailed: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    exportData () {
 | 
			
		||||
      const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces
 | 
			
		||||
 | 
			
		||||
      // Create an invisible link with a data url and simulate a click
 | 
			
		||||
      const e = document.createElement('a')
 | 
			
		||||
      e.setAttribute('download', 'pleroma_theme.json')
 | 
			
		||||
      e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
 | 
			
		||||
      e.style.display = 'none'
 | 
			
		||||
 | 
			
		||||
      document.body.appendChild(e)
 | 
			
		||||
      e.click()
 | 
			
		||||
      document.body.removeChild(e)
 | 
			
		||||
    },
 | 
			
		||||
    importData () {
 | 
			
		||||
      this.importFailed = false
 | 
			
		||||
      const filePicker = document.createElement('input')
 | 
			
		||||
      filePicker.setAttribute('type', 'file')
 | 
			
		||||
      filePicker.setAttribute('accept', '.json')
 | 
			
		||||
 | 
			
		||||
      filePicker.addEventListener('change', event => {
 | 
			
		||||
        if (event.target.files[0]) {
 | 
			
		||||
          // eslint-disable-next-line no-undef
 | 
			
		||||
          const reader = new FileReader()
 | 
			
		||||
          reader.onload = ({target}) => {
 | 
			
		||||
            try {
 | 
			
		||||
              const parsed = JSON.parse(target.result)
 | 
			
		||||
              const valid = this.validator(parsed)
 | 
			
		||||
              if (valid) {
 | 
			
		||||
                this.onImport(parsed)
 | 
			
		||||
              } else {
 | 
			
		||||
                this.importFailed = true
 | 
			
		||||
                // this.onImportFailure(valid)
 | 
			
		||||
              }
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              // This will happen both if there is a JSON syntax error or the theme is missing components
 | 
			
		||||
              this.importFailed = true
 | 
			
		||||
              // this.onImportFailure(e)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          reader.readAsText(event.target.files[0])
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      document.body.appendChild(filePicker)
 | 
			
		||||
      filePicker.click()
 | 
			
		||||
      document.body.removeChild(filePicker)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.import-export-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										58
									
								
								src/components/font_control/font_control.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/components/font_control/font_control.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
import { set } from 'vue'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: [
 | 
			
		||||
    'name', 'label', 'value', 'fallback', 'options', 'no-inherit'
 | 
			
		||||
  ],
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      lValue: this.value,
 | 
			
		||||
      availableOptions: [
 | 
			
		||||
        this.noInherit ? '' : 'inherit',
 | 
			
		||||
        'custom',
 | 
			
		||||
        ...(this.options || []),
 | 
			
		||||
        'serif',
 | 
			
		||||
        'monospace',
 | 
			
		||||
        'sans-serif'
 | 
			
		||||
      ].filter(_ => _)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeUpdate () {
 | 
			
		||||
    this.lValue = this.value
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    present () {
 | 
			
		||||
      return typeof this.lValue !== 'undefined'
 | 
			
		||||
    },
 | 
			
		||||
    dValue () {
 | 
			
		||||
      return this.lValue || this.fallback || {}
 | 
			
		||||
    },
 | 
			
		||||
    family: {
 | 
			
		||||
      get () {
 | 
			
		||||
        return this.dValue.family
 | 
			
		||||
      },
 | 
			
		||||
      set (v) {
 | 
			
		||||
        set(this.lValue, 'family', v)
 | 
			
		||||
        this.$emit('input', this.lValue)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    isCustom () {
 | 
			
		||||
      return this.preset === 'custom'
 | 
			
		||||
    },
 | 
			
		||||
    preset: {
 | 
			
		||||
      get () {
 | 
			
		||||
        if (this.family === 'serif' ||
 | 
			
		||||
            this.family === 'sans-serif' ||
 | 
			
		||||
            this.family === 'monospace' ||
 | 
			
		||||
            this.family === 'inherit') {
 | 
			
		||||
          return this.family
 | 
			
		||||
        } else {
 | 
			
		||||
          return 'custom'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      set (v) {
 | 
			
		||||
        this.family = v === 'custom' ? '' : v
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								src/components/font_control/font_control.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/components/font_control/font_control.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="font-control style-control" :class="{ custom: isCustom }">
 | 
			
		||||
  <label :for="preset === 'custom' ? name : name + '-font-switcher'" class="label">
 | 
			
		||||
    {{label}}
 | 
			
		||||
  </label>
 | 
			
		||||
  <input
 | 
			
		||||
    v-if="typeof fallback !== 'undefined'"
 | 
			
		||||
    class="opt exlcude-disabled"
 | 
			
		||||
    type="checkbox"
 | 
			
		||||
    :id="name + '-o'"
 | 
			
		||||
    :checked="present"
 | 
			
		||||
    @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)">
 | 
			
		||||
  <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
 | 
			
		||||
  <label :for="name + '-font-switcher'" class="select" :disabled="!present">
 | 
			
		||||
    <select
 | 
			
		||||
      :disabled="!present"
 | 
			
		||||
      v-model="preset"
 | 
			
		||||
      class="font-switcher"
 | 
			
		||||
      :id="name + '-font-switcher'">
 | 
			
		||||
      <option v-for="option in availableOptions" :value="option">
 | 
			
		||||
        {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
 | 
			
		||||
      </option>
 | 
			
		||||
    </select>
 | 
			
		||||
    <i class="icon-down-open"/>
 | 
			
		||||
  </label>
 | 
			
		||||
  <input
 | 
			
		||||
    v-if="isCustom"
 | 
			
		||||
    class="custom-font"
 | 
			
		||||
    type="text"
 | 
			
		||||
    :id="name"
 | 
			
		||||
    v-model="family">
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script src="./font_control.js" ></script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
@import '../../_variables.scss';
 | 
			
		||||
.font-control {
 | 
			
		||||
  input.custom-font {
 | 
			
		||||
    min-width: 10em;
 | 
			
		||||
  }
 | 
			
		||||
  &.custom {
 | 
			
		||||
    .select {
 | 
			
		||||
      border-top-right-radius: 0;
 | 
			
		||||
      border-bottom-right-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
    .custom-font {
 | 
			
		||||
      border-top-left-radius: 0;
 | 
			
		||||
      border-bottom-left-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,9 @@ const InstanceSpecificPanel = {
 | 
			
		|||
  computed: {
 | 
			
		||||
    instanceSpecificPanelContent () {
 | 
			
		||||
      return this.$store.state.instance.instanceSpecificPanelContent
 | 
			
		||||
    },
 | 
			
		||||
    show () {
 | 
			
		||||
      return !this.$store.state.config.hideISP
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="instance-specific-panel">
 | 
			
		||||
  <div v-if="show" class="instance-specific-panel">
 | 
			
		||||
    <div class="panel panel-default">
 | 
			
		||||
      <div class="panel-body">
 | 
			
		||||
        <div v-html="instanceSpecificPanelContent">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,8 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <label for="interface-language-switcher">
 | 
			
		||||
      {{ $t('settings.interfaceLanguage') }}
 | 
			
		||||
    </label>
 | 
			
		||||
    <label for="interface-language-switcher" class='select'>
 | 
			
		||||
      <select id="interface-language-switcher" v-model="language">
 | 
			
		||||
        <option v-for="(langCode, i) in languageCodes" :value="langCode">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
/* eslint-env browser */
 | 
			
		||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
 | 
			
		||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
 | 
			
		||||
 | 
			
		||||
const mediaUpload = {
 | 
			
		||||
  mounted () {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,12 @@ const mediaUpload = {
 | 
			
		|||
    uploadFile (file) {
 | 
			
		||||
      const self = this
 | 
			
		||||
      const store = this.$store
 | 
			
		||||
      if (file.size > store.state.instance.uploadlimit) {
 | 
			
		||||
        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
 | 
			
		||||
        const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)
 | 
			
		||||
        self.$emit('upload-failed', 'file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const formData = new FormData()
 | 
			
		||||
      formData.append('media', file)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +39,7 @@ const mediaUpload = {
 | 
			
		|||
          self.$emit('uploaded', fileData)
 | 
			
		||||
          self.uploading = false
 | 
			
		||||
        }, (error) => { // eslint-disable-line handle-callback-err
 | 
			
		||||
          self.$emit('upload-failed')
 | 
			
		||||
          self.$emit('upload-failed', 'default')
 | 
			
		||||
          self.uploading = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,11 +6,13 @@ import { highlightClass, highlightStyle } from '../../services/user_highlighter/
 | 
			
		|||
const Notification = {
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      userExpanded: false
 | 
			
		||||
      userExpanded: false,
 | 
			
		||||
      betterShadow: this.$store.state.interface.browserSupport.cssFilter
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  props: [
 | 
			
		||||
    'notification'
 | 
			
		||||
    'notification',
 | 
			
		||||
    'activatePanel'
 | 
			
		||||
  ],
 | 
			
		||||
  components: {
 | 
			
		||||
    Status, StillImage, UserCardContent
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
 | 
			
		||||
  <status :activatePanel="activatePanel" v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
 | 
			
		||||
  <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
 | 
			
		||||
    <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
 | 
			
		||||
      <StillImage class='avatar-compact' :src="notification.action.user.profile_image_url_original"/>
 | 
			
		||||
      <StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
 | 
			
		||||
    </a>
 | 
			
		||||
    <div class='notification-right'>
 | 
			
		||||
      <div class="usercard notification-usercard" v-if="userExpanded">
 | 
			
		||||
| 
						 | 
				
			
			@ -25,13 +25,13 @@
 | 
			
		|||
            <small>{{$t('notifications.followed_you')}}</small>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
 | 
			
		||||
        <small class="timeago"><router-link @click.native="activatePanel('timeline')" v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
 | 
			
		||||
      </span>
 | 
			
		||||
      <div class="follow-text" v-if="notification.type === 'follow'">
 | 
			
		||||
        <router-link :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link>
 | 
			
		||||
        <router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link>
 | 
			
		||||
      </div>
 | 
			
		||||
      <template v-else>
 | 
			
		||||
        <status v-if="notification.status"  class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
 | 
			
		||||
        <status :activatePanel="activatePanel" v-if="notification.status"  class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
 | 
			
		||||
        <div class="broken-favorite" v-else>
 | 
			
		||||
          {{$t('notifications.broken_favorite')}}
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ import notificationsFetcher from '../../services/notifications_fetcher/notificat
 | 
			
		|||
import { sortBy, filter } from 'lodash'
 | 
			
		||||
 | 
			
		||||
const Notifications = {
 | 
			
		||||
  props: [ 'activatePanel' ],
 | 
			
		||||
  created () {
 | 
			
		||||
    const store = this.$store
 | 
			
		||||
    const credentials = store.state.users.currentUser.credentials
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,31 +4,28 @@
 | 
			
		|||
  // a bit of a hack to allow scrolling below notifications
 | 
			
		||||
  padding-bottom: 15em;
 | 
			
		||||
 | 
			
		||||
  .unseen-count {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    background-color: $fallback--cRed;
 | 
			
		||||
    background-color: var(--cRed, $fallback--cRed);
 | 
			
		||||
    text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
 | 
			
		||||
    border-radius: 99px;
 | 
			
		||||
    min-width: 22px;
 | 
			
		||||
    max-width: 22px;
 | 
			
		||||
    min-height: 22px;
 | 
			
		||||
    max-height: 22px;
 | 
			
		||||
    color: white;
 | 
			
		||||
    font-size: 15px;
 | 
			
		||||
    line-height: 22px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    vertical-align: middle
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .loadmore-error {
 | 
			
		||||
    color: $fallback--fg;
 | 
			
		||||
    color: var(--fg, $fallback--fg);
 | 
			
		||||
    color: $fallback--text;
 | 
			
		||||
    color: var(--text, $fallback--text);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .unseen {
 | 
			
		||||
    box-shadow: inset 4px 0 0 var(--cRed, $fallback--cRed);
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
  .notification {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    .notification-overlay {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.unseen {
 | 
			
		||||
      .notification-overlay {
 | 
			
		||||
        background-image: linear-gradient(135deg, var(--badgeNotification, $fallback--cRed) 4px, transparent 10px)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,21 +39,27 @@
 | 
			
		|||
  .broken-favorite {
 | 
			
		||||
    border-radius: $fallback--tooltipRadius;
 | 
			
		||||
    border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
 | 
			
		||||
    color: $fallback--faint;
 | 
			
		||||
    color: var(--faint, $fallback--faint);
 | 
			
		||||
    background-color: $fallback--cAlertRed;
 | 
			
		||||
    background-color: var(--cAlertRed, $fallback--cAlertRed);
 | 
			
		||||
    color: $fallback--text;
 | 
			
		||||
    color: var(--alertErrorText, $fallback--text);
 | 
			
		||||
    background-color: $fallback--alertError;
 | 
			
		||||
    background-color: var(--alertError, $fallback--alertError);
 | 
			
		||||
    padding: 2px .5em
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .avatar-compact {
 | 
			
		||||
    width: 32px;
 | 
			
		||||
    height: 32px;
 | 
			
		||||
    box-shadow: var(--avatarStatusShadow);
 | 
			
		||||
    border-radius: $fallback--avatarAltRadius;
 | 
			
		||||
    border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    line-height: 0;
 | 
			
		||||
 | 
			
		||||
    &.better-shadow {
 | 
			
		||||
      box-shadow: var(--avatarStatusShadowInset);
 | 
			
		||||
      filter: var(--avatarStatusShadowFilter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.animated::before {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +93,9 @@
 | 
			
		|||
        padding: 0.25em 0;
 | 
			
		||||
        color: $fallback--faint;
 | 
			
		||||
        color: var(--faint, $fallback--faint);
 | 
			
		||||
        a {
 | 
			
		||||
          color: var(--faintLink);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      padding: 0;
 | 
			
		||||
      .media-body {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
      <div class="panel-heading">
 | 
			
		||||
        <div class="title">
 | 
			
		||||
          {{$t('notifications.notifications')}}
 | 
			
		||||
          <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
 | 
			
		||||
          <span class="badge badge-notification unseen-count" v-if="unseenCount">{{unseenCount}}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div @click.prevent class="loadmore-error alert error" v-if="error">
 | 
			
		||||
          {{$t('timeline.error_fetching')}}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,8 @@
 | 
			
		|||
      </div>
 | 
			
		||||
      <div class="panel-body">
 | 
			
		||||
        <div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
 | 
			
		||||
          <notification :notification="notification"></notification>
 | 
			
		||||
          <div class="notification-overlay"></div>
 | 
			
		||||
          <notification :activatePanel="activatePanel" :notification="notification"></notification>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="panel-footer">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										38
									
								
								src/components/opacity_input/opacity_input.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/opacity_input/opacity_input.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="opacity-control style-control" :class="{ disabled: !present || disabled }">
 | 
			
		||||
  <label :for="name" class="label">
 | 
			
		||||
    {{$t('settings.style.common.opacity')}}
 | 
			
		||||
  </label>
 | 
			
		||||
  <input
 | 
			
		||||
    v-if="typeof fallback !== 'undefined'"
 | 
			
		||||
    class="opt exclude-disabled"
 | 
			
		||||
    :id="name + '-o'"
 | 
			
		||||
    type="checkbox"
 | 
			
		||||
    :checked="present"
 | 
			
		||||
    @input="$emit('input', !present ? fallback : undefined)">
 | 
			
		||||
  <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
 | 
			
		||||
  <input
 | 
			
		||||
    :id="name"
 | 
			
		||||
    class="input-number"
 | 
			
		||||
    type="number"
 | 
			
		||||
    :value="value || fallback"
 | 
			
		||||
    :disabled="!present || disabled"
 | 
			
		||||
    @input="$emit('input', $event.target.value)"
 | 
			
		||||
    max="1"
 | 
			
		||||
    min="0"
 | 
			
		||||
    step=".05">
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: [
 | 
			
		||||
    'name', 'value', 'fallback', 'disabled'
 | 
			
		||||
  ],
 | 
			
		||||
  computed: {
 | 
			
		||||
    present () {
 | 
			
		||||
      return typeof this.value !== 'undefined'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ const PostStatusForm = {
 | 
			
		|||
      statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const scope = (this.copyMessageScope && this.$store.state.config.copyScope || this.copyMessageScope === 'direct')
 | 
			
		||||
    const scope = (this.copyMessageScope && this.$store.state.config.scopeCopy || this.copyMessageScope === 'direct')
 | 
			
		||||
          ? this.copyMessageScope
 | 
			
		||||
          : this.$store.state.users.currentUser.default_scope
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -262,6 +262,11 @@ const PostStatusForm = {
 | 
			
		|||
      let index = this.newStatus.files.indexOf(fileInfo)
 | 
			
		||||
      this.newStatus.files.splice(index, 1)
 | 
			
		||||
    },
 | 
			
		||||
    uploadFailed (errString, templateArgs) {
 | 
			
		||||
      templateArgs = templateArgs || {}
 | 
			
		||||
      this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
 | 
			
		||||
      this.enableSubmit()
 | 
			
		||||
    },
 | 
			
		||||
    disableSubmit () {
 | 
			
		||||
      this.submitDisabled = true
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,7 +64,7 @@
 | 
			
		|||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class='form-bottom'>
 | 
			
		||||
        <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload>
 | 
			
		||||
        <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
 | 
			
		||||
 | 
			
		||||
        <p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
 | 
			
		||||
        <p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
 | 
			
		||||
| 
						 | 
				
			
			@ -153,8 +153,8 @@
 | 
			
		|||
      padding-bottom: 0;
 | 
			
		||||
      margin-left: $fallback--attachmentRadius;
 | 
			
		||||
      margin-left: var(--attachmentRadius, $fallback--attachmentRadius);
 | 
			
		||||
      background-color: $fallback--btn;
 | 
			
		||||
      background-color: var(--btn, $fallback--btn);
 | 
			
		||||
      background-color: $fallback--fg;
 | 
			
		||||
      background-color: var(--btn, $fallback--fg);
 | 
			
		||||
      border-bottom-left-radius: 0;
 | 
			
		||||
      border-bottom-right-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -258,11 +258,13 @@
 | 
			
		|||
    position: absolute;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
 | 
			
		||||
    // this doesn't match original but i don't care, making it uniform.
 | 
			
		||||
    box-shadow: var(--popupShadow);
 | 
			
		||||
    min-width: 75%;
 | 
			
		||||
    background: $fallback--bg;
 | 
			
		||||
    background: var(--bg, $fallback--bg);
 | 
			
		||||
    color: $fallback--lightFg;
 | 
			
		||||
    color: var(--lightFg, $fallback--lightFg);
 | 
			
		||||
    color: $fallback--lightText;
 | 
			
		||||
    color: var(--lightText, $fallback--lightText);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .autocomplete {
 | 
			
		||||
| 
						 | 
				
			
			@ -291,8 +293,8 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    &.highlighted {
 | 
			
		||||
      background-color: $fallback--btn;
 | 
			
		||||
      background-color: var(--btn, $fallback--btn);
 | 
			
		||||
      background-color: $fallback--fg;
 | 
			
		||||
      background-color: var(--lightBg, $fallback--fg);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										48
									
								
								src/components/range_input/range_input.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/components/range_input/range_input.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="range-control style-control" :class="{ disabled: !present || disabled }">
 | 
			
		||||
  <label :for="name" class="label">
 | 
			
		||||
    {{label}}
 | 
			
		||||
  </label>
 | 
			
		||||
  <input
 | 
			
		||||
    v-if="typeof fallback !== 'undefined'"
 | 
			
		||||
    class="opt exclude-disabled"
 | 
			
		||||
    :id="name + '-o'"
 | 
			
		||||
    type="checkbox"
 | 
			
		||||
    :checked="present"
 | 
			
		||||
    @input="$emit('input', !present ? fallback : undefined)">
 | 
			
		||||
  <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
 | 
			
		||||
  <input
 | 
			
		||||
    :id="name"
 | 
			
		||||
    class="input-number"
 | 
			
		||||
    type="range"
 | 
			
		||||
    :value="value || fallback"
 | 
			
		||||
    :disabled="!present || disabled"
 | 
			
		||||
    @input="$emit('input', $event.target.value)"
 | 
			
		||||
    :max="max || hardMax || 100"
 | 
			
		||||
    :min="min || hardMin || 0"
 | 
			
		||||
    :step="step || 1">
 | 
			
		||||
  <input
 | 
			
		||||
    :id="name"
 | 
			
		||||
    class="input-number"
 | 
			
		||||
    type="number"
 | 
			
		||||
    :value="value || fallback"
 | 
			
		||||
    :disabled="!present || disabled"
 | 
			
		||||
    @input="$emit('input', $event.target.value)"
 | 
			
		||||
    :max="hardMax"
 | 
			
		||||
    :min="hardMin"
 | 
			
		||||
    :step="step || 1">
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: [
 | 
			
		||||
    'name', 'value', 'fallback', 'disabled', 'label', 'max', 'min', 'step', 'hardMin', 'hardMax'
 | 
			
		||||
  ],
 | 
			
		||||
  computed: {
 | 
			
		||||
    present () {
 | 
			
		||||
      return typeof this.value !== 'undefined'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,57 +1,61 @@
 | 
			
		|||
import oauthApi from '../../services/new_api/oauth.js'
 | 
			
		||||
import { validationMixin } from 'vuelidate'
 | 
			
		||||
import { required, sameAs } from 'vuelidate/lib/validators'
 | 
			
		||||
import { mapActions, mapState } from 'vuex'
 | 
			
		||||
 | 
			
		||||
const registration = {
 | 
			
		||||
  mixins: [validationMixin],
 | 
			
		||||
  data: () => ({
 | 
			
		||||
    user: {},
 | 
			
		||||
    error: false,
 | 
			
		||||
    registering: false
 | 
			
		||||
  }),
 | 
			
		||||
  created () {
 | 
			
		||||
    if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
 | 
			
		||||
      this.$router.push('/main/all')
 | 
			
		||||
    user: {
 | 
			
		||||
      email: '',
 | 
			
		||||
      fullname: '',
 | 
			
		||||
      username: '',
 | 
			
		||||
      password: '',
 | 
			
		||||
      confirm: ''
 | 
			
		||||
    }
 | 
			
		||||
    // Seems like this doesn't work at first page open for some reason
 | 
			
		||||
    if (this.$store.state.instance.registrationOpen && this.token) {
 | 
			
		||||
      this.$router.push('/registration')
 | 
			
		||||
  }),
 | 
			
		||||
  validations: {
 | 
			
		||||
    user: {
 | 
			
		||||
      email: { required },
 | 
			
		||||
      username: { required },
 | 
			
		||||
      fullname: { required },
 | 
			
		||||
      password: { required },
 | 
			
		||||
      confirm: {
 | 
			
		||||
        required,
 | 
			
		||||
        sameAsPassword: sameAs('password')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created () {
 | 
			
		||||
    if ((!this.registrationOpen && !this.token) || this.signedIn) {
 | 
			
		||||
      this.$router.push('/main/all')
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    termsofservice () { return this.$store.state.instance.tos },
 | 
			
		||||
    token () { return this.$route.params.token }
 | 
			
		||||
    token () { return this.$route.params.token },
 | 
			
		||||
    ...mapState({
 | 
			
		||||
      registrationOpen: (state) => state.instance.registrationOpen,
 | 
			
		||||
      signedIn: (state) => !!state.users.currentUser,
 | 
			
		||||
      isPending: (state) => state.users.signUpPending,
 | 
			
		||||
      serverValidationErrors: (state) => state.users.signUpErrors,
 | 
			
		||||
      termsOfService: (state) => state.instance.tos
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    submit () {
 | 
			
		||||
      this.registering = true
 | 
			
		||||
    ...mapActions(['signUp']),
 | 
			
		||||
    async submit () {
 | 
			
		||||
      this.user.nickname = this.user.username
 | 
			
		||||
      this.user.token = this.token
 | 
			
		||||
      this.$store.state.api.backendInteractor.register(this.user).then(
 | 
			
		||||
        (response) => {
 | 
			
		||||
          if (response.ok) {
 | 
			
		||||
            const data = {
 | 
			
		||||
              oauth: this.$store.state.oauth,
 | 
			
		||||
              instance: this.$store.state.instance.server
 | 
			
		||||
            }
 | 
			
		||||
            oauthApi.getOrCreateApp(data).then((app) => {
 | 
			
		||||
              oauthApi.getTokenWithCredentials(
 | 
			
		||||
                {
 | 
			
		||||
                  app,
 | 
			
		||||
                  instance: data.instance,
 | 
			
		||||
                  username: this.user.username,
 | 
			
		||||
                  password: this.user.password})
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                  this.$store.commit('setToken', result.access_token)
 | 
			
		||||
                  this.$store.dispatch('loginUser', result.access_token)
 | 
			
		||||
                  this.$router.push('/main/friends')
 | 
			
		||||
                })
 | 
			
		||||
            })
 | 
			
		||||
          } else {
 | 
			
		||||
            this.registering = false
 | 
			
		||||
            response.json().then((data) => {
 | 
			
		||||
              this.error = data.error
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
      this.$v.$touch()
 | 
			
		||||
 | 
			
		||||
      if (!this.$v.$invalid) {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.signUp(this.user)
 | 
			
		||||
          this.$router.push('/main/friends')
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.warn('Registration failed: ' + error)
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,50 +7,90 @@
 | 
			
		|||
      <form v-on:submit.prevent='submit(user)' class='registration-form'>
 | 
			
		||||
        <div class='container'>
 | 
			
		||||
          <div class='text-fields'>
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <label for='username'>{{$t('login.username')}}</label>
 | 
			
		||||
              <input :disabled="registering" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'>
 | 
			
		||||
            <div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
 | 
			
		||||
              <label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
 | 
			
		||||
              <input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <label for='fullname'>{{$t('registration.fullname')}}</label>
 | 
			
		||||
              <input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'>
 | 
			
		||||
            <div class="form-error" v-if="$v.user.username.$dirty">
 | 
			
		||||
              <ul>
 | 
			
		||||
                <li v-if="!$v.user.username.required">
 | 
			
		||||
                  <span>{{$t('registration.validations.username_required')}}</span>
 | 
			
		||||
                </li>
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <label for='email'>{{$t('registration.email')}}</label>
 | 
			
		||||
              <input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email">
 | 
			
		||||
 | 
			
		||||
            <div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
 | 
			
		||||
              <label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
 | 
			
		||||
              <input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <label for='bio'>{{$t('registration.bio')}}</label>
 | 
			
		||||
              <input :disabled="registering" v-model='user.bio' class='form-control' id='bio'>
 | 
			
		||||
            <div class="form-error" v-if="$v.user.fullname.$dirty">
 | 
			
		||||
              <ul>
 | 
			
		||||
                <li v-if="!$v.user.fullname.required">
 | 
			
		||||
                  <span>{{$t('registration.validations.fullname_required')}}</span>
 | 
			
		||||
                </li>
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <label for='password'>{{$t('login.password')}}</label>
 | 
			
		||||
              <input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'>
 | 
			
		||||
 | 
			
		||||
            <div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }">
 | 
			
		||||
              <label class='form--label' for='email'>{{$t('registration.email')}}</label>
 | 
			
		||||
              <input :disabled="isPending" v-model='$v.user.email.$model' class='form-control' id='email' type="email">
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <label for='password_confirmation'>{{$t('registration.password_confirm')}}</label>
 | 
			
		||||
              <input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'>
 | 
			
		||||
            <div class="form-error" v-if="$v.user.email.$dirty">
 | 
			
		||||
              <ul>
 | 
			
		||||
                <li v-if="!$v.user.email.required">
 | 
			
		||||
                  <span>{{$t('registration.validations.email_required')}}</span>
 | 
			
		||||
                </li>
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!--
 | 
			
		||||
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <label for='captcha'>Captcha</label>
 | 
			
		||||
              <img src='/qvittersimplesecurity/captcha.jpg' alt='captcha' class='captcha'>
 | 
			
		||||
              <input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
 | 
			
		||||
              <label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
 | 
			
		||||
              <input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
 | 
			
		||||
            </div>
 | 
			
		||||
            -->
 | 
			
		||||
 | 
			
		||||
            <div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
 | 
			
		||||
              <label class='form--label' for='sign-up-password'>{{$t('login.password')}}</label>
 | 
			
		||||
              <input :disabled="isPending" v-model='user.password' class='form-control' id='sign-up-password' type='password'>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-error" v-if="$v.user.password.$dirty">
 | 
			
		||||
              <ul>
 | 
			
		||||
                <li v-if="!$v.user.password.required">
 | 
			
		||||
                  <span>{{$t('registration.validations.password_required')}}</span>
 | 
			
		||||
                </li>
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class='form-group' :class="{ 'form-group--error': $v.user.confirm.$error }">
 | 
			
		||||
              <label class='form--label' for='sign-up-password-confirmation'>{{$t('registration.password_confirm')}}</label>
 | 
			
		||||
              <input :disabled="isPending" v-model='user.confirm' class='form-control' id='sign-up-password-confirmation' type='password'>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-error" v-if="$v.user.confirm.$dirty">
 | 
			
		||||
              <ul>
 | 
			
		||||
                <li v-if="!$v.user.confirm.required">
 | 
			
		||||
                  <span>{{$t('registration.validations.password_confirmation_required')}}</span>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li v-if="!$v.user.confirm.sameAsPassword">
 | 
			
		||||
                  <span>{{$t('registration.validations.password_confirmation_match')}}</span>
 | 
			
		||||
                </li>
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class='form-group' v-if='token' >
 | 
			
		||||
              <label for='token'>{{$t('registration.token')}}</label>
 | 
			
		||||
              <input disabled='true' v-model='token' class='form-control' id='token' type='text'>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class='form-group'>
 | 
			
		||||
              <button :disabled="registering" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
 | 
			
		||||
              <button :disabled="isPending" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class='terms-of-service' v-html="termsofservice">
 | 
			
		||||
 | 
			
		||||
          <div class='terms-of-service' v-html="termsOfService">
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="error" class='form-group'>
 | 
			
		||||
          <div class='alert error'>{{error}}</div>
 | 
			
		||||
        <div v-if="serverValidationErrors.length" class='form-group'>
 | 
			
		||||
          <div class='alert error'>
 | 
			
		||||
            <span v-for="error in serverValidationErrors">{{error}}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -60,6 +100,7 @@
 | 
			
		|||
<script src="./registration.js"></script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
@import '../../_variables.scss';
 | 
			
		||||
$validations-cRed: #f04124;
 | 
			
		||||
 | 
			
		||||
.registration-form {
 | 
			
		||||
  display: flex;
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +130,55 @@
 | 
			
		|||
    flex-direction: column;
 | 
			
		||||
    padding: 0.3em 0.0em 0.3em;
 | 
			
		||||
    line-height:24px;
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes shakeError {
 | 
			
		||||
    0% {
 | 
			
		||||
      transform: translateX(0); }
 | 
			
		||||
    15% {
 | 
			
		||||
      transform: translateX(0.375rem); }
 | 
			
		||||
    30% {
 | 
			
		||||
      transform: translateX(-0.375rem); }
 | 
			
		||||
    45% {
 | 
			
		||||
      transform: translateX(0.375rem); }
 | 
			
		||||
    60% {
 | 
			
		||||
      transform: translateX(-0.375rem); }
 | 
			
		||||
    75% {
 | 
			
		||||
      transform: translateX(0.375rem); }
 | 
			
		||||
    90% {
 | 
			
		||||
      transform: translateX(-0.375rem); }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: translateX(0); } }
 | 
			
		||||
 | 
			
		||||
  .form-group--error {
 | 
			
		||||
    animation-name: shakeError;
 | 
			
		||||
    animation-duration: .6s;
 | 
			
		||||
    animation-timing-function: ease-in-out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .form-group--error .form--label {
 | 
			
		||||
    color: $validations-cRed;
 | 
			
		||||
    color: var(--cRed, $validations-cRed);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .form-error {
 | 
			
		||||
    margin-top: -0.7em;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
 | 
			
		||||
    span {
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .form-error ul {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    padding: 0 0 0 5px;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
 | 
			
		||||
    li::before {
 | 
			
		||||
      content: "• ";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  form textarea {
 | 
			
		||||
| 
						 | 
				
			
			@ -102,8 +192,6 @@
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  .btn {
 | 
			
		||||
    //align-self: flex-start;
 | 
			
		||||
    //width: 10em;
 | 
			
		||||
    margin-top: 0.6em;
 | 
			
		||||
    height: 28px;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,8 @@ const settings = {
 | 
			
		|||
      hideAttachmentsLocal: user.hideAttachments,
 | 
			
		||||
      hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
 | 
			
		||||
      hideNsfwLocal: user.hideNsfw,
 | 
			
		||||
      hideISPLocal: user.hideISP,
 | 
			
		||||
      preloadImage: user.preloadImage,
 | 
			
		||||
      hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
 | 
			
		||||
        ? instance.hidePostStats
 | 
			
		||||
        : user.hidePostStats,
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +47,7 @@ const settings = {
 | 
			
		|||
      scopeCopyLocal: user.scopeCopy,
 | 
			
		||||
      scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
 | 
			
		||||
      stopGifs: user.stopGifs,
 | 
			
		||||
      webPushNotificationsLocal: user.webPushNotifications,
 | 
			
		||||
      loopSilentAvailable:
 | 
			
		||||
        // Firefox
 | 
			
		||||
        Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +86,12 @@ const settings = {
 | 
			
		|||
    hideNsfwLocal (value) {
 | 
			
		||||
      this.$store.dispatch('setOption', { name: 'hideNsfw', value })
 | 
			
		||||
    },
 | 
			
		||||
    preloadImage (value) {
 | 
			
		||||
      this.$store.dispatch('setOption', { name: 'preloadImage', value })
 | 
			
		||||
    },
 | 
			
		||||
    hideISPLocal (value) {
 | 
			
		||||
      this.$store.dispatch('setOption', { name: 'hideISP', value })
 | 
			
		||||
    },
 | 
			
		||||
    'notificationVisibilityLocal.likes' (value) {
 | 
			
		||||
      this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -134,6 +143,10 @@ const settings = {
 | 
			
		|||
    },
 | 
			
		||||
    stopGifs (value) {
 | 
			
		||||
      this.$store.dispatch('setOption', { name: 'stopGifs', value })
 | 
			
		||||
    },
 | 
			
		||||
    webPushNotificationsLocal (value) {
 | 
			
		||||
      this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
 | 
			
		||||
      if (value) this.$store.dispatch('registerPushNotifications')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,15 +14,24 @@
 | 
			
		|||
        <div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
 | 
			
		||||
          {{ $t('settings.saving_ok') }}
 | 
			
		||||
        </div>
 | 
			
		||||
    </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </transition>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="panel-body">
 | 
			
		||||
<keep-alive>
 | 
			
		||||
    <tab-switcher>
 | 
			
		||||
      <div :label="$t('settings.general')" >
 | 
			
		||||
        <div class="setting-item">
 | 
			
		||||
          <h2>{{ $t('settings.interfaceLanguage') }}</h2>
 | 
			
		||||
          <interface-language-switcher />
 | 
			
		||||
          <h2>{{ $t('settings.interface') }}</h2>
 | 
			
		||||
          <ul class="setting-list">
 | 
			
		||||
            <li>
 | 
			
		||||
              <interface-language-switcher />
 | 
			
		||||
            </li>
 | 
			
		||||
            <li>
 | 
			
		||||
              <input type="checkbox" id="hideISP" v-model="hideISPLocal">
 | 
			
		||||
              <label for="hideISP">{{$t('settings.hide_isp')}}</label>
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="setting-item">
 | 
			
		||||
          <h2>{{$t('nav.timeline')}}</h2>
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +118,12 @@
 | 
			
		|||
              <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
 | 
			
		||||
              <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
 | 
			
		||||
            </li>
 | 
			
		||||
            <ul class="setting-list suboptions" >
 | 
			
		||||
              <li>
 | 
			
		||||
                <input :disabled="!hideAttachmentsInConvLocal" type="checkbox" id="preloadImage" v-model="preloadImage">
 | 
			
		||||
                <label for="preloadImage">{{$t('settings.preload_images')}}</label>
 | 
			
		||||
              </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
            <li>
 | 
			
		||||
              <input type="checkbox" id="stopGifs" v-model="stopGifs">
 | 
			
		||||
              <label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +143,18 @@
 | 
			
		|||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
       <div class="setting-item">
 | 
			
		||||
          <h2>{{$t('settings.notifications')}}</h2>
 | 
			
		||||
          <ul class="setting-list">
 | 
			
		||||
            <li>
 | 
			
		||||
              <input type="checkbox" id="webPushNotifications" v-model="webPushNotificationsLocal">
 | 
			
		||||
              <label for="webPushNotifications">
 | 
			
		||||
                {{$t('settings.enable_web_push_notifications')}}
 | 
			
		||||
              </label>
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div :label="$t('settings.theme')" >
 | 
			
		||||
| 
						 | 
				
			
			@ -199,6 +226,7 @@
 | 
			
		|||
      </div>
 | 
			
		||||
 | 
			
		||||
    </tab-switcher>
 | 
			
		||||
</keep-alive>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -210,7 +238,7 @@
 | 
			
		|||
@import '../../_variables.scss';
 | 
			
		||||
 | 
			
		||||
.setting-item {
 | 
			
		||||
  border-bottom: 2px solid var(--btn, $fallback--btn);
 | 
			
		||||
  border-bottom: 2px solid var(--fg, $fallback--fg);
 | 
			
		||||
  margin: 1em 1em 1.4em;
 | 
			
		||||
  padding-bottom: 1.4em;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -259,12 +287,8 @@
 | 
			
		|||
 | 
			
		||||
  .btn {
 | 
			
		||||
    min-height: 28px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .submit {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
    min-height: 30px;
 | 
			
		||||
    width: 10em;
 | 
			
		||||
    min-width: 10em;
 | 
			
		||||
    padding: 0 2em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.select-multiple {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										87
									
								
								src/components/shadow_control/shadow_control.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/components/shadow_control/shadow_control.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
import ColorInput from '../color_input/color_input.vue'
 | 
			
		||||
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'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  // 'Value' and 'Fallback' can be undefined, but if they are
 | 
			
		||||
  // initially vue won't detect it when they become something else
 | 
			
		||||
  // therefore i'm using "ready" which should be passed as true when
 | 
			
		||||
  // data becomes available
 | 
			
		||||
  props: [
 | 
			
		||||
    'value', 'fallback', 'ready'
 | 
			
		||||
  ],
 | 
			
		||||
  data () {
 | 
			
		||||
    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 || []
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    ColorInput,
 | 
			
		||||
    OpacityInput
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    add () {
 | 
			
		||||
      this.cValue.push(Object.assign({}, 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
 | 
			
		||||
    },
 | 
			
		||||
    moveUp () {
 | 
			
		||||
      const movable = this.cValue.splice(this.selectedId, 1)[0]
 | 
			
		||||
      this.cValue.splice(this.selectedId - 1, 0, movable)
 | 
			
		||||
      this.selectedId -= 1
 | 
			
		||||
    },
 | 
			
		||||
    moveDn () {
 | 
			
		||||
      const movable = this.cValue.splice(this.selectedId, 1)[0]
 | 
			
		||||
      this.cValue.splice(this.selectedId + 1, 0, movable)
 | 
			
		||||
      this.selectedId += 1
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeUpdate () {
 | 
			
		||||
    this.cValue = this.value || this.fallback
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    selected () {
 | 
			
		||||
      if (this.ready && this.cValue.length > 0) {
 | 
			
		||||
        return this.cValue[this.selectedId]
 | 
			
		||||
      } else {
 | 
			
		||||
        return {
 | 
			
		||||
          x: 0,
 | 
			
		||||
          y: 0,
 | 
			
		||||
          blur: 0,
 | 
			
		||||
          spread: 0,
 | 
			
		||||
          inset: false,
 | 
			
		||||
          color: '#000000',
 | 
			
		||||
          alpha: 1
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    moveUpValid () {
 | 
			
		||||
      return this.ready && this.selectedId > 0
 | 
			
		||||
    },
 | 
			
		||||
    moveDnValid () {
 | 
			
		||||
      return this.ready && this.selectedId < this.cValue.length - 1
 | 
			
		||||
    },
 | 
			
		||||
    present () {
 | 
			
		||||
      return this.ready &&
 | 
			
		||||
        typeof this.cValue[this.selectedId] !== 'undefined' &&
 | 
			
		||||
        !this.usingFallback
 | 
			
		||||
    },
 | 
			
		||||
    usingFallback () {
 | 
			
		||||
      return typeof this.value === 'undefined'
 | 
			
		||||
    },
 | 
			
		||||
    rgb () {
 | 
			
		||||
      return hex2rgb(this.selected.color)
 | 
			
		||||
    },
 | 
			
		||||
    style () {
 | 
			
		||||
      return this.ready ? {
 | 
			
		||||
        boxShadow: getCssShadow(this.cValue)
 | 
			
		||||
      } : {}
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										243
									
								
								src/components/shadow_control/shadow_control.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								src/components/shadow_control/shadow_control.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,243 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="shadow-control" :class="{ disabled: !present }">
 | 
			
		||||
  <div class="shadow-preview-container">
 | 
			
		||||
    <div :disabled="!present" class="y-shift-control">
 | 
			
		||||
      <input
 | 
			
		||||
        v-model="selected.y"
 | 
			
		||||
        :disabled="!present"
 | 
			
		||||
        class="input-number"
 | 
			
		||||
        type="number">
 | 
			
		||||
      <div class="wrap">
 | 
			
		||||
        <input
 | 
			
		||||
          v-model="selected.y"
 | 
			
		||||
          :disabled="!present"
 | 
			
		||||
          class="input-range"
 | 
			
		||||
          type="range"
 | 
			
		||||
          max="20"
 | 
			
		||||
          min="-20">
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="preview-window">
 | 
			
		||||
      <div class="preview-block" :style="style"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div :disabled="!present" class="x-shift-control">
 | 
			
		||||
      <input
 | 
			
		||||
        v-model="selected.x"
 | 
			
		||||
        :disabled="!present"
 | 
			
		||||
        class="input-number"
 | 
			
		||||
        type="number">
 | 
			
		||||
      <div class="wrap">
 | 
			
		||||
        <input
 | 
			
		||||
          v-model="selected.x"
 | 
			
		||||
          :disabled="!present"
 | 
			
		||||
          class="input-range"
 | 
			
		||||
          type="range"
 | 
			
		||||
          max="20"
 | 
			
		||||
          min="-20">
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="shadow-tweak">
 | 
			
		||||
    <div :disabled="usingFallback" class="id-control style-control">
 | 
			
		||||
      <label for="shadow-switcher" class="select" :disabled="!ready || usingFallback">
 | 
			
		||||
        <select
 | 
			
		||||
          v-model="selectedId" class="shadow-switcher"
 | 
			
		||||
          :disabled="!ready || usingFallback"
 | 
			
		||||
          id="shadow-switcher">
 | 
			
		||||
          <option v-for="(shadow, index) in cValue" :value="index">
 | 
			
		||||
            {{$t('settings.style.shadows.shadow_id', { value: index })}}
 | 
			
		||||
          </option>
 | 
			
		||||
        </select>
 | 
			
		||||
        <i class="icon-down-open"/>
 | 
			
		||||
      </label>
 | 
			
		||||
      <button class="btn btn-default" :disabled="!ready || !present" @click="del">
 | 
			
		||||
        <i class="icon-cancel"/>
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="btn btn-default" :disabled="!moveUpValid" @click="moveUp">
 | 
			
		||||
        <i class="icon-up-open"/>
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="btn btn-default" :disabled="!moveDnValid" @click="moveDn">
 | 
			
		||||
        <i class="icon-down-open"/>
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="btn btn-default" :disabled="usingFallback" @click="add">
 | 
			
		||||
        <i class="icon-plus"/>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div :disabled="!present" class="inset-control style-control">
 | 
			
		||||
      <label for="inset" class="label">
 | 
			
		||||
        {{$t('settings.style.shadows.inset')}}
 | 
			
		||||
      </label>
 | 
			
		||||
      <input
 | 
			
		||||
        v-model="selected.inset"
 | 
			
		||||
        :disabled="!present"
 | 
			
		||||
        name="inset"
 | 
			
		||||
        id="inset"
 | 
			
		||||
        class="input-inset"
 | 
			
		||||
        type="checkbox">
 | 
			
		||||
      <label class="checkbox-label" for="inset"></label>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div :disabled="!present" class="blur-control style-control">
 | 
			
		||||
      <label for="spread" class="label">
 | 
			
		||||
        {{$t('settings.style.shadows.blur')}}
 | 
			
		||||
      </label>
 | 
			
		||||
      <input
 | 
			
		||||
        v-model="selected.blur"
 | 
			
		||||
        :disabled="!present"
 | 
			
		||||
        name="blur"
 | 
			
		||||
        id="blur"
 | 
			
		||||
        class="input-range"
 | 
			
		||||
        type="range"
 | 
			
		||||
        max="20"
 | 
			
		||||
        min="0">
 | 
			
		||||
      <input
 | 
			
		||||
        v-model="selected.blur"
 | 
			
		||||
        :disabled="!present"
 | 
			
		||||
        class="input-number"
 | 
			
		||||
        type="number"
 | 
			
		||||
        min="0">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div :disabled="!present" class="spread-control style-control">
 | 
			
		||||
      <label for="spread" class="label">
 | 
			
		||||
        {{$t('settings.style.shadows.spread')}}
 | 
			
		||||
      </label>
 | 
			
		||||
      <input
 | 
			
		||||
        v-model="selected.spread"
 | 
			
		||||
        :disabled="!present"
 | 
			
		||||
        name="spread"
 | 
			
		||||
        id="spread"
 | 
			
		||||
        class="input-range"
 | 
			
		||||
        type="range"
 | 
			
		||||
        max="20"
 | 
			
		||||
        min="-20">
 | 
			
		||||
      <input
 | 
			
		||||
        v-model="selected.spread"
 | 
			
		||||
        :disabled="!present"
 | 
			
		||||
        class="input-number"
 | 
			
		||||
        type="number">
 | 
			
		||||
    </div>
 | 
			
		||||
    <ColorInput
 | 
			
		||||
      v-model="selected.color"
 | 
			
		||||
      :disabled="!present"
 | 
			
		||||
      :label="$t('settings.style.common.color')"
 | 
			
		||||
      name="shadow"/>
 | 
			
		||||
    <OpacityInput
 | 
			
		||||
      v-model="selected.alpha"
 | 
			
		||||
      :disabled="!present"/>
 | 
			
		||||
    <p>
 | 
			
		||||
      {{$t('settings.style.shadows.hint')}}
 | 
			
		||||
    </p>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script src="./shadow_control.js" ></script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
@import '../../_variables.scss';
 | 
			
		||||
.shadow-control {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
 | 
			
		||||
  .shadow-preview-container,
 | 
			
		||||
  .shadow-tweak {
 | 
			
		||||
    margin: 5px 6px 0 0;
 | 
			
		||||
  }
 | 
			
		||||
  .shadow-preview-container {
 | 
			
		||||
    flex: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
    $side: 15em;
 | 
			
		||||
 | 
			
		||||
    input[type=number] {
 | 
			
		||||
      width: 5em;
 | 
			
		||||
      min-width: 2em;
 | 
			
		||||
    }
 | 
			
		||||
    .x-shift-control,
 | 
			
		||||
    .y-shift-control {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex: 0;
 | 
			
		||||
 | 
			
		||||
      &[disabled=disabled] *{
 | 
			
		||||
        opacity: .5
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .x-shift-control {
 | 
			
		||||
      align-items: flex-start;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .x-shift-control .wrap,
 | 
			
		||||
    input[type=range] {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      width: $side;
 | 
			
		||||
      height: 2em;
 | 
			
		||||
    }
 | 
			
		||||
    .y-shift-control {
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      align-items: flex-end;
 | 
			
		||||
      .wrap {
 | 
			
		||||
        width: 2em;
 | 
			
		||||
        height: $side;
 | 
			
		||||
      }
 | 
			
		||||
      input[type=range] {
 | 
			
		||||
        transform-origin: 1em 1em;
 | 
			
		||||
        transform: rotate(90deg);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .preview-window {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      background-color: #999999;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      background-image:
 | 
			
		||||
      linear-gradient(45deg, #666666 25%, transparent 25%),
 | 
			
		||||
      linear-gradient(-45deg, #666666 25%, transparent 25%),
 | 
			
		||||
      linear-gradient(45deg, transparent 75%, #666666 75%),
 | 
			
		||||
      linear-gradient(-45deg, transparent 75%, #666666 75%);
 | 
			
		||||
      background-size: 20px 20px;
 | 
			
		||||
      background-position:0 0, 0 10px, 10px -10px, -10px 0;
 | 
			
		||||
 | 
			
		||||
      border-radius: $fallback--inputRadius;
 | 
			
		||||
      border-radius: var(--inputRadius, $fallback--inputRadius);
 | 
			
		||||
 | 
			
		||||
      .preview-block {
 | 
			
		||||
        width: 33%;
 | 
			
		||||
        height: 33%;
 | 
			
		||||
        background-color: $fallback--bg;
 | 
			
		||||
        background-color: var(--bg, $fallback--bg);
 | 
			
		||||
        border-radius: $fallback--panelRadius;
 | 
			
		||||
        border-radius: var(--panelRadius, $fallback--panelRadius);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .shadow-tweak {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 280px;
 | 
			
		||||
 | 
			
		||||
    .id-control {
 | 
			
		||||
      align-items: stretch;
 | 
			
		||||
      .select, .btn {
 | 
			
		||||
        min-width: 1px;
 | 
			
		||||
        margin-right: 5px;
 | 
			
		||||
      }
 | 
			
		||||
      .btn {
 | 
			
		||||
        padding: 0 .4em;
 | 
			
		||||
        margin: 0 .1em;
 | 
			
		||||
      }
 | 
			
		||||
      .select {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
        select {
 | 
			
		||||
          align-self: initial;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +20,8 @@ const Status = {
 | 
			
		|||
    'replies',
 | 
			
		||||
    'noReplyLinks',
 | 
			
		||||
    'noHeading',
 | 
			
		||||
    'inlineExpanded'
 | 
			
		||||
    'inlineExpanded',
 | 
			
		||||
    'activatePanel'
 | 
			
		||||
  ],
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +34,8 @@ const Status = {
 | 
			
		|||
      showingTall: false,
 | 
			
		||||
      expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
 | 
			
		||||
        ? !this.$store.state.instance.collapseMessageWithSubject
 | 
			
		||||
        : !this.$store.state.config.collapseMessageWithSubject
 | 
			
		||||
        : !this.$store.state.config.collapseMessageWithSubject,
 | 
			
		||||
      betterShadow: this.$store.state.interface.browserSupport.cssFilter
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +55,9 @@ const Status = {
 | 
			
		|||
      const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
 | 
			
		||||
      return highlightClass(user)
 | 
			
		||||
    },
 | 
			
		||||
    deleted () {
 | 
			
		||||
      return this.statusoid.deleted
 | 
			
		||||
    },
 | 
			
		||||
    repeaterStyle () {
 | 
			
		||||
      const user = this.statusoid.user
 | 
			
		||||
      const highlight = this.$store.state.config.highlight
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,15 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="status-el" v-if="!hideReply" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
 | 
			
		||||
  <div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
 | 
			
		||||
    <template v-if="muted && !noReplyLinks">
 | 
			
		||||
      <div class="media status container muted">
 | 
			
		||||
        <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
 | 
			
		||||
        <small><router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
 | 
			
		||||
        <small class="muteWords">{{muteWordHits.join(', ')}}</small>
 | 
			
		||||
        <a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-else>
 | 
			
		||||
      <div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
 | 
			
		||||
        <StillImage v-if="retweet" class='avatar' :src="statusoid.user.profile_image_url_original"/>
 | 
			
		||||
        <StillImage v-if="retweet" class='avatar' :class='{ "better-shadow": betterShadow }' :src="statusoid.user.profile_image_url_original"/>
 | 
			
		||||
        <div class="media-body faint">
 | 
			
		||||
          <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
 | 
			
		||||
          <a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +21,7 @@
 | 
			
		|||
      <div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
 | 
			
		||||
        <div v-if="!noHeading" class="media-left">
 | 
			
		||||
          <a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
 | 
			
		||||
            <StillImage class='avatar' :class="{'avatar-compact': compact}"  :src="status.user.profile_image_url_original"/>
 | 
			
		||||
            <StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}"  :src="status.user.profile_image_url_original"/>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="status-body">
 | 
			
		||||
| 
						 | 
				
			
			@ -34,10 +34,10 @@
 | 
			
		|||
                <h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
 | 
			
		||||
                <h4 class="user-name" v-else>{{status.user.name}}</h4>
 | 
			
		||||
                <span class="links">
 | 
			
		||||
                  <router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
 | 
			
		||||
                  <router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
 | 
			
		||||
                  <span v-if="status.in_reply_to_screen_name" class="faint reply-info">
 | 
			
		||||
                    <i class="icon-right-open"></i>
 | 
			
		||||
                    <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
 | 
			
		||||
                    <router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
 | 
			
		||||
                      {{status.in_reply_to_screen_name}}
 | 
			
		||||
                    </router-link>
 | 
			
		||||
                  </span>
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +54,7 @@
 | 
			
		|||
              </h4>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="media-heading-right">
 | 
			
		||||
              <router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
 | 
			
		||||
              <router-link class="timeago" @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }">
 | 
			
		||||
                <timeago :since="status.created_at" :auto-update="60"></timeago>
 | 
			
		||||
              </router-link>
 | 
			
		||||
              <div class="visibility-icon" v-if="status.visibility">
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +73,7 @@
 | 
			
		|||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div v-if="showPreview" class="status-preview-container">
 | 
			
		||||
            <status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
 | 
			
		||||
            <status :activatePanel="activatePanel" class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
 | 
			
		||||
            <div class="status-preview status-preview-loading" v-else>
 | 
			
		||||
              <i class="icon-spin4 animate-spin"></i>
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -146,6 +146,7 @@
 | 
			
		|||
  border-radius: $fallback--tooltipRadius;
 | 
			
		||||
  border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
 | 
			
		||||
  box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
 | 
			
		||||
  box-shadow: var(--popupShadow);
 | 
			
		||||
  margin-top: 0.25em;
 | 
			
		||||
  margin-left: 0.5em;
 | 
			
		||||
  z-index: 50;
 | 
			
		||||
| 
						 | 
				
			
			@ -284,8 +285,8 @@
 | 
			
		|||
      margin-left: 0.2em;
 | 
			
		||||
    }
 | 
			
		||||
    a:hover i {
 | 
			
		||||
      color: $fallback--fg;
 | 
			
		||||
      color: var(--fg, $fallback--fg);
 | 
			
		||||
      color: $fallback--text;
 | 
			
		||||
      color: var(--text, $fallback--text);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -323,6 +324,8 @@
 | 
			
		|||
 | 
			
		||||
  .status-content {
 | 
			
		||||
    margin-right: 0.5em;
 | 
			
		||||
    font-family: var(--postFont, sans-serif);
 | 
			
		||||
 | 
			
		||||
    img, video {
 | 
			
		||||
      max-width: 100%;
 | 
			
		||||
      max-height: 400px;
 | 
			
		||||
| 
						 | 
				
			
			@ -339,6 +342,10 @@
 | 
			
		|||
      overflow: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    code, samp, kbd, var, pre {
 | 
			
		||||
      font-family: var(--postCodeFont, monospace);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      margin-top: 0.2em;
 | 
			
		||||
| 
						 | 
				
			
			@ -457,18 +464,30 @@
 | 
			
		|||
.status .avatar-compact {
 | 
			
		||||
  width: 32px;
 | 
			
		||||
  height: 32px;
 | 
			
		||||
  box-shadow: var(--avatarStatusShadow);
 | 
			
		||||
  border-radius: $fallback--avatarAltRadius;
 | 
			
		||||
  border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
 | 
			
		||||
 | 
			
		||||
  &.better-shadow {
 | 
			
		||||
    box-shadow: var(--avatarStatusShadowInset);
 | 
			
		||||
    filter: var(--avatarStatusShadowFilter)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.avatar {
 | 
			
		||||
  width: 48px;
 | 
			
		||||
  height: 48px;
 | 
			
		||||
  box-shadow: var(--avatarStatusShadow);
 | 
			
		||||
  border-radius: $fallback--avatarRadius;
 | 
			
		||||
  border-radius: var(--avatarRadius, $fallback--avatarRadius);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  &.better-shadow {
 | 
			
		||||
    box-shadow: var(--avatarStatusShadowInset);
 | 
			
		||||
    filter: var(--avatarStatusShadowFilter)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  img {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -532,6 +551,7 @@ a.unmute {
 | 
			
		|||
  .status-el:last-child {
 | 
			
		||||
    border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;;
 | 
			
		||||
    border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										78
									
								
								src/components/style_switcher/preview.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/components/style_switcher/preview.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="panel dummy">
 | 
			
		||||
  <div class="panel-heading">
 | 
			
		||||
    <div class="title">
 | 
			
		||||
      {{$t('settings.style.preview.header')}}
 | 
			
		||||
      <span class="badge badge-notification">
 | 
			
		||||
        99
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <span class="faint">
 | 
			
		||||
      {{$t('settings.style.preview.header_faint')}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <span class="alert error">
 | 
			
		||||
      {{$t('settings.style.preview.error')}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <button class="btn">
 | 
			
		||||
      {{$t('settings.style.preview.button')}}
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="panel-body theme-preview-content">
 | 
			
		||||
    <div class="post">
 | 
			
		||||
      <div class="avatar">
 | 
			
		||||
        ( ͡° ͜ʖ ͡°)
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="content">
 | 
			
		||||
        <h4>
 | 
			
		||||
          {{$t('settings.style.preview.content')}}
 | 
			
		||||
        </h4>
 | 
			
		||||
 | 
			
		||||
        <i18n path="settings.style.preview.text">
 | 
			
		||||
          <code style="font-family: var(--postCodeFont)">
 | 
			
		||||
            {{$t('settings.style.preview.mono')}}
 | 
			
		||||
          </code>
 | 
			
		||||
          <a style="color: var(--link)">
 | 
			
		||||
            {{$t('settings.style.preview.link')}}
 | 
			
		||||
          </a>
 | 
			
		||||
        </i18n>
 | 
			
		||||
 | 
			
		||||
        <div class="icons">
 | 
			
		||||
          <i style="color: var(--cBlue)" class="icon-reply"/>
 | 
			
		||||
          <i style="color: var(--cGreen)" class="icon-retweet"/>
 | 
			
		||||
          <i style="color: var(--cOrange)" class="icon-star"/>
 | 
			
		||||
          <i style="color: var(--cRed)" class="icon-cancel"/>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="after-post">
 | 
			
		||||
      <div class="avatar-alt">
 | 
			
		||||
        :^)
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="content">
 | 
			
		||||
        <i18n path="settings.style.preview.fine_print" tag="span" class="faint">
 | 
			
		||||
          <a style="color: var(--faintLink)">
 | 
			
		||||
            {{$t('settings.style.preview.faint_link')}}
 | 
			
		||||
          </a>
 | 
			
		||||
        </i18n>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="separator"></div>
 | 
			
		||||
 | 
			
		||||
    <span class="alert error">
 | 
			
		||||
      {{$t('settings.style.preview.error')}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <input :value="$t('settings.style.preview.input')" type="text">
 | 
			
		||||
 | 
			
		||||
    <div class="actions">
 | 
			
		||||
      <span class="checkbox">
 | 
			
		||||
        <input checked="very yes" type="checkbox" id="preview_checkbox">
 | 
			
		||||
        <label for="preview_checkbox">{{$t('settings.style.preview.checkbox')}}</label>
 | 
			
		||||
      </span>
 | 
			
		||||
      <button class="btn">
 | 
			
		||||
        {{$t('settings.style.preview.button')}}
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +1,101 @@
 | 
			
		|||
import { rgbstr2hex } from '../../services/color_convert/color_convert.js'
 | 
			
		||||
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 ColorInput from '../color_input/color_input.vue'
 | 
			
		||||
import RangeInput from '../range_input/range_input.vue'
 | 
			
		||||
import OpacityInput from '../opacity_input/opacity_input.vue'
 | 
			
		||||
import ShadowControl from '../shadow_control/shadow_control.vue'
 | 
			
		||||
import FontControl from '../font_control/font_control.vue'
 | 
			
		||||
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
 | 
			
		||||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
 | 
			
		||||
import Preview from './preview.vue'
 | 
			
		||||
import ExportImport from '../export_import/export_import.vue'
 | 
			
		||||
 | 
			
		||||
// List of color values used in v1
 | 
			
		||||
const v1OnlyNames = [
 | 
			
		||||
  'bg',
 | 
			
		||||
  'fg',
 | 
			
		||||
  'text',
 | 
			
		||||
  'link',
 | 
			
		||||
  'cRed',
 | 
			
		||||
  'cGreen',
 | 
			
		||||
  'cBlue',
 | 
			
		||||
  'cOrange'
 | 
			
		||||
].map(_ => _ + 'ColorLocal')
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      availableStyles: [],
 | 
			
		||||
      selected: this.$store.state.config.theme,
 | 
			
		||||
      invalidThemeImported: false,
 | 
			
		||||
      bgColorLocal: '',
 | 
			
		||||
      btnColorLocal: '',
 | 
			
		||||
 | 
			
		||||
      previewShadows: {},
 | 
			
		||||
      previewColors: {},
 | 
			
		||||
      previewRadii: {},
 | 
			
		||||
      previewFonts: {},
 | 
			
		||||
 | 
			
		||||
      shadowsInvalid: true,
 | 
			
		||||
      colorsInvalid: true,
 | 
			
		||||
      radiiInvalid: true,
 | 
			
		||||
 | 
			
		||||
      keepColor: false,
 | 
			
		||||
      keepShadows: false,
 | 
			
		||||
      keepOpacity: false,
 | 
			
		||||
      keepRoundness: false,
 | 
			
		||||
      keepFonts: false,
 | 
			
		||||
 | 
			
		||||
      textColorLocal: '',
 | 
			
		||||
      linkColorLocal: '',
 | 
			
		||||
      redColorLocal: '',
 | 
			
		||||
      blueColorLocal: '',
 | 
			
		||||
      greenColorLocal: '',
 | 
			
		||||
      orangeColorLocal: '',
 | 
			
		||||
 | 
			
		||||
      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,
 | 
			
		||||
 | 
			
		||||
      badgeOpacityLocal: undefined,
 | 
			
		||||
      badgeNotificationColorLocal: undefined,
 | 
			
		||||
 | 
			
		||||
      borderColorLocal: undefined,
 | 
			
		||||
      borderOpacityLocal: undefined,
 | 
			
		||||
 | 
			
		||||
      faintColorLocal: undefined,
 | 
			
		||||
      faintOpacityLocal: undefined,
 | 
			
		||||
      faintLinkColorLocal: undefined,
 | 
			
		||||
 | 
			
		||||
      cRedColorLocal: '',
 | 
			
		||||
      cBlueColorLocal: '',
 | 
			
		||||
      cGreenColorLocal: '',
 | 
			
		||||
      cOrangeColorLocal: '',
 | 
			
		||||
 | 
			
		||||
      shadowSelected: undefined,
 | 
			
		||||
      shadowsLocal: {},
 | 
			
		||||
      fontsLocal: {},
 | 
			
		||||
 | 
			
		||||
      btnRadiusLocal: '',
 | 
			
		||||
      inputRadiusLocal: '',
 | 
			
		||||
      checkboxRadiusLocal: '',
 | 
			
		||||
      panelRadiusLocal: '',
 | 
			
		||||
      avatarRadiusLocal: '',
 | 
			
		||||
      avatarAltRadiusLocal: '',
 | 
			
		||||
| 
						 | 
				
			
			@ -26,144 +106,470 @@ export default {
 | 
			
		|||
  created () {
 | 
			
		||||
    const self = this
 | 
			
		||||
 | 
			
		||||
    window.fetch('/static/styles.json')
 | 
			
		||||
      .then((data) => data.json())
 | 
			
		||||
      .then((themes) => {
 | 
			
		||||
        self.availableStyles = themes
 | 
			
		||||
      })
 | 
			
		||||
    getThemes().then((themesComplete) => {
 | 
			
		||||
      self.availableStyles = themesComplete
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  mounted () {
 | 
			
		||||
    this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii)
 | 
			
		||||
    this.normalizeLocalState(this.$store.state.config.customTheme)
 | 
			
		||||
    if (typeof this.shadowSelected === 'undefined') {
 | 
			
		||||
      this.shadowSelected = this.shadowsAvailable[0]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    exportCurrentTheme () {
 | 
			
		||||
      const stringified = JSON.stringify({
 | 
			
		||||
        // To separate from other random JSON files and possible future theme formats
 | 
			
		||||
        _pleroma_theme_version: 1,
 | 
			
		||||
        colors: this.$store.state.config.colors,
 | 
			
		||||
        radii: this.$store.state.config.radii
 | 
			
		||||
      }, 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')
 | 
			
		||||
      e.setAttribute('download', 'pleroma_theme.json')
 | 
			
		||||
      e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
 | 
			
		||||
      e.style.display = 'none'
 | 
			
		||||
 | 
			
		||||
      document.body.appendChild(e)
 | 
			
		||||
      e.click()
 | 
			
		||||
      document.body.removeChild(e)
 | 
			
		||||
  computed: {
 | 
			
		||||
    selectedVersion () {
 | 
			
		||||
      return Array.isArray(this.selected) ? 1 : 2
 | 
			
		||||
    },
 | 
			
		||||
    currentColors () {
 | 
			
		||||
      return {
 | 
			
		||||
        bg: this.bgColorLocal,
 | 
			
		||||
        text: this.textColorLocal,
 | 
			
		||||
        link: this.linkColorLocal,
 | 
			
		||||
 | 
			
		||||
    importTheme () {
 | 
			
		||||
      this.invalidThemeImported = false
 | 
			
		||||
      const filePicker = document.createElement('input')
 | 
			
		||||
      filePicker.setAttribute('type', 'file')
 | 
			
		||||
      filePicker.setAttribute('accept', '.json')
 | 
			
		||||
        fg: this.fgColorLocal,
 | 
			
		||||
        fgText: this.fgTextColorLocal,
 | 
			
		||||
        fgLink: this.fgLinkColorLocal,
 | 
			
		||||
 | 
			
		||||
      filePicker.addEventListener('change', event => {
 | 
			
		||||
        if (event.target.files[0]) {
 | 
			
		||||
          // eslint-disable-next-line no-undef
 | 
			
		||||
          const reader = new FileReader()
 | 
			
		||||
          reader.onload = ({target}) => {
 | 
			
		||||
            try {
 | 
			
		||||
              const parsed = JSON.parse(target.result)
 | 
			
		||||
              if (parsed._pleroma_theme_version === 1) {
 | 
			
		||||
                this.normalizeLocalState(parsed.colors, parsed.radii)
 | 
			
		||||
              } else {
 | 
			
		||||
                // A theme from the future, spooky
 | 
			
		||||
                this.invalidThemeImported = true
 | 
			
		||||
              }
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              // This will happen both if there is a JSON syntax error or the theme is missing components
 | 
			
		||||
              this.invalidThemeImported = true
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          reader.readAsText(event.target.files[0])
 | 
			
		||||
        }
 | 
			
		||||
        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,
 | 
			
		||||
        badgeNotification: this.badgeNotificationColorLocal,
 | 
			
		||||
 | 
			
		||||
        faint: this.faintColorLocal,
 | 
			
		||||
        faintLink: this.faintLinkColorLocal,
 | 
			
		||||
        border: this.borderColorLocal,
 | 
			
		||||
 | 
			
		||||
        cRed: this.cRedColorLocal,
 | 
			
		||||
        cBlue: this.cBlueColorLocal,
 | 
			
		||||
        cGreen: this.cGreenColorLocal,
 | 
			
		||||
        cOrange: this.cOrangeColorLocal
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    currentOpacity () {
 | 
			
		||||
      return {
 | 
			
		||||
        bg: this.bgOpacityLocal,
 | 
			
		||||
        btn: this.btnOpacityLocal,
 | 
			
		||||
        input: this.inputOpacityLocal,
 | 
			
		||||
        panel: this.panelOpacityLocal,
 | 
			
		||||
        topBar: this.topBarOpacityLocal,
 | 
			
		||||
        border: this.borderOpacityLocal,
 | 
			
		||||
        faint: this.faintOpacityLocal
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    currentRadii () {
 | 
			
		||||
      return {
 | 
			
		||||
        btn: this.btnRadiusLocal,
 | 
			
		||||
        input: this.inputRadiusLocal,
 | 
			
		||||
        checkbox: this.checkboxRadiusLocal,
 | 
			
		||||
        panel: this.panelRadiusLocal,
 | 
			
		||||
        avatar: this.avatarRadiusLocal,
 | 
			
		||||
        avatarAlt: this.avatarAltRadiusLocal,
 | 
			
		||||
        tooltip: this.tooltipRadiusLocal,
 | 
			
		||||
        attachment: this.attachmentRadiusLocal
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    preview () {
 | 
			
		||||
      return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
 | 
			
		||||
    },
 | 
			
		||||
    previewTheme () {
 | 
			
		||||
      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
 | 
			
		||||
      return this.preview.theme
 | 
			
		||||
    },
 | 
			
		||||
    // This needs optimization maybe
 | 
			
		||||
    previewContrast () {
 | 
			
		||||
      if (!this.previewTheme.colors.bg) return {}
 | 
			
		||||
      const colors = this.previewTheme.colors
 | 
			
		||||
      const opacity = this.previewTheme.opacity
 | 
			
		||||
      if (!colors.bg) return {}
 | 
			
		||||
      const hints = (ratio) => ({
 | 
			
		||||
        text: ratio.toPrecision(3) + ':1',
 | 
			
		||||
        // AA level, AAA level
 | 
			
		||||
        aa: ratio >= 4.5,
 | 
			
		||||
        aaa: ratio >= 7,
 | 
			
		||||
        // same but for 18pt+ texts
 | 
			
		||||
        laa: ratio >= 3,
 | 
			
		||||
        laaa: ratio >= 4.5
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      document.body.appendChild(filePicker)
 | 
			
		||||
      filePicker.click()
 | 
			
		||||
      document.body.removeChild(filePicker)
 | 
			
		||||
    },
 | 
			
		||||
      // fgsfds :DDDD
 | 
			
		||||
      const fgs = {
 | 
			
		||||
        text: hex2rgb(colors.text),
 | 
			
		||||
        panelText: hex2rgb(colors.panelText),
 | 
			
		||||
        panelLink: hex2rgb(colors.panelLink),
 | 
			
		||||
        btnText: hex2rgb(colors.btnText),
 | 
			
		||||
        topBarText: hex2rgb(colors.topBarText),
 | 
			
		||||
        inputText: hex2rgb(colors.inputText),
 | 
			
		||||
 | 
			
		||||
        link: hex2rgb(colors.link),
 | 
			
		||||
        topBarLink: hex2rgb(colors.topBarLink),
 | 
			
		||||
 | 
			
		||||
        red: hex2rgb(colors.cRed),
 | 
			
		||||
        green: hex2rgb(colors.cGreen),
 | 
			
		||||
        blue: hex2rgb(colors.cBlue),
 | 
			
		||||
        orange: hex2rgb(colors.cOrange)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const bgs = {
 | 
			
		||||
        bg: hex2rgb(colors.bg),
 | 
			
		||||
        btn: hex2rgb(colors.btn),
 | 
			
		||||
        panel: hex2rgb(colors.panel),
 | 
			
		||||
        topBar: hex2rgb(colors.topBar),
 | 
			
		||||
        input: hex2rgb(colors.input),
 | 
			
		||||
        alertError: hex2rgb(colors.alertError),
 | 
			
		||||
        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 }, {})
 | 
			
		||||
    },
 | 
			
		||||
    previewRules () {
 | 
			
		||||
      if (!this.preview.rules) return ''
 | 
			
		||||
      return [
 | 
			
		||||
        ...Object.values(this.preview.rules),
 | 
			
		||||
        'color: var(--text)',
 | 
			
		||||
        'font-family: var(--interfaceFont, sans-serif)'
 | 
			
		||||
      ].join(';')
 | 
			
		||||
    },
 | 
			
		||||
    shadowsAvailable () {
 | 
			
		||||
      return Object.keys(this.previewTheme.shadows).sort()
 | 
			
		||||
    },
 | 
			
		||||
    currentShadowOverriden: {
 | 
			
		||||
      get () {
 | 
			
		||||
        return !!this.currentShadow
 | 
			
		||||
      },
 | 
			
		||||
      set (val) {
 | 
			
		||||
        if (val) {
 | 
			
		||||
          set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
 | 
			
		||||
        } else {
 | 
			
		||||
          del(this.shadowsLocal, this.shadowSelected)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    currentShadowFallback () {
 | 
			
		||||
      return this.previewTheme.shadows[this.shadowSelected]
 | 
			
		||||
    },
 | 
			
		||||
    currentShadow: {
 | 
			
		||||
      get () {
 | 
			
		||||
        return this.shadowsLocal[this.shadowSelected]
 | 
			
		||||
      },
 | 
			
		||||
      set (v) {
 | 
			
		||||
        set(this.shadowsLocal, this.shadowSelected, v)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    themeValid () {
 | 
			
		||||
      return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
 | 
			
		||||
    },
 | 
			
		||||
    exportedTheme () {
 | 
			
		||||
      const saveEverything = (
 | 
			
		||||
        !this.keepFonts &&
 | 
			
		||||
        !this.keepShadows &&
 | 
			
		||||
        !this.keepOpacity &&
 | 
			
		||||
        !this.keepRoundness &&
 | 
			
		||||
        !this.keepColor
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      const theme = {}
 | 
			
		||||
 | 
			
		||||
      if (this.keepFonts || saveEverything) {
 | 
			
		||||
        theme.fonts = this.fontsLocal
 | 
			
		||||
      }
 | 
			
		||||
      if (this.keepShadows || saveEverything) {
 | 
			
		||||
        theme.shadows = this.shadowsLocal
 | 
			
		||||
      }
 | 
			
		||||
      if (this.keepOpacity || saveEverything) {
 | 
			
		||||
        theme.opacity = this.currentOpacity
 | 
			
		||||
      }
 | 
			
		||||
      if (this.keepColor || saveEverything) {
 | 
			
		||||
        theme.colors = this.currentColors
 | 
			
		||||
      }
 | 
			
		||||
      if (this.keepRoundness || saveEverything) {
 | 
			
		||||
        theme.radii = this.currentRadii
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        // To separate from other random JSON files and possible future theme formats
 | 
			
		||||
        _pleroma_theme_version: 2, theme
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    ColorInput,
 | 
			
		||||
    OpacityInput,
 | 
			
		||||
    RangeInput,
 | 
			
		||||
    ContrastRatio,
 | 
			
		||||
    ShadowControl,
 | 
			
		||||
    FontControl,
 | 
			
		||||
    TabSwitcher,
 | 
			
		||||
    Preview,
 | 
			
		||||
    ExportImport
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setCustomTheme () {
 | 
			
		||||
      if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) {
 | 
			
		||||
        // reset to picked themes
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const rgb = (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),
 | 
			
		||||
          g: parseInt(result[2], 16),
 | 
			
		||||
          b: parseInt(result[3], 16)
 | 
			
		||||
        } : null
 | 
			
		||||
      }
 | 
			
		||||
      const bgRgb = rgb(this.bgColorLocal)
 | 
			
		||||
      const btnRgb = rgb(this.btnColorLocal)
 | 
			
		||||
      const textRgb = rgb(this.textColorLocal)
 | 
			
		||||
      const linkRgb = rgb(this.linkColorLocal)
 | 
			
		||||
 | 
			
		||||
      const redRgb = rgb(this.redColorLocal)
 | 
			
		||||
      const blueRgb = rgb(this.blueColorLocal)
 | 
			
		||||
      const greenRgb = rgb(this.greenColorLocal)
 | 
			
		||||
      const orangeRgb = rgb(this.orangeColorLocal)
 | 
			
		||||
 | 
			
		||||
      if (bgRgb && btnRgb && linkRgb) {
 | 
			
		||||
        this.$store.dispatch('setOption', {
 | 
			
		||||
          name: 'customTheme',
 | 
			
		||||
          value: {
 | 
			
		||||
            fg: btnRgb,
 | 
			
		||||
            bg: bgRgb,
 | 
			
		||||
            text: textRgb,
 | 
			
		||||
            link: linkRgb,
 | 
			
		||||
            cRed: redRgb,
 | 
			
		||||
            cBlue: blueRgb,
 | 
			
		||||
            cGreen: greenRgb,
 | 
			
		||||
            cOrange: orangeRgb,
 | 
			
		||||
            btnRadius: this.btnRadiusLocal,
 | 
			
		||||
            inputRadius: this.inputRadiusLocal,
 | 
			
		||||
            panelRadius: this.panelRadiusLocal,
 | 
			
		||||
            avatarRadius: this.avatarRadiusLocal,
 | 
			
		||||
            avatarAltRadius: this.avatarAltRadiusLocal,
 | 
			
		||||
            tooltipRadius: this.tooltipRadiusLocal,
 | 
			
		||||
            attachmentRadius: this.attachmentRadiusLocal
 | 
			
		||||
          }})
 | 
			
		||||
      this.$store.dispatch('setOption', {
 | 
			
		||||
        name: 'customTheme',
 | 
			
		||||
        value: {
 | 
			
		||||
          shadows: this.shadowsLocal,
 | 
			
		||||
          fonts: this.fontsLocal,
 | 
			
		||||
          opacity: this.currentOpacity,
 | 
			
		||||
          colors: this.currentColors,
 | 
			
		||||
          radii: this.currentRadii
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    onImport (parsed) {
 | 
			
		||||
      if (parsed._pleroma_theme_version === 1) {
 | 
			
		||||
        this.normalizeLocalState(parsed, 1)
 | 
			
		||||
      } else if (parsed._pleroma_theme_version === 2) {
 | 
			
		||||
        this.normalizeLocalState(parsed.theme, 2)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    importValidator (parsed) {
 | 
			
		||||
      const version = parsed._pleroma_theme_version
 | 
			
		||||
      return version >= 1 || version <= 2
 | 
			
		||||
    },
 | 
			
		||||
    clearAll () {
 | 
			
		||||
      const state = this.$store.state.config.customTheme
 | 
			
		||||
      const version = state.colors ? 2 : 'l1'
 | 
			
		||||
      this.normalizeLocalState(this.$store.state.config.customTheme, version)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    normalizeLocalState (colors, radii) {
 | 
			
		||||
      this.bgColorLocal = rgbstr2hex(colors.bg)
 | 
			
		||||
      this.btnColorLocal = rgbstr2hex(colors.btn)
 | 
			
		||||
      this.textColorLocal = rgbstr2hex(colors.fg)
 | 
			
		||||
      this.linkColorLocal = rgbstr2hex(colors.link)
 | 
			
		||||
    // Clears all the extra stuff when loading V1 theme
 | 
			
		||||
    clearV1 () {
 | 
			
		||||
      Object.keys(this.$data)
 | 
			
		||||
        .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
 | 
			
		||||
        .filter(_ => !v1OnlyNames.includes(_))
 | 
			
		||||
        .forEach(key => {
 | 
			
		||||
          set(this.$data, key, undefined)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
      this.redColorLocal = rgbstr2hex(colors.cRed)
 | 
			
		||||
      this.blueColorLocal = rgbstr2hex(colors.cBlue)
 | 
			
		||||
      this.greenColorLocal = rgbstr2hex(colors.cGreen)
 | 
			
		||||
      this.orangeColorLocal = rgbstr2hex(colors.cOrange)
 | 
			
		||||
    clearRoundness () {
 | 
			
		||||
      Object.keys(this.$data)
 | 
			
		||||
        .filter(_ => _.endsWith('RadiusLocal'))
 | 
			
		||||
        .forEach(key => {
 | 
			
		||||
          set(this.$data, key, undefined)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
      this.btnRadiusLocal = radii.btnRadius || 4
 | 
			
		||||
      this.inputRadiusLocal = radii.inputRadius || 4
 | 
			
		||||
      this.panelRadiusLocal = radii.panelRadius || 10
 | 
			
		||||
      this.avatarRadiusLocal = radii.avatarRadius || 5
 | 
			
		||||
      this.avatarAltRadiusLocal = radii.avatarAltRadius || 50
 | 
			
		||||
      this.tooltipRadiusLocal = radii.tooltipRadius || 2
 | 
			
		||||
      this.attachmentRadiusLocal = radii.attachmentRadius || 5
 | 
			
		||||
    clearOpacity () {
 | 
			
		||||
      Object.keys(this.$data)
 | 
			
		||||
        .filter(_ => _.endsWith('OpacityLocal'))
 | 
			
		||||
        .forEach(key => {
 | 
			
		||||
          set(this.$data, key, undefined)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    clearShadows () {
 | 
			
		||||
      this.shadowsLocal = {}
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    clearFonts () {
 | 
			
		||||
      this.fontsLocal = {}
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This applies stored theme data onto form. Supports three versions of data:
 | 
			
		||||
     * 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 {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
 | 
			
		||||
     */
 | 
			
		||||
    normalizeLocalState (input, version = 0) {
 | 
			
		||||
      const colors = input.colors || input
 | 
			
		||||
      const radii = input.radii || input
 | 
			
		||||
      const opacity = input.opacity
 | 
			
		||||
      const shadows = input.shadows || {}
 | 
			
		||||
      const fonts = input.fonts || {}
 | 
			
		||||
 | 
			
		||||
      if (version === 0) {
 | 
			
		||||
        if (input.version) version = input.version
 | 
			
		||||
        // Old v1 naming: fg is text, btn is foreground
 | 
			
		||||
        if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
 | 
			
		||||
          version = 1
 | 
			
		||||
        }
 | 
			
		||||
        // New v2 naming: text is text, fg is foreground
 | 
			
		||||
        if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
 | 
			
		||||
          version = 2
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Stuff that differs between V1 and V2
 | 
			
		||||
      if (version === 1) {
 | 
			
		||||
        this.fgColorLocal = rgb2hex(colors.btn)
 | 
			
		||||
        this.textColorLocal = rgb2hex(colors.fg)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.keepColor) {
 | 
			
		||||
        this.clearV1()
 | 
			
		||||
        const keys = new Set(version !== 1 ? Object.keys(colors) : [])
 | 
			
		||||
        if (version === 1 || version === 'l1') {
 | 
			
		||||
          keys
 | 
			
		||||
            .add('bg')
 | 
			
		||||
            .add('link')
 | 
			
		||||
            .add('cRed')
 | 
			
		||||
            .add('cBlue')
 | 
			
		||||
            .add('cGreen')
 | 
			
		||||
            .add('cOrange')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        keys.forEach(key => {
 | 
			
		||||
          this[key + 'ColorLocal'] = rgb2hex(colors[key])
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.keepRoundness) {
 | 
			
		||||
        this.clearRoundness()
 | 
			
		||||
        Object.entries(radii).forEach(([k, v]) => {
 | 
			
		||||
          // 'Radius' is kept mostly for v1->v2 localstorage transition
 | 
			
		||||
          const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
 | 
			
		||||
          this[key + 'RadiusLocal'] = v
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.keepShadows) {
 | 
			
		||||
        this.clearShadows()
 | 
			
		||||
        this.shadowsLocal = shadows
 | 
			
		||||
        this.shadowSelected = this.shadowsAvailable[0]
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.keepFonts) {
 | 
			
		||||
        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: {
 | 
			
		||||
    currentRadii () {
 | 
			
		||||
      try {
 | 
			
		||||
        this.previewRadii = generateRadii({ radii: this.currentRadii })
 | 
			
		||||
        this.radiiInvalid = false
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        this.radiiInvalid = true
 | 
			
		||||
        console.warn(e)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    shadowsLocal: {
 | 
			
		||||
      handler () {
 | 
			
		||||
        try {
 | 
			
		||||
          this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
 | 
			
		||||
          this.shadowsInvalid = false
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          this.shadowsInvalid = true
 | 
			
		||||
          console.warn(e)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      deep: true
 | 
			
		||||
    },
 | 
			
		||||
    fontsLocal: {
 | 
			
		||||
      handler () {
 | 
			
		||||
        try {
 | 
			
		||||
          this.previewFonts = generateFonts({ fonts: this.fontsLocal })
 | 
			
		||||
          this.fontsInvalid = false
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          this.fontsInvalid = true
 | 
			
		||||
          console.warn(e)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      deep: true
 | 
			
		||||
    },
 | 
			
		||||
    currentColors () {
 | 
			
		||||
      try {
 | 
			
		||||
        this.previewColors = generateColors({
 | 
			
		||||
          opacity: this.currentOpacity,
 | 
			
		||||
          colors: this.currentColors
 | 
			
		||||
        })
 | 
			
		||||
        this.colorsInvalid = false
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        this.colorsInvalid = true
 | 
			
		||||
        console.warn(e)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    currentOpacity () {
 | 
			
		||||
      try {
 | 
			
		||||
        this.previewColors = generateColors({
 | 
			
		||||
          opacity: this.currentOpacity,
 | 
			
		||||
          colors: this.currentColors
 | 
			
		||||
        })
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.warn(e)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    selected () {
 | 
			
		||||
      this.bgColorLocal = this.selected[1]
 | 
			
		||||
      this.btnColorLocal = this.selected[2]
 | 
			
		||||
      this.textColorLocal = this.selected[3]
 | 
			
		||||
      this.linkColorLocal = this.selected[4]
 | 
			
		||||
      this.redColorLocal = this.selected[5]
 | 
			
		||||
      this.greenColorLocal = this.selected[6]
 | 
			
		||||
      this.blueColorLocal = this.selected[7]
 | 
			
		||||
      this.orangeColorLocal = this.selected[8]
 | 
			
		||||
      if (this.selectedVersion === 1) {
 | 
			
		||||
        if (!this.keepRoundness) {
 | 
			
		||||
          this.clearRoundness()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.keepShadows) {
 | 
			
		||||
          this.clearShadows()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.keepOpacity) {
 | 
			
		||||
          this.clearOpacity()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.keepColor) {
 | 
			
		||||
          this.clearV1()
 | 
			
		||||
 | 
			
		||||
          this.bgColorLocal = this.selected[1]
 | 
			
		||||
          this.fgColorLocal = this.selected[2]
 | 
			
		||||
          this.textColorLocal = this.selected[3]
 | 
			
		||||
          this.linkColorLocal = this.selected[4]
 | 
			
		||||
          this.cRedColorLocal = this.selected[5]
 | 
			
		||||
          this.cGreenColorLocal = this.selected[6]
 | 
			
		||||
          this.cBlueColorLocal = this.selected[7]
 | 
			
		||||
          this.cOrangeColorLocal = this.selected[8]
 | 
			
		||||
        }
 | 
			
		||||
      } else if (this.selectedVersion >= 2) {
 | 
			
		||||
        this.normalizeLocalState(this.selected.theme, 2)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										335
									
								
								src/components/style_switcher/style_switcher.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								src/components/style_switcher/style_switcher.scss
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,335 @@
 | 
			
		|||
@import '../../_variables.scss';
 | 
			
		||||
.style-switcher {
 | 
			
		||||
  .preset-switcher {
 | 
			
		||||
    margin-right: 1em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .style-control {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
 | 
			
		||||
    .label {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.disabled {
 | 
			
		||||
      input, select {
 | 
			
		||||
        &:not(.exclude-disabled) {
 | 
			
		||||
          opacity: .5
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    input, select {
 | 
			
		||||
      min-width: 3em;
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      flex: 0;
 | 
			
		||||
 | 
			
		||||
      &[type=color] {
 | 
			
		||||
        padding: 1px;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        height: 29px;
 | 
			
		||||
        min-width: 2em;
 | 
			
		||||
        border: none;
 | 
			
		||||
        align-self: stretch;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &[type=number] {
 | 
			
		||||
        min-width: 5em;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &[type=range] {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
        min-width: 3em;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &[type=checkbox] + label {
 | 
			
		||||
        margin: 6px 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:not([type=number]):not([type=text]) {
 | 
			
		||||
        align-self: flex-start;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tab-switcher {
 | 
			
		||||
    margin: 0 -1em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .reset-container {
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .fonts-container,
 | 
			
		||||
  .reset-container,
 | 
			
		||||
  .apply-container,
 | 
			
		||||
  .radius-container,
 | 
			
		||||
  .color-container,
 | 
			
		||||
  {
 | 
			
		||||
    display: flex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .fonts-container,
 | 
			
		||||
  .radius-container {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-container{
 | 
			
		||||
    > h4 {
 | 
			
		||||
      width: 99%;
 | 
			
		||||
    }
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .fonts-container,
 | 
			
		||||
  .color-container,
 | 
			
		||||
  .shadow-container,
 | 
			
		||||
  .radius-container,
 | 
			
		||||
  .presets-container {
 | 
			
		||||
    margin: 1em 1em 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tab-header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    min-height: 30px;
 | 
			
		||||
 | 
			
		||||
    .btn {
 | 
			
		||||
      min-width: 1px;
 | 
			
		||||
      flex: 0 auto;
 | 
			
		||||
      padding: 0 1em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      margin-right: .5em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .shadow-selector {
 | 
			
		||||
    .override {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      margin-left: .5em;
 | 
			
		||||
    }
 | 
			
		||||
    .select-container {
 | 
			
		||||
      margin-top: -4px;
 | 
			
		||||
      margin-bottom: -3px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .save-load,
 | 
			
		||||
  .save-load-options {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
    .presets,
 | 
			
		||||
    .import-export {
 | 
			
		||||
      margin-bottom: .5em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .import-export {
 | 
			
		||||
      display: flex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .override {
 | 
			
		||||
      margin-left: .5em;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .save-load-options {
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    margin-top: .5em;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    .keep-option {
 | 
			
		||||
      margin: 0 .5em .5em;
 | 
			
		||||
      min-width: 25%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .preview-container {
 | 
			
		||||
    border-top: 1px dashed;
 | 
			
		||||
    border-bottom: 1px dashed;
 | 
			
		||||
    border-color: $fallback--border;
 | 
			
		||||
    border-color: var(--border, $fallback--border);
 | 
			
		||||
    margin: 1em -1em 0;
 | 
			
		||||
    padding: 1em;
 | 
			
		||||
    background: var(--body-background-image);
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
    background-position: 50% 50%;
 | 
			
		||||
 | 
			
		||||
    .dummy {
 | 
			
		||||
      .post {
 | 
			
		||||
        font-family: var(--postFont);
 | 
			
		||||
        display: flex;
 | 
			
		||||
 | 
			
		||||
        .content {
 | 
			
		||||
          flex: 1;
 | 
			
		||||
 | 
			
		||||
          h4 {
 | 
			
		||||
            margin-bottom: .25em;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .icons {
 | 
			
		||||
            margin-top: .5em;
 | 
			
		||||
            display: flex;
 | 
			
		||||
 | 
			
		||||
            i {
 | 
			
		||||
              margin-right: 1em;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .after-post {
 | 
			
		||||
        margin-top: 1em;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .avatar, .avatar-alt{
 | 
			
		||||
        background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
 | 
			
		||||
        color: black;
 | 
			
		||||
        font-family: sans-serif;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        margin-right: 1em;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .avatar-alt {
 | 
			
		||||
        flex: 0 auto;
 | 
			
		||||
        margin-left: 28px;
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
        min-width: 20px;
 | 
			
		||||
        min-height: 20px;
 | 
			
		||||
        line-height: 20px;
 | 
			
		||||
        border-radius: $fallback--avatarAltRadius;
 | 
			
		||||
        border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .avatar {
 | 
			
		||||
        flex: 0 auto;
 | 
			
		||||
        width: 48px;
 | 
			
		||||
        height: 48px;
 | 
			
		||||
        font-size: 14px;
 | 
			
		||||
        line-height: 48px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .actions {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: baseline;
 | 
			
		||||
 | 
			
		||||
        .checkbox {
 | 
			
		||||
          display: inline-flex;
 | 
			
		||||
          align-items: baseline;
 | 
			
		||||
          margin-right: 1em;
 | 
			
		||||
          flex: 1;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .separator {
 | 
			
		||||
        margin: 1em;
 | 
			
		||||
        border-bottom: 1px solid;
 | 
			
		||||
        border-color: $fallback--border;
 | 
			
		||||
        border-color: var(--border, $fallback--border);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .panel-heading {
 | 
			
		||||
        .badge, .alert, .btn, .faint {
 | 
			
		||||
          margin-left: 1em;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
        }
 | 
			
		||||
        .faint {
 | 
			
		||||
          text-overflow: ellipsis;
 | 
			
		||||
          min-width: 2em;
 | 
			
		||||
          overflow-x: hidden;
 | 
			
		||||
        }
 | 
			
		||||
        .flex-spacer {
 | 
			
		||||
          flex: 1;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      .btn {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
        padding: 0 1em;
 | 
			
		||||
        min-width: 3em;
 | 
			
		||||
        min-height: 30px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .apply-container {
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .radius-item,
 | 
			
		||||
  .color-item {
 | 
			
		||||
    min-width: 20em;
 | 
			
		||||
    margin: 5px 6px 0 0;
 | 
			
		||||
    display:flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex: 1 1 0;
 | 
			
		||||
 | 
			
		||||
    &.wide {
 | 
			
		||||
      min-width: 60%
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:not(.wide):nth-child(2n+1) {
 | 
			
		||||
      margin-right: 7px;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .color, .opacity {
 | 
			
		||||
      display:flex;
 | 
			
		||||
      align-items: baseline;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .radius-item {
 | 
			
		||||
    flex-basis: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .theme-radius-rn,
 | 
			
		||||
  .theme-color-cl {
 | 
			
		||||
    border: 0;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    color: var(--faint, $fallback--faint);
 | 
			
		||||
    align-self: stretch;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .theme-color-cl,
 | 
			
		||||
  .theme-radius-in,
 | 
			
		||||
  .theme-color-in {
 | 
			
		||||
    margin-left: 4px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .theme-radius-in {
 | 
			
		||||
    min-width: 1em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .theme-radius-in {
 | 
			
		||||
    max-width: 7em;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .theme-radius-lb{
 | 
			
		||||
    max-width: 50em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .theme-preview-content {
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn {
 | 
			
		||||
    margin-left: .25em;
 | 
			
		||||
    margin-right: .25em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,300 +1,276 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
<div class="style-switcher">
 | 
			
		||||
  <div class="presets-container">
 | 
			
		||||
    <div>
 | 
			
		||||
      {{$t('settings.presets')}}
 | 
			
		||||
      <label for="style-switcher" class='select'>
 | 
			
		||||
        <select id="style-switcher" v-model="selected" class="style-switcher">
 | 
			
		||||
          <option v-for="style in availableStyles"
 | 
			
		||||
                  :value="style"
 | 
			
		||||
                  :style="{
 | 
			
		||||
                          backgroundColor: style[1],
 | 
			
		||||
                          color: style[3]
 | 
			
		||||
                          }">
 | 
			
		||||
            {{style[0]}}
 | 
			
		||||
          </option>
 | 
			
		||||
        </select>
 | 
			
		||||
        <i class="icon-down-open"/>
 | 
			
		||||
      </label>
 | 
			
		||||
    <div class="save-load">
 | 
			
		||||
      <export-import
 | 
			
		||||
        :exportObject='exportedTheme'
 | 
			
		||||
        :exportLabel='$t("settings.export_theme")'
 | 
			
		||||
        :importLabel='$t("settings.import_theme")'
 | 
			
		||||
        :importFailedText='$t("settings.invalid_theme_imported")'
 | 
			
		||||
        :onImport='onImport'
 | 
			
		||||
        :validator='importValidator'>
 | 
			
		||||
        <template slot="before">
 | 
			
		||||
          <div class="presets">
 | 
			
		||||
            {{$t('settings.presets')}}
 | 
			
		||||
            <label for="preset-switcher" class='select'>
 | 
			
		||||
              <select id="preset-switcher" v-model="selected" class="preset-switcher">
 | 
			
		||||
                <option v-for="style in availableStyles"
 | 
			
		||||
                        :value="style"
 | 
			
		||||
                        :style="{
 | 
			
		||||
                                backgroundColor: style[1] || style.theme.colors.bg,
 | 
			
		||||
                                color: style[3] || style.theme.colors.text
 | 
			
		||||
                                }">
 | 
			
		||||
                  {{style[0] || style.name}}
 | 
			
		||||
                </option>
 | 
			
		||||
              </select>
 | 
			
		||||
              <i class="icon-down-open"/>
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
        </template>
 | 
			
		||||
      </export-import>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="import-export">
 | 
			
		||||
      <button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button>
 | 
			
		||||
      <button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button>
 | 
			
		||||
      <p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p>
 | 
			
		||||
    <div class="save-load-options">
 | 
			
		||||
      <span class="keep-option">
 | 
			
		||||
        <input
 | 
			
		||||
          id="keep-color"
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          v-model="keepColor">
 | 
			
		||||
        <label for="keep-color">{{$t('settings.style.switcher.keep_color')}}</label>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="keep-option">
 | 
			
		||||
        <input
 | 
			
		||||
          id="keep-shadows"
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          v-model="keepShadows">
 | 
			
		||||
        <label for="keep-shadows">{{$t('settings.style.switcher.keep_shadows')}}</label>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="keep-option">
 | 
			
		||||
        <input
 | 
			
		||||
          id="keep-opacity"
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          v-model="keepOpacity">
 | 
			
		||||
        <label for="keep-opacity">{{$t('settings.style.switcher.keep_opacity')}}</label>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="keep-option">
 | 
			
		||||
        <input
 | 
			
		||||
          id="keep-roundness"
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          v-model="keepRoundness">
 | 
			
		||||
        <label for="keep-roundness">{{$t('settings.style.switcher.keep_roundness')}}</label>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="keep-option">
 | 
			
		||||
        <input
 | 
			
		||||
          id="keep-fonts"
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          v-model="keepFonts">
 | 
			
		||||
        <label for="keep-fonts">{{$t('settings.style.switcher.keep_fonts')}}</label>
 | 
			
		||||
      </span>
 | 
			
		||||
      <p>{{$t('settings.style.switcher.save_load_hint')}}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="preview-container">
 | 
			
		||||
    <div :style="{
 | 
			
		||||
                 '--btnRadius': btnRadiusLocal + 'px',
 | 
			
		||||
                 '--inputRadius': inputRadiusLocal + 'px',
 | 
			
		||||
                 '--panelRadius': panelRadiusLocal + 'px',
 | 
			
		||||
                 '--avatarRadius': avatarRadiusLocal + 'px',
 | 
			
		||||
                 '--avatarAltRadius': avatarAltRadiusLocal + 'px',
 | 
			
		||||
                 '--tooltipRadius': tooltipRadiusLocal + 'px',
 | 
			
		||||
                 '--attachmentRadius': attachmentRadiusLocal + 'px'
 | 
			
		||||
                 }">
 | 
			
		||||
      <div class="panel dummy">
 | 
			
		||||
        <div class="panel-heading" :style="{ 'background-color': btnColorLocal, 'color': textColorLocal }">Preview</div>
 | 
			
		||||
        <div class="panel-body theme-preview-content" :style="{ 'background-color': bgColorLocal, 'color': textColorLocal }">
 | 
			
		||||
          <div class="avatar" :style="{
 | 
			
		||||
                                      'border-radius': avatarRadiusLocal + 'px'
 | 
			
		||||
                                      }">
 | 
			
		||||
            ( ͡° ͜ʖ ͡°)
 | 
			
		||||
          </div>
 | 
			
		||||
          <h4>Content</h4>
 | 
			
		||||
          <br>
 | 
			
		||||
          A bunch of more content and
 | 
			
		||||
          <a :style="{ color: linkColorLocal }">a nice lil' link</a>
 | 
			
		||||
          <i :style="{ color: blueColorLocal }" class="icon-reply"/>
 | 
			
		||||
          <i :style="{ color: greenColorLocal }" class="icon-retweet"/>
 | 
			
		||||
          <i :style="{ color: redColorLocal }" class="icon-cancel"/>
 | 
			
		||||
          <i :style="{ color: orangeColorLocal }" class="icon-star"/>
 | 
			
		||||
          <br>
 | 
			
		||||
          <button class="btn" :style="{ 'background-color': btnColorLocal, 'color': textColorLocal }">Button</button>
 | 
			
		||||
    <preview :style="previewRules"/>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <keep-alive>
 | 
			
		||||
    <tab-switcher key="style-tweak">
 | 
			
		||||
      <div :label="$t('settings.style.common_colors._tab_label')" class="color-container">
 | 
			
		||||
        <div class="tab-header">
 | 
			
		||||
          <p>{{$t('settings.theme_help')}}</p>
 | 
			
		||||
          <button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button>
 | 
			
		||||
          <button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p>{{$t('settings.theme_help_v2_1')}}</p>
 | 
			
		||||
        <h4>{{ $t('settings.style.common_colors.main') }}</h4>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <ColorInput name="bgColor" v-model="bgColorLocal" :label="$t('settings.background')"/>
 | 
			
		||||
          <OpacityInput name="bgOpacity" v-model="bgOpacityLocal" :fallback="previewTheme.opacity.bg || 1"/>
 | 
			
		||||
          <ColorInput name="textColor" v-model="textColorLocal" :label="$t('settings.text')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.bgText"/>
 | 
			
		||||
          <ColorInput name="linkColor" v-model="linkColorLocal" :label="$t('settings.links')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.bgLink"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <ColorInput name="fgColor" v-model="fgColorLocal" :label="$t('settings.foreground')"/>
 | 
			
		||||
          <ColorInput name="fgTextColor" v-model="fgTextColorLocal" :label="$t('settings.text')" :fallback="previewTheme.colors.fgText"/>
 | 
			
		||||
          <ColorInput name="fgLinkColor" v-model="fgLinkColorLocal" :label="$t('settings.links')" :fallback="previewTheme.colors.fgLink"/>
 | 
			
		||||
          <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <ColorInput name="cRedColor" v-model="cRedColorLocal" :label="$t('settings.cRed')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.bgRed"/>
 | 
			
		||||
          <ColorInput name="cBlueColor" v-model="cBlueColorLocal" :label="$t('settings.cBlue')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.bgBlue"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <ColorInput name="cGreenColor" v-model="cGreenColorLocal" :label="$t('settings.cGreen')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.bgGreen"/>
 | 
			
		||||
          <ColorInput name="cOrangeColor" v-model="cOrangeColorLocal" :label="$t('settings.cOrange')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.bgOrange"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p>{{$t('settings.theme_help_v2_2')}}</p>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div :label="$t('settings.style.advanced_colors._tab_label')" class="color-container">
 | 
			
		||||
        <div class="tab-header">
 | 
			
		||||
          <p>{{$t('settings.theme_help')}}</p>
 | 
			
		||||
          <button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button>
 | 
			
		||||
          <button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
 | 
			
		||||
          <ColorInput name="alertError" v-model="alertErrorColorLocal" :label="$t('settings.style.advanced_colors.alert_error')" :fallback="previewTheme.colors.alertError"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.alertError"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
 | 
			
		||||
          <ColorInput name="badgeNotification" v-model="badgeNotificationColorLocal" :label="$t('settings.style.advanced_colors.badge_notification')" :fallback="previewTheme.colors.badgeNotification"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
 | 
			
		||||
          <ColorInput name="panelColor" v-model="panelColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
 | 
			
		||||
          <OpacityInput name="panelOpacity" v-model="panelOpacityLocal" :fallback="previewTheme.opacity.panel || 1"/>
 | 
			
		||||
          <ColorInput name="panelTextColor" v-model="panelTextColorLocal" :fallback="previewTheme.colors.panelText" :label="$t('settings.text')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.panelText" large="1"/>
 | 
			
		||||
          <ColorInput name="panelLinkColor" v-model="panelLinkColorLocal" :fallback="previewTheme.colors.panelLink" :label="$t('settings.links')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.panelLink" large="1"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
 | 
			
		||||
          <ColorInput name="topBarColor" v-model="topBarColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
 | 
			
		||||
          <ColorInput name="topBarTextColor" v-model="topBarTextColorLocal" :fallback="previewTheme.colors.topBarText" :label="$t('settings.text')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.topBarText"/>
 | 
			
		||||
          <ColorInput name="topBarLinkColor" v-model="topBarLinkColorLocal" :fallback="previewTheme.colors.topBarLink" :label="$t('settings.links')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.topBarLink"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
 | 
			
		||||
          <ColorInput name="inputColor" v-model="inputColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
 | 
			
		||||
          <OpacityInput name="inputOpacity" v-model="inputOpacityLocal" :fallback="previewTheme.opacity.input || 1"/>
 | 
			
		||||
          <ColorInput name="inputTextColor" v-model="inputTextColorLocal" :fallback="previewTheme.colors.inputText" :label="$t('settings.text')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.inputText"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
 | 
			
		||||
          <ColorInput name="btnColor" v-model="btnColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
 | 
			
		||||
          <OpacityInput name="btnOpacity" v-model="btnOpacityLocal" :fallback="previewTheme.opacity.btn || 1"/>
 | 
			
		||||
          <ColorInput name="btnTextColor" v-model="btnTextColorLocal" :fallback="previewTheme.colors.btnText" :label="$t('settings.text')"/>
 | 
			
		||||
          <ContrastRatio :contrast="previewContrast.btnText"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
 | 
			
		||||
          <ColorInput name="borderColor" v-model="borderColorLocal" :fallback="previewTheme.colors.border" :label="$t('settings.style.common.color')"/>
 | 
			
		||||
          <OpacityInput name="borderOpacity" v-model="borderOpacityLocal" :fallback="previewTheme.opacity.border || 1"/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="color-item">
 | 
			
		||||
          <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
 | 
			
		||||
          <ColorInput name="faintColor" v-model="faintColorLocal" :fallback="previewTheme.colors.faint || 1" :label="$t('settings.text')"/>
 | 
			
		||||
          <ColorInput name="faintLinkColor" v-model="faintLinkColorLocal" :fallback="previewTheme.colors.faintLink" :label="$t('settings.links')"/>
 | 
			
		||||
          <ColorInput name="panelFaintColor" v-model="panelFaintColorLocal" :fallback="previewTheme.colors.panelFaint" :label="$t('settings.style.advanced_colors.panel_header')"/>
 | 
			
		||||
          <OpacityInput name="faintOpacity" v-model="faintOpacityLocal" :fallback="previewTheme.opacity.faint || 0.5"/>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="color-container">
 | 
			
		||||
    <p>{{$t('settings.theme_help')}}</p>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="bgcolor" class="theme-color-lb">{{$t('settings.background')}}</label>
 | 
			
		||||
      <input id="bgcolor" class="theme-color-cl" type="color" v-model="bgColorLocal">
 | 
			
		||||
      <input id="bgcolor-t" class="theme-color-in" type="text" v-model="bgColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="fgcolor" class="theme-color-lb">{{$t('settings.foreground')}}</label>
 | 
			
		||||
      <input id="fgcolor" class="theme-color-cl" type="color" v-model="btnColorLocal">
 | 
			
		||||
      <input id="fgcolor-t" class="theme-color-in" type="text" v-model="btnColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="textcolor" class="theme-color-lb">{{$t('settings.text')}}</label>
 | 
			
		||||
      <input id="textcolor" class="theme-color-cl" type="color" v-model="textColorLocal">
 | 
			
		||||
      <input id="textcolor-t" class="theme-color-in" type="text" v-model="textColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="linkcolor" class="theme-color-lb">{{$t('settings.links')}}</label>
 | 
			
		||||
      <input id="linkcolor" class="theme-color-cl" type="color" v-model="linkColorLocal">
 | 
			
		||||
      <input id="linkcolor-t" class="theme-color-in" type="text" v-model="linkColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="redcolor" class="theme-color-lb">{{$t('settings.cRed')}}</label>
 | 
			
		||||
      <input id="redcolor" class="theme-color-cl" type="color" v-model="redColorLocal">
 | 
			
		||||
      <input id="redcolor-t" class="theme-color-in" type="text" v-model="redColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="bluecolor" class="theme-color-lb">{{$t('settings.cBlue')}}</label>
 | 
			
		||||
      <input id="bluecolor" class="theme-color-cl" type="color" v-model="blueColorLocal">
 | 
			
		||||
      <input id="bluecolor-t" class="theme-color-in" type="text" v-model="blueColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="greencolor" class="theme-color-lb">{{$t('settings.cGreen')}}</label>
 | 
			
		||||
      <input id="greencolor" class="theme-color-cl" type="color" v-model="greenColorLocal">
 | 
			
		||||
      <input id="greencolor-t" class="theme-color-in" type="green" v-model="greenColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="color-item">
 | 
			
		||||
      <label for="orangecolor" class="theme-color-lb">{{$t('settings.cOrange')}}</label>
 | 
			
		||||
      <input id="orangecolor" class="theme-color-cl" type="color" v-model="orangeColorLocal">
 | 
			
		||||
      <input id="orangecolor-t" class="theme-color-in" type="text" v-model="orangeColorLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
      <div :label="$t('settings.style.radii._tab_label')" class="radius-container">
 | 
			
		||||
        <div class="tab-header">
 | 
			
		||||
          <p>{{$t('settings.radii_help')}}</p>
 | 
			
		||||
          <button class="btn" @click="clearRoundness">{{$t('settings.style.switcher.clear_all')}}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <RangeInput name="btnRadius" :label="$t('settings.btnRadius')" v-model="btnRadiusLocal" :fallback="previewTheme.radii.btn" max="16" hardMin="0"/>
 | 
			
		||||
        <RangeInput name="inputRadius" :label="$t('settings.inputRadius')" v-model="inputRadiusLocal" :fallback="previewTheme.radii.input" max="9" hardMin="0"/>
 | 
			
		||||
        <RangeInput name="checkboxRadius" :label="$t('settings.checkboxRadius')" v-model="checkboxRadiusLocal" :fallback="previewTheme.radii.checkbox" max="16" hardMin="0"/>
 | 
			
		||||
        <RangeInput name="panelRadius" :label="$t('settings.panelRadius')" v-model="panelRadiusLocal" :fallback="previewTheme.radii.panel" max="50" hardMin="0"/>
 | 
			
		||||
        <RangeInput name="avatarRadius" :label="$t('settings.avatarRadius')" v-model="avatarRadiusLocal" :fallback="previewTheme.radii.avatar" max="28" hardMin="0"/>
 | 
			
		||||
        <RangeInput name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" v-model="avatarAltRadiusLocal" :fallback="previewTheme.radii.avatarAlt" max="28" hardMin="0"/>
 | 
			
		||||
        <RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/>
 | 
			
		||||
        <RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
  <div class="radius-container">
 | 
			
		||||
    <p>{{$t('settings.radii_help')}}</p>
 | 
			
		||||
    <div class="radius-item">
 | 
			
		||||
      <label for="btnradius" class="theme-radius-lb">{{$t('settings.btnRadius')}}</label>
 | 
			
		||||
      <input id="btnradius" class="theme-radius-rn" type="range" v-model="btnRadiusLocal" max="16">
 | 
			
		||||
      <input id="btnradius-t" class="theme-radius-in" type="text" v-model="btnRadiusLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="radius-item">
 | 
			
		||||
      <label for="inputradius" class="theme-radius-lb">{{$t('settings.inputRadius')}}</label>
 | 
			
		||||
      <input id="inputradius" class="theme-radius-rn" type="range" v-model="inputRadiusLocal" max="16">
 | 
			
		||||
      <input id="inputradius-t" class="theme-radius-in" type="text" v-model="inputRadiusLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="radius-item">
 | 
			
		||||
      <label for="panelradius" class="theme-radius-lb">{{$t('settings.panelRadius')}}</label>
 | 
			
		||||
      <input id="panelradius" class="theme-radius-rn" type="range" v-model="panelRadiusLocal" max="50">
 | 
			
		||||
      <input id="panelradius-t" class="theme-radius-in" type="text" v-model="panelRadiusLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="radius-item">
 | 
			
		||||
      <label for="avatarradius" class="theme-radius-lb">{{$t('settings.avatarRadius')}}</label>
 | 
			
		||||
      <input id="avatarradius" class="theme-radius-rn" type="range" v-model="avatarRadiusLocal" max="28">
 | 
			
		||||
      <input id="avatarradius-t" class="theme-radius-in" type="green" v-model="avatarRadiusLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="radius-item">
 | 
			
		||||
      <label for="avataraltradius" class="theme-radius-lb">{{$t('settings.avatarAltRadius')}}</label>
 | 
			
		||||
      <input id="avataraltradius" class="theme-radius-rn" type="range" v-model="avatarAltRadiusLocal" max="28">
 | 
			
		||||
      <input id="avataraltradius-t" class="theme-radius-in" type="text" v-model="avatarAltRadiusLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="radius-item">
 | 
			
		||||
      <label for="attachmentradius" class="theme-radius-lb">{{$t('settings.attachmentRadius')}}</label>
 | 
			
		||||
      <input id="attachmentrradius" class="theme-radius-rn" type="range" v-model="attachmentRadiusLocal" max="50">
 | 
			
		||||
      <input id="attachmentradius-t" class="theme-radius-in" type="text" v-model="attachmentRadiusLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="radius-item">
 | 
			
		||||
      <label for="tooltipradius" class="theme-radius-lb">{{$t('settings.tooltipRadius')}}</label>
 | 
			
		||||
      <input id="tooltipradius" class="theme-radius-rn" type="range" v-model="tooltipRadiusLocal" max="20">
 | 
			
		||||
      <input id="tooltipradius-t" class="theme-radius-in" type="text" v-model="tooltipRadiusLocal">
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
      <div :label="$t('settings.style.shadows._tab_label')" class="shadow-container">
 | 
			
		||||
        <div class="tab-header shadow-selector">
 | 
			
		||||
          <div class="select-container">
 | 
			
		||||
            {{$t('settings.style.shadows.component')}}
 | 
			
		||||
            <label for="shadow-switcher" class="select">
 | 
			
		||||
              <select id="shadow-switcher" v-model="shadowSelected" class="shadow-switcher">
 | 
			
		||||
                <option v-for="shadow in shadowsAvailable"
 | 
			
		||||
                        :value="shadow">
 | 
			
		||||
                  {{$t('settings.style.shadows.components.' + shadow)}}
 | 
			
		||||
                </option>
 | 
			
		||||
              </select>
 | 
			
		||||
              <i class="icon-down-open"/>
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="override">
 | 
			
		||||
            <label for="override" class="label">
 | 
			
		||||
              {{$t('settings.style.shadows.override')}}
 | 
			
		||||
            </label>
 | 
			
		||||
            <input
 | 
			
		||||
              v-model="currentShadowOverriden"
 | 
			
		||||
              name="override"
 | 
			
		||||
              id="override"
 | 
			
		||||
              class="input-override"
 | 
			
		||||
              type="checkbox">
 | 
			
		||||
            <label class="checkbox-label" for="override"></label>
 | 
			
		||||
          </div>
 | 
			
		||||
          <button class="btn" @click="clearShadows">{{$t('settings.style.switcher.clear_all')}}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <shadow-control :ready="!!currentShadowFallback" :fallback="currentShadowFallback" v-model="currentShadow"/>
 | 
			
		||||
        <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
 | 
			
		||||
          <i18n path="settings.style.shadows.filter_hint.always_drop_shadow" tag="p">
 | 
			
		||||
            <code>filter: drop-shadow()</code>
 | 
			
		||||
          </i18n>
 | 
			
		||||
          <p>{{$t('settings.style.shadows.filter_hint.avatar_inset')}}</p>
 | 
			
		||||
          <i18n path="settings.style.shadows.filter_hint.drop_shadow_syntax" tag="p">
 | 
			
		||||
            <code>drop-shadow</code>
 | 
			
		||||
            <code>spread-radius</code>
 | 
			
		||||
            <code>inset</code>
 | 
			
		||||
          </i18n>
 | 
			
		||||
          <i18n path="settings.style.shadows.filter_hint.inset_classic" tag="p">
 | 
			
		||||
            <code>box-shadow</code>
 | 
			
		||||
          </i18n>
 | 
			
		||||
          <p>{{$t('settings.style.shadows.filter_hint.spread_zero')}}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div :label="$t('settings.style.fonts._tab_label')" class="fonts-container">
 | 
			
		||||
        <div class="tab-header">
 | 
			
		||||
          <p>{{$t('settings.style.fonts.help')}}</p>
 | 
			
		||||
          <button class="btn" @click="clearFonts">{{$t('settings.style.switcher.clear_all')}}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <FontControl
 | 
			
		||||
          name="ui"
 | 
			
		||||
          v-model="fontsLocal.interface"
 | 
			
		||||
          :label="$t('settings.style.fonts.components.interface')"
 | 
			
		||||
          :fallback="previewTheme.fonts.interface"
 | 
			
		||||
          no-inherit="1"/>
 | 
			
		||||
        <FontControl
 | 
			
		||||
          name="input"
 | 
			
		||||
          v-model="fontsLocal.input"
 | 
			
		||||
          :label="$t('settings.style.fonts.components.input')"
 | 
			
		||||
          :fallback="previewTheme.fonts.input"/>
 | 
			
		||||
        <FontControl
 | 
			
		||||
          name="post"
 | 
			
		||||
          v-model="fontsLocal.post"
 | 
			
		||||
          :label="$t('settings.style.fonts.components.post')"
 | 
			
		||||
          :fallback="previewTheme.fonts.post"/>
 | 
			
		||||
        <FontControl
 | 
			
		||||
          name="postCode"
 | 
			
		||||
          v-model="fontsLocal.postCode"
 | 
			
		||||
          :label="$t('settings.style.fonts.components.postCode')"
 | 
			
		||||
          :fallback="previewTheme.fonts.postCode"/>
 | 
			
		||||
      </div>
 | 
			
		||||
    </tab-switcher>
 | 
			
		||||
  </keep-alive>
 | 
			
		||||
 | 
			
		||||
  <div class="apply-container">
 | 
			
		||||
    <button class="btn submit" @click="setCustomTheme">{{$t('general.apply')}}</button>
 | 
			
		||||
    <button class="btn submit" :disabled="!themeValid" @click="setCustomTheme">{{$t('general.apply')}}</button>
 | 
			
		||||
    <button class="btn" @click="clearAll">{{$t('settings.style.switcher.reset')}}</button>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script src="./style_switcher.js"></script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
@import '../../_variables.scss';
 | 
			
		||||
.style-switcher {
 | 
			
		||||
  margin-right: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.import-warning {
 | 
			
		||||
  color: $fallback--cRed;
 | 
			
		||||
  color: var(--cRed, $fallback--cRed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.apply-container,
 | 
			
		||||
.radius-container,
 | 
			
		||||
.color-container,
 | 
			
		||||
.presets-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
 | 
			
		||||
  p {
 | 
			
		||||
    flex: 2 0 100%;
 | 
			
		||||
    margin-top: 2em;
 | 
			
		||||
    margin-bottom: .5em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.radius-container {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-container {
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.presets-container {
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  .import-export {
 | 
			
		||||
    display: flex;
 | 
			
		||||
 | 
			
		||||
    .btn {
 | 
			
		||||
      margin-left: .5em;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview-container {
 | 
			
		||||
  border-top: 1px dashed;
 | 
			
		||||
  border-bottom: 1px dashed;
 | 
			
		||||
  border-color: $fallback--border;
 | 
			
		||||
  border-color: var(--border, $fallback--border);
 | 
			
		||||
  margin: 1em -1em 0;
 | 
			
		||||
  padding: 1em;
 | 
			
		||||
 | 
			
		||||
  .btn {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
    min-height: 30px;
 | 
			
		||||
    width: 10em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.apply-container {
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.radius-item,
 | 
			
		||||
.color-item {
 | 
			
		||||
  min-width: 20em;
 | 
			
		||||
  display:flex;
 | 
			
		||||
  flex: 1 1 0;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  margin: 5px 6px 5px 0;
 | 
			
		||||
 | 
			
		||||
  label {
 | 
			
		||||
    color: var(--faint, $fallback--faint);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.radius-item {
 | 
			
		||||
  flex-basis: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-radius-rn,
 | 
			
		||||
.theme-color-cl {
 | 
			
		||||
  border: 0;
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  color: var(--faint, $fallback--faint);
 | 
			
		||||
  align-self: stretch;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-color-cl,
 | 
			
		||||
.theme-radius-in,
 | 
			
		||||
.theme-color-in {
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-color-in {
 | 
			
		||||
  min-width: 4em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-radius-in {
 | 
			
		||||
  min-width: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-radius-in,
 | 
			
		||||
.theme-color-in {
 | 
			
		||||
  max-width: 7em;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-radius-lb,
 | 
			
		||||
.theme-color-lb {
 | 
			
		||||
  flex: 2;
 | 
			
		||||
  min-width: 7em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-radius-lb{
 | 
			
		||||
  max-width: 50em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-color-lb {
 | 
			
		||||
  max-width: 10em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-color-cl {
 | 
			
		||||
  padding: 1px;
 | 
			
		||||
  max-width: 8em;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  flex: 0;
 | 
			
		||||
  min-width: 2em;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  max-height: 29px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-preview-content {
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dummy {
 | 
			
		||||
  .avatar {
 | 
			
		||||
    background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
 | 
			
		||||
    color: black;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    height: 48px;
 | 
			
		||||
    line-height: 48px;
 | 
			
		||||
    width: 48px;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-right: 1em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
<style src="./style_switcher.scss" lang="scss"></style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,11 +25,14 @@ export default Vue.component('tab-switcher', {
 | 
			
		|||
            }
 | 
			
		||||
            return (<button onClick={this.activateTab(index)} class={ classes.join(' ') }>{slot.data.attrs.label}</button>)
 | 
			
		||||
          });
 | 
			
		||||
    const contents = (
 | 
			
		||||
      <div>
 | 
			
		||||
        {this.$slots.default.filter(slot => slot.data)[this.active]}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
    const contents = this.$slots.default.filter(_=>_.data).map(( slot, index ) => {
 | 
			
		||||
      const active = index === this.active
 | 
			
		||||
      return (
 | 
			
		||||
        <div class={active ? 'active' : 'hidden'}>
 | 
			
		||||
          {slot}
 | 
			
		||||
        </div>
 | 
			
		||||
      )
 | 
			
		||||
    });
 | 
			
		||||
    return (
 | 
			
		||||
      <div class="tab-switcher">
 | 
			
		||||
        <div class="tabs">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,21 @@
 | 
			
		|||
@import '../../_variables.scss';
 | 
			
		||||
 | 
			
		||||
.tab-switcher {
 | 
			
		||||
  .contents {
 | 
			
		||||
    .hidden {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .tabs {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    overflow-y: hidden;
 | 
			
		||||
    overflow-x: auto;
 | 
			
		||||
    padding-top: 5px;
 | 
			
		||||
    height: 32px;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
    &::after, &::before {
 | 
			
		||||
      display: block;
 | 
			
		||||
| 
						 | 
				
			
			@ -17,20 +25,34 @@
 | 
			
		|||
 | 
			
		||||
    .tab, &::after, &::before {
 | 
			
		||||
      border-bottom: 1px solid;
 | 
			
		||||
      border-bottom-color: $fallback--btn;
 | 
			
		||||
      border-bottom-color: var(--btn, $fallback--btn);
 | 
			
		||||
      border-bottom-color: $fallback--border;
 | 
			
		||||
      border-bottom-color: var(--border, $fallback--border);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .tab {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      border-bottom-left-radius: 0;
 | 
			
		||||
      border-bottom-right-radius: 0;
 | 
			
		||||
      padding: .3em 1em;
 | 
			
		||||
      padding: 5px 1em 99px;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
 | 
			
		||||
      &:not(.active) {
 | 
			
		||||
        border-bottom: 1px solid;
 | 
			
		||||
        border-bottom-color: $fallback--btn;
 | 
			
		||||
        border-bottom-color: var(--btn, $fallback--btn);
 | 
			
		||||
        z-index: 4;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
          z-index: 6;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &::after {
 | 
			
		||||
          content: '';
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          left: 0;
 | 
			
		||||
          right: 0;
 | 
			
		||||
          top: 26px;
 | 
			
		||||
          border-bottom: 1px solid;
 | 
			
		||||
          border-bottom-color: $fallback--border;
 | 
			
		||||
          border-bottom-color: var(--border, $fallback--border);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.active {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import Status from '../status/status.vue'
 | 
			
		|||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
 | 
			
		||||
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
 | 
			
		||||
import UserCard from '../user_card/user_card.vue'
 | 
			
		||||
import { throttle } from 'lodash'
 | 
			
		||||
 | 
			
		||||
const Timeline = {
 | 
			
		||||
  props: [
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +89,7 @@ const Timeline = {
 | 
			
		|||
        this.paused = false
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    fetchOlderStatuses () {
 | 
			
		||||
    fetchOlderStatuses: throttle(function () {
 | 
			
		||||
      const store = this.$store
 | 
			
		||||
      const credentials = store.state.users.currentUser.credentials
 | 
			
		||||
      store.commit('setLoading', { timeline: this.timelineName, value: true })
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +102,7 @@ const Timeline = {
 | 
			
		|||
        userId: this.userId,
 | 
			
		||||
        tag: this.tag
 | 
			
		||||
      }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
 | 
			
		||||
    },
 | 
			
		||||
    }, 1000, this),
 | 
			
		||||
    fetchFollowers () {
 | 
			
		||||
      const id = this.userId
 | 
			
		||||
      this.$store.state.api.backendInteractor.fetchFollowers({ id })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
      <button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
 | 
			
		||||
        {{$t('timeline.show_new')}}{{newStatusCountStr}}
 | 
			
		||||
      </button>
 | 
			
		||||
      <div @click.prevent class="loadmore-text" v-if="!timeline.newStatusCount > 0 && !timelineError">
 | 
			
		||||
      <div @click.prevent class="loadmore-text faint" v-if="!timeline.newStatusCount > 0 && !timelineError">
 | 
			
		||||
        {{$t('timeline.up_to_date')}}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -58,15 +58,7 @@
 | 
			
		|||
 | 
			
		||||
.timeline {
 | 
			
		||||
  .loadmore-text {
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    color: $fallback--faint;
 | 
			
		||||
    color: var(--faint, $fallback--faint);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .loadmore-error {
 | 
			
		||||
    color: $fallback--fg;
 | 
			
		||||
    color: var(--fg, $fallback--fg);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +71,7 @@
 | 
			
		|||
  border-color: var(--border, $fallback--border);
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  background-color: $fallback--btn;
 | 
			
		||||
  background-color: var(--btn, $fallback--btn);
 | 
			
		||||
  background-color: $fallback--fg;
 | 
			
		||||
  background-color: var(--panel, $fallback--fg);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,9 @@ const UserCard = {
 | 
			
		|||
  components: {
 | 
			
		||||
    UserCardContent
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    currentUser () { return this.$store.state.users.currentUser }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    toggleUserExpanded () {
 | 
			
		||||
      this.userExpanded = !this.userExpanded
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,13 +10,13 @@
 | 
			
		|||
      <div :title="user.name" v-if="user.name_html" class="user-name">
 | 
			
		||||
        <span v-html="user.name_html"></span>
 | 
			
		||||
        <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
 | 
			
		||||
          {{ $t('user_card.follows_you') }}
 | 
			
		||||
          {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div :title="user.name" v-else class="user-name">
 | 
			
		||||
        {{ user.name }}
 | 
			
		||||
        <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
 | 
			
		||||
          {{ $t('user_card.follows_you') }}
 | 
			
		||||
          {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <router-link class='user-screen-name' :to="{ name: 'user-profile', params: { id: user.id } }">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,24 +2,38 @@ import StillImage from '../still-image/still-image.vue'
 | 
			
		|||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: [ 'user', 'switcher', 'selected', 'hideBio' ],
 | 
			
		||||
  props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ],
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
 | 
			
		||||
        ? this.$store.state.instance.hideUserStats
 | 
			
		||||
        : this.$store.state.config.hideUserStats
 | 
			
		||||
        : this.$store.state.config.hideUserStats,
 | 
			
		||||
      betterShadow: this.$store.state.interface.browserSupport.cssFilter
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    headingStyle () {
 | 
			
		||||
      const color = this.$store.state.config.colors.bg
 | 
			
		||||
      const color = this.$store.state.config.customTheme.colors
 | 
			
		||||
            ? this.$store.state.config.customTheme.colors.bg  // v2
 | 
			
		||||
            : this.$store.state.config.colors.bg // v1
 | 
			
		||||
 | 
			
		||||
      if (color) {
 | 
			
		||||
        const rgb = hex2rgb(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)`
 | 
			
		||||
 | 
			
		||||
        const gradient = [
 | 
			
		||||
          [tintColor, this.hideBio ? '60%' : ''],
 | 
			
		||||
          this.hideBio ? [
 | 
			
		||||
            color, '100%'
 | 
			
		||||
          ] : [
 | 
			
		||||
            tintColor, ''
 | 
			
		||||
          ]
 | 
			
		||||
        ].map(_ => _.join(' ')).join(', ')
 | 
			
		||||
 | 
			
		||||
        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, ${gradient})`,
 | 
			
		||||
            `url(${this.user.cover_photo})`
 | 
			
		||||
          ].join(', ')
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +112,14 @@ export default {
 | 
			
		|||
        const store = this.$store
 | 
			
		||||
        store.commit('setProfileView', { v })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    linkClicked ({target}) {
 | 
			
		||||
      if (target.tagName === 'SPAN') {
 | 
			
		||||
        target = target.parentNode
 | 
			
		||||
      }
 | 
			
		||||
      if (target.tagName === 'A') {
 | 
			
		||||
        window.open(target.href, '_blank')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,20 +2,20 @@
 | 
			
		|||
<div id="heading" class="profile-panel-background" :style="headingStyle">
 | 
			
		||||
  <div class="panel-heading text-center">
 | 
			
		||||
    <div class='user-info'>
 | 
			
		||||
      <router-link to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
 | 
			
		||||
        <i class="icon-cog usersettings"></i>
 | 
			
		||||
      <router-link @click.native="activatePanel && activatePanel('timeline')" to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
 | 
			
		||||
        <i class="icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
 | 
			
		||||
      </router-link>
 | 
			
		||||
      <a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
 | 
			
		||||
        <i class="icon-link-ext usersettings"></i>
 | 
			
		||||
      </a>
 | 
			
		||||
      <div class='container'>
 | 
			
		||||
        <router-link :to="{ name: 'user-profile', params: { id: user.id } }">
 | 
			
		||||
          <StillImage class="avatar" :src="user.profile_image_url_original"/>
 | 
			
		||||
        <router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: user.id } }">
 | 
			
		||||
          <StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
 | 
			
		||||
        </router-link>
 | 
			
		||||
        <div class="name-and-screen-name">
 | 
			
		||||
          <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
 | 
			
		||||
          <div :title="user.name" class='user-name' v-else>{{user.name}}</div>
 | 
			
		||||
          <router-link class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
 | 
			
		||||
          <router-link @click.native="activatePanel && activatePanel('timeline')" class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
 | 
			
		||||
            <span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
 | 
			
		||||
            <span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
 | 
			
		||||
          </router-link>
 | 
			
		||||
| 
						 | 
				
			
			@ -41,74 +41,74 @@
 | 
			
		|||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-if="isOtherUser" class="user-interactions">
 | 
			
		||||
          <div class="follow" v-if="loggedIn">
 | 
			
		||||
            <span v-if="user.following">
 | 
			
		||||
              <!--Following them!-->
 | 
			
		||||
              <button @click="unfollowUser" class="pressed">
 | 
			
		||||
                {{ $t('user_card.following') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </span>
 | 
			
		||||
            <span v-if="!user.following">
 | 
			
		||||
              <button @click="followUser">
 | 
			
		||||
                {{ $t('user_card.follow') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class='mute' v-if='isOtherUser'>
 | 
			
		||||
            <span v-if='user.muted'>
 | 
			
		||||
              <button @click="toggleMute" class="pressed">
 | 
			
		||||
                {{ $t('user_card.muted') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </span>
 | 
			
		||||
            <span v-if='!user.muted'>
 | 
			
		||||
              <button @click="toggleMute">
 | 
			
		||||
                {{ $t('user_card.mute') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="remote-follow" v-if='!loggedIn && user.is_local'>
 | 
			
		||||
            <form method="POST" :action='subscribeUrl'>
 | 
			
		||||
              <input type="hidden" name="nickname" :value="user.screen_name">
 | 
			
		||||
              <input type="hidden" name="profile" value="">
 | 
			
		||||
              <button click="submit" class="remote-button">
 | 
			
		||||
                {{ $t('user_card.remote_follow') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </form>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class='block' v-if='isOtherUser && loggedIn'>
 | 
			
		||||
            <span v-if='user.statusnet_blocking'>
 | 
			
		||||
              <button @click="unblockUser" class="pressed">
 | 
			
		||||
                {{ $t('user_card.blocked') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </span>
 | 
			
		||||
            <span v-if='!user.statusnet_blocking'>
 | 
			
		||||
              <button @click="blockUser">
 | 
			
		||||
                {{ $t('user_card.block') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
        <div class="follow" v-if="loggedIn">
 | 
			
		||||
          <span v-if="user.following">
 | 
			
		||||
            <!--Following them!-->
 | 
			
		||||
            <button @click="unfollowUser" class="pressed">
 | 
			
		||||
              {{ $t('user_card.following') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span v-if="!user.following">
 | 
			
		||||
            <button @click="followUser">
 | 
			
		||||
              {{ $t('user_card.follow') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class='mute' v-if='isOtherUser'>
 | 
			
		||||
          <span v-if='user.muted'>
 | 
			
		||||
            <button @click="toggleMute" class="pressed">
 | 
			
		||||
              {{ $t('user_card.muted') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span v-if='!user.muted'>
 | 
			
		||||
            <button @click="toggleMute">
 | 
			
		||||
              {{ $t('user_card.mute') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="remote-follow" v-if='!loggedIn && user.is_local'>
 | 
			
		||||
          <form method="POST" :action='subscribeUrl'>
 | 
			
		||||
            <input type="hidden" name="nickname" :value="user.screen_name">
 | 
			
		||||
            <input type="hidden" name="profile" value="">
 | 
			
		||||
            <button click="submit" class="remote-button">
 | 
			
		||||
              {{ $t('user_card.remote_follow') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </form>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class='block' v-if='isOtherUser && loggedIn'>
 | 
			
		||||
          <span v-if='user.statusnet_blocking'>
 | 
			
		||||
            <button @click="unblockUser" class="pressed">
 | 
			
		||||
              {{ $t('user_card.blocked') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span v-if='!user.statusnet_blocking'>
 | 
			
		||||
            <button @click="blockUser">
 | 
			
		||||
              {{ $t('user_card.block') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="panel-body profile-panel-body">
 | 
			
		||||
      <div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
 | 
			
		||||
        <div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
 | 
			
		||||
          <h5>{{ $t('user_card.statuses') }}</h5>
 | 
			
		||||
          <span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="user-count" v-on:click.prevent="setProfileView('friends')" :class="{selected: selected === 'friends'}">
 | 
			
		||||
          <h5>{{ $t('user_card.followees') }}</h5>
 | 
			
		||||
          <span v-if="!hideUserStatsLocal">{{user.friends_count}}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="user-count" v-on:click.prevent="setProfileView('followers')" :class="{selected: selected === 'followers'}">
 | 
			
		||||
          <h5>{{ $t('user_card.followers') }}</h5>
 | 
			
		||||
          <span v-if="!hideUserStatsLocal">{{user.followers_count}}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
 | 
			
		||||
      <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="panel-body profile-panel-body" v-if="!hideBio">
 | 
			
		||||
    <div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
 | 
			
		||||
      <div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
 | 
			
		||||
        <h5>{{ $t('user_card.statuses') }}</h5>
 | 
			
		||||
        <span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="user-count" v-on:click.prevent="setProfileView('friends')" :class="{selected: selected === 'friends'}">
 | 
			
		||||
        <h5>{{ $t('user_card.followees') }}</h5>
 | 
			
		||||
        <span v-if="!hideUserStatsLocal">{{user.friends_count}}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="user-count" v-on:click.prevent="setProfileView('followers')" :class="{selected: selected === 'followers'}">
 | 
			
		||||
        <h5>{{ $t('user_card.followers') }}</h5>
 | 
			
		||||
        <span v-if="!hideUserStatsLocal">{{user.followers_count}}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
 | 
			
		||||
    <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script src="./user_card_content.js"></script>
 | 
			
		||||
| 
						 | 
				
			
			@ -120,10 +120,15 @@
 | 
			
		|||
  background-size: cover;
 | 
			
		||||
  border-radius: $fallback--panelRadius;
 | 
			
		||||
  border-radius: var(--panelRadius, $fallback--panelRadius);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
 | 
			
		||||
  border-bottom-left-radius: 0;
 | 
			
		||||
  border-bottom-right-radius: 0;
 | 
			
		||||
 | 
			
		||||
  .panel-heading {
 | 
			
		||||
    padding: 0.6em 0em;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -138,15 +143,14 @@
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.user-info {
 | 
			
		||||
  color: $fallback--lightFg;
 | 
			
		||||
  color: var(--lightFg, $fallback--lightFg);
 | 
			
		||||
  color: $fallback--lightText;
 | 
			
		||||
  color: var(--lightText, $fallback--lightText);
 | 
			
		||||
  padding: 0 16px;
 | 
			
		||||
 | 
			
		||||
  .container {
 | 
			
		||||
    padding: 16px 10px 6px 10px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    max-height: 56px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    .avatar {
 | 
			
		||||
      border-radius: $fallback--avatarRadius;
 | 
			
		||||
| 
						 | 
				
			
			@ -155,8 +159,14 @@
 | 
			
		|||
      width: 56px;
 | 
			
		||||
      height: 56px;
 | 
			
		||||
      box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
 | 
			
		||||
      box-shadow: var(--avatarShadow);
 | 
			
		||||
      object-fit: cover;
 | 
			
		||||
 | 
			
		||||
      &.better-shadow {
 | 
			
		||||
        box-shadow: var(--avatarShadowInset);
 | 
			
		||||
        filter: var(--avatarShadowFilter)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.animated::before {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -173,8 +183,8 @@
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  .usersettings {
 | 
			
		||||
    color: $fallback--lightFg;
 | 
			
		||||
    color: var(--lightFg, $fallback--lightFg);
 | 
			
		||||
    color: $fallback--lightText;
 | 
			
		||||
    color: var(--lightText, $fallback--lightText);
 | 
			
		||||
    opacity: .8;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +195,16 @@
 | 
			
		|||
    text-overflow: ellipsis;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    flex: 1 1 0;
 | 
			
		||||
    // This is so that text doesn't get overlapped by avatar's shadow if it has
 | 
			
		||||
    // big one
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
 | 
			
		||||
    img {
 | 
			
		||||
      width: 26px;
 | 
			
		||||
      height: 26px;
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
      object-fit: contain
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .user-name{
 | 
			
		||||
| 
						 | 
				
			
			@ -193,8 +213,8 @@
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  .user-screen-name {
 | 
			
		||||
    color: $fallback--lightFg;
 | 
			
		||||
    color: var(--lightFg, $fallback--lightFg);
 | 
			
		||||
    color: $fallback--lightText;
 | 
			
		||||
    color: var(--lightText, $fallback--lightText);
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-weight: light;
 | 
			
		||||
    font-size: 15px;
 | 
			
		||||
| 
						 | 
				
			
			@ -269,8 +289,8 @@
 | 
			
		|||
  padding: .5em 1.5em 0em 1.5em;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  color: $fallback--lightFg;
 | 
			
		||||
  color: var(--lightFg, $fallback--lightFg);
 | 
			
		||||
  color: $fallback--lightText;
 | 
			
		||||
  color: var(--lightText, $fallback--lightText);
 | 
			
		||||
 | 
			
		||||
  &.clickable {
 | 
			
		||||
    .user-count {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <span class="user-finder-container">
 | 
			
		||||
  <div class="user-finder-container">
 | 
			
		||||
    <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
 | 
			
		||||
    <a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
 | 
			
		||||
    <template v-else>
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +9,7 @@
 | 
			
		|||
      </button>
 | 
			
		||||
      <i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
 | 
			
		||||
    </template>
 | 
			
		||||
  </span>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script src="./user_finder.js"></script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
 | 
			
		|||
import UserCardContent from '../user_card_content/user_card_content.vue'
 | 
			
		||||
 | 
			
		||||
const UserPanel = {
 | 
			
		||||
  props: [ 'activatePanel' ],
 | 
			
		||||
  computed: {
 | 
			
		||||
    user () { return this.$store.state.users.currentUser }
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="user-panel">
 | 
			
		||||
    <div v-if='user' class="panel panel-default" style="overflow: visible;">
 | 
			
		||||
      <user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content>
 | 
			
		||||
      <user-card-content :activatePanel="activatePanel" :user="user" :switcher="false" :hideBio="true"></user-card-content>
 | 
			
		||||
      <div class="panel-footer">
 | 
			
		||||
        <post-status-form v-if='user'></post-status-form>
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,16 @@
 | 
			
		|||
    <div v-if="user" class="user-profile panel panel-default">
 | 
			
		||||
      <user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-else class="panel user-profile-placeholder">
 | 
			
		||||
      <div class="panel-heading">
 | 
			
		||||
        <div class="title">
 | 
			
		||||
          {{ $t('settings.profile_tab') }}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="panel-body">
 | 
			
		||||
        <i class="icon-spin3 animate-spin"></i>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <Timeline :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -21,4 +31,12 @@
 | 
			
		|||
    align-items: stretch;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.user-profile-placeholder {
 | 
			
		||||
  .panel-body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: middle;
 | 
			
		||||
    padding: 7em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,30 @@
 | 
			
		|||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
 | 
			
		||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
 | 
			
		||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
 | 
			
		||||
 | 
			
		||||
const UserSettings = {
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      newname: this.$store.state.users.currentUser.name,
 | 
			
		||||
      newbio: this.$store.state.users.currentUser.description,
 | 
			
		||||
      newlocked: this.$store.state.users.currentUser.locked,
 | 
			
		||||
      newnorichtext: this.$store.state.users.currentUser.no_rich_text,
 | 
			
		||||
      newdefaultScope: this.$store.state.users.currentUser.default_scope,
 | 
			
		||||
      newName: this.$store.state.users.currentUser.name,
 | 
			
		||||
      newBio: this.$store.state.users.currentUser.description,
 | 
			
		||||
      newLocked: this.$store.state.users.currentUser.locked,
 | 
			
		||||
      newNoRichText: this.$store.state.users.currentUser.no_rich_text,
 | 
			
		||||
      newDefaultScope: this.$store.state.users.currentUser.default_scope,
 | 
			
		||||
      newHideNetwork: this.$store.state.users.currentUser.hide_network,
 | 
			
		||||
      followList: null,
 | 
			
		||||
      followImportError: false,
 | 
			
		||||
      followsImported: false,
 | 
			
		||||
      enableFollowsExport: true,
 | 
			
		||||
      uploading: [ false, false, false, false ],
 | 
			
		||||
      previews: [ null, null, null ],
 | 
			
		||||
      avatarUploading: false,
 | 
			
		||||
      bannerUploading: false,
 | 
			
		||||
      backgroundUploading: false,
 | 
			
		||||
      followListUploading: false,
 | 
			
		||||
      avatarPreview: null,
 | 
			
		||||
      bannerPreview: null,
 | 
			
		||||
      backgroundPreview: null,
 | 
			
		||||
      avatarUploadError: null,
 | 
			
		||||
      bannerUploadError: null,
 | 
			
		||||
      backgroundUploadError: null,
 | 
			
		||||
      deletingAccount: false,
 | 
			
		||||
      deleteAccountConfirmPasswordInput: '',
 | 
			
		||||
      deleteAccountError: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -40,48 +50,67 @@ const UserSettings = {
 | 
			
		|||
    },
 | 
			
		||||
    vis () {
 | 
			
		||||
      return {
 | 
			
		||||
        public: { selected: this.newdefaultScope === 'public' },
 | 
			
		||||
        unlisted: { selected: this.newdefaultScope === 'unlisted' },
 | 
			
		||||
        private: { selected: this.newdefaultScope === 'private' },
 | 
			
		||||
        direct: { selected: this.newdefaultScope === 'direct' }
 | 
			
		||||
        public: { selected: this.newDefaultScope === 'public' },
 | 
			
		||||
        unlisted: { selected: this.newDefaultScope === 'unlisted' },
 | 
			
		||||
        private: { selected: this.newDefaultScope === 'private' },
 | 
			
		||||
        direct: { selected: this.newDefaultScope === 'direct' }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    updateProfile () {
 | 
			
		||||
      const name = this.newname
 | 
			
		||||
      const description = this.newbio
 | 
			
		||||
      const locked = this.newlocked
 | 
			
		||||
      const description = this.newBio
 | 
			
		||||
      const locked = this.newLocked
 | 
			
		||||
      // Backend notation.
 | 
			
		||||
      /* eslint-disable camelcase */
 | 
			
		||||
      const default_scope = this.newdefaultScope
 | 
			
		||||
      const no_rich_text = this.newnorichtext
 | 
			
		||||
      this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope, no_rich_text}}).then((user) => {
 | 
			
		||||
        if (!user.error) {
 | 
			
		||||
          this.$store.commit('addNewUsers', [user])
 | 
			
		||||
          this.$store.commit('setCurrentUser', user)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const default_scope = this.newDefaultScope
 | 
			
		||||
      const no_rich_text = this.newNoRichText
 | 
			
		||||
      const hide_network = this.newHideNetwork
 | 
			
		||||
      /* eslint-enable camelcase */
 | 
			
		||||
      this.$store.state.api.backendInteractor
 | 
			
		||||
        .updateProfile({
 | 
			
		||||
          params: {
 | 
			
		||||
            name,
 | 
			
		||||
            description,
 | 
			
		||||
            locked,
 | 
			
		||||
            // Backend notation.
 | 
			
		||||
            /* eslint-disable camelcase */
 | 
			
		||||
            default_scope,
 | 
			
		||||
            no_rich_text,
 | 
			
		||||
            hide_network
 | 
			
		||||
            /* eslint-enable camelcase */
 | 
			
		||||
          }}).then((user) => {
 | 
			
		||||
            if (!user.error) {
 | 
			
		||||
              this.$store.commit('addNewUsers', [user])
 | 
			
		||||
              this.$store.commit('setCurrentUser', user)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
    },
 | 
			
		||||
    changeVis (visibility) {
 | 
			
		||||
      this.newdefaultScope = visibility
 | 
			
		||||
      this.newDefaultScope = visibility
 | 
			
		||||
    },
 | 
			
		||||
    uploadFile (slot, e) {
 | 
			
		||||
      const file = e.target.files[0]
 | 
			
		||||
      if (!file) { return }
 | 
			
		||||
      if (file.size > this.$store.state.instance[slot + 'limit']) {
 | 
			
		||||
        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
 | 
			
		||||
        const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
 | 
			
		||||
        this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      // eslint-disable-next-line no-undef
 | 
			
		||||
      const reader = new FileReader()
 | 
			
		||||
      reader.onload = ({target}) => {
 | 
			
		||||
        const img = target.result
 | 
			
		||||
        this.previews[slot] = img
 | 
			
		||||
        this.$forceUpdate() // just changing the array with the index doesn't update the view
 | 
			
		||||
        this[slot + 'Preview'] = img
 | 
			
		||||
      }
 | 
			
		||||
      reader.readAsDataURL(file)
 | 
			
		||||
    },
 | 
			
		||||
    submitAvatar () {
 | 
			
		||||
      if (!this.previews[0]) { return }
 | 
			
		||||
      if (!this.avatarPreview) { return }
 | 
			
		||||
 | 
			
		||||
      let img = this.previews[0]
 | 
			
		||||
      let img = this.avatarPreview
 | 
			
		||||
      // eslint-disable-next-line no-undef
 | 
			
		||||
      let imginfo = new Image()
 | 
			
		||||
      let cropX, cropY, cropW, cropH
 | 
			
		||||
| 
						 | 
				
			
			@ -97,20 +126,25 @@ const UserSettings = {
 | 
			
		|||
        cropX = Math.floor((imginfo.width - imginfo.height) / 2)
 | 
			
		||||
        cropW = imginfo.height
 | 
			
		||||
      }
 | 
			
		||||
      this.uploading[0] = true
 | 
			
		||||
      this.avatarUploading = true
 | 
			
		||||
      this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
 | 
			
		||||
        if (!user.error) {
 | 
			
		||||
          this.$store.commit('addNewUsers', [user])
 | 
			
		||||
          this.$store.commit('setCurrentUser', user)
 | 
			
		||||
          this.previews[0] = null
 | 
			
		||||
          this.avatarPreview = null
 | 
			
		||||
        } else {
 | 
			
		||||
          this.avatarUploadError = this.$t('upload.error.base') + user.error
 | 
			
		||||
        }
 | 
			
		||||
        this.uploading[0] = false
 | 
			
		||||
        this.avatarUploading = false
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    clearUploadError (slot) {
 | 
			
		||||
      this[slot + 'UploadError'] = null
 | 
			
		||||
    },
 | 
			
		||||
    submitBanner () {
 | 
			
		||||
      if (!this.previews[1]) { return }
 | 
			
		||||
      if (!this.bannerPreview) { return }
 | 
			
		||||
 | 
			
		||||
      let banner = this.previews[1]
 | 
			
		||||
      let banner = this.bannerPreview
 | 
			
		||||
      // eslint-disable-next-line no-undef
 | 
			
		||||
      let imginfo = new Image()
 | 
			
		||||
      /* eslint-disable camelcase */
 | 
			
		||||
| 
						 | 
				
			
			@ -120,22 +154,24 @@ const UserSettings = {
 | 
			
		|||
      height = imginfo.height
 | 
			
		||||
      offset_top = 0
 | 
			
		||||
      offset_left = 0
 | 
			
		||||
      this.uploading[1] = true
 | 
			
		||||
      this.bannerUploading = true
 | 
			
		||||
      this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
 | 
			
		||||
        if (!data.error) {
 | 
			
		||||
          let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
 | 
			
		||||
          clone.cover_photo = data.url
 | 
			
		||||
          this.$store.commit('addNewUsers', [clone])
 | 
			
		||||
          this.$store.commit('setCurrentUser', clone)
 | 
			
		||||
          this.previews[1] = null
 | 
			
		||||
          this.bannerPreview = null
 | 
			
		||||
        } else {
 | 
			
		||||
          this.bannerUploadError = this.$t('upload.error.base') + data.error
 | 
			
		||||
        }
 | 
			
		||||
        this.uploading[1] = false
 | 
			
		||||
        this.bannerUploading = false
 | 
			
		||||
      })
 | 
			
		||||
      /* eslint-enable camelcase */
 | 
			
		||||
    },
 | 
			
		||||
    submitBg () {
 | 
			
		||||
      if (!this.previews[2]) { return }
 | 
			
		||||
      let img = this.previews[2]
 | 
			
		||||
      if (!this.backgroundPreview) { return }
 | 
			
		||||
      let img = this.backgroundPreview
 | 
			
		||||
      // eslint-disable-next-line no-undef
 | 
			
		||||
      let imginfo = new Image()
 | 
			
		||||
      let cropX, cropY, cropW, cropH
 | 
			
		||||
| 
						 | 
				
			
			@ -144,20 +180,22 @@ const UserSettings = {
 | 
			
		|||
      cropY = 0
 | 
			
		||||
      cropW = imginfo.width
 | 
			
		||||
      cropH = imginfo.width
 | 
			
		||||
      this.uploading[2] = true
 | 
			
		||||
      this.backgroundUploading = true
 | 
			
		||||
      this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
 | 
			
		||||
        if (!data.error) {
 | 
			
		||||
          let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
 | 
			
		||||
          clone.background_image = data.url
 | 
			
		||||
          this.$store.commit('addNewUsers', [clone])
 | 
			
		||||
          this.$store.commit('setCurrentUser', clone)
 | 
			
		||||
          this.previews[2] = null
 | 
			
		||||
          this.backgroundPreview = null
 | 
			
		||||
        } else {
 | 
			
		||||
          this.backgroundUploadError = this.$t('upload.error.base') + data.error
 | 
			
		||||
        }
 | 
			
		||||
        this.uploading[2] = false
 | 
			
		||||
        this.backgroundUploading = false
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    importFollows () {
 | 
			
		||||
      this.uploading[3] = true
 | 
			
		||||
      this.followListUploading = true
 | 
			
		||||
      const followList = this.followList
 | 
			
		||||
      this.$store.state.api.backendInteractor.followImport({params: followList})
 | 
			
		||||
        .then((status) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +204,7 @@ const UserSettings = {
 | 
			
		|||
          } else {
 | 
			
		||||
            this.followImportError = true
 | 
			
		||||
          }
 | 
			
		||||
          this.uploading[3] = false
 | 
			
		||||
          this.followListUploading = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    /* This function takes an Array of Users
 | 
			
		||||
| 
						 | 
				
			
			@ -198,6 +236,7 @@ const UserSettings = {
 | 
			
		|||
        .fetchFriends({id: this.$store.state.users.currentUser.id})
 | 
			
		||||
        .then((friendList) => {
 | 
			
		||||
          this.exportPeople(friendList, 'friends.csv')
 | 
			
		||||
          setTimeout(() => { this.enableFollowsExport = true }, 2000)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    followListChange () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,11 +9,11 @@
 | 
			
		|||
          <div class="setting-item" >
 | 
			
		||||
            <h2>{{$t('settings.name_bio')}}</h2>
 | 
			
		||||
            <p>{{$t('settings.name')}}</p>
 | 
			
		||||
            <input class='name-changer' id='username' v-model="newname"></input>
 | 
			
		||||
            <input class='name-changer' id='username' v-model="newName"></input>
 | 
			
		||||
            <p>{{$t('settings.bio')}}</p>
 | 
			
		||||
            <textarea class="bio" v-model="newbio"></textarea>
 | 
			
		||||
            <textarea class="bio" v-model="newBio"></textarea>
 | 
			
		||||
            <p>
 | 
			
		||||
              <input type="checkbox" v-model="newlocked" id="account-locked">
 | 
			
		||||
              <input type="checkbox" v-model="newLocked" id="account-locked">
 | 
			
		||||
              <label for="account-locked">{{$t('settings.lock_account_description')}}</label>
 | 
			
		||||
            </p>
 | 
			
		||||
            <div v-if="scopeOptionsEnabled">
 | 
			
		||||
| 
						 | 
				
			
			@ -26,47 +26,63 @@
 | 
			
		|||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <p>
 | 
			
		||||
              <input type="checkbox" v-model="newnorichtext" id="account-no-rich-text">
 | 
			
		||||
              <input type="checkbox" v-model="newNoRichText" id="account-no-rich-text">
 | 
			
		||||
              <label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
 | 
			
		||||
            </p>
 | 
			
		||||
            <button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
 | 
			
		||||
            <p>
 | 
			
		||||
              <input type="checkbox" v-model="newHideNetwork" id="account-hide-network">
 | 
			
		||||
              <label for="account-no-rich-text">{{$t('settings.hide_network_description')}}</label>
 | 
			
		||||
            </p>
 | 
			
		||||
            <button :disabled='newName.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="setting-item">
 | 
			
		||||
            <h2>{{$t('settings.avatar')}}</h2>
 | 
			
		||||
            <p>{{$t('settings.current_avatar')}}</p>
 | 
			
		||||
            <img :src="user.profile_image_url_original" class="old-avatar"></img>
 | 
			
		||||
            <p>{{$t('settings.set_new_avatar')}}</p>
 | 
			
		||||
            <img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]">
 | 
			
		||||
            <img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
 | 
			
		||||
            </img>
 | 
			
		||||
            <div>
 | 
			
		||||
              <input type="file" @change="uploadFile(0, $event)" ></input>
 | 
			
		||||
              <input type="file" @change="uploadFile('avatar', $event)" ></input>
 | 
			
		||||
            </div>
 | 
			
		||||
            <i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
 | 
			
		||||
            <button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
 | 
			
		||||
            <div class='alert error' v-if="avatarUploadError">
 | 
			
		||||
              Error: {{ avatarUploadError }}
 | 
			
		||||
              <i class="icon-cancel" @click="clearUploadError('avatar')"></i>
 | 
			
		||||
            </div>
 | 
			
		||||
            <i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
 | 
			
		||||
            <button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="setting-item">
 | 
			
		||||
            <h2>{{$t('settings.profile_banner')}}</h2>
 | 
			
		||||
            <p>{{$t('settings.current_profile_banner')}}</p>
 | 
			
		||||
            <img :src="user.cover_photo" class="banner"></img>
 | 
			
		||||
            <p>{{$t('settings.set_new_profile_banner')}}</p>
 | 
			
		||||
            <img class="banner" v-bind:src="previews[1]" v-if="previews[1]">
 | 
			
		||||
            <img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview">
 | 
			
		||||
            </img>
 | 
			
		||||
            <div>
 | 
			
		||||
              <input type="file" @change="uploadFile(1, $event)" ></input>
 | 
			
		||||
              <input type="file" @change="uploadFile('banner', $event)" ></input>
 | 
			
		||||
            </div>
 | 
			
		||||
            <i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
 | 
			
		||||
            <button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
 | 
			
		||||
            <div class='alert error' v-if="bannerUploadError">
 | 
			
		||||
              Error: {{ bannerUploadError }}
 | 
			
		||||
              <i class="icon-cancel" @click="clearUploadError('banner')"></i>
 | 
			
		||||
            </div>
 | 
			
		||||
            <i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
 | 
			
		||||
            <button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="setting-item">
 | 
			
		||||
            <h2>{{$t('settings.profile_background')}}</h2>
 | 
			
		||||
            <p>{{$t('settings.set_new_profile_background')}}</p>
 | 
			
		||||
            <img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
 | 
			
		||||
            <img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview">
 | 
			
		||||
            </img>
 | 
			
		||||
            <div>
 | 
			
		||||
              <input type="file" @change="uploadFile(2, $event)" ></input>
 | 
			
		||||
              <input type="file" @change="uploadFile('background', $event)" ></input>
 | 
			
		||||
            </div>
 | 
			
		||||
            <i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
 | 
			
		||||
            <button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
 | 
			
		||||
            <div class='alert error' v-if="backgroundUploadError">
 | 
			
		||||
              Error: {{ backgroundUploadError }}
 | 
			
		||||
              <i class="icon-cancel" @click="clearUploadError('background')"></i>
 | 
			
		||||
            </div>
 | 
			
		||||
            <i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
 | 
			
		||||
            <button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +129,7 @@
 | 
			
		|||
            <form v-model="followImportForm">
 | 
			
		||||
              <input type="file" ref="followlist" v-on:change="followListChange"></input>
 | 
			
		||||
            </form>
 | 
			
		||||
            <i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i>
 | 
			
		||||
            <i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
 | 
			
		||||
            <button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
 | 
			
		||||
            <div v-if="followsImported">
 | 
			
		||||
              <i class="icon-cross" @click="dismissImported"></i>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										149
									
								
								src/i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										149
									
								
								src/i18n/en.json
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -29,6 +29,7 @@
 | 
			
		|||
    "username": "Username"
 | 
			
		||||
  },
 | 
			
		||||
  "nav": {
 | 
			
		||||
    "back": "Back",
 | 
			
		||||
    "chat": "Local Chat",
 | 
			
		||||
    "friend_requests": "Follow Requests",
 | 
			
		||||
    "mentions": "Mentions",
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +73,15 @@
 | 
			
		|||
    "fullname": "Display name",
 | 
			
		||||
    "password_confirm": "Password confirmation",
 | 
			
		||||
    "registration": "Registration",
 | 
			
		||||
    "token": "Invite token"
 | 
			
		||||
    "token": "Invite token",
 | 
			
		||||
    "validations": {
 | 
			
		||||
      "username_required": "cannot be left blank",
 | 
			
		||||
      "fullname_required": "cannot be left blank",
 | 
			
		||||
      "email_required": "cannot be left blank",
 | 
			
		||||
      "password_required": "cannot be left blank",
 | 
			
		||||
      "password_confirmation_required": "cannot be left blank",
 | 
			
		||||
      "password_confirmation_match": "should be the same as password"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "attachmentRadius": "Attachments",
 | 
			
		||||
| 
						 | 
				
			
			@ -116,13 +125,17 @@
 | 
			
		|||
    "general": "General",
 | 
			
		||||
    "hide_attachments_in_convo": "Hide attachments in conversations",
 | 
			
		||||
    "hide_attachments_in_tl": "Hide attachments in timeline",
 | 
			
		||||
    "hide_isp": "Hide instance-specific panel",
 | 
			
		||||
    "preload_images": "Preload images",
 | 
			
		||||
    "hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
 | 
			
		||||
    "hide_user_stats": "Hide user statistics (e.g. the number of followers)",
 | 
			
		||||
    "import_followers_from_a_csv_file": "Import follows from a csv file",
 | 
			
		||||
    "import_theme": "Load preset",
 | 
			
		||||
    "inputRadius": "Input fields",
 | 
			
		||||
    "checkboxRadius": "Checkboxes",
 | 
			
		||||
    "instance_default": "(default: {value})",
 | 
			
		||||
    "instance_default_simple" : "(default)",
 | 
			
		||||
    "instance_default_simple": "(default)",
 | 
			
		||||
    "interface": "Interface",
 | 
			
		||||
    "interfaceLanguage": "Interface language",
 | 
			
		||||
    "invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
 | 
			
		||||
    "limited_availability": "Unavailable in your browser",
 | 
			
		||||
| 
						 | 
				
			
			@ -139,6 +152,7 @@
 | 
			
		|||
    "notification_visibility_mentions": "Mentions",
 | 
			
		||||
    "notification_visibility_repeats": "Repeats",
 | 
			
		||||
    "no_rich_text_description": "Strip rich text formatting from all posts",
 | 
			
		||||
    "hide_network_description": "Don't show who I'm following and who's following me",
 | 
			
		||||
    "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
 | 
			
		||||
    "panelRadius": "Panels",
 | 
			
		||||
    "pause_on_unfocused": "Pause streaming when tab is not focused",
 | 
			
		||||
| 
						 | 
				
			
			@ -170,11 +184,124 @@
 | 
			
		|||
    "text": "Text",
 | 
			
		||||
    "theme": "Theme",
 | 
			
		||||
    "theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
 | 
			
		||||
    "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",
 | 
			
		||||
    "user_settings": "User Settings",
 | 
			
		||||
    "values": {
 | 
			
		||||
      "false": "no",
 | 
			
		||||
      "true": "yes"
 | 
			
		||||
    },
 | 
			
		||||
    "notifications": "Notifications",
 | 
			
		||||
    "enable_web_push_notifications": "Enable web push notifications",
 | 
			
		||||
    "style": {
 | 
			
		||||
      "switcher": {
 | 
			
		||||
        "keep_color": "Keep colors",
 | 
			
		||||
        "keep_shadows": "Keep shadows",
 | 
			
		||||
        "keep_opacity": "Keep opacity",
 | 
			
		||||
        "keep_roundness": "Keep roundness",
 | 
			
		||||
        "keep_fonts": "Keep fonts",
 | 
			
		||||
        "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"
 | 
			
		||||
      },
 | 
			
		||||
      "common": {
 | 
			
		||||
        "color": "Color",
 | 
			
		||||
        "opacity": "Opacity",
 | 
			
		||||
        "contrast": {
 | 
			
		||||
          "hint": "Contrast ratio is {ratio}, it {level} {context}",
 | 
			
		||||
          "level": {
 | 
			
		||||
            "aa": "meets Level AA guideline (minimal)",
 | 
			
		||||
            "aaa": "meets Level AAA guideline (recommended)",
 | 
			
		||||
            "bad": "doesn't meet any accessibility guidelines"
 | 
			
		||||
          },
 | 
			
		||||
          "context": {
 | 
			
		||||
            "18pt": "for large (18pt+) text",
 | 
			
		||||
            "text": "for text"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "common_colors": {
 | 
			
		||||
        "_tab_label": "Common",
 | 
			
		||||
        "main": "Common colors",
 | 
			
		||||
        "foreground_hint": "See \"Advanced\" tab for more detailed control",
 | 
			
		||||
        "rgbo": "Icons, accents, badges"
 | 
			
		||||
      },
 | 
			
		||||
      "advanced_colors": {
 | 
			
		||||
        "_tab_label": "Advanced",
 | 
			
		||||
        "alert": "Alert background",
 | 
			
		||||
        "alert_error": "Error",
 | 
			
		||||
        "badge": "Badge background",
 | 
			
		||||
        "badge_notification": "Notification",
 | 
			
		||||
        "panel_header": "Panel header",
 | 
			
		||||
        "top_bar": "Top bar",
 | 
			
		||||
        "borders": "Borders",
 | 
			
		||||
        "buttons": "Buttons",
 | 
			
		||||
        "inputs": "Input fields",
 | 
			
		||||
        "faint_text": "Faded text"
 | 
			
		||||
      },
 | 
			
		||||
      "radii": {
 | 
			
		||||
        "_tab_label": "Roundness"
 | 
			
		||||
      },
 | 
			
		||||
      "shadows": {
 | 
			
		||||
        "_tab_label": "Shadow and lighting",
 | 
			
		||||
        "component": "Component",
 | 
			
		||||
        "override": "Override",
 | 
			
		||||
        "shadow_id": "Shadow #{value}",
 | 
			
		||||
        "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.",
 | 
			
		||||
        "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.",
 | 
			
		||||
          "avatar_inset": "Please note that combining both inset and non-inset shadows on avatars might give unexpected results with transparent avatars.",
 | 
			
		||||
          "spread_zero": "Shadows with spread > 0 will appear as if it was set to zero",
 | 
			
		||||
          "inset_classic": "Inset shadows will be using {0}"
 | 
			
		||||
        },
 | 
			
		||||
        "components": {
 | 
			
		||||
          "panel": "Panel",
 | 
			
		||||
          "panelHeader": "Panel header",
 | 
			
		||||
          "topBar": "Top bar",
 | 
			
		||||
          "avatar": "User avatar (in profile view)",
 | 
			
		||||
          "avatarStatus": "User avatar (in post display)",
 | 
			
		||||
          "popup": "Popups and tooltips",
 | 
			
		||||
          "button": "Button",
 | 
			
		||||
          "buttonHover": "Button (hover)",
 | 
			
		||||
          "buttonPressed": "Button (pressed)",
 | 
			
		||||
          "buttonPressedHover": "Button (pressed+hover)",
 | 
			
		||||
          "input": "Input field"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "fonts": {
 | 
			
		||||
        "_tab_label": "Fonts",
 | 
			
		||||
        "help": "Select font to use for elements of UI. For \"custom\" you have to enter exact font name as it appears in system.",
 | 
			
		||||
        "components": {
 | 
			
		||||
          "interface": "Interface",
 | 
			
		||||
          "input": "Input fields",
 | 
			
		||||
          "post": "Post text",
 | 
			
		||||
          "postCode": "Monospaced text in a post (rich text)"
 | 
			
		||||
        },
 | 
			
		||||
        "family": "Font name",
 | 
			
		||||
        "size": "Size (in px)",
 | 
			
		||||
        "weight": "Weight (boldness)",
 | 
			
		||||
        "custom": "Custom"
 | 
			
		||||
      },
 | 
			
		||||
      "preview": {
 | 
			
		||||
        "header": "Preview",
 | 
			
		||||
        "content": "Content",
 | 
			
		||||
        "error": "Example error",
 | 
			
		||||
        "button": "Button",
 | 
			
		||||
        "text": "A bunch of more {0} and {1}",
 | 
			
		||||
        "mono": "content",
 | 
			
		||||
        "input": "Just landed in L.A.",
 | 
			
		||||
        "faint_link": "helpful manual",
 | 
			
		||||
        "fine_print": "Read our {0} to learn nothing useful!",
 | 
			
		||||
        "header_faint": "This is fine",
 | 
			
		||||
        "checkbox": "I have skimmed over terms and conditions",
 | 
			
		||||
        "link": "a nice lil' link"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "timeline": {
 | 
			
		||||
| 
						 | 
				
			
			@ -197,6 +324,7 @@
 | 
			
		|||
    "followers": "Followers",
 | 
			
		||||
    "following": "Following!",
 | 
			
		||||
    "follows_you": "Follows you!",
 | 
			
		||||
    "its_you": "It's you!",
 | 
			
		||||
    "mute": "Mute",
 | 
			
		||||
    "muted": "Muted",
 | 
			
		||||
    "per_day": "per day",
 | 
			
		||||
| 
						 | 
				
			
			@ -214,6 +342,21 @@
 | 
			
		|||
    "media_upload": "Upload Media",
 | 
			
		||||
    "repeat": "Repeat",
 | 
			
		||||
    "reply": "Reply",
 | 
			
		||||
    "favorite": "Favorite"
 | 
			
		||||
    "favorite": "Favorite",
 | 
			
		||||
    "user_settings": "User Settings"
 | 
			
		||||
  },
 | 
			
		||||
  "upload":{
 | 
			
		||||
    "error": {
 | 
			
		||||
    "base": "Upload failed.",
 | 
			
		||||
    "file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
 | 
			
		||||
    "default": "Try again later"
 | 
			
		||||
    },
 | 
			
		||||
    "file_size_units": {
 | 
			
		||||
      "B": "B",
 | 
			
		||||
      "KiB": "KiB",
 | 
			
		||||
      "MiB": "MiB",
 | 
			
		||||
      "GiB": "GiB",
 | 
			
		||||
      "TiB": "TiB"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										128
									
								
								src/i18n/ru.json
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								src/i18n/ru.json
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -19,6 +19,7 @@
 | 
			
		|||
    "username": "Имя пользователя"
 | 
			
		||||
  },
 | 
			
		||||
  "nav": {
 | 
			
		||||
    "back": "Назад",
 | 
			
		||||
    "chat": "Локальный чат",
 | 
			
		||||
    "mentions": "Упоминания",
 | 
			
		||||
    "public_tl": "Публичная лента",
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +56,15 @@
 | 
			
		|||
    "fullname": "Отображаемое имя",
 | 
			
		||||
    "password_confirm": "Подтверждение пароля",
 | 
			
		||||
    "registration": "Регистрация",
 | 
			
		||||
    "token": "Код приглашения"
 | 
			
		||||
    "token": "Код приглашения",
 | 
			
		||||
    "validations": {
 | 
			
		||||
      "username_required": "не должно быть пустым",
 | 
			
		||||
      "fullname_required": "не должно быть пустым",
 | 
			
		||||
      "email_required": "не должен быть пустым",
 | 
			
		||||
      "password_required": "не должен быть пустым",
 | 
			
		||||
      "password_confirmation_required": "не должно быть пустым",
 | 
			
		||||
      "password_confirmation_match": "должно совпадать с паролем"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "attachmentRadius": "Прикреплённые файлы",
 | 
			
		||||
| 
						 | 
				
			
			@ -97,9 +106,12 @@
 | 
			
		|||
    "general": "Общие",
 | 
			
		||||
    "hide_attachments_in_convo": "Прятать вложения в разговорах",
 | 
			
		||||
    "hide_attachments_in_tl": "Прятать вложения в ленте",
 | 
			
		||||
    "hide_isp": "Скрыть серверную панель",
 | 
			
		||||
    "import_followers_from_a_csv_file": "Импортировать читаемых из файла .csv",
 | 
			
		||||
    "import_theme": "Загрузить Тему",
 | 
			
		||||
    "inputRadius": "Поля ввода",
 | 
			
		||||
    "checkboxRadius": "Чекбоксы",
 | 
			
		||||
    "interface": "Интерфейс",
 | 
			
		||||
    "interfaceLanguage": "Язык интерфейса",
 | 
			
		||||
    "limited_availability": "Не доступно в вашем браузере",
 | 
			
		||||
    "links": "Ссылки",
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +127,7 @@
 | 
			
		|||
    "notification_visibility_mentions": "Упоминания",
 | 
			
		||||
    "notification_visibility_repeats": "Повторы",
 | 
			
		||||
    "no_rich_text_description": "Убрать форматирование из всех постов",
 | 
			
		||||
    "hide_network_description": "Не показывать кого я читаю и кто меня читает",
 | 
			
		||||
    "nsfw_clickthrough": "Включить скрытие NSFW вложений",
 | 
			
		||||
    "panelRadius": "Панели",
 | 
			
		||||
    "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
 | 
			
		||||
| 
						 | 
				
			
			@ -139,8 +152,119 @@
 | 
			
		|||
    "text": "Текст",
 | 
			
		||||
    "theme": "Тема",
 | 
			
		||||
    "theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.",
 | 
			
		||||
    "theme_help_v2_1": "Вы так же можете перепоределить цвета определенных компонентов нажав соотв. галочку. Используйте кнопку \"Очистить всё\" чтобы снять все переопределения",
 | 
			
		||||
    "theme_help_v2_2": "Под некоторыми полями ввода это идикаторы контрастности, наведите на них мышью чтобы узнать больше. Приспользовании прозрачности контраст расчитывается для наихудшего варианта.",
 | 
			
		||||
    "tooltipRadius": "Всплывающие подсказки/уведомления",
 | 
			
		||||
    "user_settings": "Настройки пользователя"
 | 
			
		||||
    "user_settings": "Настройки пользователя",
 | 
			
		||||
    "style": {
 | 
			
		||||
      "switcher": {
 | 
			
		||||
        "keep_color": "Оставить цвета",
 | 
			
		||||
        "keep_shadows": "Оставить тени",
 | 
			
		||||
        "keep_opacity": "Оставить прозрачность",
 | 
			
		||||
        "keep_roundness": "Оставить скругление",
 | 
			
		||||
        "keep_fonts": "Оставить шрифты",
 | 
			
		||||
        "save_load_hint": "Опции \"оставить...\" позволяют сохранить текущие настройки при выборе другой темы или импорта её из файла. Так же они влияют на то какие компоненты будут сохранены при экспорте темы. Когда все галочки сняты все компоненты будут экспортированы.",
 | 
			
		||||
        "reset": "Сбросить",
 | 
			
		||||
        "clear_all": "Очистить всё",
 | 
			
		||||
        "clear_opacity": "Очистить прозрачность"
 | 
			
		||||
      },
 | 
			
		||||
      "common": {
 | 
			
		||||
        "color": "Цвет",
 | 
			
		||||
        "opacity": "Прозрачность",
 | 
			
		||||
        "contrast": {
 | 
			
		||||
          "hint": "Уровень контраста: {ratio}, что {level} {context}",
 | 
			
		||||
          "level": {
 | 
			
		||||
            "aa": "соответствует гайдлайну Level AA (минимальный)",
 | 
			
		||||
            "aaa": "соответствует гайдлайну Level AAA (рекомендуемый)",
 | 
			
		||||
            "bad": "не соответствует каким либо гайдлайнам"
 | 
			
		||||
          },
 | 
			
		||||
          "context": {
 | 
			
		||||
            "18pt": "для крупного (18pt+) текста",
 | 
			
		||||
            "text": "для текста"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "common_colors": {
 | 
			
		||||
        "_tab_label": "Общие",
 | 
			
		||||
        "main": "Общие цвета",
 | 
			
		||||
        "foreground_hint": "См. вкладку \"Дополнительно\" для более детального контроля",
 | 
			
		||||
        "rgbo": "Иконки, акценты, ярылки"
 | 
			
		||||
      },
 | 
			
		||||
      "advanced_colors": {
 | 
			
		||||
        "_tab_label": "Дополнительно",
 | 
			
		||||
        "alert": "Фон уведомлений",
 | 
			
		||||
        "alert_error": "Ошибки",
 | 
			
		||||
        "badge": "Фон значков",
 | 
			
		||||
        "badge_notification": "Уведомления",
 | 
			
		||||
        "panel_header": "Заголовок панели",
 | 
			
		||||
        "top_bar": "Верняя полоска",
 | 
			
		||||
        "borders": "Границы",
 | 
			
		||||
        "buttons": "Кнопки",
 | 
			
		||||
        "inputs": "Поля ввода",
 | 
			
		||||
        "faint_text": "Маловажный текст"
 | 
			
		||||
      },
 | 
			
		||||
      "radii": {
 | 
			
		||||
        "_tab_label": "Скругление"
 | 
			
		||||
      },
 | 
			
		||||
      "shadows": {
 | 
			
		||||
        "_tab_label": "Светотень",
 | 
			
		||||
        "component": "Компонент",
 | 
			
		||||
        "override": "Переопределить",
 | 
			
		||||
        "shadow_id": "Тень №{value}",
 | 
			
		||||
        "blur": "Размытие",
 | 
			
		||||
        "spread": "Разброс",
 | 
			
		||||
        "inset": "Внутренняя",
 | 
			
		||||
        "hint": "Для теней вы так же можете использовать --variable в качестве цвета чтобы использовать CSS3-переменные. В таком случае прозрачность работать не будет.",
 | 
			
		||||
        "filter_hint": {
 | 
			
		||||
          "always_drop_shadow": "Внимание, эта тень всегда использует {0} когда браузер поддерживает это",
 | 
			
		||||
          "drop_shadow_syntax": "{0} не поддерживает параметр {1} и ключевое слово {2}",
 | 
			
		||||
          "avatar_inset": "Одновременное использование внутренних и внешних теней на (прозрачных) аватарках может дать не те результаты что вы ожидаете",
 | 
			
		||||
          "spread_zero": "Тени с разбросом > 0 будут выглядеть как если бы разброс установлен в 0",
 | 
			
		||||
          "inset_classic": "Внутренние тени будут использовать {0}"
 | 
			
		||||
        },
 | 
			
		||||
        "components": {
 | 
			
		||||
          "panel": "Панель",
 | 
			
		||||
          "panelHeader": "Заголовок панели",
 | 
			
		||||
          "topBar": "Верхняя полоска",
 | 
			
		||||
          "avatar": "Аватарка (профиль)",
 | 
			
		||||
          "avatarStatus": "Аватарка (в ленте)",
 | 
			
		||||
          "popup": "Всплывающие подсказки",
 | 
			
		||||
          "button": "Кнопки",
 | 
			
		||||
          "buttonHover": "Кнопки (наведен курсор)",
 | 
			
		||||
          "buttonPressed": "Кнопки (нажата)",
 | 
			
		||||
          "buttonPressedHover": "Кнопки (нажата+наведен курсор)",
 | 
			
		||||
          "input": "Поля ввода"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "fonts": {
 | 
			
		||||
        "_tab_label": "Шрифты",
 | 
			
		||||
        "help": "Выберите тип шрифта для использования в интерфейсе. При выборе варианта \"другой\" надо ввести название шрифта в точности как он называется в системе.",
 | 
			
		||||
        "components": {
 | 
			
		||||
          "interface": "Интерфейс",
 | 
			
		||||
          "input": "Поля ввода",
 | 
			
		||||
          "post": "Текст постов",
 | 
			
		||||
          "postCode": "Моноширинный текст в посте (форматирование)"
 | 
			
		||||
        },
 | 
			
		||||
        "family": "Шрифт",
 | 
			
		||||
        "size": "Размер (в пикселях)",
 | 
			
		||||
        "weight": "Ширина",
 | 
			
		||||
        "custom": "Другой"
 | 
			
		||||
      },
 | 
			
		||||
      "preview": {
 | 
			
		||||
        "header": "Пример",
 | 
			
		||||
        "content": "Контент",
 | 
			
		||||
        "error": "Ошибка стоп 000",
 | 
			
		||||
        "button": "Кнопка",
 | 
			
		||||
        "text": "Еще немного {0} и масенькая {1}",
 | 
			
		||||
        "mono": "контента",
 | 
			
		||||
        "input": "Что нового?",
 | 
			
		||||
        "faint_link": "Его придется убрать",
 | 
			
		||||
        "fine_print": "Если проблемы остались — ваш гуртовщик мыши плохо стоит. {0}.",
 | 
			
		||||
        "header_faint": "Все идет по плану",
 | 
			
		||||
        "checkbox": "Я подтверждаю что не было ни единого разрыва",
 | 
			
		||||
        "link": "ссылка"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "timeline": {
 | 
			
		||||
    "collapse": "Свернуть",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,6 +75,7 @@ export default function createPersistedState ({
 | 
			
		|||
        loaded = true
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.log("Couldn't load state")
 | 
			
		||||
        console.error(e)
 | 
			
		||||
        loaded = true
 | 
			
		||||
      }
 | 
			
		||||
      subscriber(store)((mutation, state) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										36
									
								
								src/main.js
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								src/main.js
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -50,6 +50,32 @@ const persistedStateOptions = {
 | 
			
		|||
    'oauth'
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const registerPushNotifications = store => {
 | 
			
		||||
  store.subscribe((mutation, state) => {
 | 
			
		||||
    const vapidPublicKey = state.instance.vapidPublicKey
 | 
			
		||||
    const permission = state.interface.notificationPermission === 'granted'
 | 
			
		||||
    const isUserMutation = mutation.type === 'setCurrentUser'
 | 
			
		||||
 | 
			
		||||
    if (isUserMutation && vapidPublicKey && permission) {
 | 
			
		||||
      return store.dispatch('registerPushNotifications')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = state.users.currentUser
 | 
			
		||||
    const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey'
 | 
			
		||||
 | 
			
		||||
    if (isVapidMutation && user && permission) {
 | 
			
		||||
      return store.dispatch('registerPushNotifications')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted'
 | 
			
		||||
 | 
			
		||||
    if (isPermMutation && user && vapidPublicKey) {
 | 
			
		||||
      return store.dispatch('registerPushNotifications')
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
createPersistedState(persistedStateOptions).then((persistedState) => {
 | 
			
		||||
  const store = new Vuex.Store({
 | 
			
		||||
    modules: {
 | 
			
		||||
| 
						 | 
				
			
			@ -62,10 +88,16 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
 | 
			
		|||
      chat: chatModule,
 | 
			
		||||
      oauth: oauthModule
 | 
			
		||||
    },
 | 
			
		||||
    plugins: [persistedState],
 | 
			
		||||
    plugins: [persistedState, registerPushNotifications],
 | 
			
		||||
    strict: false // Socket modifies itself, let's ignore this for now.
 | 
			
		||||
    // strict: process.env.NODE_ENV !== 'production'
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  afterStoreSetup({store, i18n})
 | 
			
		||||
  afterStoreSetup({ store, i18n })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// These are inlined by webpack's DefinePlugin
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
window.___pleromafe_mode = process.env
 | 
			
		||||
window.___pleromafe_commit_hash = COMMIT_HASH
 | 
			
		||||
window.___pleromafe_dev_overrides = DEV_OVERRIDES
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { set, delete as del } from 'vue'
 | 
			
		||||
import StyleSetter from '../services/style_setter/style_setter.js'
 | 
			
		||||
import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
 | 
			
		||||
 | 
			
		||||
const browserLocale = (window.navigator.language || 'en').split('-')[0]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ const defaultState = {
 | 
			
		|||
  hideAttachments: false,
 | 
			
		||||
  hideAttachmentsInConv: false,
 | 
			
		||||
  hideNsfw: true,
 | 
			
		||||
  preloadImage: true,
 | 
			
		||||
  loopVideo: true,
 | 
			
		||||
  loopVideoSilentOnly: true,
 | 
			
		||||
  autoLoad: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +24,7 @@ const defaultState = {
 | 
			
		|||
    likes: true,
 | 
			
		||||
    repeats: true
 | 
			
		||||
  },
 | 
			
		||||
  webPushNotifications: true,
 | 
			
		||||
  muteWords: [],
 | 
			
		||||
  highlight: {},
 | 
			
		||||
  interfaceLanguage: browserLocale,
 | 
			
		||||
| 
						 | 
				
			
			@ -54,10 +56,10 @@ const config = {
 | 
			
		|||
      commit('setOption', {name, value})
 | 
			
		||||
      switch (name) {
 | 
			
		||||
        case 'theme':
 | 
			
		||||
          StyleSetter.setPreset(value, commit)
 | 
			
		||||
          setPreset(value, commit)
 | 
			
		||||
          break
 | 
			
		||||
        case 'customTheme':
 | 
			
		||||
          StyleSetter.setColors(value, commit)
 | 
			
		||||
          applyTheme(value, commit)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										12
									
								
								src/modules/errors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/modules/errors.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
import { capitalize } from 'lodash'
 | 
			
		||||
 | 
			
		||||
export function humanizeErrors (errors) {
 | 
			
		||||
  return Object.entries(errors).reduce((errs, [k, val]) => {
 | 
			
		||||
    let message = val.reduce((acc, message) => {
 | 
			
		||||
      let key = capitalize(k.replace(/_/g, ' '))
 | 
			
		||||
      return acc + [key, message].join(' ') + '. '
 | 
			
		||||
    }, '')
 | 
			
		||||
    return [...errs, message]
 | 
			
		||||
  }, [])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { set } from 'vue'
 | 
			
		||||
import StyleSetter from '../services/style_setter/style_setter.js'
 | 
			
		||||
import { setPreset } from '../services/style_setter/style_setter.js'
 | 
			
		||||
 | 
			
		||||
const defaultState = {
 | 
			
		||||
  // Stuff from static/config.json and apiConfig
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +25,8 @@ const defaultState = {
 | 
			
		|||
  scopeCopy: true,
 | 
			
		||||
  subjectLineBehavior: 'email',
 | 
			
		||||
  loginMethod: 'password',
 | 
			
		||||
  nsfwCensorImage: undefined,
 | 
			
		||||
  vapidPublicKey: undefined,
 | 
			
		||||
 | 
			
		||||
  // Nasty stuff
 | 
			
		||||
  pleromaBackend: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +62,7 @@ const instance = {
 | 
			
		|||
          dispatch('setPageTitle')
 | 
			
		||||
          break
 | 
			
		||||
        case 'theme':
 | 
			
		||||
          StyleSetter.setPreset(value, commit)
 | 
			
		||||
          setPreset(value, commit)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,14 @@ import { set, delete as del } from 'vue'
 | 
			
		|||
const defaultState = {
 | 
			
		||||
  settings: {
 | 
			
		||||
    currentSaveStateNotice: null,
 | 
			
		||||
    noticeClearTimeout: null
 | 
			
		||||
    noticeClearTimeout: null,
 | 
			
		||||
    notificationPermission: null
 | 
			
		||||
  },
 | 
			
		||||
  browserSupport: {
 | 
			
		||||
    cssFilter: window.CSS && window.CSS.supports && (
 | 
			
		||||
      window.CSS.supports('filter', 'drop-shadow(0 0)') ||
 | 
			
		||||
      window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +24,13 @@ const interfaceMod = {
 | 
			
		|||
        }
 | 
			
		||||
        set(state.settings, 'currentSaveStateNotice', { error: false, data: success })
 | 
			
		||||
        set(state.settings, 'noticeClearTimeout',
 | 
			
		||||
            setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
 | 
			
		||||
          setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
 | 
			
		||||
      } else {
 | 
			
		||||
        set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setNotificationPermission (state, permission) {
 | 
			
		||||
      state.notificationPermission = permission
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  actions: {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +39,9 @@ const interfaceMod = {
 | 
			
		|||
    },
 | 
			
		||||
    settingsSaved ({ commit, dispatch }, { success, error }) {
 | 
			
		||||
      commit('settingsSaved', { success, error })
 | 
			
		||||
    },
 | 
			
		||||
    setNotificationPermission ({ commit }, permission) {
 | 
			
		||||
      commit('setNotificationPermission', permission)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,9 @@
 | 
			
		|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
 | 
			
		||||
import { compact, map, each, merge } from 'lodash'
 | 
			
		||||
import { set } from 'vue'
 | 
			
		||||
import registerPushNotifications from '../services/push/push.js'
 | 
			
		||||
import oauthApi from '../services/new_api/oauth'
 | 
			
		||||
import { humanizeErrors } from './errors'
 | 
			
		||||
 | 
			
		||||
// TODO: Unify with mergeOrAdd in statuses.js
 | 
			
		||||
export const mergeOrAdd = (arr, obj, item) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -9,17 +12,28 @@ export const mergeOrAdd = (arr, obj, item) => {
 | 
			
		|||
  if (oldItem) {
 | 
			
		||||
    // We already have this, so only merge the new info.
 | 
			
		||||
    merge(oldItem, item)
 | 
			
		||||
    return {item: oldItem, new: false}
 | 
			
		||||
    return { item: oldItem, new: false }
 | 
			
		||||
  } else {
 | 
			
		||||
    // This is a new item, prepare it
 | 
			
		||||
    arr.push(item)
 | 
			
		||||
    obj[item.id] = item
 | 
			
		||||
    return {item, new: true}
 | 
			
		||||
    if (item.screen_name && !item.screen_name.includes('@')) {
 | 
			
		||||
      obj[item.screen_name] = item
 | 
			
		||||
    }
 | 
			
		||||
    return { item, new: true }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getNotificationPermission = () => {
 | 
			
		||||
  const Notification = window.Notification
 | 
			
		||||
 | 
			
		||||
  if (!Notification) return Promise.resolve(null)
 | 
			
		||||
  if (Notification.permission === 'default') return Notification.requestPermission()
 | 
			
		||||
  return Promise.resolve(Notification.permission)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const mutations = {
 | 
			
		||||
  setMuted (state, { user: {id}, muted }) {
 | 
			
		||||
  setMuted (state, { user: { id }, muted }) {
 | 
			
		||||
    const user = state.usersObject[id]
 | 
			
		||||
    set(user, 'muted', muted)
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -43,18 +57,31 @@ export const mutations = {
 | 
			
		|||
  setUserForStatus (state, status) {
 | 
			
		||||
    status.user = state.usersObject[status.user.id]
 | 
			
		||||
  },
 | 
			
		||||
  setColor (state, { user: {id}, highlighted }) {
 | 
			
		||||
  setColor (state, { user: { id }, highlighted }) {
 | 
			
		||||
    const user = state.usersObject[id]
 | 
			
		||||
    set(user, 'highlight', highlighted)
 | 
			
		||||
  },
 | 
			
		||||
  signUpPending (state) {
 | 
			
		||||
    state.signUpPending = true
 | 
			
		||||
    state.signUpErrors = []
 | 
			
		||||
  },
 | 
			
		||||
  signUpSuccess (state) {
 | 
			
		||||
    state.signUpPending = false
 | 
			
		||||
  },
 | 
			
		||||
  signUpFailure (state, errors) {
 | 
			
		||||
    state.signUpPending = false
 | 
			
		||||
    state.signUpErrors = errors
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const defaultState = {
 | 
			
		||||
  loggingIn: false,
 | 
			
		||||
  lastLoginName: false,
 | 
			
		||||
  currentUser: false,
 | 
			
		||||
  loggingIn: false,
 | 
			
		||||
  users: [],
 | 
			
		||||
  usersObject: {}
 | 
			
		||||
  usersObject: {},
 | 
			
		||||
  signUpPending: false,
 | 
			
		||||
  signUpErrors: []
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const users = {
 | 
			
		||||
| 
						 | 
				
			
			@ -62,8 +89,15 @@ const users = {
 | 
			
		|||
  mutations,
 | 
			
		||||
  actions: {
 | 
			
		||||
    fetchUser (store, id) {
 | 
			
		||||
      store.rootState.api.backendInteractor.fetchUser({id})
 | 
			
		||||
        .then((user) => store.commit('addNewUsers', user))
 | 
			
		||||
      store.rootState.api.backendInteractor.fetchUser({ id })
 | 
			
		||||
        .then((user) => store.commit('addNewUsers', [user]))
 | 
			
		||||
    },
 | 
			
		||||
    registerPushNotifications (store) {
 | 
			
		||||
      const token = store.state.currentUser.credentials
 | 
			
		||||
      const vapidPublicKey = store.rootState.instance.vapidPublicKey
 | 
			
		||||
      const isEnabled = store.rootState.config.webPushNotifications
 | 
			
		||||
 | 
			
		||||
      registerPushNotifications(isEnabled, vapidPublicKey, token)
 | 
			
		||||
    },
 | 
			
		||||
    addNewStatuses (store, { statuses }) {
 | 
			
		||||
      const users = map(statuses, 'user')
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +114,34 @@ const users = {
 | 
			
		|||
        store.commit('setUserForStatus', status)
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    async signUp (store, userInfo) {
 | 
			
		||||
      store.commit('signUpPending')
 | 
			
		||||
 | 
			
		||||
      let rootState = store.rootState
 | 
			
		||||
 | 
			
		||||
      let response = await rootState.api.backendInteractor.register(userInfo)
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        const data = {
 | 
			
		||||
          oauth: rootState.oauth,
 | 
			
		||||
          instance: rootState.instance.server
 | 
			
		||||
        }
 | 
			
		||||
        let app = await oauthApi.getOrCreateApp(data)
 | 
			
		||||
        let result = await oauthApi.getTokenWithCredentials({
 | 
			
		||||
          app,
 | 
			
		||||
          instance: data.instance,
 | 
			
		||||
          username: userInfo.username,
 | 
			
		||||
          password: userInfo.password
 | 
			
		||||
        })
 | 
			
		||||
        store.commit('signUpSuccess')
 | 
			
		||||
        store.commit('setToken', result.access_token)
 | 
			
		||||
        store.dispatch('loginUser', result.access_token)
 | 
			
		||||
      } else {
 | 
			
		||||
        let data = await response.json()
 | 
			
		||||
        let errors = humanizeErrors(JSON.parse(data.error))
 | 
			
		||||
        store.commit('signUpFailure', errors)
 | 
			
		||||
        throw Error(errors)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    logout (store) {
 | 
			
		||||
      store.commit('clearCurrentUser')
 | 
			
		||||
      store.commit('setToken', false)
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +162,9 @@ const users = {
 | 
			
		|||
                  commit('setCurrentUser', user)
 | 
			
		||||
                  commit('addNewUsers', [user])
 | 
			
		||||
 | 
			
		||||
                  getNotificationPermission()
 | 
			
		||||
                    .then(permission => commit('setNotificationPermission', permission))
 | 
			
		||||
 | 
			
		||||
                  // Set our new backend interactor
 | 
			
		||||
                  commit('setBackendInteractor', backendInteractorService(accessToken))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -118,12 +183,8 @@ const users = {
 | 
			
		|||
                    store.commit('addNewUsers', mutedUsers)
 | 
			
		||||
                  })
 | 
			
		||||
 | 
			
		||||
                  if ('Notification' in window && window.Notification.permission === 'default') {
 | 
			
		||||
                    window.Notification.requestPermission()
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  // Fetch our friends
 | 
			
		||||
                  store.rootState.api.backendInteractor.fetchFriends({id: user.id})
 | 
			
		||||
                  store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
 | 
			
		||||
                    .then((friends) => commit('addNewUsers', friends))
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,15 @@
 | 
			
		|||
import { map } from 'lodash'
 | 
			
		||||
 | 
			
		||||
const rgb2hex = (r, g, b) => {
 | 
			
		||||
  if (r === null || typeof r === 'undefined') {
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
  if (r[0] === '#') {
 | 
			
		||||
    return r
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof r === 'object') {
 | 
			
		||||
    ({ r, g, b } = r)
 | 
			
		||||
  }
 | 
			
		||||
  [r, g, b] = map([r, g, b], (val) => {
 | 
			
		||||
    val = Math.ceil(val)
 | 
			
		||||
    val = val < 0 ? 0 : val
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +19,91 @@ const rgb2hex = (r, g, b) => {
 | 
			
		|||
  return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts 8-bit RGB component into linear component
 | 
			
		||||
 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
 | 
			
		||||
 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml
 | 
			
		||||
 * https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Number} bit - color component [0..255]
 | 
			
		||||
 * @returns {Number} linear component [0..1]
 | 
			
		||||
 */
 | 
			
		||||
const c2linear = (bit) => {
 | 
			
		||||
  // W3C gives 0.03928 while wikipedia states 0.04045
 | 
			
		||||
  // what those magical numbers mean - I don't know.
 | 
			
		||||
  // something about gamma-correction, i suppose.
 | 
			
		||||
  // Sticking with W3C example.
 | 
			
		||||
  const c = bit / 255
 | 
			
		||||
  if (c < 0.03928) {
 | 
			
		||||
    return c / 12.92
 | 
			
		||||
  } else {
 | 
			
		||||
    return Math.pow((c + 0.055) / 1.055, 2.4)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts sRGB into linear RGB
 | 
			
		||||
 * @param {Object} srgb - sRGB color
 | 
			
		||||
 * @returns {Object} linear rgb color
 | 
			
		||||
 */
 | 
			
		||||
const srgbToLinear = (srgb) => {
 | 
			
		||||
  return 'rgb'.split('').reduce((acc, c) => { acc[c] = c2linear(srgb[c]); return acc }, {})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculates relative luminance for given color
 | 
			
		||||
 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
 | 
			
		||||
 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} srgb - sRGB color
 | 
			
		||||
 * @returns {Number} relative luminance
 | 
			
		||||
 */
 | 
			
		||||
const relativeLuminance = (srgb) => {
 | 
			
		||||
  const {r, g, b} = srgbToLinear(srgb)
 | 
			
		||||
  return 0.2126 * r + 0.7152 * g + 0.0722 * b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generates color ratio between two colors. Order is unimporant
 | 
			
		||||
 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} a - sRGB color
 | 
			
		||||
 * @param {Object} b - sRGB color
 | 
			
		||||
 * @returns {Number} color ratio
 | 
			
		||||
 */
 | 
			
		||||
const getContrastRatio = (a, b) => {
 | 
			
		||||
  const la = relativeLuminance(a)
 | 
			
		||||
  const lb = relativeLuminance(b)
 | 
			
		||||
  const [l1, l2] = la > lb ? [la, lb] : [lb, la]
 | 
			
		||||
 | 
			
		||||
  return (l1 + 0.05) / (l2 + 0.05)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This performs alpha blending between solid background and semi-transparent foreground
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} fg - top layer color
 | 
			
		||||
 * @param {Number} fga - top layer's alpha
 | 
			
		||||
 * @param {Object} bg - bottom layer color
 | 
			
		||||
 * @returns {Object} sRGB of resulting color
 | 
			
		||||
 */
 | 
			
		||||
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
 | 
			
		||||
    // for opaque bg and transparent fg
 | 
			
		||||
    acc[c] = (fg[c] * fga + bg[c] * (1 - fga))
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const invert = (rgb) => {
 | 
			
		||||
  return 'rgb'.split('').reduce((acc, c) => {
 | 
			
		||||
    acc[c] = 255 - rgb[c]
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const hex2rgb = (hex) => {
 | 
			
		||||
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
 | 
			
		||||
  return result ? {
 | 
			
		||||
| 
						 | 
				
			
			@ -19,16 +113,18 @@ const hex2rgb = (hex) => {
 | 
			
		|||
  } : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const rgbstr2hex = (rgb) => {
 | 
			
		||||
  if (rgb[0] === '#') {
 | 
			
		||||
    return rgb
 | 
			
		||||
  }
 | 
			
		||||
  rgb = rgb.match(/\d+/g)
 | 
			
		||||
  return `#${((Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2])).toString(16)}`
 | 
			
		||||
const mixrgb = (a, b) => {
 | 
			
		||||
  return Object.keys(a).reduce((acc, k) => {
 | 
			
		||||
    acc[k] = (a[k] + b[k]) / 2
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  rgb2hex,
 | 
			
		||||
  hex2rgb,
 | 
			
		||||
  rgbstr2hex
 | 
			
		||||
  mixrgb,
 | 
			
		||||
  invert,
 | 
			
		||||
  getContrastRatio,
 | 
			
		||||
  alphaBlend
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								src/services/file_size_format/file_size_format.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/services/file_size_format/file_size_format.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
const fileSizeFormat = (num) => {
 | 
			
		||||
  var exponent
 | 
			
		||||
  var unit
 | 
			
		||||
  var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
 | 
			
		||||
  if (num < 1) {
 | 
			
		||||
    return num + ' ' + units[0]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
 | 
			
		||||
  num = (num / Math.pow(1024, exponent)).toFixed(2) * 1
 | 
			
		||||
  unit = units[exponent]
 | 
			
		||||
  return {num: num, unit: unit}
 | 
			
		||||
}
 | 
			
		||||
const fileSizeFormatService = {
 | 
			
		||||
  fileSizeFormat
 | 
			
		||||
}
 | 
			
		||||
export default fileSizeFormatService
 | 
			
		||||
							
								
								
									
										69
									
								
								src/services/push/push.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/services/push/push.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
import runtime from 'serviceworker-webpack-plugin/lib/runtime'
 | 
			
		||||
 | 
			
		||||
function urlBase64ToUint8Array (base64String) {
 | 
			
		||||
  const padding = '='.repeat((4 - base64String.length % 4) % 4)
 | 
			
		||||
  const base64 = (base64String + padding)
 | 
			
		||||
    .replace(/-/g, '+')
 | 
			
		||||
    .replace(/_/g, '/')
 | 
			
		||||
 | 
			
		||||
  const rawData = window.atob(base64)
 | 
			
		||||
  return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isPushSupported () {
 | 
			
		||||
  return 'serviceWorker' in navigator && 'PushManager' in window
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function registerServiceWorker () {
 | 
			
		||||
  return runtime.register()
 | 
			
		||||
    .catch((err) => console.error('Unable to register service worker.', err))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function subscribe (registration, isEnabled, vapidPublicKey) {
 | 
			
		||||
  if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config'))
 | 
			
		||||
  if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
 | 
			
		||||
 | 
			
		||||
  const subscribeOptions = {
 | 
			
		||||
    userVisibleOnly: true,
 | 
			
		||||
    applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
 | 
			
		||||
  }
 | 
			
		||||
  return registration.pushManager.subscribe(subscribeOptions)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendSubscriptionToBackEnd (subscription, token) {
 | 
			
		||||
  return window.fetch('/api/v1/push/subscription/', {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Content-Type': 'application/json',
 | 
			
		||||
      'Authorization': `Bearer ${token}`
 | 
			
		||||
    },
 | 
			
		||||
    body: JSON.stringify({
 | 
			
		||||
      subscription,
 | 
			
		||||
      data: {
 | 
			
		||||
        alerts: {
 | 
			
		||||
          follow: true,
 | 
			
		||||
          favourite: true,
 | 
			
		||||
          mention: true,
 | 
			
		||||
          reblog: true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
    .then((response) => {
 | 
			
		||||
      if (!response.ok) throw new Error('Bad status code from server.')
 | 
			
		||||
      return response.json()
 | 
			
		||||
    })
 | 
			
		||||
    .then((responseData) => {
 | 
			
		||||
      if (!responseData.id) throw new Error('Bad response from server.')
 | 
			
		||||
      return responseData
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function registerPushNotifications (isEnabled, vapidPublicKey, token) {
 | 
			
		||||
  if (isPushSupported()) {
 | 
			
		||||
    registerServiceWorker()
 | 
			
		||||
      .then((registration) => subscribe(registration, isEnabled, vapidPublicKey))
 | 
			
		||||
      .then((subscription) => sendSubscriptionToBackEnd(subscription, token))
 | 
			
		||||
      .catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import { times } from 'lodash'
 | 
			
		||||
import { rgb2hex, hex2rgb } from '../color_convert/color_convert.js'
 | 
			
		||||
import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
 | 
			
		||||
import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.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
 | 
			
		||||
| 
						 | 
				
			
			@ -39,8 +40,6 @@ const setStyle = (href, commit) => {
 | 
			
		|||
      colors[name] = color
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    commit('setOption', { name: 'colors', value: colors })
 | 
			
		||||
 | 
			
		||||
    body.removeChild(baseEl)
 | 
			
		||||
 | 
			
		||||
    const styleEl = document.createElement('style')
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +52,27 @@ const setStyle = (href, commit) => {
 | 
			
		|||
  cssEl.addEventListener('load', setDynamic)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const setColors = (col, commit) => {
 | 
			
		||||
const rgb2rgba = function (rgba) {
 | 
			
		||||
  return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getTextColor = function (bg, text, preserve) {
 | 
			
		||||
  const bgIsLight = convert(bg).hsl.l > 50
 | 
			
		||||
  const textIsLight = convert(text).hsl.l > 50
 | 
			
		||||
 | 
			
		||||
  if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
 | 
			
		||||
    const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
 | 
			
		||||
    const result = Object.assign(base, invertLightness(text).rgb)
 | 
			
		||||
    if (!preserve && getContrastRatio(bg, result) < 4.5) {
 | 
			
		||||
      return contrastRatio(bg, text).rgb
 | 
			
		||||
    }
 | 
			
		||||
    return result
 | 
			
		||||
  }
 | 
			
		||||
  return text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const applyTheme = (input, commit) => {
 | 
			
		||||
  const { rules, theme } = generatePreset(input)
 | 
			
		||||
  const head = document.head
 | 
			
		||||
  const body = document.body
 | 
			
		||||
  body.style.display = 'none'
 | 
			
		||||
| 
						 | 
				
			
			@ -62,56 +81,411 @@ const setColors = (col, commit) => {
 | 
			
		|||
  head.appendChild(styleEl)
 | 
			
		||||
  const styleSheet = styleEl.sheet
 | 
			
		||||
 | 
			
		||||
  const isDark = (col.text.r + col.text.g + col.text.b) > (col.bg.r + col.bg.g + col.bg.b)
 | 
			
		||||
  let colors = {}
 | 
			
		||||
  let radii = {}
 | 
			
		||||
 | 
			
		||||
  const mod = isDark ? -10 : 10
 | 
			
		||||
 | 
			
		||||
  colors.bg = rgb2hex(col.bg.r, col.bg.g, col.bg.b)                         // background
 | 
			
		||||
  colors.lightBg = rgb2hex((col.bg.r + col.fg.r) / 2, (col.bg.g + col.fg.g) / 2, (col.bg.b + col.fg.b) / 2) // hilighted bg
 | 
			
		||||
  colors.btn = rgb2hex(col.fg.r, col.fg.g, col.fg.b)                         // panels & buttons
 | 
			
		||||
  colors.input = `rgba(${col.fg.r}, ${col.fg.g}, ${col.fg.b}, .5)`
 | 
			
		||||
  colors.border = rgb2hex(col.fg.r - mod, col.fg.g - mod, col.fg.b - mod)       // borders
 | 
			
		||||
  colors.faint = `rgba(${col.text.r}, ${col.text.g}, ${col.text.b}, .5)`
 | 
			
		||||
  colors.fg = rgb2hex(col.text.r, col.text.g, col.text.b)                   // text
 | 
			
		||||
  colors.lightFg = rgb2hex(col.text.r - mod * 5, col.text.g - mod * 5, col.text.b - mod * 5) // strong text
 | 
			
		||||
 | 
			
		||||
  colors['base07'] = rgb2hex(col.text.r - mod * 2, col.text.g - mod * 2, col.text.b - mod * 2)
 | 
			
		||||
 | 
			
		||||
  colors.link = rgb2hex(col.link.r, col.link.g, col.link.b)                   // links
 | 
			
		||||
  colors.icon = rgb2hex((col.bg.r + col.text.r) / 2, (col.bg.g + col.text.g) / 2, (col.bg.b + col.text.b) / 2) // icons
 | 
			
		||||
 | 
			
		||||
  colors.cBlue = col.cBlue && rgb2hex(col.cBlue.r, col.cBlue.g, col.cBlue.b)
 | 
			
		||||
  colors.cRed = col.cRed && rgb2hex(col.cRed.r, col.cRed.g, col.cRed.b)
 | 
			
		||||
  colors.cGreen = col.cGreen && rgb2hex(col.cGreen.r, col.cGreen.g, col.cGreen.b)
 | 
			
		||||
  colors.cOrange = col.cOrange && rgb2hex(col.cOrange.r, col.cOrange.g, col.cOrange.b)
 | 
			
		||||
 | 
			
		||||
  colors.cAlertRed = col.cRed && `rgba(${col.cRed.r}, ${col.cRed.g}, ${col.cRed.b}, .5)`
 | 
			
		||||
 | 
			
		||||
  radii.btnRadius = col.btnRadius
 | 
			
		||||
  radii.inputRadius = col.inputRadius
 | 
			
		||||
  radii.panelRadius = col.panelRadius
 | 
			
		||||
  radii.avatarRadius = col.avatarRadius
 | 
			
		||||
  radii.avatarAltRadius = col.avatarAltRadius
 | 
			
		||||
  radii.tooltipRadius = col.tooltipRadius
 | 
			
		||||
  radii.attachmentRadius = col.attachmentRadius
 | 
			
		||||
 | 
			
		||||
  styleSheet.toString()
 | 
			
		||||
  styleSheet.insertRule(`body { ${Object.entries(colors).filter(([k, v]) => v).map(([k, v]) => `--${k}: ${v}`).join(';')} }`, 'index-max')
 | 
			
		||||
  styleSheet.insertRule(`body { ${Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}: ${v}px`).join(';')} }`, 'index-max')
 | 
			
		||||
  styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
 | 
			
		||||
  styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
 | 
			
		||||
  styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
 | 
			
		||||
  styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
 | 
			
		||||
  body.style.display = 'initial'
 | 
			
		||||
 | 
			
		||||
  commit('setOption', { name: 'colors', value: colors })
 | 
			
		||||
  commit('setOption', { name: 'radii', value: radii })
 | 
			
		||||
  commit('setOption', { name: 'customTheme', value: col })
 | 
			
		||||
  // 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) => {
 | 
			
		||||
  if (input.length === 0) {
 | 
			
		||||
    return 'none'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return input
 | 
			
		||||
    .filter(_ => usesDropShadow ? _.inset : _)
 | 
			
		||||
    .map((shad) => [
 | 
			
		||||
      shad.x,
 | 
			
		||||
      shad.y,
 | 
			
		||||
      shad.blur,
 | 
			
		||||
      shad.spread
 | 
			
		||||
    ].map(_ => _ + 'px').concat([
 | 
			
		||||
      getCssColor(shad.color, shad.alpha),
 | 
			
		||||
      shad.inset ? 'inset' : ''
 | 
			
		||||
    ]).join(' ')).join(', ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getCssShadowFilter = (input) => {
 | 
			
		||||
  if (input.length === 0) {
 | 
			
		||||
    return 'none'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return input
 | 
			
		||||
  // drop-shadow doesn't support inset or spread
 | 
			
		||||
    .filter((shad) => !shad.inset && Number(shad.spread) === 0)
 | 
			
		||||
    .map((shad) => [
 | 
			
		||||
      shad.x,
 | 
			
		||||
      shad.y,
 | 
			
		||||
      // drop-shadow's blur is twice as strong compared to box-shadow
 | 
			
		||||
      shad.blur / 2
 | 
			
		||||
    ].map(_ => _ + 'px').concat([
 | 
			
		||||
      getCssColor(shad.color, shad.alpha)
 | 
			
		||||
    ]).join(' '))
 | 
			
		||||
    .map(_ => `drop-shadow(${_})`)
 | 
			
		||||
    .join(' ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getCssColor = (input, a) => {
 | 
			
		||||
  let rgb = {}
 | 
			
		||||
  if (typeof input === 'object') {
 | 
			
		||||
    rgb = input
 | 
			
		||||
  } else if (typeof input === 'string') {
 | 
			
		||||
    if (input.startsWith('#')) {
 | 
			
		||||
      rgb = hex2rgb(input)
 | 
			
		||||
    } else if (input.startsWith('--')) {
 | 
			
		||||
      return `var(${input})`
 | 
			
		||||
    } else {
 | 
			
		||||
      return input
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return rgb2rgba({ ...rgb, a })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generateColors = (input) => {
 | 
			
		||||
  const colors = {}
 | 
			
		||||
  const opacity = Object.assign({
 | 
			
		||||
    alert: 0.5,
 | 
			
		||||
    input: 0.5,
 | 
			
		||||
    faint: 0.5
 | 
			
		||||
  }, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => {
 | 
			
		||||
    if (typeof v !== 'undefined') {
 | 
			
		||||
      acc[k] = v
 | 
			
		||||
    }
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {}))
 | 
			
		||||
  const col = Object.entries(input.colors || input).reduce((acc, [k, v]) => {
 | 
			
		||||
    if (typeof v === 'object') {
 | 
			
		||||
      acc[k] = v
 | 
			
		||||
    } else {
 | 
			
		||||
      acc[k] = hex2rgb(v)
 | 
			
		||||
    }
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {})
 | 
			
		||||
 | 
			
		||||
  const isLightOnDark = convert(col.bg).hsl.l < convert(col.text).hsl.l
 | 
			
		||||
  const mod = isLightOnDark ? 1 : -1
 | 
			
		||||
 | 
			
		||||
  colors.text = col.text
 | 
			
		||||
  colors.lightText = brightness(20 * mod, colors.text).rgb
 | 
			
		||||
  colors.link = col.link
 | 
			
		||||
  colors.faint = col.faint || Object.assign({}, col.text)
 | 
			
		||||
 | 
			
		||||
  colors.bg = col.bg
 | 
			
		||||
  colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb
 | 
			
		||||
 | 
			
		||||
  colors.fg = col.fg
 | 
			
		||||
  colors.fgText = col.fgText || getTextColor(colors.fg, colors.text)
 | 
			
		||||
  colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true)
 | 
			
		||||
 | 
			
		||||
  colors.border = col.border || brightness(2 * mod, colors.fg).rgb
 | 
			
		||||
 | 
			
		||||
  colors.btn = col.btn || Object.assign({}, col.fg)
 | 
			
		||||
  colors.btnText = col.btnText || getTextColor(colors.btn, colors.fgText)
 | 
			
		||||
 | 
			
		||||
  colors.input = col.input || Object.assign({}, col.fg)
 | 
			
		||||
  colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText)
 | 
			
		||||
 | 
			
		||||
  colors.panel = col.panel || Object.assign({}, col.fg)
 | 
			
		||||
  colors.panelText = col.panelText || getTextColor(colors.panel, colors.fgText)
 | 
			
		||||
  colors.panelLink = col.panelLink || getTextColor(colors.panel, colors.fgLink)
 | 
			
		||||
  colors.panelFaint = col.panelFaint || getTextColor(colors.panel, colors.faint)
 | 
			
		||||
 | 
			
		||||
  colors.topBar = col.topBar || Object.assign({}, col.fg)
 | 
			
		||||
  colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText)
 | 
			
		||||
  colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink)
 | 
			
		||||
 | 
			
		||||
  colors.faintLink = col.faintLink || Object.assign({}, col.link)
 | 
			
		||||
 | 
			
		||||
  colors.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.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
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (k === 'faint') {
 | 
			
		||||
      colors[k + 'Link'].a = v
 | 
			
		||||
      colors['panelFaint'].a = v
 | 
			
		||||
    }
 | 
			
		||||
    if (k === 'bg') {
 | 
			
		||||
      colors['lightBg'].a = v
 | 
			
		||||
    }
 | 
			
		||||
    if (colors[k]) {
 | 
			
		||||
      colors[k].a = v
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error('Wrong key ' + k)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const htmlColors = Object.entries(colors)
 | 
			
		||||
        .reduce((acc, [k, v]) => {
 | 
			
		||||
          if (!v) return acc
 | 
			
		||||
          acc.solid[k] = rgb2hex(v)
 | 
			
		||||
          acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v)
 | 
			
		||||
          return acc
 | 
			
		||||
        }, { complete: {}, solid: {} })
 | 
			
		||||
  return {
 | 
			
		||||
    rules: {
 | 
			
		||||
      colors: Object.entries(htmlColors.complete)
 | 
			
		||||
        .filter(([k, v]) => v)
 | 
			
		||||
        .map(([k, v]) => `--${k}: ${v}`)
 | 
			
		||||
        .join(';')
 | 
			
		||||
    },
 | 
			
		||||
    theme: {
 | 
			
		||||
      colors: htmlColors.solid,
 | 
			
		||||
      opacity
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generateRadii = (input) => {
 | 
			
		||||
  let inputRadii = input.radii || {}
 | 
			
		||||
  // v1 -> v2
 | 
			
		||||
  if (typeof input.btnRadius !== 'undefined') {
 | 
			
		||||
    inputRadii = Object
 | 
			
		||||
      .entries(input)
 | 
			
		||||
      .filter(([k, v]) => k.endsWith('Radius'))
 | 
			
		||||
      .reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {})
 | 
			
		||||
  }
 | 
			
		||||
  const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => {
 | 
			
		||||
    acc[k] = v
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {
 | 
			
		||||
    btn: 4,
 | 
			
		||||
    input: 4,
 | 
			
		||||
    checkbox: 2,
 | 
			
		||||
    panel: 10,
 | 
			
		||||
    avatar: 5,
 | 
			
		||||
    avatarAlt: 50,
 | 
			
		||||
    tooltip: 2,
 | 
			
		||||
    attachment: 5
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    rules: {
 | 
			
		||||
      radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
 | 
			
		||||
    },
 | 
			
		||||
    theme: {
 | 
			
		||||
      radii
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
      return acc
 | 
			
		||||
    }, acc[k])
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {
 | 
			
		||||
    interface: {
 | 
			
		||||
      family: 'sans-serif'
 | 
			
		||||
    },
 | 
			
		||||
    input: {
 | 
			
		||||
      family: 'inherit'
 | 
			
		||||
    },
 | 
			
		||||
    post: {
 | 
			
		||||
      family: 'inherit'
 | 
			
		||||
    },
 | 
			
		||||
    postCode: {
 | 
			
		||||
      family: 'monospace'
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    rules: {
 | 
			
		||||
      fonts: Object
 | 
			
		||||
        .entries(fonts)
 | 
			
		||||
        .filter(([k, v]) => v)
 | 
			
		||||
        .map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
 | 
			
		||||
    },
 | 
			
		||||
    theme: {
 | 
			
		||||
      fonts
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generateShadows = (input) => {
 | 
			
		||||
  const border = (top, shadow) => ({
 | 
			
		||||
    x: 0,
 | 
			
		||||
    y: top ? 1 : -1,
 | 
			
		||||
    blur: 0,
 | 
			
		||||
    spread: 0,
 | 
			
		||||
    color: shadow ? '#000000' : '#FFFFFF',
 | 
			
		||||
    alpha: 0.2,
 | 
			
		||||
    inset: true
 | 
			
		||||
  })
 | 
			
		||||
  const buttonInsetFakeBorders = [border(true, false), border(false, true)]
 | 
			
		||||
  const inputInsetFakeBorders = [border(true, true), border(false, false)]
 | 
			
		||||
  const hoverGlow = {
 | 
			
		||||
    x: 0,
 | 
			
		||||
    y: 0,
 | 
			
		||||
    blur: 4,
 | 
			
		||||
    spread: 0,
 | 
			
		||||
    color: '--faint',
 | 
			
		||||
    alpha: 1
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const shadows = {
 | 
			
		||||
    panel: [{
 | 
			
		||||
      x: 1,
 | 
			
		||||
      y: 1,
 | 
			
		||||
      blur: 4,
 | 
			
		||||
      spread: 0,
 | 
			
		||||
      color: '#000000',
 | 
			
		||||
      alpha: 0.6
 | 
			
		||||
    }],
 | 
			
		||||
    topBar: [{
 | 
			
		||||
      x: 0,
 | 
			
		||||
      y: 0,
 | 
			
		||||
      blur: 4,
 | 
			
		||||
      spread: 0,
 | 
			
		||||
      color: '#000000',
 | 
			
		||||
      alpha: 0.6
 | 
			
		||||
    }],
 | 
			
		||||
    popup: [{
 | 
			
		||||
      x: 2,
 | 
			
		||||
      y: 2,
 | 
			
		||||
      blur: 3,
 | 
			
		||||
      spread: 0,
 | 
			
		||||
      color: '#000000',
 | 
			
		||||
      alpha: 0.5
 | 
			
		||||
    }],
 | 
			
		||||
    avatar: [{
 | 
			
		||||
      x: 0,
 | 
			
		||||
      y: 1,
 | 
			
		||||
      blur: 8,
 | 
			
		||||
      spread: 0,
 | 
			
		||||
      color: '#000000',
 | 
			
		||||
      alpha: 0.7
 | 
			
		||||
    }],
 | 
			
		||||
    avatarStatus: [],
 | 
			
		||||
    panelHeader: [],
 | 
			
		||||
    button: [{
 | 
			
		||||
      x: 0,
 | 
			
		||||
      y: 0,
 | 
			
		||||
      blur: 2,
 | 
			
		||||
      spread: 0,
 | 
			
		||||
      color: '#000000',
 | 
			
		||||
      alpha: 1
 | 
			
		||||
    }, ...buttonInsetFakeBorders],
 | 
			
		||||
    buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
 | 
			
		||||
    buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
 | 
			
		||||
    input: [...inputInsetFakeBorders, {
 | 
			
		||||
      x: 0,
 | 
			
		||||
      y: 0,
 | 
			
		||||
      blur: 2,
 | 
			
		||||
      inset: true,
 | 
			
		||||
      spread: 0,
 | 
			
		||||
      color: '#000000',
 | 
			
		||||
      alpha: 1
 | 
			
		||||
    }],
 | 
			
		||||
    ...(input.shadows || {})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    rules: {
 | 
			
		||||
      shadows: Object
 | 
			
		||||
        .entries(shadows)
 | 
			
		||||
      // TODO for v2.1: 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)}`,
 | 
			
		||||
          `--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
 | 
			
		||||
          `--${k}ShadowInset: ${getCssShadow(v, true)}`
 | 
			
		||||
        ].join(';'))
 | 
			
		||||
        .join(';')
 | 
			
		||||
    },
 | 
			
		||||
    theme: {
 | 
			
		||||
      shadows
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const composePreset = (colors, radii, shadows, fonts) => {
 | 
			
		||||
  return {
 | 
			
		||||
    rules: {
 | 
			
		||||
      ...shadows.rules,
 | 
			
		||||
      ...colors.rules,
 | 
			
		||||
      ...radii.rules,
 | 
			
		||||
      ...fonts.rules
 | 
			
		||||
    },
 | 
			
		||||
    theme: {
 | 
			
		||||
      ...shadows.theme,
 | 
			
		||||
      ...colors.theme,
 | 
			
		||||
      ...radii.theme,
 | 
			
		||||
      ...fonts.theme
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generatePreset = (input) => {
 | 
			
		||||
  const shadows = generateShadows(input)
 | 
			
		||||
  const colors = generateColors(input)
 | 
			
		||||
  const radii = generateRadii(input)
 | 
			
		||||
  const fonts = generateFonts(input)
 | 
			
		||||
 | 
			
		||||
  return composePreset(colors, radii, shadows, fonts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getThemes = () => {
 | 
			
		||||
  return window.fetch('/static/styles.json')
 | 
			
		||||
    .then((data) => data.json())
 | 
			
		||||
    .then((themes) => {
 | 
			
		||||
      return Promise.all(Object.entries(themes).map(([k, v]) => {
 | 
			
		||||
        if (typeof v === 'object') {
 | 
			
		||||
          return Promise.resolve([k, v])
 | 
			
		||||
        } else if (typeof v === 'string') {
 | 
			
		||||
          return window.fetch(v)
 | 
			
		||||
            .then((data) => data.json())
 | 
			
		||||
            .then((theme) => {
 | 
			
		||||
              return [k, theme]
 | 
			
		||||
            })
 | 
			
		||||
            .catch((e) => {
 | 
			
		||||
              console.error(e)
 | 
			
		||||
              return []
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
      }))
 | 
			
		||||
    })
 | 
			
		||||
    .then((promises) => {
 | 
			
		||||
      return promises
 | 
			
		||||
        .filter(([k, v]) => v)
 | 
			
		||||
        .reduce((acc, [k, v]) => {
 | 
			
		||||
          acc[k] = v
 | 
			
		||||
          return acc
 | 
			
		||||
        }, {})
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const setPreset = (val, commit) => {
 | 
			
		||||
  window.fetch('/static/styles.json')
 | 
			
		||||
    .then((data) => data.json())
 | 
			
		||||
    .then((themes) => {
 | 
			
		||||
      const theme = themes[val] ? themes[val] : themes['pleroma-dark']
 | 
			
		||||
  getThemes().then((themes) => {
 | 
			
		||||
    const theme = themes[val] ? themes[val] : themes['pleroma-dark']
 | 
			
		||||
    const isV1 = Array.isArray(theme)
 | 
			
		||||
    const data = isV1 ? {} : theme.theme
 | 
			
		||||
 | 
			
		||||
    if (isV1) {
 | 
			
		||||
      const bgRgb = hex2rgb(theme[1])
 | 
			
		||||
      const fgRgb = hex2rgb(theme[2])
 | 
			
		||||
      const textRgb = hex2rgb(theme[3])
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +496,7 @@ const setPreset = (val, commit) => {
 | 
			
		|||
      const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
 | 
			
		||||
      const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
 | 
			
		||||
 | 
			
		||||
      const col = {
 | 
			
		||||
      data.colors = {
 | 
			
		||||
        bg: bgRgb,
 | 
			
		||||
        fg: fgRgb,
 | 
			
		||||
        text: textRgb,
 | 
			
		||||
| 
						 | 
				
			
			@ -132,23 +506,32 @@ const setPreset = (val, commit) => {
 | 
			
		|||
        cGreen: cGreenRgb,
 | 
			
		||||
        cOrange: cOrangeRgb
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      // 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) {
 | 
			
		||||
        setColors(col, commit)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    // 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)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyleSetter = {
 | 
			
		||||
export {
 | 
			
		||||
  setStyle,
 | 
			
		||||
  setPreset,
 | 
			
		||||
  setColors
 | 
			
		||||
  applyTheme,
 | 
			
		||||
  getTextColor,
 | 
			
		||||
  generateColors,
 | 
			
		||||
  generateRadii,
 | 
			
		||||
  generateShadows,
 | 
			
		||||
  generateFonts,
 | 
			
		||||
  generatePreset,
 | 
			
		||||
  getThemes,
 | 
			
		||||
  composePreset,
 | 
			
		||||
  getCssShadow,
 | 
			
		||||
  getCssShadowFilter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default StyleSetter
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ const highlightStyle = (prefs) => {
 | 
			
		|||
  if (type === 'striped') {
 | 
			
		||||
    return {
 | 
			
		||||
      backgroundImage: [
 | 
			
		||||
        'repeating-linear-gradient(-45deg,',
 | 
			
		||||
        'repeating-linear-gradient(135deg,',
 | 
			
		||||
        `${tintColor} ,`,
 | 
			
		||||
        `${tintColor} 20px,`,
 | 
			
		||||
        `${tintColor2} 20px,`,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										38
									
								
								src/sw.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/sw.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
/* eslint-env serviceworker */
 | 
			
		||||
 | 
			
		||||
import localForage from 'localforage'
 | 
			
		||||
 | 
			
		||||
function isEnabled () {
 | 
			
		||||
  return localForage.getItem('vuex-lz')
 | 
			
		||||
    .then(data => data.config.webPushNotifications)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getWindowClients () {
 | 
			
		||||
  return clients.matchAll({ includeUncontrolled: true })
 | 
			
		||||
    .then((clientList) => clientList.filter(({ type }) => type === 'window'))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
self.addEventListener('push', (event) => {
 | 
			
		||||
  if (event.data) {
 | 
			
		||||
    event.waitUntil(isEnabled().then((isEnabled) => {
 | 
			
		||||
      return isEnabled && getWindowClients().then((list) => {
 | 
			
		||||
        const data = event.data.json()
 | 
			
		||||
 | 
			
		||||
        if (list.length === 0) return self.registration.showNotification(data.title, data)
 | 
			
		||||
      })
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
self.addEventListener('notificationclick', (event) => {
 | 
			
		||||
  event.notification.close()
 | 
			
		||||
 | 
			
		||||
  event.waitUntil(getWindowClients().then((list) => {
 | 
			
		||||
    for (var i = 0; i < list.length; i++) {
 | 
			
		||||
      var client = list[i]
 | 
			
		||||
      if (client.url === '/' && 'focus' in client) { return client.focus() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (clients.openWindow) return clients.openWindow('/')
 | 
			
		||||
  }))
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -66,12 +66,6 @@
 | 
			
		|||
      "code": 61925,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "1a5cfa186647e8c929c2b17b9fc4dac1",
 | 
			
		||||
      "css": "plus-squared",
 | 
			
		||||
      "code": 59398,
 | 
			
		||||
      "src": "font-awesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "e99461abfef3923546da8d745372c995",
 | 
			
		||||
      "css": "cog",
 | 
			
		||||
| 
						 | 
				
			
			@ -197,6 +191,36 @@
 | 
			
		|||
      "css": "search",
 | 
			
		||||
      "code": 59398,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "ca90da02d2c6a3183f2458e4dc416285",
 | 
			
		||||
      "css": "adjust",
 | 
			
		||||
      "code": 59414,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "5e2ab018e3044337bcef5f7e94098ea1",
 | 
			
		||||
      "css": "thumbs-up-alt",
 | 
			
		||||
      "code": 61796,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "c76b7947c957c9b78b11741173c8349b",
 | 
			
		||||
      "css": "attention",
 | 
			
		||||
      "code": 59412,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "1a5cfa186647e8c929c2b17b9fc4dac1",
 | 
			
		||||
      "css": "plus-squared",
 | 
			
		||||
      "code": 61694,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "44e04715aecbca7f266a17d5a7863c68",
 | 
			
		||||
      "css": "plus",
 | 
			
		||||
      "code": 59413,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								static/font/css/fontello-codes.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								static/font/css/fontello-codes.css
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -6,7 +6,6 @@
 | 
			
		|||
.icon-retweet:before { content: '\e804'; } /* '' */
 | 
			
		||||
.icon-eye-off:before { content: '\e805'; } /* '' */
 | 
			
		||||
.icon-search:before { content: '\e806'; } /* '' */
 | 
			
		||||
.icon-plus-squared:before { content: '\e806'; } /* '' */
 | 
			
		||||
.icon-cog:before { content: '\e807'; } /* '' */
 | 
			
		||||
.icon-logout:before { content: '\e808'; } /* '' */
 | 
			
		||||
.icon-down-open:before { content: '\e809'; } /* '' */
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +19,9 @@
 | 
			
		|||
.icon-lock:before { content: '\e811'; } /* '' */
 | 
			
		||||
.icon-globe:before { content: '\e812'; } /* '' */
 | 
			
		||||
.icon-brush:before { content: '\e813'; } /* '' */
 | 
			
		||||
.icon-attention:before { content: '\e814'; } /* '' */
 | 
			
		||||
.icon-plus:before { content: '\e815'; } /* '' */
 | 
			
		||||
.icon-adjust:before { content: '\e816'; } /* '' */
 | 
			
		||||
.icon-spin3:before { content: '\e832'; } /* '' */
 | 
			
		||||
.icon-spin4:before { content: '\e834'; } /* '' */
 | 
			
		||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +29,9 @@
 | 
			
		|||
.icon-menu:before { content: '\f0c9'; } /* '' */
 | 
			
		||||
.icon-mail-alt:before { content: '\f0e0'; } /* '' */
 | 
			
		||||
.icon-comment-empty:before { content: '\f0e5'; } /* '' */
 | 
			
		||||
.icon-plus-squared:before { content: '\f0fe'; } /* '' */
 | 
			
		||||
.icon-reply:before { content: '\f112'; } /* '' */
 | 
			
		||||
.icon-lock-open-alt:before { content: '\f13e'; } /* '' */
 | 
			
		||||
.icon-thumbs-up-alt:before { content: '\f164'; } /* '' */
 | 
			
		||||
.icon-binoculars:before { content: '\f1e5'; } /* '' */
 | 
			
		||||
.icon-user-plus:before { content: '\f234'; } /* '' */
 | 
			
		||||
							
								
								
									
										18
									
								
								static/font/css/fontello-embedded.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								static/font/css/fontello-embedded.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										6
									
								
								static/font/css/fontello-ie7-codes.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								static/font/css/fontello-ie7-codes.css
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -6,7 +6,6 @@
 | 
			
		|||
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +19,9 @@
 | 
			
		|||
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +29,9 @@
 | 
			
		|||
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
							
								
								
									
										6
									
								
								static/font/css/fontello-ie7.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								static/font/css/fontello-ie7.css
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -17,7 +17,6 @@
 | 
			
		|||
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +30,9 @@
 | 
			
		|||
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +40,9 @@
 | 
			
		|||
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
 | 
			
		||||
							
								
								
									
										20
									
								
								static/font/css/fontello.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								static/font/css/fontello.css
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
@font-face {
 | 
			
		||||
  font-family: 'fontello';
 | 
			
		||||
  src: url('../font/fontello.eot?43308757');
 | 
			
		||||
  src: url('../font/fontello.eot?43308757#iefix') format('embedded-opentype'),
 | 
			
		||||
       url('../font/fontello.woff2?43308757') format('woff2'),
 | 
			
		||||
       url('../font/fontello.woff?43308757') format('woff'),
 | 
			
		||||
       url('../font/fontello.ttf?43308757') format('truetype'),
 | 
			
		||||
       url('../font/fontello.svg?43308757#fontello') format('svg');
 | 
			
		||||
  src: url('../font/fontello.eot?97335193');
 | 
			
		||||
  src: url('../font/fontello.eot?97335193#iefix') format('embedded-opentype'),
 | 
			
		||||
       url('../font/fontello.woff2?97335193') format('woff2'),
 | 
			
		||||
       url('../font/fontello.woff?97335193') format('woff'),
 | 
			
		||||
       url('../font/fontello.ttf?97335193') format('truetype'),
 | 
			
		||||
       url('../font/fontello.svg?97335193#fontello') format('svg');
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
  font-style: normal;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +15,7 @@
 | 
			
		|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
 | 
			
		||||
  @font-face {
 | 
			
		||||
    font-family: 'fontello';
 | 
			
		||||
    src: url('../font/fontello.svg?43308757#fontello') format('svg');
 | 
			
		||||
    src: url('../font/fontello.svg?97335193#fontello') format('svg');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,6 @@
 | 
			
		|||
.icon-retweet:before { content: '\e804'; } /* '' */
 | 
			
		||||
.icon-eye-off:before { content: '\e805'; } /* '' */
 | 
			
		||||
.icon-search:before { content: '\e806'; } /* '' */
 | 
			
		||||
.icon-plus-squared:before { content: '\e806'; } /* '' */
 | 
			
		||||
.icon-cog:before { content: '\e807'; } /* '' */
 | 
			
		||||
.icon-logout:before { content: '\e808'; } /* '' */
 | 
			
		||||
.icon-down-open:before { content: '\e809'; } /* '' */
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +75,9 @@
 | 
			
		|||
.icon-lock:before { content: '\e811'; } /* '' */
 | 
			
		||||
.icon-globe:before { content: '\e812'; } /* '' */
 | 
			
		||||
.icon-brush:before { content: '\e813'; } /* '' */
 | 
			
		||||
.icon-attention:before { content: '\e814'; } /* '' */
 | 
			
		||||
.icon-plus:before { content: '\e815'; } /* '' */
 | 
			
		||||
.icon-adjust:before { content: '\e816'; } /* '' */
 | 
			
		||||
.icon-spin3:before { content: '\e832'; } /* '' */
 | 
			
		||||
.icon-spin4:before { content: '\e834'; } /* '' */
 | 
			
		||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +85,9 @@
 | 
			
		|||
.icon-menu:before { content: '\f0c9'; } /* '' */
 | 
			
		||||
.icon-mail-alt:before { content: '\f0e0'; } /* '' */
 | 
			
		||||
.icon-comment-empty:before { content: '\f0e5'; } /* '' */
 | 
			
		||||
.icon-plus-squared:before { content: '\f0fe'; } /* '' */
 | 
			
		||||
.icon-reply:before { content: '\f112'; } /* '' */
 | 
			
		||||
.icon-lock-open-alt:before { content: '\f13e'; } /* '' */
 | 
			
		||||
.icon-thumbs-up-alt:before { content: '\f164'; } /* '' */
 | 
			
		||||
.icon-binoculars:before { content: '\f1e5'; } /* '' */
 | 
			
		||||
.icon-user-plus:before { content: '\f234'; } /* '' */
 | 
			
		||||
| 
						 | 
				
			
			@ -229,11 +229,11 @@ body {
 | 
			
		|||
}
 | 
			
		||||
@font-face {
 | 
			
		||||
      font-family: 'fontello';
 | 
			
		||||
      src: url('./font/fontello.eot?75371834');
 | 
			
		||||
      src: url('./font/fontello.eot?75371834#iefix') format('embedded-opentype'),
 | 
			
		||||
           url('./font/fontello.woff?75371834') format('woff'),
 | 
			
		||||
           url('./font/fontello.ttf?75371834') format('truetype'),
 | 
			
		||||
           url('./font/fontello.svg?75371834#fontello') format('svg');
 | 
			
		||||
      src: url('./font/fontello.eot?32716429');
 | 
			
		||||
      src: url('./font/fontello.eot?32716429#iefix') format('embedded-opentype'),
 | 
			
		||||
           url('./font/fontello.woff?32716429') format('woff'),
 | 
			
		||||
           url('./font/fontello.ttf?32716429') format('truetype'),
 | 
			
		||||
           url('./font/fontello.svg?32716429#fontello') format('svg');
 | 
			
		||||
      font-weight: normal;
 | 
			
		||||
      font-style: normal;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -307,41 +307,47 @@ body {
 | 
			
		|||
        <div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-retweet"></i> <span class="i-name">icon-retweet</span><span class="i-code">0xe804</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe805"><i class="demo-icon icon-eye-off"></i> <span class="i-name">icon-eye-off</span><span class="i-code">0xe805</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-search"></i> <span class="i-name">icon-search</span><span class="i-code">0xe806</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xe806</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-cog"></i> <span class="i-name">icon-cog</span><span class="i-code">0xe807</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-cog"></i> <span class="i-name">icon-cog</span><span class="i-code">0xe807</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe808"><i class="demo-icon icon-logout"></i> <span class="i-name">icon-logout</span><span class="i-code">0xe808</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe809"><i class="demo-icon icon-down-open"></i> <span class="i-name">icon-down-open</span><span class="i-code">0xe809</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80a"><i class="demo-icon icon-attach"></i> <span class="i-name">icon-attach</span><span class="i-code">0xe80a</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80b"><i class="demo-icon icon-picture"></i> <span class="i-name">icon-picture</span><span class="i-code">0xe80b</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80b"><i class="demo-icon icon-picture"></i> <span class="i-name">icon-picture</span><span class="i-code">0xe80b</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80c"><i class="demo-icon icon-video"></i> <span class="i-name">icon-video</span><span class="i-code">0xe80c</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80d"><i class="demo-icon icon-right-open"></i> <span class="i-name">icon-right-open</span><span class="i-code">0xe80d</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80e"><i class="demo-icon icon-left-open"></i> <span class="i-name">icon-left-open</span><span class="i-code">0xe80e</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-up-open"></i> <span class="i-name">icon-up-open</span><span class="i-code">0xe80f</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-up-open"></i> <span class="i-name">icon-up-open</span><span class="i-code">0xe80f</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-bell"></i> <span class="i-name">icon-bell</span><span class="i-code">0xe810</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-lock"></i> <span class="i-name">icon-lock</span><span class="i-code">0xe811</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-globe"></i> <span class="i-name">icon-globe</span><span class="i-code">0xe812</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-brush"></i> <span class="i-name">icon-brush</span><span class="i-code">0xe813</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-brush"></i> <span class="i-name">icon-brush</span><span class="i-code">0xe813</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-attention"></i> <span class="i-name">icon-attention</span><span class="i-code">0xe814</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-plus"></i> <span class="i-name">icon-plus</span><span class="i-code">0xe815</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-adjust"></i> <span class="i-name">icon-adjust</span><span class="i-code">0xe816</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext"></i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt"></i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty"></i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt"></i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty"></i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt"></i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
 | 
			
		||||
        <div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -20,8 +20,6 @@
 | 
			
		|||
 | 
			
		||||
<glyph glyph-name="search" unicode="" d="M643 393q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="plus-squared" unicode="" d="M714 321v72q0 14-10 25t-25 10h-179v179q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-179h-178q-15 0-25-10t-11-25v-72q0-14 11-25t25-10h178v-179q0-14 11-25t25-11h71q15 0 25 11t11 25v179h179q14 0 25 10t10 25z m143 304v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="cog" unicode="" d="M571 357q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="logout" unicode="" d="M357 53q0-2 1-11t0-14-2-14-5-10-12-4h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +46,12 @@
 | 
			
		|||
 | 
			
		||||
<glyph glyph-name="brush" unicode="" d="M464 209q0-124-87-212t-210-87q-81 0-149 40 68 39 109 108t40 151q0 61 44 105t105 44 105-44 43-105z m415 562q32-32 32-79t-33-79l-318-318q-20 55-61 97t-97 62l318 318q32 32 79 32t80-33z" horiz-adv-x="928" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="attention" unicode="" d="M571 90v106q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-106q0-8 5-13t12-6h108q7 0 12 6t5 13z m-1 208l10 257q0 6-5 10-7 6-14 6h-122q-6 0-14-6-5-4-5-12l9-255q0-5 6-9t13-3h103q8 0 14 3t5 9z m-7 522l428-786q20-35-1-70-9-17-26-26t-35-10h-858q-18 0-35 10t-26 26q-21 35-1 70l429 786q9 17 26 27t36 10 36-10 27-27z" horiz-adv-x="1000" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="plus" unicode="" d="M786 446v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q23 0 38-16t16-38z" horiz-adv-x="785.7" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="adjust" unicode="" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
 | 
			
		||||
| 
						 | 
				
			
			@ -62,10 +66,14 @@
 | 
			
		|||
 | 
			
		||||
<glyph glyph-name="comment-empty" unicode="" d="M500 643q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="plus-squared" unicode="" d="M714 321v72q0 14-10 25t-25 10h-179v179q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-179h-178q-15 0-25-10t-11-25v-72q0-14 11-25t25-10h178v-179q0-14 11-25t25-11h71q15 0 25 11t11 25v179h179q14 0 25 10t10 25z m143 304v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="reply" unicode="" d="M1000 232q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="lock-open-alt" unicode="" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="thumbs-up-alt" unicode="" d="M143 107q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="binoculars" unicode="" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
 | 
			
		||||
 | 
			
		||||
<glyph glyph-name="user-plus" unicode="" d="M393 357q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB  | 
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -5,5 +5,11 @@
 | 
			
		|||
  "bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
 | 
			
		||||
  "ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
 | 
			
		||||
  "monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
 | 
			
		||||
  "mammal": [ "Mammal", "#272c37", "#444b5d", "#f8f8f8", "#9bacc8", "#7f3142", "#2bd850", "#2b90d9", "#ca8f04" ]
 | 
			
		||||
  "mammal": [ "Mammal", "#272c37", "#444b5d", "#f8f8f8", "#9bacc8", "#7f3142", "#2bd850", "#2b90d9", "#ca8f04" ],
 | 
			
		||||
 | 
			
		||||
  "redmond-xx": "/static/themes/redmond-xx.json",
 | 
			
		||||
  "redmond-xx-se": "/static/themes/redmond-xx-se.json",
 | 
			
		||||
  "redmond-xxi": "/static/themes/redmond-xxi.json",
 | 
			
		||||
  "breezy-dark": "/static/themes/breezy-dark.json",
 | 
			
		||||
  "breezy-light": "/static/themes/breezy-light.json"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										139
									
								
								static/themes/breezy-dark.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								static/themes/breezy-dark.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,139 @@
 | 
			
		|||
{
 | 
			
		||||
  "_pleroma_theme_version": 2,
 | 
			
		||||
  "name": "Breezy Dark (beta)",
 | 
			
		||||
  "theme": {
 | 
			
		||||
    "shadows": {
 | 
			
		||||
      "panel": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "6",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": 0.6
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "button": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "#ffffff",
 | 
			
		||||
          "alpha": "0.15",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "1",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": false
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "panelHeader": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "40",
 | 
			
		||||
          "blur": "40",
 | 
			
		||||
          "spread": "-40",
 | 
			
		||||
          "inset": true,
 | 
			
		||||
          "color": "#ffffff",
 | 
			
		||||
          "alpha": "0.1"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonHover": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "--link",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "1",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": false
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonPressed": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": 0,
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "50",
 | 
			
		||||
          "color": "--faint",
 | 
			
		||||
          "alpha": 1,
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "#ffffff",
 | 
			
		||||
          "alpha": 0.2,
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": false
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "input": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "0.2",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "fonts": {},
 | 
			
		||||
    "opacity": {
 | 
			
		||||
      "input": "1",
 | 
			
		||||
      "panel": "0"
 | 
			
		||||
    },
 | 
			
		||||
    "colors": {
 | 
			
		||||
      "bg": "#31363b",
 | 
			
		||||
      "text": "#eff0f1",
 | 
			
		||||
      "link": "#3daee9",
 | 
			
		||||
      "fg": "#31363b",
 | 
			
		||||
      "panel": "#31363b",
 | 
			
		||||
      "input": "#232629",
 | 
			
		||||
      "topBarLink": "#eff0f1",
 | 
			
		||||
      "btn": "#31363b",
 | 
			
		||||
      "border": "#4c545b",
 | 
			
		||||
      "cRed": "#da4453",
 | 
			
		||||
      "cBlue": "#3daee9",
 | 
			
		||||
      "cGreen": "#27ae60",
 | 
			
		||||
      "cOrange": "#f67400"
 | 
			
		||||
    },
 | 
			
		||||
    "radii": {
 | 
			
		||||
      "btn": "2",
 | 
			
		||||
      "input": "2",
 | 
			
		||||
      "checkbox": "1",
 | 
			
		||||
      "panel": "2",
 | 
			
		||||
      "avatar": "2",
 | 
			
		||||
      "avatarAlt": "2",
 | 
			
		||||
      "tooltip": "2",
 | 
			
		||||
      "attachment": "2"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										139
									
								
								static/themes/breezy-light.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								static/themes/breezy-light.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,139 @@
 | 
			
		|||
{
 | 
			
		||||
  "_pleroma_theme_version": 2,
 | 
			
		||||
  "name": "Breezy Light (beta)",
 | 
			
		||||
  "theme": {
 | 
			
		||||
    "shadows": {
 | 
			
		||||
      "panel": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "6",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": 0.6
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "button": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "1",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": false
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "panelHeader": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "40",
 | 
			
		||||
          "blur": "40",
 | 
			
		||||
          "spread": "-40",
 | 
			
		||||
          "inset": true,
 | 
			
		||||
          "color": "#ffffff",
 | 
			
		||||
          "alpha": "0.1"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonHover": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "--link",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "1",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": false
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonPressed": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": 0,
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "50",
 | 
			
		||||
          "color": "--faint",
 | 
			
		||||
          "alpha": 1,
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "#ffffff",
 | 
			
		||||
          "alpha": 0.2,
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.3",
 | 
			
		||||
          "inset": false
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "input": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "1",
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "0.2",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "fonts": {},
 | 
			
		||||
    "opacity": {
 | 
			
		||||
      "input": "1"
 | 
			
		||||
    },
 | 
			
		||||
    "colors": {
 | 
			
		||||
      "bg": "#eff0f1",
 | 
			
		||||
      "text": "#232627",
 | 
			
		||||
      "link": "#2980b9",
 | 
			
		||||
      "fg": "#bcc2c7",
 | 
			
		||||
      "panel": "#475057",
 | 
			
		||||
      "panelText": "#fcfcfc",
 | 
			
		||||
      "input": "#fcfcfc",
 | 
			
		||||
      "topBar": "#475057",
 | 
			
		||||
      "topBarLink": "#eff0f1",
 | 
			
		||||
      "btn": "#eff0f1",
 | 
			
		||||
      "cRed": "#da4453",
 | 
			
		||||
      "cBlue": "#2980b9",
 | 
			
		||||
      "cGreen": "#27ae60",
 | 
			
		||||
      "cOrange": "#f67400"
 | 
			
		||||
    },
 | 
			
		||||
    "radii": {
 | 
			
		||||
      "btn": "2",
 | 
			
		||||
      "input": "2",
 | 
			
		||||
      "checkbox": "1",
 | 
			
		||||
      "panel": "2",
 | 
			
		||||
      "avatar": "2",
 | 
			
		||||
      "avatarAlt": "2",
 | 
			
		||||
      "tooltip": "2",
 | 
			
		||||
      "attachment": "2"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										297
									
								
								static/themes/redmond-xx-se.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								static/themes/redmond-xx-se.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,297 @@
 | 
			
		|||
{
 | 
			
		||||
  "_pleroma_theme_version": 2,
 | 
			
		||||
  "name": "Redmond XX SE",
 | 
			
		||||
  "theme": {
 | 
			
		||||
    "shadows": {
 | 
			
		||||
      "panel": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "panelHeader": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": 0,
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "inset": true,
 | 
			
		||||
          "color": "#c0c0c0",
 | 
			
		||||
          "alpha": 1
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2200",
 | 
			
		||||
          "y": 0,
 | 
			
		||||
          "blur": "200",
 | 
			
		||||
          "spread": "-2000",
 | 
			
		||||
          "inset": true,
 | 
			
		||||
          "color": "#1084d0",
 | 
			
		||||
          "alpha": 1
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "button": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonHover": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonPressed": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "input": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--input",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "fonts": {},
 | 
			
		||||
    "opacity": {
 | 
			
		||||
      "input": "1",
 | 
			
		||||
      "faint": "1"
 | 
			
		||||
    },
 | 
			
		||||
    "colors": {
 | 
			
		||||
      "bg": "#c0c0c0",
 | 
			
		||||
      "text": "#000000",
 | 
			
		||||
      "link": "#0000ff",
 | 
			
		||||
      "fg": "#c0c0c0",
 | 
			
		||||
      "panel": "#000080",
 | 
			
		||||
      "panelFaint": "#c0c0c0",
 | 
			
		||||
      "input": "#ffffff",
 | 
			
		||||
      "topBar": "#000080",
 | 
			
		||||
      "topBarLink": "#ffffff",
 | 
			
		||||
      "btn": "#c0c0c0",
 | 
			
		||||
      "faint": "#3f3f3f",
 | 
			
		||||
      "faintLink": "#404080",
 | 
			
		||||
      "border": "#808080",
 | 
			
		||||
      "cRed": "#FF0000",
 | 
			
		||||
      "cBlue": "#008080",
 | 
			
		||||
      "cGreen": "#008000",
 | 
			
		||||
      "cOrange": "#808000"
 | 
			
		||||
    },
 | 
			
		||||
    "radii": {
 | 
			
		||||
      "btn": "0",
 | 
			
		||||
      "input": "0",
 | 
			
		||||
      "checkbox": "0",
 | 
			
		||||
      "panel": "0",
 | 
			
		||||
      "avatar": "0",
 | 
			
		||||
      "avatarAlt": "0",
 | 
			
		||||
      "tooltip": "0",
 | 
			
		||||
      "attachment": "0"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										288
									
								
								static/themes/redmond-xx.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								static/themes/redmond-xx.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,288 @@
 | 
			
		|||
{
 | 
			
		||||
  "_pleroma_theme_version": 2,
 | 
			
		||||
  "name": "Redmond XX",
 | 
			
		||||
  "theme": {
 | 
			
		||||
    "shadows": {
 | 
			
		||||
      "panel": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "panelHeader": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": 0,
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "inset": true,
 | 
			
		||||
          "color": "#c0c0c0",
 | 
			
		||||
          "alpha": 1
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "button": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonHover": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonPressed": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "input": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#000000",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--input",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "fonts": {},
 | 
			
		||||
    "opacity": {
 | 
			
		||||
      "input": "1",
 | 
			
		||||
      "faint": "1"
 | 
			
		||||
    },
 | 
			
		||||
    "colors": {
 | 
			
		||||
      "bg": "#c0c0c0",
 | 
			
		||||
      "text": "#000000",
 | 
			
		||||
      "link": "#0000ff",
 | 
			
		||||
      "fg": "#c0c0c0",
 | 
			
		||||
      "panel": "#000080",
 | 
			
		||||
      "panelFaint": "#c0c0c0",
 | 
			
		||||
      "input": "#ffffff",
 | 
			
		||||
      "topBar": "#000080",
 | 
			
		||||
      "topBarLink": "#ffffff",
 | 
			
		||||
      "btn": "#c0c0c0",
 | 
			
		||||
      "faint": "#3f3f3f",
 | 
			
		||||
      "faintLink": "#404080",
 | 
			
		||||
      "border": "#808080",
 | 
			
		||||
      "cRed": "#FF0000",
 | 
			
		||||
      "cBlue": "#008080",
 | 
			
		||||
      "cGreen": "#008000",
 | 
			
		||||
      "cOrange": "#808000"
 | 
			
		||||
    },
 | 
			
		||||
    "radii": {
 | 
			
		||||
      "btn": "0",
 | 
			
		||||
      "input": "0",
 | 
			
		||||
      "checkbox": "0",
 | 
			
		||||
      "panel": "0",
 | 
			
		||||
      "avatar": "0",
 | 
			
		||||
      "avatarAlt": "0",
 | 
			
		||||
      "tooltip": "0",
 | 
			
		||||
      "attachment": "0"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										270
									
								
								static/themes/redmond-xxi.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								static/themes/redmond-xxi.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,270 @@
 | 
			
		|||
{
 | 
			
		||||
  "_pleroma_theme_version": 2,
 | 
			
		||||
  "name": "Redmond XXI",
 | 
			
		||||
  "theme": {
 | 
			
		||||
    "shadows": {
 | 
			
		||||
      "panel": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#404040",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#dfdfdf",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "panelHeader": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": 0,
 | 
			
		||||
          "y": 0,
 | 
			
		||||
          "blur": 0,
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "inset": true,
 | 
			
		||||
          "color": "#d6d6ce",
 | 
			
		||||
          "alpha": 1
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2200",
 | 
			
		||||
          "y": 0,
 | 
			
		||||
          "blur": "200",
 | 
			
		||||
          "spread": "-2000",
 | 
			
		||||
          "inset": true,
 | 
			
		||||
          "color": "#a5cef7",
 | 
			
		||||
          "alpha": 1
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "button": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#404040",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonHover": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#404040",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "buttonPressed": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#404040",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--bg",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "input": [
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-1",
 | 
			
		||||
          "y": "-1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#FFFFFF",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "1",
 | 
			
		||||
          "y": "1",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#848484",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "-2",
 | 
			
		||||
          "y": "-2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#d4d0c8",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "2",
 | 
			
		||||
          "y": "2",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": 0,
 | 
			
		||||
          "color": "#404040",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "x": "0",
 | 
			
		||||
          "y": "0",
 | 
			
		||||
          "blur": "0",
 | 
			
		||||
          "spread": "3",
 | 
			
		||||
          "color": "--input",
 | 
			
		||||
          "alpha": "1",
 | 
			
		||||
          "inset": true
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "fonts": {},
 | 
			
		||||
    "opacity": {
 | 
			
		||||
      "input": "1",
 | 
			
		||||
      "faint": "1"
 | 
			
		||||
    },
 | 
			
		||||
    "colors": {
 | 
			
		||||
      "bg": "#d6d6ce",
 | 
			
		||||
      "text": "#000000",
 | 
			
		||||
      "link": "#0000ff",
 | 
			
		||||
      "fg": "#d6d6ce",
 | 
			
		||||
      "panel": "#042967",
 | 
			
		||||
      "panelFaint": "#FFFFFF",
 | 
			
		||||
      "input": "#ffffff",
 | 
			
		||||
      "topBar": "#042967",
 | 
			
		||||
      "topBarLink": "#ffffff",
 | 
			
		||||
      "btn": "#d6d6ce",
 | 
			
		||||
      "faint": "#3f3f3f",
 | 
			
		||||
      "faintLink": "#404080",
 | 
			
		||||
      "border": "#808080",
 | 
			
		||||
      "cRed": "#c42726",
 | 
			
		||||
      "cBlue": "#6699cc",
 | 
			
		||||
      "cGreen": "#669966",
 | 
			
		||||
      "cOrange": "#cc6633"
 | 
			
		||||
    },
 | 
			
		||||
    "radii": {
 | 
			
		||||
      "btn": "0",
 | 
			
		||||
      "input": "0",
 | 
			
		||||
      "checkbox": "0",
 | 
			
		||||
      "panel": "0",
 | 
			
		||||
      "avatar": "0",
 | 
			
		||||
      "avatarAlt": "0",
 | 
			
		||||
      "tooltip": "0",
 | 
			
		||||
      "attachment": "0"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
 import fileSizeFormatService from '../../../../../src/services/file_size_format/file_size_format.js'
 | 
			
		||||
 describe('fileSizeFormat', () => {
 | 
			
		||||
   it('Formats file size', () => {
 | 
			
		||||
     const values = [1, 1024, 1048576, 1073741824, 1099511627776]
 | 
			
		||||
     const expected = [
 | 
			
		||||
       {
 | 
			
		||||
         num: 1,
 | 
			
		||||
         unit: 'B'
 | 
			
		||||
       },
 | 
			
		||||
       {
 | 
			
		||||
         num: 1,
 | 
			
		||||
         unit: 'KiB'
 | 
			
		||||
       },
 | 
			
		||||
       {
 | 
			
		||||
         num: 1,
 | 
			
		||||
         unit: 'MiB'
 | 
			
		||||
       },
 | 
			
		||||
       {
 | 
			
		||||
         num: 1,
 | 
			
		||||
         unit: 'GiB'
 | 
			
		||||
       },
 | 
			
		||||
       {
 | 
			
		||||
         num: 1,
 | 
			
		||||
         unit: 'TiB'
 | 
			
		||||
       }
 | 
			
		||||
     ]
 | 
			
		||||
 | 
			
		||||
     var res = []
 | 
			
		||||
     for (var value in values) {
 | 
			
		||||
       res.push(fileSizeFormatService.fileSizeFormat(values[value]))
 | 
			
		||||
     }
 | 
			
		||||
     expect(res).to.eql(expected)
 | 
			
		||||
   })
 | 
			
		||||
 })
 | 
			
		||||
		Loading…
	
		Reference in a new issue