From e5a34870f0f7154712783fb6d9c20edf4c06ad35 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 28 Dec 2019 15:55:42 +0200 Subject: Accent works --- src/App.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/App.scss') diff --git a/src/App.scss b/src/App.scss index 754ca62e..3b03a761 100644 --- a/src/App.scss +++ b/src/App.scss @@ -198,7 +198,7 @@ input, textarea, .select { &:checked + label::before { box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset; box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset; - background-color: var(--link, $fallback--link); + background-color: var(--accent, $fallback--link); } &:disabled { &, -- cgit v1.2.3-70-g09d2 From 6e11924c27288e590de8f5b675fb53704581bcc8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 28 Dec 2019 18:49:35 +0200 Subject: underlay customization, updated contrast calculations to account for alpha blending --- src/App.scss | 1 + src/components/style_switcher/style_switcher.js | 47 ++++++++++++++---------- src/components/style_switcher/style_switcher.vue | 14 +++++++ src/services/color_convert/color_convert.js | 23 ++++++++++++ src/services/style_setter/style_setter.js | 37 +++++++++++++------ 5 files changed, 91 insertions(+), 31 deletions(-) (limited to 'src/App.scss') diff --git a/src/App.scss b/src/App.scss index 3b03a761..b6d4943a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -32,6 +32,7 @@ h4 { min-height: 100vh; max-width: 980px; background-color: rgba(0,0,0,0.15); + background-color: var(--underlay, rgba(0,0,0,0.15)); align-content: flex-start; } diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index b450dc8e..602a635e 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -1,4 +1,4 @@ -import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js' +import { rgb2hex, hex2rgb, getContrastRatio, getContrastRatioLayers, alphaBlend } from '../../services/color_convert/color_convert.js' import { set, delete as del } from 'vue' import { merge } from 'lodash' import { generateCompat, generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js' @@ -53,6 +53,9 @@ export default { bgColorLocal: '', bgOpacityLocal: undefined, + underlayColorLocal: '', + underlayOpacityLocal: undefined, + fgColorLocal: '', fgTextColorLocal: undefined, fgLinkColorLocal: undefined, @@ -145,6 +148,8 @@ export default { accent: this.accentColorLocal, + underlay: this.underlayColorLocal, + panel: this.panelColorLocal, panelText: this.panelTextColorLocal, panelLink: this.panelLinkColorLocal, @@ -182,7 +187,8 @@ export default { panel: this.panelOpacityLocal, topBar: this.topBarOpacityLocal, border: this.borderOpacityLocal, - faint: this.faintOpacityLocal + faint: this.faintOpacityLocal, + underlay: this.underlayOpacityLocal } }, currentRadii () { @@ -240,6 +246,7 @@ export default { const bgs = { bg: hex2rgb(colors.bg), + underlay: hex2rgb(colors.underlay), btn: hex2rgb(colors.btn), panel: hex2rgb(colors.panel), topBar: hex2rgb(colors.topBar), @@ -249,29 +256,31 @@ export default { badgeNotification: hex2rgb(colors.badgeNotification) } - /* This is a bit confusing because "bottom layer" used is text color - * This is done to get worst case scenario when background below transparent - * layer matches text color, making it harder to read the lower alpha is. - */ - const ratios = { - bgText: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.text), fgs.text), - bgLink: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.link), fgs.link), - bgRed: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.red), fgs.red), - bgGreen: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.green), fgs.green), - bgBlue: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.blue), fgs.blue), - bgOrange: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.orange), fgs.orange), + const bg = [bgs.bg, opacity.bg] + const underlay = [bgs.underlay, opacity.underlay] + const panel = [underlay, bg] + + const ratios = { + bgText: getContrastRatioLayers(fgs.text, panel, fgs.text), + bgLink: getContrastRatioLayers(fgs.link, panel, fgs.link), + bgRed: getContrastRatioLayers(fgs.red, panel, fgs.red), + bgGreen: getContrastRatioLayers(fgs.green, panel, fgs.green), + bgBlue: getContrastRatioLayers(fgs.blue, panel, fgs.blue), + bgOrange: getContrastRatioLayers(fgs.orange, panel, fgs.orange), + + // TODO what's this? tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text), - panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText), - panelLink: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelLink), fgs.panelLink), + panelText: getContrastRatioLayers(fgs.text, [...panel, [bgs.panel, opacity.panel]], fgs.panelText), + panelLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.panel, opacity.panel]], fgs.panelLink), - btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText), + btnText: getContrastRatioLayers(fgs.text, [...panel, [bgs.btn, opacity.btn]], fgs.btnText), - inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText), + inputText: getContrastRatioLayers(fgs.text, [...panel, [bgs.input, opacity.input]], fgs.inputText), - topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText), - topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink) + topBarText: getContrastRatioLayers(fgs.text, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarText), + topBarLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarLink) } return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 2ecd275a..38ca2017 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -366,6 +366,20 @@ :fallback="previewTheme.opacity.faint || 0.5" /> +
+

{{ $t('settings.style.advanced_colors.underlay') }}

+ + +
console.log('%c##########', 'background: ' + color + '; color: ' + color) + const rgb2hex = (r, g, b) => { if (r === null || typeof r === 'undefined') { return undefined @@ -78,6 +81,16 @@ const getContrastRatio = (a, b) => { return (l1 + 0.05) / (l2 + 0.05) } +/** + * Same as `getContrastRatio` but for multiple layers in-between + * + * @param {Object} text - text color (topmost layer) + * @param {[Object, Number]} layers[] - layers between text and bedrock + * @param {Object} bedrock - layer at the very bottom + */ +export const getContrastRatioLayers = (text, layers, bedrock) => { + return getContrastRatio(alphaBlendLayers(bedrock, layers), text) +} /** * This performs alpha blending between solid background and semi-transparent foreground @@ -97,6 +110,16 @@ const alphaBlend = (fg, fga, bg) => { }, {}) } +/** + * Same as `alphaBlend` but for multiple layers in-between + * + * @param {Object} bedrock - layer at the very bottom + * @param {[Object, Number]} layers[] - layers between text and bedrock + */ +export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color, opacity]) => { + return alphaBlend(color, opacity, acc) +}, bedrock) + const invert = (rgb) => { return 'rgb'.split('').reduce((acc, c) => { acc[c] = 255 - rgb[c] diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 19a06587..8740fc55 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,6 +1,6 @@ import { times } from 'lodash' import { brightness, invertLightness, convert, contrastRatio } from 'chromatism' -import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.js' +import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } from '../color_convert/color_convert.js' // While this is not used anymore right now, I left it in if we want to do custom // styles that aren't just colors, so user can pick from a few different distinct @@ -64,8 +64,10 @@ const getTextColor = function (bg, text, preserve) { const base = typeof text.a !== 'undefined' ? { a: text.a } : {} const result = Object.assign(base, invertLightness(text).rgb) if (!preserve && getContrastRatio(bg, result) < 4.5) { + // B&W return contrastRatio(bg, text).rgb } + // Inverted color return result } return text @@ -173,7 +175,8 @@ const generateColors = (input) => { const opacity = Object.assign({ alert: 0.5, input: 0.5, - faint: 0.5 + faint: 0.5, + underlay: 0.15 }, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => { if (typeof v !== 'undefined') { acc[k] = v @@ -210,28 +213,37 @@ const generateColors = (input) => { colors.faint = col.faint || Object.assign({}, col.text) colors.bg = col.bg - colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb + colors.lightBg = col.lightBg || brightness(5 * mod, colors.bg).rgb + + const underlay = [col.underlay, opacity.underlay] + const fg = [col.fg, opacity.fg] + const bg = [col.bg, opacity.bg] colors.fg = col.fg - colors.fgText = col.fgText || getTextColor(colors.fg, colors.text) - colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true) + colors.fgText = col.fgText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, fg]), colors.text) + colors.fgLink = col.fgLink || getTextColor(alphaBlendLayers(colors.link, [underlay, bg, fg]), colors.link, true) + colors.underlay = col.underlay || hex2rgb('#000000') colors.border = col.border || brightness(2 * mod, colors.fg).rgb colors.btn = col.btn || Object.assign({}, col.fg) - colors.btnText = col.btnText || getTextColor(colors.btn, colors.fgText) + const btn = [colors.btn, opacity.btn || 1] + colors.btnText = col.btnText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, fg, btn]), colors.fgText) colors.input = col.input || Object.assign({}, col.fg) - colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText) + const inputCol = [colors.input, opacity.input] + colors.inputText = col.inputText || getTextColor(alphaBlendLayers(colors.lightText, [underlay, bg, fg, inputCol]), colors.lightText) colors.panel = col.panel || Object.assign({}, col.fg) - colors.panelText = col.panelText || getTextColor(colors.panel, colors.fgText) - colors.panelLink = col.panelLink || getTextColor(colors.panel, colors.fgLink) - colors.panelFaint = col.panelFaint || getTextColor(colors.panel, colors.faint) + const panel = [colors.panel, opacity.panel] + colors.panelText = col.panelText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, panel]), colors.fgText) + colors.panelLink = col.panelLink || getTextColor(alphaBlendLayers(colors.fgLink, [underlay, bg, panel]), colors.fgLink) + colors.panelFaint = col.panelFaint || getTextColor(alphaBlendLayers(colors.faint, [underlay, bg, panel]), colors.faint) colors.topBar = col.topBar || Object.assign({}, col.fg) - colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText) - colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink) + const topBar = [colors.topBar, opacity.topBar] + colors.topBarText = col.topBarText || getTextColor(alphaBlendLayers(colors.fgText, [topBar]), colors.fgText) + colors.topBarLink = col.topBarLink || getTextColor(alphaBlendLayers(colors.fgLink, [topBar]), colors.fgLink) colors.faintLink = col.faintLink || Object.assign({}, col.link || col.accent) colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg) @@ -255,6 +267,7 @@ const generateColors = (input) => { colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb Object.entries(opacity).forEach(([ k, v ]) => { + console.log(k) if (typeof v === 'undefined') return if (k === 'alert') { colors.alertError.a = v -- cgit v1.2.3-70-g09d2 From a2f676d31741f51d037fa4031855f63e1c43f2b3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 2 Jan 2020 23:48:52 +0200 Subject: Improved ColorInput to showcase transparent color, improved global input styles a bit --- src/App.scss | 17 +++++--- src/components/color_input/color_input.vue | 50 +++++++++++++++-------- src/components/style_switcher/style_switcher.scss | 16 ++------ 3 files changed, 48 insertions(+), 35 deletions(-) (limited to 'src/App.scss') diff --git a/src/App.scss b/src/App.scss index b6d4943a..7c9c91af 100644 --- a/src/App.scss +++ b/src/App.scss @@ -122,12 +122,15 @@ button { } } -label.select { - padding: 0; +input, textarea, .select, .input { -} + &.unstyled { + border-radius: 0; + background: none; + box-shadow: none; + height: unset; + } -input, textarea, .select { border: none; border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); @@ -141,13 +144,17 @@ input, textarea, .select { font-family: var(--inputFont, sans-serif); font-size: 14px; margin: 0; - padding: 8px .5em; box-sizing: border-box; display: inline-block; position: relative; height: 28px; line-height: 16px; hyphens: none; + padding: 8px .5em; + + &.select { + padding: 0; + } &:disabled, &[disabled=disabled] { cursor: not-allowed; diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue index aa289288..7fe04433 100644 --- a/src/components/color_input/color_input.vue +++ b/src/components/color_input/color_input.vue @@ -1,6 +1,6 @@ - + + + diff --git a/src/components/settings_modal/tabs/data_import_export.js b/src/components/settings_modal/tabs/data_import_export.js new file mode 100644 index 00000000..f68d12e9 --- /dev/null +++ b/src/components/settings_modal/tabs/data_import_export.js @@ -0,0 +1,65 @@ +import Importer from '../../importer/importer.vue' +import Exporter from '../../exporter/exporter.vue' +import Checkbox from '../../checkbox/checkbox.vue' + +const DataImportExport = { + data () { + return { + activeTab: 'profile', + newDomainToMute: '' + } + }, + created () { + this.$store.dispatch('fetchTokens') + }, + components: { + Importer, + Exporter, + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + } + }, + methods: { + getFollowsContent () { + return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id }) + .then(this.generateExportableUsersContent) + }, + getBlocksContent () { + return this.$store.state.api.backendInteractor.fetchBlocks() + .then(this.generateExportableUsersContent) + }, + importFollows (file) { + return this.$store.state.api.backendInteractor.importFollows({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + importBlocks (file) { + return this.$store.state.api.backendInteractor.importBlocks({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + generateExportableUsersContent (users) { + // Get addresses + return users.map((user) => { + // check is it's a local user + if (user && user.is_local) { + // append the instance address + // eslint-disable-next-line no-undef + return user.screen_name + '@' + location.hostname + } + return user.screen_name + }).join('\n') + } + } +} + +export default DataImportExport diff --git a/src/components/settings_modal/tabs/data_import_export.vue b/src/components/settings_modal/tabs/data_import_export.vue new file mode 100644 index 00000000..464df6d3 --- /dev/null +++ b/src/components/settings_modal/tabs/data_import_export.vue @@ -0,0 +1,43 @@ + + + + diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.js b/src/components/settings_modal/tabs/mutes_and_blocks.js new file mode 100644 index 00000000..51895ddc --- /dev/null +++ b/src/components/settings_modal/tabs/mutes_and_blocks.js @@ -0,0 +1,124 @@ +import get from 'lodash/get' +import map from 'lodash/map' +import reject from 'lodash/reject' +import Autosuggest from '../../autosuggest/autosuggest.vue' +import TabSwitcher from '../../tab_switcher/tab_switcher.js' +import BlockCard from '../../block_card/block_card.vue' +import MuteCard from '../../mute_card/mute_card.vue' +import DomainMuteCard from '../../domain_mute_card/domain_mute_card.vue' +import SelectableList from '../../selectable_list/selectable_list.vue' +import ProgressButton from '../../progress_button/progress_button.vue' +import withSubscription from '../../../hocs/with_subscription/with_subscription' +import Checkbox from '../../checkbox/checkbox.vue' + +const BlockList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchBlocks'), + select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), + childPropName: 'items' +})(SelectableList) + +const MuteList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchMutes'), + select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), + childPropName: 'items' +})(SelectableList) + +const DomainMuteList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchDomainMutes'), + select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []), + childPropName: 'items' +})(SelectableList) + +const MutesAndBlocks = { + data () { + return { + activeTab: 'profile', + newDomainToMute: '' + } + }, + created () { + this.$store.dispatch('fetchTokens') + }, + components: { + TabSwitcher, + BlockList, + MuteList, + DomainMuteList, + BlockCard, + MuteCard, + DomainMuteCard, + ProgressButton, + Autosuggest, + Checkbox + }, + methods: { + importFollows (file) { + return this.$store.state.api.backendInteractor.importFollows({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + importBlocks (file) { + return this.$store.state.api.backendInteractor.importBlocks({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + generateExportableUsersContent (users) { + // Get addresses + return users.map((user) => { + // check is it's a local user + if (user && user.is_local) { + // append the instance address + // eslint-disable-next-line no-undef + return user.screen_name + '@' + location.hostname + } + return user.screen_name + }).join('\n') + }, + activateTab (tabName) { + this.activeTab = tabName + }, + filterUnblockedUsers (userIds) { + return reject(userIds, (userId) => { + const user = this.$store.getters.findUser(userId) + return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id + }) + }, + filterUnMutedUsers (userIds) { + return reject(userIds, (userId) => { + const user = this.$store.getters.findUser(userId) + return !user || user.muted || user.id === this.$store.state.users.currentUser.id + }) + }, + queryUserIds (query) { + return this.$store.dispatch('searchUsers', query) + .then((users) => map(users, 'id')) + }, + blockUsers (ids) { + return this.$store.dispatch('blockUsers', ids) + }, + unblockUsers (ids) { + return this.$store.dispatch('unblockUsers', ids) + }, + muteUsers (ids) { + return this.$store.dispatch('muteUsers', ids) + }, + unmuteUsers (ids) { + return this.$store.dispatch('unmuteUsers', ids) + }, + unmuteDomains (domains) { + return this.$store.dispatch('unmuteDomains', domains) + }, + muteDomain () { + return this.$store.dispatch('muteDomain', this.newDomainToMute) + .then(() => { this.newDomainToMute = '' }) + } + } +} + +export default MutesAndBlocks diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.vue b/src/components/settings_modal/tabs/mutes_and_blocks.vue new file mode 100644 index 00000000..3aff47a0 --- /dev/null +++ b/src/components/settings_modal/tabs/mutes_and_blocks.vue @@ -0,0 +1,173 @@ + + + + diff --git a/src/components/settings_modal/tabs/notifications.js b/src/components/settings_modal/tabs/notifications.js new file mode 100644 index 00000000..0a870b3f --- /dev/null +++ b/src/components/settings_modal/tabs/notifications.js @@ -0,0 +1,27 @@ +import Checkbox from '../../checkbox/checkbox.vue' + +const Notifications = { + data () { + return { + activeTab: 'profile', + notificationSettings: this.$store.state.users.currentUser.notification_settings, + newDomainToMute: '' + } + }, + components: { + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + } + }, + methods: { + updateNotificationSettings () { + this.$store.state.api.backendInteractor + .updateNotificationSettings({ settings: this.notificationSettings }) + } + } +} + +export default Notifications diff --git a/src/components/settings_modal/tabs/notifications.vue b/src/components/settings_modal/tabs/notifications.vue new file mode 100644 index 00000000..f9a7c17b --- /dev/null +++ b/src/components/settings_modal/tabs/notifications.vue @@ -0,0 +1,42 @@ + + + + diff --git a/src/components/settings_modal/tabs/profile.js b/src/components/settings_modal/tabs/profile.js new file mode 100644 index 00000000..18c44024 --- /dev/null +++ b/src/components/settings_modal/tabs/profile.js @@ -0,0 +1,179 @@ +import unescape from 'lodash/unescape' +import ImageCropper from '../../image_cropper/image_cropper.vue' +import ScopeSelector from '../../scope_selector/scope_selector.vue' +import fileSizeFormatService from '../../../services/file_size_format/file_size_format.js' +import ProgressButton from '../../progress_button/progress_button.vue' +import EmojiInput from '../../emoji_input/emoji_input.vue' +import suggestor from '../../emoji_input/suggestor.js' +import Autosuggest from '../../autosuggest/autosuggest.vue' +import Checkbox from '../../checkbox/checkbox.vue' + +const ProfileTab = { + data () { + return { + newName: this.$store.state.users.currentUser.name, + newBio: unescape(this.$store.state.users.currentUser.description), + newLocked: this.$store.state.users.currentUser.locked, + newNoRichText: this.$store.state.users.currentUser.no_rich_text, + newDefaultScope: this.$store.state.users.currentUser.default_scope, + hideFollows: this.$store.state.users.currentUser.hide_follows, + hideFollowers: this.$store.state.users.currentUser.hide_followers, + hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count, + hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count, + showRole: this.$store.state.users.currentUser.show_role, + role: this.$store.state.users.currentUser.role, + discoverable: this.$store.state.users.currentUser.discoverable, + allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, + pickAvatarBtnVisible: true, + bannerUploading: false, + backgroundUploading: false, + banner: null, + bannerPreview: null, + background: null, + backgroundPreview: null, + bannerUploadError: null, + backgroundUploadError: null, + } + }, + components: { + ScopeSelector, + ImageCropper, + EmojiInput, + Autosuggest, + ProgressButton, + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + emojiUserSuggestor () { + return suggestor({ + emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ], + users: this.$store.state.users.users, + updateUsersList: (input) => this.$store.dispatch('searchUsers', input) + }) + }, + emojiSuggestor () { + return suggestor({ emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ] }) + } + }, + methods: { + updateProfile () { + this.$store.state.api.backendInteractor + .updateProfile({ + params: { + note: this.newBio, + locked: this.newLocked, + // Backend notation. + /* eslint-disable camelcase */ + display_name: this.newName, + default_scope: this.newDefaultScope, + no_rich_text: this.newNoRichText, + hide_follows: this.hideFollows, + hide_followers: this.hideFollowers, + discoverable: this.discoverable, + allow_following_move: this.allowFollowingMove, + hide_follows_count: this.hideFollowsCount, + hide_followers_count: this.hideFollowersCount, + show_role: this.showRole + /* eslint-enable camelcase */ + } }).then((user) => { + this.$store.commit('addNewUsers', [user]) + this.$store.commit('setCurrentUser', user) + }) + }, + changeVis (visibility) { + this.newDefaultScope = visibility + }, + uploadFile (slot, e) { + const file = e.target.files[0] + if (!file) { return } + if (file.size > this.$store.state.instance[slot + 'limit']) { + const filesize = fileSizeFormatService.fileSizeFormat(file.size) + const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit']) + this[slot + 'UploadError'] = [ + this.$t('upload.error.base'), + this.$t( + 'upload.error.file_too_big', + { + filesize: filesize.num, + filesizeunit: filesize.unit, + allowedsize: allowedsize.num, + allowedsizeunit: allowedsize.unit + } + ) + ].join(' ') + return + } + // eslint-disable-next-line no-undef + const reader = new FileReader() + reader.onload = ({ target }) => { + const img = target.result + this[slot + 'Preview'] = img + this[slot] = file + } + reader.readAsDataURL(file) + }, + submitAvatar (cropper, file) { + const that = this + return new Promise((resolve, reject) => { + function updateAvatar (avatar) { + that.$store.state.api.backendInteractor.updateAvatar({ avatar }) + .then((user) => { + that.$store.commit('addNewUsers', [user]) + that.$store.commit('setCurrentUser', user) + resolve() + }) + .catch((err) => { + reject(new Error(that.$t('upload.error.base') + ' ' + err.message)) + }) + } + + if (cropper) { + cropper.getCroppedCanvas().toBlob(updateAvatar, file.type) + } else { + updateAvatar(file) + } + }) + }, + submitBanner () { + if (!this.bannerPreview) { return } + + this.bannerUploading = true + this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner }) + .then((user) => { + this.$store.commit('addNewUsers', [user]) + this.$store.commit('setCurrentUser', user) + this.bannerPreview = null + }) + .catch((err) => { + this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message + }) + .then(() => { this.bannerUploading = false }) + }, + submitBg () { + if (!this.backgroundPreview) { return } + let background = this.background + this.backgroundUploading = true + this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => { + if (!data.error) { + this.$store.commit('addNewUsers', [data]) + this.$store.commit('setCurrentUser', data) + this.backgroundPreview = null + } else { + this.backgroundUploadError = this.$t('upload.error.base') + data.error + } + this.backgroundUploading = false + }) + } + } +} + +export default ProfileTab diff --git a/src/components/settings_modal/tabs/profile.scss b/src/components/settings_modal/tabs/profile.scss new file mode 100644 index 00000000..4aab81eb --- /dev/null +++ b/src/components/settings_modal/tabs/profile.scss @@ -0,0 +1,82 @@ +@import '../../../_variables.scss'; +.profile-tab { + .bio { + margin: 0; + } + + .visibility-tray { + padding-top: 5px; + } + + input[type=file] { + padding: 5px; + height: auto; + } + + .banner { + max-width: 100%; + } + + .uploading { + font-size: 1.5em; + margin: 0.25em; + } + + .name-changer { + width: 100%; + } + + .bg { + max-width: 100%; + } + + .current-avatar { + display: block; + width: 150px; + height: 150px; + border-radius: $fallback--avatarRadius; + border-radius: var(--avatarRadius, $fallback--avatarRadius); + } + + .oauth-tokens { + width: 100%; + + th { + text-align: left; + } + + .actions { + text-align: right; + } + } + + &-usersearch-wrapper { + padding: 1em; + } + + &-bulk-actions { + text-align: right; + padding: 0 1em; + min-height: 28px; + + button { + width: 10em; + } + } + + &-domain-mute-form { + padding: 1em; + display: flex; + flex-direction: column; + + button { + align-self: flex-end; + margin-top: 1em; + width: 10em; + } + } + + .setting-subitem { + margin-left: 1.75em; + } +} diff --git a/src/components/settings_modal/tabs/profile.vue b/src/components/settings_modal/tabs/profile.vue new file mode 100644 index 00000000..335fc12e --- /dev/null +++ b/src/components/settings_modal/tabs/profile.vue @@ -0,0 +1,213 @@ +