Page MenuHomePhorge

No OneTemporary

Size
58 KB
Referenced Files
None
Subscribers
None
diff --git a/src/App.scss b/src/App.scss
index 3002072210..a41140efd6 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,576 +1,581 @@
@import './_variables.scss';
#app {
background-size: cover;
background-attachment: fixed;
background-repeat: no-repeat;
background-position: 0 50px;
min-height: 100vh;
max-width: 100%;
overflow: hidden;
}
i {
user-select: none;
}
h4 {
margin: 0;
}
#content {
box-sizing: border-box;
padding-top: 60px;
margin: auto;
min-height: 100vh;
max-width: 980px;
background-color: rgba(0,0,0,0.15);
align-content: flex-start;
}
.text-center {
text-align: center;
}
body {
font-family: sans-serif;
font-size: 14px;
margin: 0;
color: $fallback--text;
color: var(--text, $fallback--text);
max-width: 100vw;
overflow-x: hidden;
}
a {
text-decoration: none;
color: $fallback--link;
color: var(--link, $fallback--link);
}
button {
user-select: none;
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);
+ /* 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: var(--buttonShadow);
font-size: 14px;
font-family: sans-serif;
i[class*=icon-] {
color: $fallback--text;
color: var(--btnText, $fallback--text);
}
&::-moz-focus-inner {
border: none;
}
&: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);
+ /* border-bottom: 1px solid rgba(255, 255, 255, 0.2); */
+ /* border-top: 1px solid rgba(0, 0, 0, 0.2); */
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.pressed {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg)
}
}
label.select {
padding: 0;
}
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);
+ /* 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;
+ 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-size: 14px;
padding: 8px 7px;
box-sizing: border-box;
display: inline-block;
position: relative;
height: 29px;
line-height: 16px;
hyphens: none;
.icon-down-open {
position: absolute;
top: 0;
bottom: 0;
right: 5px;
height: 100%;
color: $fallback--text;
color: var(--text, $fallback--text);
line-height: 29px;
z-index: 0;
pointer-events: none;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: transparent;
border: none;
margin: 0;
color: $fallback--text;
color: var(--text, $fallback--text);
padding: 4px 2em 3px 3px;
width: 100%;
z-index: 1;
height: 29px;
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--text;
color: var(--text, $fallback--text);
}
&:disabled,
{
&,
& + label,
& + label::before {
opacity: .5;
}
}
+ label::before {
display: inline-block;
content: '✔';
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);
box-shadow: 0px 0px 2px black inset;
+ box-shadow: var(--inputShadow);
margin-right: .5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
text-align: center;
line-height: 1.1em;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
box-sizing: border-box;
}
}
}
option {
color: $fallback--text;
color: var(--text, $fallback--text);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
i[class*=icon-] {
color: $fallback--icon;
color: var(--icon, $fallback--icon)
}
.container {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0 10px 0 10px;
}
.gaps {
margin: -1em 0 0 -1em;
}
.item {
flex: 1;
line-height: 50px;
height: 50px;
overflow: hidden;
.nav-icon {
font-size: 1.1em;
margin-left: 0.4em;
}
}
.gaps > .item {
padding: 1em 0 0 1em;
}
.auto-size {
flex: 1
}
nav {
width: 100%;
align-items: center;
position: fixed;
height: 50px;
.logo {
display: flex;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
align-items: stretch;
justify-content: center;
flex: 0 0 auto;
z-index: -1;
.mask {
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $fallback--fg;
background-color: var(--topBarText, $fallback--fg);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
img {
height: 100%;
object-fit: contain;
display: block;
flex: 0;
}
}
.inner-nav {
padding-left: 20px;
padding-right: 20px;
display: flex;
align-items: center;
flex-basis: 970px;
margin: auto;
height: 50px;
a, a i {
color: $fallback--link;
color: var(--topBarLink, $fallback--link);
}
}
}
main-router {
flex: 1;
}
.status.compact {
color: rgba(0, 0, 0, 0.42);
font-weight: 300;
p {
margin: 0;
font-size: 0.8em
}
}
/* Panel */
.panel {
display: flex;
position: relative;
flex-direction: column;
margin: 0.5em;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
&::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 {
content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
display: block;
margin: 1em;
text-align: center;
}
.panel-heading {
display: flex;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
padding: .6em .6em;
text-align: left;
line-height: 28px;
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;
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
button {
flex-shrink: 0;
}
button, .alert {
// height: 100%;
line-height: 21px;
min-height: 0;
box-sizing: border-box;
margin: 0;
margin-left: .25em;
min-width: 1px;
align-self: stretch;
}
}
.panel-heading.stub {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
.panel-footer {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
}
.panel-body > p {
line-height: 18px;
padding: 1em;
margin: 0;
}
.container > * {
min-width: 0px;
}
.fa {
color: grey;
}
nav {
z-index: 1000;
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);
}
.fade-enter-active, .fade-leave-active {
transition: opacity .2s
}
.fade-enter, .fade-leave-active {
opacity: 0
}
.main {
flex-basis: 60%;
flex-grow: 1;
flex-shrink: 1;
}
.sidebar-bounds {
flex: 0;
flex-basis: 35%;
}
.sidebar-flexer {
flex: 1;
flex-basis: 345px;
width: 365px;
}
.mobile-shown {
display: none;
}
.panel-switcher {
display: none;
width: 100%;
height: 46px;
button {
display: block;
flex: 1;
max-height: 32px;
margin: 0.5em;
padding: 0.5em;
}
}
@media all and (min-width: 960px) {
body {
overflow-y: scroll;
}
.sidebar-bounds {
overflow: hidden;
max-height: 100vh;
width: 345px;
position: fixed;
margin-top: -10px;
.sidebar-scroller {
height: 96vh;
width: 365px;
padding-top: 10px;
padding-right: 50px;
overflow-x: hidden;
overflow-y: scroll;
}
.sidebar {
width: 345px;
}
}
.sidebar-flexer {
max-height: 96vh;
flex-shrink: 0;
flex-grow: 0;
}
}
.alert {
margin: 0.35em;
padding: 0.25em;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
min-height: 28px;
line-height: 28px;
&.error {
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);
}
}
}
.faint {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
}
@media all and (max-width: 959px) {
.mobile-hidden {
display: none;
}
.panel-switcher {
display: flex;
}
.container {
padding: 0 0 0 0;
}
.panel {
margin: 0.5em 0 0.5em 0;
}
}
.item.right {
text-align: right;
padding-right: 20px;
}
.visibility-tray {
font-size: 1.2em;
padding: 3px;
cursor: pointer;
.selected {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
.text-format {
float: right;
}
div {
padding-top: 5px;
}
}
.visibility-notice {
padding: .5em;
border: 1px solid $fallback--faint;
border: 1px solid var(--faint, $fallback--faint);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 57a007d9e8..c4a268d0b6 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,561 +1,562 @@
<template>
<div class="status-el" v-if="!hideReply" :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 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"/>
<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>
<i class='fa icon-retweet retweeted'></i>
{{$t('timeline.repeated')}}
</div>
</div>
<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"/>
</a>
</div>
<div class="status-body">
<div class="usercard media-body" v-if="userExpanded">
<user-card-content :user="status.user" :switcher="false"></user-card-content>
</div>
<div v-if="!noHeading" class="media-body container media-heading">
<div class="media-heading-left">
<div class="name-and-links">
<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>
<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 } }">
{{status.in_reply_to_screen_name}}
</router-link>
</span>
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)">
<i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
</a>
</span>
</div>
<h4 class="replies" v-if="inConversation && !noReplyLinks">
<small v-if="replies.length">Replies:</small>
<small class="reply-link" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
</h4>
</div>
<div class="media-heading-right">
<router-link class="timeago" :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">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
</div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url" title="Source">
<i class="icon-link-ext-alt"></i>
</a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="icon-eye-off"></i></a>
</div>
</div>
<div v-if="showPreview" class="status-preview-container">
<status 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>
</div>
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary" v-else></div>
<a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
<div v-if='status.attachments && !hideSubjectStatus' class='attachments media-body'>
<attachment :size="attachmentSize" :status-id="status.id" :nsfw="nsfwClickthrough" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id">
</attachment>
</div>
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
<div v-if="loggedIn">
<a href="#" v-on:click.prevent="toggleReplying">
<i class="icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
<delete-button :status='status'></delete-button>
</div>
</div>
</div>
<div class="container" v-if="replying">
<div class="reply-left"/>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
</div>
</template>
</div>
</template>
<script src="./status.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.status-body {
flex: 1;
min-width: 0;
}
.status-preview.status-el {
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.status-preview-container {
position: relative;
max-width: 100%;
}
.status-preview {
position: absolute;
max-width: 95%;
display: flex;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-style: solid;
border-width: 1px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
+ box-shadow: var(--popupShadow);
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
.status {
flex: 1;
border: 0;
min-width: 15em;
}
}
.status-preview-loading {
display: block;
min-width: 15em;
padding: 1em;
text-align: center;
border-width: 1px;
border-style: solid;
i {
font-size: 2em;
}
}
.status-el {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
border-left-width: 0px;
line-height: 18px;
min-width: 0;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-left: 4px $fallback--cRed;
border-left: 4px var(--cRed, $fallback--cRed);
&_focused {
background-color: $fallback--lightBg;
background-color: var(--lightBg, $fallback--lightBg);
}
.timeline & {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.media-body {
flex: 1;
padding: 0;
margin: 0 0 0.25em 0.8em;
}
.usercard {
margin-bottom: .7em
}
.media-heading {
flex-wrap: nowrap;
line-height: 18px;
}
.media-heading-left {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
small {
font-weight: lighter;
}
h4 {
white-space: nowrap;
font-size: 14px;
margin-right: 0.25em;
overflow: hidden;
text-overflow: ellipsis;
}
.name-and-links {
padding: 0;
flex: 1 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
.user-name {
margin-right: .45em;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
}
.links {
display: flex;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
max-width: 100%;
a {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.reply-info {
display: flex;
}
.replies {
line-height: 16px;
}
.reply-link {
margin-right: 0.2em;
}
}
.media-heading-right {
display: inline-flex;
flex-shrink: 0;
flex-wrap: nowrap;
margin-left: .25em;
align-self: baseline;
.timeago {
margin-right: 0.2em;
font-size: 12px;
align-self: last baseline;
}
> * {
margin-left: 0.2em;
}
a:hover i {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
a {
display: inline-block;
word-break: break-all;
}
.tall-status {
position: relative;
height: 220px;
overflow-x: hidden;
overflow-y: hidden;
}
.tall-status-hider {
position: absolute;
height: 70px;
margin-top: 150px;
width: 100%;
text-align: center;
line-height: 110px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
&_focused {
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--lightBg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--lightBg, $fallback--lightBg) 80%);
}
}
.status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
}
.status-content {
margin-right: 0.5em;
img, video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
}
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
pre {
overflow: auto;
}
p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
}
h1 {
font-size: 1.1em;
line-height: 1.2em;
margin: 1.4em 0;
}
h2 {
font-size: 1.1em;
margin: 1.0em 0;
}
h3 {
font-size: 1em;
margin: 1.2em 0;
}
h4 {
margin: 1.1em 0;
}
}
.retweet-info {
padding: 0.4em 0.6em 0 0.6em;
margin: 0;
.avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px;
width: 20px;
height: 20px;
}
.media-body {
font-size: 1em;
line-height: 22px;
display: flex;
align-content: center;
flex-wrap: wrap;
.user-name {
font-weight: bold;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
i {
padding: 0 0.2em;
}
a {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.status-fadein {
animation-duration: 0.4s;
animation-name: fadein;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.greentext {
color: green;
}
.status-conversation {
border-left-style: solid;
}
.status-actions {
width: 100%;
display: flex;
div, favorite-button {
padding-top: 0.25em;
max-width: 6em;
flex: 1;
}
}
.icon-reply:hover {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-reply.icon-reply-active {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.status .avatar-compact {
width: 32px;
height: 32px;
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
}
.avatar {
width: 48px;
height: 48px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
overflow: hidden;
position: relative;
img {
width: 100%;
height: 100%;
}
&.animated::before {
display: none;
}
&.retweeted {
}
}
.status:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
}
.status {
display: flex;
padding: 0.6em;
&.is-retweet {
padding-top: 0.1em;
}
}
.status-conversation:last-child {
border-bottom: none;
}
.muted {
padding: 0.25em 0.5em;
button {
margin-left: auto;
}
.muteWords {
margin-left: 10px;
}
}
a.unmute {
display: block;
margin-left: auto;
}
.reply-left {
flex: 0;
min-width: 48px;
}
.reply-body {
flex: 1;
}
.timeline > {
.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);
}
}
@media all and (max-width: 960px) {
.status-el {
.retweet-info {
.avatar {
margin-left: 20px;
}
}
}
.status {
max-width: 100%;
}
.status .avatar {
width: 40px;
height: 40px;
}
.status .avatar-compact {
width: 32px;
height: 32px;
}
}
</style>
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index f1b54fadb0..a2bb99a1a0 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -1,345 +1,346 @@
<template>
<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>
<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>
<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 } }">
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
<span class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
</div>
</div>
<div class="user-meta">
<div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
{{ $t('user_card.follows_you') }}
</div>
<div class="floater" v-if="switcher || isOtherUser">
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
<input class="userHighlightText" type="text" :id="'userHighlightColorTx'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
<input class="userHighlightCl" type="color" :id="'userHighlightColor'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
<label for="style-switcher" class='userHighlightSel select'>
<select class="userHighlightSel" :id="'userHighlightSel'+user.id" v-model="userHighlightType">
<option value="disabled">No highlight</option>
<option value="solid">Solid bg</option>
<option value="striped">Striped bg</option>
<option value="side">Side stripe</option>
</select>
<i class="icon-down-open"/>
</label>
</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>
</div>
</div>
<div class="panel-body profile-panel-body">
<div 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>{{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>{{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>{{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>
</template>
<script src="./user_card_content.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.profile-panel-background {
background-size: cover;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
.panel-heading {
padding: 0.6em 0em;
text-align: center;
}
}
.profile-panel-body {
word-wrap: break-word;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
.profile-bio {
text-align: center;
}
}
.user-info {
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;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
flex: 1 0 100%;
width: 56px;
height: 56px;
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
+ box-shadow: var(--avatarShadow);
object-fit: cover;
&.animated::before {
display: none;
}
}
}
&:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
}
.usersettings {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
opacity: .8;
}
.name-and-screen-name {
display: block;
margin-left: 0.6em;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 0;
}
.user-name{
text-overflow: ellipsis;
overflow: hidden;
}
.user-screen-name {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
display: inline-block;
font-weight: light;
font-size: 15px;
padding-right: 0.1em;
}
.user-meta {
margin-bottom: .4em;
.following {
font-size: 14px;
flex: 0 0 100%;
margin: 0;
padding-left: 16px;
text-align: left;
float: left;
}
.floater {
margin: 0;
}
&::after {
display: block;
content: '';
clear: both;
}
}
.user-interactions {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
div {
flex: 1;
}
.mute {
max-width: 220px;
min-height: 28px;
}
.remote-follow {
max-width: 220px;
min-height: 28px;
}
.follow {
max-width: 220px;
min-height: 28px;
}
button {
width: 92%;
height: 100%;
}
.remote-button {
height: 28px !important;
width: 92%;
}
.pressed {
border-bottom-color: rgba(255, 255, 255, 0.2);
border-top-color: rgba(0, 0, 0, 0.2);
}
}
}
.user-counts {
display: flex;
line-height:16px;
padding: .5em 1.5em 0em 1.5em;
text-align: center;
justify-content: space-between;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
&.clickable {
.user-count {
cursor: pointer;
&:hover:not(.selected) {
transition: border-bottom 100ms;
border-bottom: 3px solid $fallback--link;
border-bottom: 3px solid var(--link, $fallback--link);
}
}
}
}
.user-count {
flex: 1;
padding: .5em 0 .5em 0;
margin: 0 .5em;
&.selected {
transition: none;
border-bottom: 5px solid $fallback--link;
border-bottom: 5px solid var(--link, $fallback--link);
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
}
h5 {
font-size:1em;
font-weight: bolder;
margin: 0 0 0.25em;
}
a {
text-decoration: none;
}
}
.dailyAvg {
margin-left: 1em;
font-size: 0.7em;
color: #CCC;
}
.floater {
float: right;
margin-top: 16px;
.userHighlightCl {
padding: 2px 10px;
}
.userHighlightSel,
.userHighlightSel.select {
padding-top: 0;
padding-bottom: 0;
}
.userHighlightSel.select i {
line-height: 22px;
}
.userHighlightText {
width: 70px;
}
.userHighlightCl,
.userHighlightText,
.userHighlightSel,
.userHighlightSel.select {
height: 22px;
vertical-align: top;
margin-right: 0
}
}
</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 18b47ba793..3fcc6da246 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1,257 +1,262 @@
{
"chat": {
"title": "Chat"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Media proxy",
"scope_options": "Scope options",
"text_limit": "Text limit",
"title": "Features",
"who_to_follow": "Who to follow"
},
"finder": {
"error_fetching_user": "Error fetching user",
"find_user": "Find user"
},
"general": {
"apply": "Apply",
"submit": "Submit"
},
"login": {
"login": "Log in",
"logout": "Log out",
"password": "Password",
"placeholder": "e.g. lain",
"register": "Register",
"username": "Username"
},
"nav": {
"chat": "Local Chat",
"friend_requests": "Follow Requests",
"mentions": "Mentions",
"public_tl": "Public Timeline",
"timeline": "Timeline",
"twkn": "The Whole Known Network"
},
"notifications": {
"broken_favorite": "Unknown status, searching for it...",
"favorited_you": "favorited your status",
"followed_you": "followed you",
"load_older": "Load older notifications",
"notifications": "Notifications",
"read": "Read!",
"repeated_you": "repeated your status"
},
"post_status": {
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
"account_not_locked_warning_link": "locked",
"attachments_sensitive": "Mark attachments as sensitive",
"content_type": {
"plain_text": "Plain text"
},
"content_warning": "Subject (optional)",
"default": "Just landed in L.A.",
"direct_warning": "This post will only be visible to all the mentioned users.",
"posting": "Posting",
"scope": {
"direct": "Direct - Post to mentioned users only",
"private": "Followers-only - Post to followers only",
"public": "Public - Post to public timelines",
"unlisted": "Unlisted - Do not post to public timelines"
}
},
"registration": {
"bio": "Bio",
"email": "Email",
"fullname": "Display name",
"password_confirm": "Password confirmation",
"registration": "Registration",
"token": "Invite token"
},
"settings": {
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom",
"avatar": "Avatar",
"avatarAltRadius": "Avatars (Notifications)",
"avatarRadius": "Avatars",
"background": "Background",
"bio": "Bio",
"btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)",
"cRed": "Red (Cancel)",
"change_password": "Change Password",
"change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!",
"collapse_subject": "Collapse posts with subjects",
"confirm_new_password": "Confirm new password",
"current_avatar": "Your current avatar",
"current_password": "Current password",
"current_profile_banner": "Your current profile banner",
"data_import_export_tab": "Data Import / Export",
"default_vis": "Default visibility scope",
"delete_account": "Delete Account",
"delete_account_description": "Permanently delete your account and all your messages.",
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"export_theme": "Save preset",
"filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line",
"follow_export": "Follow export",
"follow_export_button": "Export your follows to a csv file",
"follow_export_processing": "Processing, you'll soon be asked to download your file",
"follow_import": "Follow import",
"follow_import_error": "Error importing followers",
"follows_imported": "Follows imported! Processing them will take a while.",
"foreground": "Foreground",
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline",
"import_followers_from_a_csv_file": "Import follows from a csv file",
"import_theme": "Load preset",
"inputRadius": "Input fields",
"instance_default": "(default: {value})",
"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",
"links": "Links",
"lock_account_description": "Restrict your account to approved followers only",
"loop_video": "Loop videos",
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"name": "Name",
"name_bio": "Name & Bio",
"new_password": "New password",
"notification_visibility": "Types of notifications to show",
"notification_visibility_follows": "Follows",
"notification_visibility_likes": "Likes",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"no_rich_text_description": "Disable rich text support",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets",
"profile_background": "Profile Background",
"profile_banner": "Profile Banner",
"profile_tab": "Profile",
"radii_help": "Set up interface edge rounding (in pixels)",
"replies_in_timeline": "Replies in timeline",
"reply_link_preview": "Enable reply-link preview on mouse hover",
"reply_visibility_all": "Show all replies",
"reply_visibility_following": "Only show replies directed at me or users I'm following",
"reply_visibility_self": "Only show replies directed at me",
"saving_err": "Error saving settings",
"saving_ok": "Settings saved",
"security_tab": "Security",
"set_new_avatar": "Set new avatar",
"set_new_profile_background": "Set new profile background",
"set_new_profile_banner": "Set new profile banner",
"settings": "Settings",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
"text": "Text",
"theme": "Theme",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
"tooltipRadius": "Tooltips/alerts",
"user_settings": "User Settings",
"values": {
"false": "no",
"true": "yes"
},
"style": {
"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"
}
}
},
"basic_colors": {
"_tab_label": "Basic",
"main": "Basic colors",
"foreground": "Panel header, top bar, buttons, text fields",
"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",
"components": {
"panel": "Panel",
+ "panelHeader": "Panel header",
+ "topBar": "Top bar",
+ "avatar": "User avatar (in post display)",
+ "avatarStatus": "User avatar (in profile view)",
+ "popup": "Popups and tooltips",
"button": "Button",
- "button_hover": "Button (hover)",
- "button_pressed": "Button (pressed)",
- "button_pressed_hover": "Button (pressed+hover)",
- "input_box": "Input field"
+ "buttonHover": "Button (hover)",
+ "buttonPressed": "Button (pressed)",
+ "buttonPressedHover": "Button (pressed+hover)",
+ "input": "Input field"
}
}
}
},
"timeline": {
"collapse": "Collapse",
"conversation": "Conversation",
"error_fetching": "Error fetching updates",
"load_older": "Load older statuses",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"repeated": "repeated",
"show_new": "Show new",
"up_to_date": "Up-to-date"
},
"user_card": {
"approve": "Approve",
"block": "Block",
"blocked": "Blocked!",
"deny": "Deny",
"follow": "Follow",
"followees": "Following",
"followers": "Followers",
"following": "Following!",
"follows_you": "Follows you!",
"mute": "Mute",
"muted": "Muted",
"per_day": "per day",
"remote_follow": "Remote follow",
"statuses": "Statuses"
},
"user_profile": {
"timeline_title": "User Timeline"
},
"who_to_follow": {
"more": "More",
"who_to_follow": "Who to follow"
}
}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 57bd2841c0..8142da8b0d 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,355 +1,431 @@
import { times } from 'lodash'
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
// styles as well as set their own colors in the future.
const setStyle = (href, commit) => {
/***
What's going on here?
I want to make it easy for admins to style this application. To have
a good set of default themes, I chose the system from base16
(https://chriskempson.github.io/base16/) to style all elements. They
all have the base00..0F classes. So the only thing an admin needs to
do to style Pleroma is to change these colors in that one css file.
Some default things (body text color, link color) need to be set dy-
namically, so this is done here by waiting for the stylesheet to be
loaded and then creating an element with the respective classes.
It is a bit weird, but should make life for admins somewhat easier.
***/
const head = document.head
const body = document.body
body.style.display = 'none'
const cssEl = document.createElement('link')
cssEl.setAttribute('rel', 'stylesheet')
cssEl.setAttribute('href', href)
head.appendChild(cssEl)
const setDynamic = () => {
const baseEl = document.createElement('div')
body.appendChild(baseEl)
let colors = {}
times(16, (n) => {
const name = `base0${n.toString(16).toUpperCase()}`
baseEl.setAttribute('class', name)
const color = window.getComputedStyle(baseEl).getPropertyValue('color')
colors[name] = color
})
body.removeChild(baseEl)
const styleEl = document.createElement('style')
head.appendChild(styleEl)
// const styleSheet = styleEl.sheet
body.style.display = 'initial'
}
cssEl.addEventListener('load', setDynamic)
}
const rgb2rgba = function (rgba) {
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
}
const getTextColor = function (bg, text, preserve) {
const bgIsLight = convert(bg).hsl.l > 50
const textIsLight = convert(text).hsl.l > 50
if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
const result = Object.assign(base, invertLightness(text).rgb)
if (!preserve && getContrastRatio(bg, result) < 4.5) {
return contrastRatio(bg, text).rgb
}
return result
}
return text
}
const setColors = (input, commit) => {
const { rules, theme } = generatePreset(input)
const head = document.head
const body = document.body
body.style.display = 'none'
const styleEl = document.createElement('style')
head.appendChild(styleEl)
const styleSheet = styleEl.sheet
styleSheet.toString()
styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
body.style.display = 'initial'
// 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) => {
// >shad
return input.map((shad) => [
shad.x,
shad.y,
shad.blur,
shad.spread
].map(_ => _ + 'px').concat([
getCssColor(shad.color, shad.alpha),
shad.inset ? 'inset' : ''
]).join(' ')).join(', ')
}
const getCssColor = (input, a) => {
console.log(input)
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
}
}
console.log(rgb2rgba({ ...rgb, a }))
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.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
colors.cRed = col.cRed
colors.cGreen = col.cGreen
colors.cOrange = col.cOrange
colors.alertError = col.alertError || Object.assign({}, col.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({}, col.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) => {
const radii = Object.entries(input.radii || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
acc[k] = v
return acc
}, {
btn: 4,
input: 4,
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 generateShadows = (input) => {
+ const buttonInsetFakeBorders = [{
+ x: 0,
+ y: 101,
+ blur: 0,
+ spread: -100,
+ color: '#FFFFFF',
+ alpha: 0.2,
+ inset: true
+ }, {
+ x: 0,
+ y: -101,
+ blur: 0,
+ spread: -100,
+ color: '#000000',
+ alpha: 0.2,
+ inset: true
+ }]
+ const inputInsetFakeBorders = [{
+ x: 0,
+ y: 101,
+ blur: 0,
+ spread: -100,
+ color: '#000000',
+ alpha: 0.2,
+ inset: true
+ }, {
+ x: 0,
+ y: -101,
+ blur: 0,
+ spread: -100,
+ color: '#FFFFFF',
+ alpha: 0.3,
+ inset: true
+ }]
const shadows = {
panel: [{
x: 1,
y: 1,
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
+ }],
+ panelHeader: [],
+ button: [{
+ x: 0,
+ y: 0,
+ blur: 2,
+ spread: 0,
+ color: '#000000',
+ alpha: 1
+ }, ...buttonInsetFakeBorders],
+ buttonHover: [{
+ x: 0,
+ y: 0,
+ blur: 4,
+ spread: 0,
+ color: '--faint',
+ alpha: 1
+ }, ...buttonInsetFakeBorders],
+ input: [{
+ x: 0,
+ y: 0,
+ blur: 2,
+ inset: true,
+ spread: 0,
+ color: '#000000',
+ alpha: 1
+ }, ...inputInsetFakeBorders],
...(input.shadows || {})
}
return {
rules: {
shadows: Object.entries(shadows).filter(([k, v]) => v).map(([k, v]) => `--${k}Shadow: ${getCssShadow(v)}`).join(';')
},
theme: {
shadows
}
}
}
const composePreset = (colors, radii, shadows) => {
return {
rules: {
...shadows.rules,
...colors.rules,
...radii.rules
},
theme: {
...shadows.theme,
...colors.theme,
...radii.theme
}
}
}
const generatePreset = (input) => {
const shadows = generateShadows(input)
const colors = generateColors(input)
const radii = generateRadii(input)
return composePreset(colors, radii, shadows)
}
const setPreset = (val, commit) => {
window.fetch('/static/styles.json')
.then((data) => data.json())
.then((themes) => {
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
const bgRgb = hex2rgb(theme[1])
const fgRgb = hex2rgb(theme[2])
const textRgb = hex2rgb(theme[3])
const linkRgb = hex2rgb(theme[4])
const cRedRgb = hex2rgb(theme[5] || '#FF0000')
const cGreenRgb = hex2rgb(theme[6] || '#00FF00')
const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
const colors = {
bg: bgRgb,
fg: fgRgb,
text: textRgb,
link: linkRgb,
cRed: cRedRgb,
cBlue: cBlueRgb,
cGreen: cGreenRgb,
cOrange: cOrangeRgb
}
// This is a hack, this function is only called during initial load.
// 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({ colors }, commit)
}
})
}
export {
setStyle,
setPreset,
setColors,
getTextColor,
generateColors,
generateRadii,
generateShadows,
generatePreset,
composePreset,
getCssShadow
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 9, 5:12 AM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784461
Default Alt Text
(58 KB)

Event Timeline