diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/attachment/attachment.js | 38 | ||||
| -rw-r--r-- | src/components/attachment/attachment.vue | 9 | ||||
| -rw-r--r-- | src/components/login_form/login_form.vue | 5 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.js | 2 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.vue | 72 | ||||
| -rw-r--r-- | src/components/settings/settings.js | 26 | ||||
| -rw-r--r-- | src/components/settings/settings.vue | 124 | ||||
| -rw-r--r-- | src/components/status/status.js | 52 | ||||
| -rw-r--r-- | src/components/status/status.vue | 69 | ||||
| -rw-r--r-- | src/components/style_switcher/style_switcher.js | 91 | ||||
| -rw-r--r-- | src/components/style_switcher/style_switcher.vue | 10 | ||||
| -rw-r--r-- | src/components/timeline/timeline.js | 5 | ||||
| -rw-r--r-- | src/components/user_settings/user_settings.js | 26 | ||||
| -rw-r--r-- | src/components/user_settings/user_settings.vue | 43 |
14 files changed, 422 insertions, 150 deletions
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index d9bc4477..cc19714d 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -13,9 +13,10 @@ const Attachment = { return { nsfwImage, hideNsfwLocal: this.$store.state.config.hideNsfw, + loopVideo: this.$store.state.config.loopVideo, showHidden: false, loading: false, - img: document.createElement('img') + img: this.type === 'image' && document.createElement('img') } }, components: { @@ -45,14 +46,35 @@ const Attachment = { } }, toggleHidden () { - if (this.img.onload) { - this.img.onload() + if (this.img) { + if (this.img.onload) { + this.img.onload() + } else { + this.loading = true + this.img.src = this.attachment.url + this.img.onload = () => { + this.loading = false + this.showHidden = !this.showHidden + } + } } else { - this.loading = true - this.img.src = this.attachment.url - this.img.onload = () => { - this.loading = false - this.showHidden = !this.showHidden + this.showHidden = !this.showHidden + } + }, + onVideoDataLoad (e) { + if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') { + // non-zero if video has audio track + if (e.srcElement.webkitAudioDecodedByteCount > 0) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } else if (typeof e.srcElement.mozHasAudio !== 'undefined') { + // true if video has audio track + if (e.srcElement.mozHasAudio) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } else if (typeof e.srcElement.audioTracks !== 'undefined') { + if (e.srcElement.audioTracks.length > 0) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly } } } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index c48fb16b..d01c8566 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -2,7 +2,7 @@ <div v-if="size==='hide'"> <a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a> </div> - <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth}" v-show="!isEmpty"> + <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty"> <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()"> <img :key="nsfwImage" :src="nsfwImage"/> </a> @@ -14,7 +14,7 @@ <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" :src="attachment.url" controls loop></video> + <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo"></video> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio> @@ -38,7 +38,6 @@ .attachments { display: flex; flex-wrap: wrap; - margin-right: -0.7em; .attachment.media-upload-container { flex: 0 0 auto; @@ -50,6 +49,10 @@ margin-right: 0.5em; } + .nsfw-placeholder { + cursor: pointer; + } + .small-attachment { &.image, &.video { max-width: 35%; diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index b7fed48a..d2bdffcb 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -34,11 +34,6 @@ @import '../../_variables.scss'; .login-form { - .btn { - min-height: 28px; - width: 10em; - } - .error { text-align: center; } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 61f2ac0a..ff3bb906 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -54,7 +54,7 @@ const PostStatusForm = { newStatus: { status: statusText, files: [], - visibility: this.messageScope || 'public' + visibility: this.messageScope || this.$store.state.users.currentUser.default_scope }, caret: 0 } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 7aa0e7c4..1e1c6f1d 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -65,12 +65,14 @@ <i class="icon-cancel" @click="clearError"></i> </div> <div class="attachments"> - <div class="media-upload-container attachment" v-for="file in newStatus.files"> + <div class="media-upload-wrapper" v-for="file in newStatus.files"> <i class="fa icon-cancel" @click="removeMediaFile(file)"></i> - <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img> - <video v-if="type(file) === 'video'" :src="file.image" controls></video> - <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio> - <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a> + <div class="media-upload-container attachment"> + <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img> + <video v-if="type(file) === 'video'" :src="file.image" controls></video> + <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio> + <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a> + </div> </div> </div> </form> @@ -99,35 +101,12 @@ } } -.post-status-form .visibility-tray { - font-size: 1.2em; - padding: 3px; - cursor: pointer; - - .selected { - color: $fallback--lightFg; - color: var(--lightFg, $fallback--lightFg); - } -} - -.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); -} - .post-status-form, .login { .form-bottom { display: flex; padding: 0.5em; height: 32px; - button { - width: 10em; - } - p { margin: 0.35em; padding: 0.35em; @@ -139,14 +118,49 @@ text-align: center; } + .media-upload-wrapper { + flex: 0 0 auto; + max-width: 100%; + min-width: 50px; + margin-right: .2em; + margin-bottom: .5em; + + .icon-cancel { + display: inline-block; + position: static; + margin: 0; + padding-bottom: 0; + margin-left: $fallback--attachmentRadius; + margin-left: var(--attachmentRadius, $fallback--attachmentRadius); + background-color: $fallback--btn; + background-color: var(--btn, $fallback--btn); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } + .attachments { padding: 0 0.5em; .attachment { + margin: 0; position: relative; + flex: 0 0 auto; border: 1px solid $fallback--border; border: 1px solid var(--border, $fallback--border); - margin: 0.5em 0.8em 0.2em 0; + text-align: center; + + audio { + min-width: 300px; + flex: 1 0 auto; + } + + a { + display: block; + text-align: left; + line-height: 1.2; + padding: .5em; + } } i { diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index a26111d6..c85ef59f 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -1,3 +1,4 @@ +/* eslint-env browser */ import StyleSwitcher from '../style_switcher/style_switcher.vue' import { filter, trim } from 'lodash' @@ -7,11 +8,22 @@ const settings = { hideAttachmentsLocal: this.$store.state.config.hideAttachments, hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv, hideNsfwLocal: this.$store.state.config.hideNsfw, + loopVideoLocal: this.$store.state.config.loopVideo, + loopVideoSilentOnlyLocal: this.$store.state.config.loopVideoSilentOnly, muteWordsString: this.$store.state.config.muteWords.join('\n'), autoLoadLocal: this.$store.state.config.autoLoad, streamingLocal: this.$store.state.config.streaming, + pauseOnUnfocusedLocal: this.$store.state.config.pauseOnUnfocused, hoverPreviewLocal: this.$store.state.config.hoverPreview, - stopGifs: this.$store.state.config.stopGifs + collapseMessageWithSubjectLocal: this.$store.state.config.collapseMessageWithSubject, + stopGifs: this.$store.state.config.stopGifs, + loopSilentAvailable: + // Firefox + Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || + // Chrome-likes + Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || + // Future spec, still not supported in Nightly 63 as of 08/2018 + Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks') } }, components: { @@ -32,12 +44,21 @@ const settings = { hideNsfwLocal (value) { this.$store.dispatch('setOption', { name: 'hideNsfw', value }) }, + loopVideoLocal (value) { + this.$store.dispatch('setOption', { name: 'loopVideo', value }) + }, + loopVideoSilentOnlyLocal (value) { + this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value }) + }, autoLoadLocal (value) { this.$store.dispatch('setOption', { name: 'autoLoad', value }) }, streamingLocal (value) { this.$store.dispatch('setOption', { name: 'streaming', value }) }, + pauseOnUnfocusedLocal (value) { + this.$store.dispatch('setOption', { name: 'pauseOnUnfocused', value }) + }, hoverPreviewLocal (value) { this.$store.dispatch('setOption', { name: 'hoverPreview', value }) }, @@ -45,6 +66,9 @@ const settings = { value = filter(value.split('\n'), (word) => trim(word).length > 0) this.$store.dispatch('setOption', { name: 'muteWords', value }) }, + collapseMessageWithSubjectLocal (value) { + this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value }) + }, stopGifs (value) { this.$store.dispatch('setOption', { name: 'stopGifs', value }) } diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 6245e758..170f5773 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -1,53 +1,81 @@ <template> - <div class="settings panel panel-default"> - <div class="panel-heading"> - {{$t('settings.settings')}} +<div class="settings panel panel-default"> + <div class="panel-heading"> + {{$t('settings.settings')}} + </div> + <div class="panel-body"> + <div class="setting-item"> + <h2>{{$t('settings.theme')}}</h2> + <style-switcher></style-switcher> </div> - <div class="panel-body"> - <div class="setting-item"> - <h2>{{$t('settings.theme')}}</h2> - <style-switcher></style-switcher> - </div> - <div class="setting-item"> - <h2>{{$t('settings.filtering')}}</h2> - <p>{{$t('settings.filtering_explanation')}}</p> - <textarea id="muteWords" v-model="muteWordsString"></textarea> - </div> - <div class="setting-item"> - <h2>{{$t('settings.attachments')}}</h2> - <ul class="setting-list"> - <li> - <input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal"> - <label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label> - </li> - <li> - <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal"> - <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label> - </li> - <li> - <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal"> - <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label> - </li> - <li> - <input type="checkbox" id="autoload" v-model="autoLoadLocal"> - <label for="autoload">{{$t('settings.autoload')}}</label> - </li> - <li> - <input type="checkbox" id="streaming" v-model="streamingLocal"> - <label for="streaming">{{$t('settings.streaming')}}</label> - </li> + <div class="setting-item"> + <h2>{{$t('settings.filtering')}}</h2> + <p>{{$t('settings.filtering_explanation')}}</p> + <textarea id="muteWords" v-model="muteWordsString"></textarea> + </div> + <div class="setting-item"> + <h2>{{$t('nav.timeline')}}</h2> + <ul class="setting-list"> + <li> + <input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal"> + <label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}}</label> + </li> + <li> + <input type="checkbox" id="streaming" v-model="streamingLocal"> + <label for="streaming">{{$t('settings.streaming')}}</label> + <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]"> <li> - <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal"> - <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label> + <input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal"> + <label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</label> </li> + </ul> + </li> + <li> + <input type="checkbox" id="autoload" v-model="autoLoadLocal"> + <label for="autoload">{{$t('settings.autoload')}}</label> + </li> + <li> + <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal"> + <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{$t('settings.attachments')}}</h2> + <ul class="setting-list"> + <li> + <input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal"> + <label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label> + </li> + <li> + <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal"> + <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label> + </li> + <li> + <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal"> + <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label> + </li> + <li> + <input type="checkbox" id="stopGifs" v-model="stopGifs"> + <label for="stopGifs">{{$t('settings.stop_gifs')}}</label> + </li> + <li> + <input type="checkbox" id="loopVideo" v-model="loopVideoLocal"> + <label for="loopVideo">{{$t('settings.loop_video')}}</label> + <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]"> <li> - <input type="checkbox" id="stopGifs" v-model="stopGifs"> - <label for="stopGifs">{{$t('settings.stop_gifs')}}</label> + <input :disabled="!loopVideoLocal || !loopSilentAvailable" type="checkbox" id="loopVideoSilentOnly" v-model="loopVideoSilentOnlyLocal"> + <label for="loopVideoSilentOnly">{{$t('settings.loop_video_silent_only')}}</label> + <div v-if="!loopSilentAvailable" class="unavailable"> + <i class="icon-globe"/>! {{$t('settings.limited_availability')}} + </div> </li> - </ul> - </div> + </ul> + </li> + </ul> </div> </div> +</div> </template> <script src="./settings.js"> @@ -67,6 +95,12 @@ height: 100px; } + .unavailable, + .unavailable i { + color: var(--cRed, $fallback--cRed); + color: $fallback--cRed; + } + .old-avatar { width: 128px; border-radius: $fallback--avatarRadius; @@ -83,14 +117,16 @@ .btn { margin-top: 1em; - min-height: 28px; - width: 10em; } } .setting-list { list-style-type: none; + padding-left: 2em; li { margin-bottom: 0.5em; } + .suboptions { + margin-top: 0.3em + } } </style> diff --git a/src/components/status/status.js b/src/components/status/status.js index a2d6f41f..9670f69d 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -22,15 +22,18 @@ const Status = { 'noHeading', 'inlineExpanded' ], - data: () => ({ - replying: false, - expanded: false, - unmuted: false, - userExpanded: false, - preview: null, - showPreview: false, - showingTall: false - }), + data () { + return { + replying: false, + expanded: false, + unmuted: false, + userExpanded: false, + preview: null, + showPreview: false, + showingTall: false, + expandingSubject: !this.$store.state.config.collapseMessageWithSubject + } + }, computed: { muteWords () { return this.$store.state.config.muteWords @@ -98,12 +101,27 @@ const Status = { // // Using max-height + overflow: auto for status components resulted in false positives // very often with japanese characters, and it was very annoying. + tallStatus () { + const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80 + return lengthScore > 20 + }, + hideSubjectStatus () { + if (this.tallStatus && !this.$store.state.config.collapseMessageWithSubject) { + return false + } + return !this.expandingSubject && this.status.summary + }, hideTallStatus () { + if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) { + return false + } if (this.showingTall) { return false } - const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80 - return lengthScore > 20 + return this.tallStatus + }, + showingMore () { + return this.showingTall || (this.status.summary && this.expandingSubject) }, attachmentSize () { if ((this.$store.state.config.hideAttachments && !this.inConversation) || @@ -163,8 +181,16 @@ const Status = { toggleUserExpanded () { this.userExpanded = !this.userExpanded }, - toggleShowTall () { - this.showingTall = !this.showingTall + toggleShowMore () { + if (this.showingTall) { + this.showingTall = false + } else if (this.expandingSubject) { + this.expandingSubject = false + } else if (this.hideTallStatus) { + this.showingTall = true + } else if (this.hideSubjectStatus) { + this.expandingSubject = true + } }, replyEnter (id, event) { this.showPreview = true diff --git a/src/components/status/status.vue b/src/components/status/status.vue index e2fb5d36..e7d5ed7a 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -11,8 +11,8 @@ <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" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a> - <a v-else :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a> + <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> @@ -57,8 +57,10 @@ <router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }"> <timeago :since="status.created_at" :auto-update="60"></timeago> </router-link> - <span v-if="status.visibility"><i :class="visibilityIcon(status.visibility)"></i> </span> - <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext"></i></a> + <div class="visibility-icon" v-if="status.visibility"> + <i :class="visibilityIcon(status.visibility)"></i> + </div> + <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext-alt"></i></a> <template v-if="expandable"> <a href="#" @click.prevent="toggleExpanded"><i class="icon-plus-squared"></i></a> </template> @@ -74,9 +76,11 @@ </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="toggleShowTall">Show more</a> - <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div> - <a v-if="showingTall" href="#" class="tall-status-unhider" @click.prevent="toggleShowTall">Show less</a> + <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' class='attachments media-body'> @@ -141,6 +145,7 @@ margin-top: 0.25em; margin-left: 0.5em; z-index: 50; + .status { flex: 1; border: 0; @@ -155,6 +160,7 @@ text-align: center; border-width: 1px; border-style: solid; + i { font-size: 2em; } @@ -196,6 +202,7 @@ .media-heading { flex-wrap: nowrap; + line-height: 18px; } .media-heading-left { @@ -218,12 +225,22 @@ flex: 1 0; display: flex; flex-wrap: wrap; - align-content: center; + align-items: baseline; + + .user-name { + margin-right: .45em; + + img { + width: 14px; + height: 14px; + vertical-align: middle; + object-fit: contain + } + } } + .links { display: flex; - padding-top: 1px; - margin-left: 0.2em; font-size: 12px; color: $fallback--link; color: var(--link, $fallback--link); @@ -247,19 +264,25 @@ } .media-heading-right { + display: inline-flex; flex-shrink: 0; - display: flex; flex-wrap: nowrap; - max-height: 1.5em; - margin-left: 0.25em; + margin-left: .25em; + align-self: baseline; + .timeago { margin-right: 0.2em; font-size: 12px; - padding-top: 1px; + align-self: last baseline; } - i { + + > * { margin-left: 0.2em; } + a:hover i { + color: $fallback--fg; + color: var(--fg, $fallback--fg); + } } a { @@ -289,7 +312,7 @@ } } - .tall-status-unhider { + .status-unhider, .cw-status-hider { width: 100%; text-align: center; } @@ -318,6 +341,7 @@ .retweet-info { padding: 0.4em 0.6em 0 0.6em; margin: 0; + .avatar { border-radius: $fallback--avatarAltRadius; border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); @@ -333,9 +357,22 @@ 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; diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 6f4845c4..95c15b49 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -5,6 +5,7 @@ export default { return { availableStyles: [], selected: this.$store.state.config.theme, + invalidThemeImported: false, bgColorLocal: '', btnColorLocal: '', textColorLocal: '', @@ -32,25 +33,61 @@ export default { }) }, mounted () { - this.bgColorLocal = rgbstr2hex(this.$store.state.config.colors.bg) - this.btnColorLocal = rgbstr2hex(this.$store.state.config.colors.btn) - this.textColorLocal = rgbstr2hex(this.$store.state.config.colors.fg) - this.linkColorLocal = rgbstr2hex(this.$store.state.config.colors.link) - - this.redColorLocal = rgbstr2hex(this.$store.state.config.colors.cRed) - this.blueColorLocal = rgbstr2hex(this.$store.state.config.colors.cBlue) - this.greenColorLocal = rgbstr2hex(this.$store.state.config.colors.cGreen) - this.orangeColorLocal = rgbstr2hex(this.$store.state.config.colors.cOrange) - - this.btnRadiusLocal = this.$store.state.config.radii.btnRadius || 4 - this.inputRadiusLocal = this.$store.state.config.radii.inputRadius || 4 - this.panelRadiusLocal = this.$store.state.config.radii.panelRadius || 10 - this.avatarRadiusLocal = this.$store.state.config.radii.avatarRadius || 5 - this.avatarAltRadiusLocal = this.$store.state.config.radii.avatarAltRadius || 50 - this.tooltipRadiusLocal = this.$store.state.config.radii.tooltipRadius || 2 - this.attachmentRadiusLocal = this.$store.state.config.radii.attachmentRadius || 5 + this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii) }, 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) + }, + + importTheme () { + this.invalidThemeImported = 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) + 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]) + } + }) + + document.body.appendChild(filePicker) + filePicker.click() + document.body.removeChild(filePicker) + }, + setCustomTheme () { if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) { // reset to picked themes @@ -95,6 +132,26 @@ export default { attachmentRadius: this.attachmentRadiusLocal }}) } + }, + + normalizeLocalState (colors, radii) { + this.bgColorLocal = rgbstr2hex(colors.bg) + this.btnColorLocal = rgbstr2hex(colors.btn) + this.textColorLocal = rgbstr2hex(colors.fg) + this.linkColorLocal = rgbstr2hex(colors.link) + + this.redColorLocal = rgbstr2hex(colors.cRed) + this.blueColorLocal = rgbstr2hex(colors.cBlue) + this.greenColorLocal = rgbstr2hex(colors.cGreen) + this.orangeColorLocal = rgbstr2hex(colors.cOrange) + + 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 } }, watch: { diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 112bbc1e..59bd2971 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -11,6 +11,11 @@ <i class="icon-down-open"/> </label> </div> + <div> + <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> <div class="color-container"> <p>{{$t('settings.theme_help')}}</p> <div class="color-item"> @@ -134,6 +139,11 @@ margin-right: 1em; } +.import-warning { + color: $fallback--cRed; + color: var(--cRed, $fallback--cRed); +} + .radius-container, .color-container { display: flex; diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 5c179567..a651f619 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -133,7 +133,10 @@ const Timeline = { } if (count > 0) { // only 'stream' them when you're scrolled to the top - if (window.pageYOffset < 15 && !this.paused && !this.unfocused) { + if (window.pageYOffset < 15 && + !this.paused && + !(this.unfocused && this.$store.state.config.pauseOnUnfocused) + ) { this.showNewStatuses() } else { this.paused = true diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index 443e63dd..f046885e 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -6,6 +6,7 @@ const UserSettings = { newname: this.$store.state.users.currentUser.name, newbio: this.$store.state.users.currentUser.description, newlocked: this.$store.state.users.currentUser.locked, + newdefaultScope: this.$store.state.users.currentUser.default_scope, followList: null, followImportError: false, followsImported: false, @@ -17,7 +18,8 @@ const UserSettings = { deleteAccountError: false, changePasswordInputs: [ '', '', '' ], changedPassword: false, - changePasswordError: false + changePasswordError: false, + activeTab: 'profile' } }, components: { @@ -29,6 +31,17 @@ const UserSettings = { }, pleromaBackend () { return this.$store.state.config.pleromaBackend + }, + scopeOptionsEnabled () { + return this.$store.state.config.scopeOptionsEnabled + }, + vis () { + return { + public: { selected: this.newdefaultScope === 'public' }, + unlisted: { selected: this.newdefaultScope === 'unlisted' }, + private: { selected: this.newdefaultScope === 'private' }, + direct: { selected: this.newdefaultScope === 'direct' } + } } }, methods: { @@ -36,12 +49,18 @@ const UserSettings = { const name = this.newname const description = this.newbio const locked = this.newlocked - this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked}}).then((user) => { + /* eslint-disable camelcase */ + const default_scope = this.newdefaultScope + this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope}}).then((user) => { if (!user.error) { this.$store.commit('addNewUsers', [user]) this.$store.commit('setCurrentUser', user) } }) + /* eslint-enable camelcase */ + }, + changeVis (visibility) { + this.newdefaultScope = visibility }, uploadFile (slot, e) { const file = e.target.files[0] @@ -217,6 +236,9 @@ const UserSettings = { this.changePasswordError = res.error } }) + }, + activateTab (tabName) { + this.activeTab = tabName } } } diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 881b0fa1..c3ca1dbd 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -4,19 +4,33 @@ {{$t('settings.user_settings')}} </div> <div class="panel-body profile-edit"> - <div class="setting-item"> + <div class="tab-switcher"> + <button class="btn btn-default" @click="activateTab('profile')">{{$t('settings.profile_tab')}}</button> + <button class="btn btn-default" @click="activateTab('security')">{{$t('settings.security_tab')}}</button> + <button class="btn btn-default" @click="activateTab('data_import_export')" v-if="pleromaBackend">{{$t('settings.data_import_export_tab')}}</button> + </div> + <div class="setting-item" v-if="activeTab == 'profile'"> <h2>{{$t('settings.name_bio')}}</h2> <p>{{$t('settings.name')}}</p> <input class='name-changer' id='username' v-model="newname"></input> <p>{{$t('settings.bio')}}</p> <textarea class="bio" v-model="newbio"></textarea> - <div class="setting-item"> + <p> <input type="checkbox" v-model="newlocked" id="account-locked"> <label for="account-locked">{{$t('settings.lock_account_description')}}</label> + </p> + <div v-if="scopeOptionsEnabled"> + <label for="default-vis">{{$t('settings.default_vis')}}</label> + <div id="default-vis" class="visibility-tray"> + <i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i> + <i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i> + <i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i> + <i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i> + </div> </div> <button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button> </div> - <div class="setting-item"> + <div class="setting-item" v-if="activeTab == 'profile'"> <h2>{{$t('settings.avatar')}}</h2> <p>{{$t('settings.current_avatar')}}</p> <img :src="user.profile_image_url_original" class="old-avatar"></img> @@ -29,7 +43,7 @@ <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"> + <div class="setting-item" v-if="activeTab == 'profile'"> <h2>{{$t('settings.profile_banner')}}</h2> <p>{{$t('settings.current_profile_banner')}}</p> <img :src="user.cover_photo" class="banner"></img> @@ -42,7 +56,7 @@ <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"> + <div class="setting-item" v-if="activeTab == 'profile'"> <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]"> @@ -53,7 +67,7 @@ <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 class="setting-item"> + <div class="setting-item" v-if="activeTab == 'security'"> <h2>{{$t('settings.change_password')}}</h2> <div> <p>{{$t('settings.current_password')}}</p> @@ -72,7 +86,7 @@ <p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p> <p v-if="changePasswordError">{{changePasswordError}}</p> </div> - <div class="setting-item" v-if="pleromaBackend"> + <div class="setting-item" v-if="pleromaBackend && activeTab == 'data_import_export'"> <h2>{{$t('settings.follow_import')}}</h2> <p>{{$t('settings.import_followers_from_a_csv_file')}}</p> <form v-model="followImportForm"> @@ -89,15 +103,15 @@ <p>{{$t('settings.follow_import_error')}}</p> </div> </div> - <div class="setting-item" v-if="enableFollowsExport"> + <div class="setting-item" v-if="enableFollowsExport && activeTab == 'data_import_export'"> <h2>{{$t('settings.follow_export')}}</h2> <button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button> </div> - <div class="setting-item" v-else> + <div class="setting-item" v-else-if="activeTab == 'data_import_export'"> <h2>{{$t('settings.follow_export_processing')}}</h2> </div> <hr> - <div class="setting-item"> + <div class="setting-item" v-if="activeTab == 'security'"> <h2>{{$t('settings.delete_account')}}</h2> <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p> <div v-if="deletingAccount"> @@ -137,4 +151,13 @@ margin: 0.25em; } } + +.tab-switcher { + margin: 7px 7px; + display: inline-block; + + button { + height: 30px; + } +} </style> |
