diff options
Diffstat (limited to 'src')
27 files changed, 763 insertions, 426 deletions
@@ -18,7 +18,14 @@ export default { ChatPanel }, data: () => ({ - mobileActivePanel: 'timeline' + mobileActivePanel: 'timeline', + supportsMask: window.CSS && window.CSS.supports && ( + window.CSS.supports('mask-size', 'contain') || + window.CSS.supports('-webkit-mask-size', 'contain') || + window.CSS.supports('-moz-mask-size', 'contain') || + window.CSS.supports('-ms-mask-size', 'contain') || + window.CSS.supports('-o-mask-size', 'contain') + ) }), created () { // Load the locale from the storage @@ -29,7 +36,27 @@ export default { background () { return this.currentUser.background_image || this.$store.state.config.background }, - logoStyle () { return { 'background-image': `url(${this.$store.state.config.logo})` } }, + enableMask () { return this.supportsMask && this.$store.state.config.logoMask }, + logoStyle () { + return { + 'visibility': this.enableMask ? 'hidden' : 'visible' + } + }, + logoMaskStyle () { + return this.enableMask ? { + 'mask-image': `url(${this.$store.state.config.logo})` + } : { + 'background-color': this.enableMask ? '' : 'transparent' + } + }, + logoBgStyle () { + return Object.assign({ + 'margin': `${this.$store.state.config.logoMargin} 0` + }, this.enableMask ? {} : { + 'background-color': this.enableMask ? '' : 'transparent' + }) + }, + logo () { return this.$store.state.config.logo }, style () { return { 'background-image': `url(${this.background})` } }, sitename () { return this.$store.state.config.name }, chat () { return this.$store.state.chat.channel.state === 'joined' }, diff --git a/src/App.scss b/src/App.scss index becea1c9..70769fad 100644 --- a/src/App.scss +++ b/src/App.scss @@ -48,7 +48,7 @@ a { color: var(--link, $fallback--link); } -button{ +button { user-select: none; color: $fallback--fg; color: var(--fg, $fallback--fg); @@ -64,10 +64,19 @@ button{ font-size: 14px; font-family: sans-serif; + &::-moz-focus-inner { + border: none; + } + &:hover { box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3); } + &:active { + 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; @@ -105,6 +114,7 @@ input, textarea, .select { position: relative; height: 29px; line-height: 16px; + hyphens: none; .icon-down-open { position: absolute; @@ -226,6 +236,36 @@ nav { 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(--fg, $fallback--fg); + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + img { + display: block; + flex: 0; + } + } + .inner-nav { padding-left: 20px; padding-right: 20px; @@ -234,9 +274,6 @@ nav { flex-basis: 970px; margin: auto; height: 50px; - background-repeat: no-repeat; - background-position: center; - background-size: auto 80%; a i { color: $fallback--link; @@ -282,15 +319,25 @@ main-router { } .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: 0.6em 1.0em; + padding: .6em .6em; text-align: left; font-size: 1.3em; line-height: 24px; background-color: $fallback--btn; background-color: var(--btn, $fallback--btn); + align-items: baseline; + + .title { + flex: 1 0 auto; + } + + button { + height: 100%; + } } .panel-heading.stub { @@ -464,4 +511,3 @@ nav { border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); } - diff --git a/src/App.vue b/src/App.vue index 71e90289..fc446c57 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,11 @@ <template> <div id="app" v-bind:style="style"> <nav class='container' @click="scrollToTop()" id="nav"> - <div class='inner-nav' :style="logoStyle"> + <div class='logo' :style='logoBgStyle'> + <div class='mask' :style='logoMaskStyle'></div> + <img :src='logo' :style='logoStyle'> + </div> + <div class='inner-nav'> <div class='item'> <router-link :to="{ name: 'root'}">{{sitename}}</router-link> </div> diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index cc19714d..41730720 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -16,7 +16,7 @@ const Attachment = { loopVideo: this.$store.state.config.loopVideo, showHidden: false, loading: false, - img: this.type === 'image' && document.createElement('img') + img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img') } }, components: { diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index bbb43679..8795b131 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -51,6 +51,10 @@ .nsfw-placeholder { cursor: pointer; + + &.loading { + cursor: progress; + } } .small-attachment { @@ -61,6 +65,7 @@ } .attachment { + position: relative; flex: 1 0 30%; margin: 0.5em 0.7em 0.6em 0.0em; align-self: flex-start; @@ -88,10 +93,6 @@ display: flex; } - &.loading { - cursor: progress; - } - .hider { position: absolute; margin: 10px; diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index bfcd3fe7..e41929fd 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -1,8 +1,8 @@ <template> <div class="timeline panel panel-default"> <div class="panel-heading conversation-heading"> - {{ $t('timeline.conversation') }} - <span v-if="collapsable" style="float:right;"> + <span class="title"> {{ $t('timeline.conversation') }} </span> + <span v-if="collapsable"> <small><a href="#" @click.prevent="$emit('toggleExpanded')">{{ $t('timeline.collapse') }}</a></small> </span> </div> diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js index 8b4e7ad4..66337c3f 100644 --- a/src/components/media_upload/media_upload.js +++ b/src/components/media_upload/media_upload.js @@ -6,8 +6,10 @@ const mediaUpload = { const input = this.$el.querySelector('input') input.addEventListener('change', ({target}) => { - const file = target.files[0] - this.uploadFile(file) + for (var i = 0; i < target.files.length; i++) { + let file = target.files[i] + this.uploadFile(file) + } }) }, data () { diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index 8b931d2d..88094ebb 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -3,7 +3,7 @@ <label class="btn btn-default"> <i class="icon-spin4 animate-spin" v-if="uploading"></i> <i class="icon-upload" v-if="!uploading"></i> - <input type=file style="position: fixed; top: -100em"></input> + <input type="file" style="position: fixed; top: -100em" multiple="true"></input> </label> </div> </template> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index b24250b0..58956f98 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -11,6 +11,14 @@ const Notifications = { notificationsFetcher.startFetching({ store, credentials }) }, computed: { + visibleTypes () { + return [ + this.$store.state.config.notificationVisibility.likes && 'like', + this.$store.state.config.notificationVisibility.mentions && 'mention', + this.$store.state.config.notificationVisibility.repeats && 'repeat', + this.$store.state.config.notificationVisibility.follows && 'follow' + ].filter(_ => _) + }, notifications () { return this.$store.state.statuses.notifications.data }, @@ -18,13 +26,13 @@ const Notifications = { return this.$store.state.statuses.notifications.error }, unseenNotifications () { - return filter(this.notifications, ({seen}) => !seen) + return filter(this.visibleNotifications, ({seen}) => !seen) }, visibleNotifications () { // Don't know why, but sortBy([seen, -action.id]) doesn't work. let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id) sortedNotifications = sortBy(sortedNotifications, 'seen') - return sortedNotifications + return sortedNotifications.filter((notification) => this.visibleTypes.includes(notification.type)) }, unseenCount () { return this.unseenNotifications.length diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 5b09685b..4dbceede 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -4,58 +4,25 @@ // a bit of a hack to allow scrolling below notifications padding-bottom: 15em; - .title { - display: inline-block; - } - - .panel { - background: $fallback--bg; - background: var(--bg, $fallback--bg) - } - - .panel-body { - border-color: $fallback--border; - border-color: var(--border, $fallback--border) - } - - .panel-heading { - // force the text to stay centered, while keeping - // the button in the right side of the panel heading - position: relative; - background: $fallback--btn; - background: var(--btn, $fallback--btn); - color: $fallback--fg; - color: var(--fg, $fallback--fg); - display: flex; - align-items: baseline; - .read-button { - position: absolute; - right: 0.7em; - height: 1.8em; - line-height: 100%; - } - } - .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); - min-width: 1.3em; - border-radius: 1.3em; - margin: 0 0.2em 0 -0.4em; + border-radius: 99px; + min-width: 22px; + max-width: 22px; + min-height: 22px; + max-height: 22px; color: white; - font-size: 0.9em; + font-size: 15px; + line-height: 22px; text-align: center; - line-height: 1.3em; + vertical-align: middle } .loadmore-error { - position: absolute; - right: 0.6em; - font-size: 14px; min-width: 6em; - font-family: sans-serif; text-align: center; padding: 0 0.25em 0 0.25em; margin: 0; @@ -73,7 +40,8 @@ box-sizing: border-box; display: flex; border-bottom: 1px solid; - border-bottom-color: inherit; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); .broken-favorite { border-radius: $fallback--tooltipRadius; diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index a0b0e5f5..7a4322f9 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -2,8 +2,10 @@ <div class="notifications"> <div class="panel panel-default"> <div class="panel-heading"> - <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span> - <div class="title"> {{$t('notifications.notifications')}}</div> + <div class="title"> + {{$t('notifications.notifications')}} + <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span> + </div> <div @click.prevent class="loadmore-error alert error" v-if="error"> {{$t('timeline.error_fetching')}} </div> diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index f5b00599..ee5722bd 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -1,7 +1,12 @@ <template> - <div v-if="loggedIn && visibility !== 'private' && visibility !== 'direct'"> - <i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i> - <span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span> + <div v-if="loggedIn"> + <template v-if="visibility !== 'private' && visibility !== 'direct'"> + <i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i> + <span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span> + </template> + <template v-else> + <i :class='classes' class='icon-lock' :title="$t('timeline.no_retweet_hint')"></i> + </template> </div> <div v-else-if="!loggedIn"> <i :class='classes' class='icon-retweet'></i> diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index f8eaad00..de12894b 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -1,4 +1,5 @@ /* eslint-env browser */ +import TabSwitcher from '../tab_switcher/tab_switcher.jsx' import StyleSwitcher from '../style_switcher/style_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' import { filter, trim } from 'lodash' @@ -9,6 +10,7 @@ const settings = { hideAttachmentsLocal: this.$store.state.config.hideAttachments, hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv, hideNsfwLocal: this.$store.state.config.hideNsfw, + notificationVisibilityLocal: this.$store.state.config.notificationVisibility, replyVisibilityLocal: this.$store.state.config.replyVisibility, loopVideoLocal: this.$store.state.config.loopVideo, loopVideoSilentOnlyLocal: this.$store.state.config.loopVideoSilentOnly, @@ -29,6 +31,7 @@ const settings = { } }, components: { + TabSwitcher, StyleSwitcher, InterfaceLanguageSwitcher }, @@ -47,6 +50,18 @@ const settings = { hideNsfwLocal (value) { this.$store.dispatch('setOption', { name: 'hideNsfw', value }) }, + 'notificationVisibilityLocal.likes' (value) { + this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) + }, + 'notificationVisibilityLocal.follows' (value) { + this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) + }, + 'notificationVisibilityLocal.repeats' (value) { + this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) + }, + 'notificationVisibilityLocal.mentions' (value) { + this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) + }, replyVisibilityLocal (value) { this.$store.dispatch('setOption', { name: 'replyVisibility', value }) }, diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index f500a1b0..c106b79c 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -4,90 +4,132 @@ {{$t('settings.settings')}} </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('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}]"> + <tab-switcher> + <div :label="$t('settings.general')" > + <div class="setting-item"> + <h2>{{ $t('settings.interfaceLanguage') }}</h2> + <interface-language-switcher /> + </div> + <div class="setting-item"> + <h2>{{$t('nav.timeline')}}</h2> + <ul class="setting-list"> <li> - <input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal"> - <label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</label> + <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 :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> - </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> - <li> - <label for="replyVisibility" class="select"> - <select id="replyVisibility" v-model="replyVisibilityLocal"> - <option value="all" selected>{{$t('settings.reply_visibility_all')}}</option> - <option value="following">{{$t('settings.reply_visibility_following')}}</option> - <option value="self">{{$t('settings.reply_visibility_self')}}</option> - </select> - <i class="icon-down-open"/> - </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}]"> + </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 :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> + <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 :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> </li> </ul> - </li> - </ul> - </div> - <div class="setting-item"> - <h2>{{ $t('settings.interfaceLanguage') }}</h2> - <interface-language-switcher /> - </div> + </div> + </div> + + <div :label="$t('settings.theme')" > + <div class="setting-item"> + <style-switcher></style-switcher> + </div> + </div> + + <div :label="$t('settings.filtering')" > + <div class="setting-item"> + <div class="select-multiple"> + <span class="label">{{$t('settings.notification_visibility')}}</span> + <ul class="option-list"> + <li> + <input type="checkbox" id="notification-visibility-likes" v-model="notificationVisibilityLocal.likes"> + <label for="notification-visibility-likes"> + {{$t('settings.notification_visibility_likes')}} + </label> + </li> + <li> + <input type="checkbox" id="notification-visibility-repeats" v-model="notificationVisibilityLocal.repeats"> + <label for="notification-visibility-repeats"> + {{$t('settings.notification_visibility_repeats')}} + </label> + </li> + <li> + <input type="checkbox" id="notification-visibility-follows" v-model="notificationVisibilityLocal.follows"> + <label for="notification-visibility-follows"> + {{$t('settings.notification_visibility_follows')}} + </label> + </li> + <li> + <input type="checkbox" id="notification-visibility-mentions" v-model="notificationVisibilityLocal.mentions"> + <label for="notification-visibility-mentions"> + {{$t('settings.notification_visibility_mentions')}} + </label> + </li> + </ul> + </label> + </div> + <div> + {{$t('settings.replies_in_timeline')}} + <label for="replyVisibility" class="select"> + <select id="replyVisibility" v-model="replyVisibilityLocal"> + <option value="all" selected>{{$t('settings.reply_visibility_all')}}</option> + <option value="following">{{$t('settings.reply_visibility_following')}}</option> + <option value="self">{{$t('settings.reply_visibility_self')}}</option> + </select> + <i class="icon-down-open"/> + </label> + </div> + </div> + <div class="setting-item"> + <p>{{$t('settings.filtering_explanation')}}</p> + <textarea id="muteWords" v-model="muteWordsString"></textarea> + </div> + </div> + + </tab-switcher> </div> </div> </template> @@ -103,6 +145,23 @@ margin: 1em 1em 1.4em; padding-bottom: 1.4em; + > div { + margin-bottom: .5em; + &:last-child { + margin-bottom: 0; + } + } + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 1em; + } + + select { + min-width: 10em; + } + textarea { width: 100%; @@ -130,12 +189,24 @@ } .btn { - margin-top: 1em; min-height: 28px; + } + + .submit { + margin-top: 1em; + min-height: 30px; width: 10em; } } -.setting-list { +.select-multiple { + display: flex; + .option-list { + margin: 0; + padding-left: .5em; + } +} +.setting-list, +.option-list{ list-style-type: none; padding-left: 2em; li { diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js index 0839aca5..5ad06dc2 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -18,7 +18,11 @@ const StillImage = { onLoad () { const canvas = this.$refs.canvas if (!canvas) return - canvas.getContext('2d').drawImage(this.$refs.src, 1, 1, canvas.width, canvas.height) + const width = this.$refs.src.naturalWidth + const height = this.$refs.src.naturalHeight + canvas.width = width + canvas.height = height + canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height) } } } diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index a37c678d..e23f8bc1 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -60,6 +60,7 @@ right: 0; width: 100%; height: 100%; + object-fit: contain; } } </style> diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 59bd2971..72a338bd 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -1,102 +1,30 @@ <template> - <div> - <div>{{$t('settings.presets')}} +<div> + <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> + <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> - <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> - <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 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> + + <div class="preview-container"> <div :style="{ '--btnRadius': btnRadiusLocal + 'px', '--inputRadius': inputRadiusLocal + 'px', @@ -127,8 +55,95 @@ </div> </div> </div> - <button class="btn" @click="setCustomTheme">{{$t('general.apply')}}</button> </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 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 class="apply-container"> + <button class="btn submit" @click="setCustomTheme">{{$t('general.apply')}}</button> + </div> +</div> </template> <script src="./style_switcher.js"></script> @@ -144,15 +159,19 @@ color: var(--cRed, $fallback--cRed); } +.apply-container, .radius-container, -.color-container { +.color-container, +.presets-container { display: flex; p { + flex: 2 0 100%; margin-top: 2em; margin-bottom: .5em; } } + .radius-container { flex-direction: column; } @@ -162,6 +181,36 @@ 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; @@ -229,6 +278,7 @@ flex: 0; min-width: 2em; cursor: pointer; + max-height: 29px; } .theme-preview-content { diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx new file mode 100644 index 00000000..3fff38f6 --- /dev/null +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -0,0 +1,44 @@ +import Vue from 'vue' + +import './tab_switcher.scss' + +export default Vue.component('tab-switcher', { + name: 'TabSwitcher', + data () { + return { + active: 0 + } + }, + methods: { + activateTab(index) { + return () => this.active = index; + } + }, + render(h) { + const tabs = this.$slots.default + .filter(slot => slot.data) + .map((slot, index) => { + const classes = ['tab'] + + if (index === this.active) { + classes.push('active') + } + 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> + ); + return ( + <div class="tab-switcher"> + <div class="tabs"> + {tabs} + </div> + <div class="contents"> + {contents} + </div> + </div> + ) + } +}) diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss new file mode 100644 index 00000000..374a19c5 --- /dev/null +++ b/src/components/tab_switcher/tab_switcher.scss @@ -0,0 +1,43 @@ +@import '../../_variables.scss'; + +.tab-switcher { + .tabs { + display: flex; + position: relative; + justify-content: center; + width: 100%; + overflow: hidden; + padding-top: 5px; + + &::after, &::before { + display: block; + content: ''; + flex: 1 1 auto; + } + + .tab, &::after, &::before { + border-bottom: 1px solid; + border-bottom-color: $fallback--btn; + border-bottom-color: var(--btn, $fallback--btn); + } + + .tab { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + padding: .3em 1em; + + &:not(.active) { + border-bottom: 1px solid; + border-bottom-color: $fallback--btn; + border-bottom-color: var(--btn, $fallback--btn); + z-index: 4; + } + + &.active { + background: transparent; + border-bottom: none; + z-index: 5; + } + } + } +} diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index c4e0fbce..e42c0c4b 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -57,36 +57,8 @@ @import '../../_variables.scss'; .timeline { - .timeline-heading { - position: relative; - display: flex; - } - - .title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 70%; - } - - .loadmore-button { - position: absolute; - right: 0.6em; - font-size: 14px; - - min-width: 6em; - height: 1.8em; - line-height: 100%; - } - .loadmore-text { - position: absolute; - right: 0.6em; font-size: 14px; - min-width: 6em; - font-family: sans-serif; - text-align: center; - padding: 0 0.5em 0 0.5em; opacity: 0.8; background-color: transparent; color: $fallback--faint; @@ -94,11 +66,8 @@ } .loadmore-error { - position: absolute; - right: 0.6em; font-size: 14px; min-width: 6em; - font-family: sans-serif; text-align: center; padding: 0 0.25em 0 0.25em; margin: 0; diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index f8502907..91d4acd2 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -17,6 +17,8 @@ padding-bottom: 10px; .panel-heading { background: transparent; + flex-direction: column; + align-items: stretch; } } </style> diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index f046885e..0b13a668 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -1,3 +1,4 @@ +import TabSwitcher from '../tab_switcher/tab_switcher.jsx' import StyleSwitcher from '../style_switcher/style_switcher.vue' const UserSettings = { @@ -23,7 +24,8 @@ const UserSettings = { } }, components: { - StyleSwitcher + StyleSwitcher, + TabSwitcher }, computed: { user () { diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index c3ca1dbd..9daafdce 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -4,126 +4,131 @@ {{$t('settings.user_settings')}} </div> <div class="panel-body profile-edit"> - <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> - <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> + <tab-switcher> + <div :label="$t('settings.profile_tab')"> + <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> + <p>{{$t('settings.bio')}}</p> + <textarea class="bio" v-model="newbio"></textarea> + <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"> + <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> + <div> + <input type="file" @change="uploadFile(0, $event)" ></input> + </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> + <div> + <input type="file" @change="uploadFile(1, $event)" ></input> + </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> + <div> + <input type="file" @change="uploadFile(2, $event)" ></input> + </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> - <button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button> - </div> - <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> - <p>{{$t('settings.set_new_avatar')}}</p> - <img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]"> - </img> - <div> - <input type="file" @change="uploadFile(0, $event)" ></input> - </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" 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> - <p>{{$t('settings.set_new_profile_banner')}}</p> - <img class="banner" v-bind:src="previews[1]" v-if="previews[1]"> - </img> - <div> - <input type="file" @change="uploadFile(1, $event)" ></input> - </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" 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]"> - </img> - <div> - <input type="file" @change="uploadFile(2, $event)" ></input> - </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 class="setting-item" v-if="activeTab == 'security'"> - <h2>{{$t('settings.change_password')}}</h2> - <div> - <p>{{$t('settings.current_password')}}</p> - <input type="password" v-model="changePasswordInputs[0]"> - </div> - <div> - <p>{{$t('settings.new_password')}}</p> - <input type="password" v-model="changePasswordInputs[1]"> - </div> - <div> - <p>{{$t('settings.confirm_new_password')}}</p> - <input type="password" v-model="changePasswordInputs[2]"> - </div> - <button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button> - <p v-if="changedPassword">{{$t('settings.changed_password')}}</p> - <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 && activeTab == 'data_import_export'"> - <h2>{{$t('settings.follow_import')}}</h2> - <p>{{$t('settings.import_followers_from_a_csv_file')}}</p> - <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> - <button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button> - <div v-if="followsImported"> - <i class="icon-cross" @click="dismissImported"></i> - <p>{{$t('settings.follows_imported')}}</p> - </div> - <div v-else-if="followImportError"> - <i class="icon-cross" @click="dismissImported"></i> - <p>{{$t('settings.follow_import_error')}}</p> + + <div :label="$t('settings.security_tab')"> + <div class="setting-item"> + <h2>{{$t('settings.change_password')}}</h2> + <div> + <p>{{$t('settings.current_password')}}</p> + <input type="password" v-model="changePasswordInputs[0]"> + </div> + <div> + <p>{{$t('settings.new_password')}}</p> + <input type="password" v-model="changePasswordInputs[1]"> + </div> + <div> + <p>{{$t('settings.confirm_new_password')}}</p> + <input type="password" v-model="changePasswordInputs[2]"> + </div> + <button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button> + <p v-if="changedPassword">{{$t('settings.changed_password')}}</p> + <p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p> + <p v-if="changePasswordError">{{changePasswordError}}</p> + </div> + + <div class="setting-item"> + <h2>{{$t('settings.delete_account')}}</h2> + <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p> + <div v-if="deletingAccount"> + <p>{{$t('settings.delete_account_instructions')}}</p> + <p>{{$t('login.password')}}</p> + <input type="password" v-model="deleteAccountConfirmPasswordInput"> + <button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button> + </div> + <p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p> + <p v-if="deleteAccountError">{{deleteAccountError}}</p> + <button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button> + </div> </div> - </div> - <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-if="activeTab == 'data_import_export'"> - <h2>{{$t('settings.follow_export_processing')}}</h2> - </div> - <hr> - <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"> - <p>{{$t('settings.delete_account_instructions')}}</p> - <p>{{$t('login.password')}}</p> - <input type="password" v-model="deleteAccountConfirmPasswordInput"> - <button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button> + + <div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend"> + <div class="setting-item"> + <h2>{{$t('settings.follow_import')}}</h2> + <p>{{$t('settings.import_followers_from_a_csv_file')}}</p> + <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> + <button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button> + <div v-if="followsImported"> + <i class="icon-cross" @click="dismissImported"></i> + <p>{{$t('settings.follows_imported')}}</p> + </div> + <div v-else-if="followImportError"> + <i class="icon-cross" @click="dismissImported"></i> + <p>{{$t('settings.follow_import_error')}}</p> + </div> + </div> + <div class="setting-item" v-if="enableFollowsExport"> + <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> + <h2>{{$t('settings.follow_export_processing')}}</h2> + </div> </div> - <p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p> - <p v-if="deleteAccountError">{{deleteAccountError}}</p> - <button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button> - </div> + </tab-switcher> </div> </div> </template> @@ -151,13 +156,4 @@ margin: 0.25em; } } - -.tab-switcher { - margin: 7px 7px; - display: inline-block; - - button { - height: 30px; - } -} </style> diff --git a/src/i18n/messages.js b/src/i18n/messages.js index bfe0d92b..42e7e9d4 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -48,8 +48,8 @@ const de = { settings: 'Einstellungen', theme: 'Farbschema', presets: 'Voreinstellungen', - export_theme: 'Aktuelles Theme exportieren', - import_theme: 'Gespeichertes Theme laden', + export_theme: 'Farbschema speichern', + import_theme: 'Farbschema laden', invalid_theme_imported: 'Die ausgewählte Datei ist kein unterstütztes Pleroma-Theme. Keine Änderungen wurden vorgenommen.', theme_help: 'Benutze HTML Farbcodes (#rrggbb) um dein Farbschema anzupassen', radii_help: 'Kantenrundung (in Pixel) der Oberfläche anpassen', @@ -273,9 +273,11 @@ const en = { load_older: 'Load older statuses', conversation: 'Conversation', collapse: 'Collapse', - repeated: 'repeated' + repeated: 'repeated', + no_retweet_hint: 'Post is marked as followers-only or direct and cannot be repeated' }, settings: { + general: 'General', user_settings: 'User Settings', name_bio: 'Name & Bio', name: 'Name', @@ -291,8 +293,8 @@ const en = { settings: 'Settings', theme: 'Theme', presets: 'Presets', - export_theme: 'Export current theme', - import_theme: 'Load saved theme', + export_theme: 'Save preset', + import_theme: 'Load preset', theme_help: 'Use hex color codes (#rrggbb) to customize your color theme.', invalid_theme_imported: 'The selected file is not a supported Pleroma theme. No changes to your theme were made.', radii_help: 'Set up interface edge rounding (in pixels)', @@ -325,9 +327,15 @@ const en = { loop_video: 'Loop videos', loop_video_silent_only: 'Loop only videos without sound (i.e. Mastodon\'s "gifs")', reply_link_preview: 'Enable reply-link preview on mouse hover', + replies_in_timeline: 'Replies in timeline', 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', + notification_visibility: 'Types of notifications to show', + notification_visibility_likes: 'Likes', + notification_visibility_mentions: 'Mentions', + notification_visibility_repeats: 'Repeats', + notification_visibility_follows: 'Follows', follow_import: 'Follow import', import_followers_from_a_csv_file: 'Import follows from a csv file', follows_imported: 'Follows imported! Processing them will take a while.', @@ -1623,9 +1631,11 @@ const ru = { load_older: 'Загрузить старые статусы', conversation: 'Разговор', collapse: 'Свернуть', - repeated: 'повторил(а)' + repeated: 'повторил(а)', + no_retweet_hint: 'Пост помечен как "только для подписчиков" или "личное" и поэтому не может быть повторён' }, settings: { + general: 'Общие', user_settings: 'Настройки пользователя', name_bio: 'Имя и описание', name: 'Имя', @@ -1640,11 +1650,11 @@ const ru = { set_new_profile_background: 'Загрузить новый фон профиля', settings: 'Настройки', theme: 'Тема', - export_theme: 'Экспортировать текущую тему', - import_theme: 'Загрузить сохранённую тему', + export_theme: 'Сохранить Тему', + import_theme: 'Загрузить Тему', presets: 'Пресеты', theme_help: 'Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.', - radii_help: 'Округление краёв элементов интерфейса (в пикселях)', + radii_help: 'Скругление углов элементов интерфейса (в пикселях)', background: 'Фон', foreground: 'Передний план', text: 'Текст', @@ -1673,6 +1683,15 @@ const ru = { loop_video: 'Зациливать видео', loop_video_silent_only: 'Зацикливать только беззвучные видео (т.е. "гифки" с Mastodon)', reply_link_preview: 'Включить предварительный просмотр ответа при наведении мыши', + replies_in_timeline: 'Ответы в ленте', + reply_visibility_all: 'Показывать все ответы', + reply_visibility_following: 'Показывать только ответы мне и тех на кого я подписан', + reply_visibility_self: 'Показывать только ответы мне', + notification_visibility: 'Показывать уведомления', + notification_visibility_likes: 'Лайки', + notification_visibility_mentions: 'Упоминания', + notification_visibility_repeats: 'Повторы', + notification_visibility_follows: 'Подписки', follow_import: 'Импортировать читаемых', import_followers_from_a_csv_file: 'Импортировать читаемых из файла .csv', follows_imported: 'Список читаемых импортирован. Обработка займёт некоторое время..', @@ -1725,7 +1744,18 @@ const ru = { }, post_status: { posting: 'Отправляется', - default: 'Что нового?' + content_warning: 'Тема (не обязательно)', + default: 'Что нового?', + account_not_locked_warning: 'Ваш аккаунт не {0}. Кто угодно может зафоловить вас чтобы прочитать посты только для подписчиков', + account_not_locked_warning_link: 'залочен', + direct_warning: 'Этот пост будет видет только упомянутым пользователям', + attachments_sensitive: 'Вложения содержат чувствительный контент', + scope: { + public: 'Публичный - этот пост виден всем', + unlisted: 'Непубличный - этот пост не виден на публичных лентах', + private: 'Для подписчиков - этот пост видят только подписчики', + direct: 'Личное - этот пост видят только те кто в нём упомянут' + } }, finder: { find_user: 'Найти пользователя', diff --git a/src/main.js b/src/main.js index 5258fbd9..72806d10 100644 --- a/src/main.js +++ b/src/main.js @@ -50,6 +50,7 @@ const persistedStateOptions = { 'config.hideAttachmentsInConv', 'config.hideNsfw', 'config.replyVisibility', + 'config.notificationVisibility', 'config.autoLoad', 'config.hoverPreview', 'config.streaming', @@ -102,23 +103,29 @@ window.fetch('/api/statusnet/config.json') .then((res) => res.json()) .then((data) => { var staticConfig = data - - var theme = (apiConfig.theme || staticConfig.theme) - var background = (apiConfig.background || staticConfig.background) - var logo = (apiConfig.logo || staticConfig.logo) - var redirectRootNoLogin = (apiConfig.redirectRootNoLogin || staticConfig.redirectRootNoLogin) - var redirectRootLogin = (apiConfig.redirectRootLogin || staticConfig.redirectRootLogin) - var chatDisabled = (apiConfig.chatDisabled || staticConfig.chatDisabled) - var showWhoToFollowPanel = (apiConfig.showWhoToFollowPanel || staticConfig.showWhoToFollowPanel) - var whoToFollowProvider = (apiConfig.whoToFollowProvider || staticConfig.whoToFollowProvider) - var whoToFollowLink = (apiConfig.whoToFollowLink || staticConfig.whoToFollowLink) - var showInstanceSpecificPanel = (apiConfig.showInstanceSpecificPanel || staticConfig.showInstanceSpecificPanel) - var scopeOptionsEnabled = (apiConfig.scopeOptionsEnabled || staticConfig.scopeOptionsEnabled) - var collapseMessageWithSubject = (apiConfig.collapseMessageWithSubject || staticConfig.collapseMessageWithSubject) + // This takes static config and overrides properties that are present in apiConfig + var config = Object.assign({}, staticConfig, apiConfig) + + var theme = (config.theme) + var background = (config.background) + var logo = (config.logo) + var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask) + var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin) + var redirectRootNoLogin = (config.redirectRootNoLogin) + var redirectRootLogin = (config.redirectRootLogin) + var chatDisabled = (config.chatDisabled) + var showWhoToFollowPanel = (config.showWhoToFollowPanel) + var whoToFollowProvider = (config.whoToFollowProvider) + var whoToFollowLink = (config.whoToFollowLink) + var showInstanceSpecificPanel = (config.showInstanceSpecificPanel) + var scopeOptionsEnabled = (config.scopeOptionsEnabled) + var collapseMessageWithSubject = (config.collapseMessageWithSubject) store.dispatch('setOption', { name: 'theme', value: theme }) store.dispatch('setOption', { name: 'background', value: background }) store.dispatch('setOption', { name: 'logo', value: logo }) + store.dispatch('setOption', { name: 'logoMask', value: logoMask }) + store.dispatch('setOption', { name: 'logoMargin', value: logoMargin }) store.dispatch('setOption', { name: 'showWhoToFollowPanel', value: showWhoToFollowPanel }) store.dispatch('setOption', { name: 'whoToFollowProvider', value: whoToFollowProvider }) store.dispatch('setOption', { name: 'whoToFollowLink', value: whoToFollowLink }) diff --git a/src/modules/config.js b/src/modules/config.js index ac163316..60a34bc1 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -18,6 +18,12 @@ const defaultState = { pauseOnUnfocused: true, stopGifs: false, replyVisibility: 'all', + notificationVisibility: { + follows: true, + mentions: true, + likes: true, + repeats: true + }, muteWords: [], highlight: {}, interfaceLanguage: browserLocale diff --git a/src/modules/statuses.js b/src/modules/statuses.js index ff2cb098..f980f53d 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -68,6 +68,15 @@ export const prepareStatus = (status) => { return status } +const visibleNotificationTypes = (rootState) => { + return [ + rootState.config.notificationVisibility.likes && 'like', + rootState.config.notificationVisibility.mentions && 'mention', + rootState.config.notificationVisibility.repeats && 'repeat', + rootState.config.notificationVisibility.follows && 'follow' + ].filter(_ => _) +} + export const statusType = (status) => { if (status.is_post_verb) { return 'status' @@ -86,8 +95,7 @@ export const statusType = (status) => { return 'deletion' } - // TODO change to status.activity_type === 'follow' when gs supports it - if (status.text.match(/started following/)) { + if (status.text.match(/started following/) || status.activity_type === 'follow') { return 'follow' } @@ -187,11 +195,11 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const favoriteStatus = (favorite, counter) => { const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) }) if (status) { - status.fave_num += 1 - // This is our favorite, so the relevant bit. if (favorite.user.id === user.id) { status.favorited = true + } else { + status.fave_num += 1 } } return status @@ -225,6 +233,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us }, 'favorite': (favorite) => { // Only update if this is a new favorite. + // Ignore our own favorites because we get info about likes as response to like request if (!state.favorites.has(favorite.id)) { state.favorites.add(favorite.id) favoriteStatus(favorite) @@ -268,7 +277,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } -const addNewNotifications = (state, { dispatch, notifications, older }) => { +const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => { const allStatuses = state.allStatuses const allStatusesObject = state.allStatusesObject each(notifications, (notification) => { @@ -317,7 +326,7 @@ const addNewNotifications = (state, { dispatch, notifications, older }) => { result.image = action.attachments[0].url } - if (fresh && !state.notifications.desktopNotificationSilence) { + if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) { let notification = new window.Notification(title, result) // Chrome is known for not closing notifications automatically // according to MDN, anyway. @@ -347,6 +356,11 @@ export const mutations = { const newStatus = state.allStatusesObject[status.id] newStatus.favorited = value }, + setFavoritedConfirm (state, { status }) { + const newStatus = state.allStatusesObject[status.id] + newStatus.favorited = status.favorited + newStatus.fave_num = status.fave_num + }, setRetweeted (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] newStatus.repeated = value @@ -399,7 +413,7 @@ const statuses = { commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser }) }, addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) { - commit('addNewNotifications', { dispatch, notifications, older }) + commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older }) }, setError ({ rootState, commit }, { value }) { commit('setError', { value }) @@ -424,11 +438,31 @@ const statuses = { // Optimistic favoriting... commit('setFavorited', { status, value: true }) apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then(status => { + commit('setFavoritedConfirm', { status }) + }) }, unfavorite ({ rootState, commit }, status) { // Optimistic favoriting... commit('setFavorited', { status, value: false }) apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then(status => { + commit('setFavoritedConfirm', { status }) + }) }, retweet ({ rootState, commit }, status) { // Optimistic retweeting... |
