diff --git a/changelog.d/appearance-tab.change b/changelog.d/appearance-tab.change new file mode 100644 index 000000000..7fe1b45e6 --- /dev/null +++ b/changelog.d/appearance-tab.change @@ -0,0 +1 @@ +Reorganized Settings modal to move out visual stuff into Appearance tab diff --git a/changelog.d/emoji-scale.add b/changelog.d/emoji-scale.add new file mode 100644 index 000000000..791d80d96 --- /dev/null +++ b/changelog.d/emoji-scale.add @@ -0,0 +1 @@ +Ability to change size of emoji diff --git a/changelog.d/firefox-redmon.fix b/changelog.d/firefox-redmon.fix new file mode 100644 index 000000000..64ab9b141 --- /dev/null +++ b/changelog.d/firefox-redmon.fix @@ -0,0 +1 @@ +Bug with firefox and redmond themes diff --git a/changelog.d/theme-selector.add b/changelog.d/theme-selector.add new file mode 100644 index 000000000..c303f97c0 --- /dev/null +++ b/changelog.d/theme-selector.add @@ -0,0 +1 @@ +Theme selector with visual previews of the theme diff --git a/changelog.d/ui-scale.add b/changelog.d/ui-scale.add new file mode 100644 index 000000000..594a9aa58 --- /dev/null +++ b/changelog.d/ui-scale.add @@ -0,0 +1 @@ +Ability to resize UI (and certain components) scale independent of browser/text scale diff --git a/changelog.d/user-overrides.add b/changelog.d/user-overrides.add new file mode 100644 index 000000000..c0cb839a3 --- /dev/null +++ b/changelog.d/user-overrides.add @@ -0,0 +1 @@ +Ability to override certain aspects of UI style independent of theme used (UI roundness, fonts, underlay) diff --git a/preview.style.js b/preview.style.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/App.scss b/src/App.scss index 6e0aabcaa..bd9244871 100644 --- a/src/App.scss +++ b/src/App.scss @@ -3,9 +3,10 @@ @import "./panel"; :root { - --font-size: 14px; + --fontSize: 14px; --status-margin: 0.75em; - --navbar-height: 3.5rem; + --navbar-height: var(--navbarSize, 3.5rem); + --panel-header-height: var(--panelHeaderSize, 3.2rem); --post-line-height: 1.4; // Z-Index stuff --ZI_media_modal: 9000; @@ -20,7 +21,10 @@ } html { - font-size: var(--font-size); + font-size: var(--textSize); + + --navbar-height: var(--navbarSize, 3.5rem); + --emoji-size: var(--emojiSize, 32px); // overflow-x: clip causes my browser's tab to crash with SIGILL lul } @@ -156,6 +160,7 @@ nav { box-shadow: var(--shadow); box-sizing: border-box; height: var(--navbar-height); + font-size: calc(var(--navbar-height) / 3.5); position: fixed; } @@ -207,7 +212,7 @@ nav { .app-layout { --miniColumn: 25rem; --maxiColumn: 45rem; - --columnGap: 1em; + --columnGap: 1rem; --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); @@ -371,7 +376,6 @@ nav { user-select: none; color: var(--text); border: none; - border-radius: var(--roundness); cursor: pointer; background-color: var(--background); box-shadow: var(--shadow); @@ -507,7 +511,6 @@ textarea { --_padding: 0.5em; border: none; - border-radius: var(--roundness); background-color: var(--background); color: var(--text); box-shadow: var(--shadow); @@ -613,6 +616,17 @@ textarea { } } +.input, +.button-default { + --_roundness-left: var(--roundness); + --_roundness-right: var(--roundness); + + border-top-left-radius: var(--_roundness-left); + border-bottom-left-radius: var(--_roundness-left); + border-top-right-radius: var(--_roundness-right); + border-bottom-right-radius: var(--_roundness-right); +} + // Textareas should have stock line-height + vertical padding instead of huge line-height textarea.input { padding: var(--_padding); @@ -658,22 +672,23 @@ option { display: inline-flex; vertical-align: middle; - button, - .button-dropdown { + > *, + > * .button-default { + --_roundness-left: 0; + --_roundness-right: 0; + position: relative; flex: 1 1 auto; + } - &:not(:last-child), - &:not(:last-child) .button-default { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } + > *:first-child, + > *:first-child .button-default { + --_roundness-left: var(--roundness); + } - &:not(:first-child), - &:not(:first-child) .button-default { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } + > *:last-child, + > *:last-child .button-default { + --_roundness-right: var(--roundness); } } diff --git a/src/boot/after_store.js b/src/boot/after_store.js index bcab7a663..a486bd4c7 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -13,8 +13,7 @@ import VBodyScrollLock from 'src/directives/body_scroll_lock' import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' -import { applyTheme, applyConfig, tryLoadCache } from '../services/style_setter/style_setter.js' +import { applyConfig } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js' @@ -160,8 +159,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('showFeaturesPanel') copyInstanceOption('hideSitename') copyInstanceOption('sidebarRight') - - return store.dispatch('setTheme', config.theme) } const getTOS = async ({ store }) => { @@ -352,27 +349,7 @@ const afterStoreSetup = async ({ store, i18n }) => { store.dispatch('setInstanceOption', { name: 'server', value: server }) await setConfig({ store }) - - const { customTheme, customThemeSource, forceThemeRecompilation } = store.state.config - const { theme } = store.state.instance - const customThemePresent = customThemeSource || customTheme - - if (!forceThemeRecompilation && tryLoadCache()) { - store.commit('setThemeApplied') - } else { - if (customThemePresent) { - if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { - applyTheme(customThemeSource) - } else { - applyTheme(customTheme) - } - store.commit('setThemeApplied') - } else if (theme) { - // do nothing, it will load asynchronously - } else { - console.error('Failed to load any theme!') - } - } + await store.dispatch('setTheme') applyConfig(store.state.config) diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index eb665c406..d71bc1bbf 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -120,6 +120,7 @@ const EmojiPicker = { groupRefs: {}, emojiRefs: {}, filteredEmojiGroups: [], + emojiSize: 0, width: 0 } }, @@ -130,6 +131,23 @@ const EmojiPicker = { Popover }, methods: { + updateEmojiSize () { + const css = window.getComputedStyle(this.$refs.popover.$el) + const emojiSize = css.getPropertyValue('--emojiSize') + const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '') + const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, '')) + const fontSize = css.getPropertyValue('font-size').replace(/[^0-9,.]+/, '') + + let emojiSizeReal + if (emojiSizeUnit.endsWith('em')) { + emojiSizeReal = emojiSizeValue * fontSize + } else { + emojiSizeReal = emojiSizeValue + } + + const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSize) + this.emojiSize = fullEmojiSize + }, showPicker () { this.$refs.popover.showPopover() this.onShowing() @@ -224,6 +242,7 @@ const EmojiPicker = { }, onShowing () { const oldContentLoaded = this.contentLoaded + this.updateEmojiSize() this.recalculateItemPerRow() this.$nextTick(() => { this.$refs.search.focus() @@ -266,16 +285,20 @@ const EmojiPicker = { }, computed: { minItemSize () { - return this.emojiHeight + return this.emojiSize }, - emojiHeight () { - return 32 + 4 + // used to watch it + fontSize () { + this.$nextTick(() => { + this.updateEmojiSize() + }) + return this.$store.getters.mergedConfig.fontSize }, - emojiWidth () { - return 32 + 4 + emojiHeight () { + return this.emojiSize }, itemPerRow () { - return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6 + return this.width ? Math.floor(this.width / this.emojiSize) : 6 }, activeGroupView () { return this.showingStickers ? '' : this.activeGroup diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index 5602a16b7..12c09388f 100644 --- a/src/components/emoji_picker/emoji_picker.scss +++ b/src/components/emoji_picker/emoji_picker.scss @@ -1,9 +1,6 @@ -$emoji-picker-header-height: 36px; -$emoji-picker-header-picture-width: 32px; -$emoji-picker-header-picture-height: 32px; -$emoji-picker-emoji-size: 32px; - .emoji-picker { + --__emoji-picker-header: 2.2em; + width: 25em; max-width: calc(100vw - 20px); // popover gives 10px margin from window edge display: flex; @@ -13,24 +10,26 @@ $emoji-picker-emoji-size: 32px; display: inline-flex; justify-content: center; align-items: center; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); .still-image { - max-width: 100%; - max-height: 100%; - height: 100%; - width: 100%; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); object-fit: contain; + + --_still_image-label-scale: 0.5; } } .keep-open, .too-many-emoji, .hide-custom-emoji { - padding: 7px; + padding: 0.5em; line-height: normal; } @@ -44,13 +43,13 @@ $emoji-picker-emoji-size: 32px; } .keep-open-label { - padding: 0 7px; + padding: 0 0.5em; display: flex; } .heading { display: flex; - padding: 10px 7px 5px; + padding: 0.7em 0.5em 0; } .content { @@ -65,13 +64,14 @@ $emoji-picker-emoji-size: 32px; display: flex; flex-flow: row nowrap; overflow-x: auto; + overflow-y: hidden; } .additional-tabs { display: flex; border-left: 1px solid; border-left-color: var(--border); - padding-left: 7px; + padding-left: 0.5em; flex: 0 0 auto; } @@ -80,25 +80,29 @@ $emoji-picker-emoji-size: 32px; flex-basis: auto; display: flex; align-content: center; + scrollbar-width: thin; &-item { - padding: 0 7px; + padding: 0 0.5em; cursor: pointer; - font-size: 1.85em; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); display: flex; align-items: center; + .svg-inline--fa { + font-size: 1.85em; + } + &.disabled { opacity: 0.5; pointer-events: none; } &.toggled { - border-bottom: 4px solid; + border-bottom: 0.2em solid; } } } @@ -125,7 +129,7 @@ $emoji-picker-emoji-size: 32px; .emoji { &-search { - padding: 5px; + padding: 0.3em; flex: 0 0 auto; input { @@ -139,6 +143,7 @@ $emoji-picker-emoji-size: 32px; flex: 1 1 1px; position: relative; overflow: auto; + scrollbar-gutter: stable both-edges; user-select: none; mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, @@ -165,13 +170,13 @@ $emoji-picker-emoji-size: 32px; display: flex; align-items: center; flex-wrap: wrap; - padding-left: 5px; justify-content: left; &-title { font-size: 0.85em; width: 100%; margin: 0; + padding-left: 0.3em; &.disabled { display: none; @@ -180,24 +185,28 @@ $emoji-picker-emoji-size: 32px; } &-item { - width: $emoji-picker-emoji-size; - height: $emoji-picker-emoji-size; + width: var(--emoji-size); + height: var(--emoji-size); box-sizing: border-box; display: flex; - line-height: $emoji-picker-emoji-size; + line-height: var(--emoji-size); align-items: center; justify-content: center; - margin: 4px; + margin: 0.2em; cursor: pointer; .emoji-picker-emoji.-custom { object-fit: contain; - max-width: 100%; - max-height: 100%; + width: var(--emoji-size); + max-width: var(--emoji-size); + height: var(--emoji-size); + max-height: var(--emoji-size); + + --_still_image-label-scale: 0.5; } .emoji-picker-emoji.-unicode { - font-size: 24px; + font-size: 1.6em; overflow: hidden; } } diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index ad4a3c0bd..3ab4c125e 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -79,7 +79,7 @@ margin-top: 0.25em; flex-wrap: wrap; - --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1)); + --emoji-size: calc(var(--emojiSize, 1.25em) * var(--emojiReactionsScale, 1)); .emoji-reaction-container { display: flex; diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index 92ee3f306..d93949450 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,63 +1,59 @@ -import { set } from 'lodash' import Select from '../select/select.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import Popover from 'src/components/popover/popover.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faExclamationTriangle, + faKeyboard, + faFont +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faExclamationTriangle, + faKeyboard, + faFont +) export default { components: { - Select + Select, + Checkbox, + Popover }, props: [ 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' ], + mounted () { + this.$store.dispatch('queryLocalFonts') + }, emits: ['update:modelValue'], data () { return { - lValue: this.modelValue, + manualEntry: false, availableOptions: [ this.noInherit ? '' : 'inherit', - 'custom', - ...(this.options || []), 'serif', + 'sans-serif', 'monospace', - 'sans-serif' + ...(this.options || []) ].filter(_ => _) } }, - beforeUpdate () { - this.lValue = this.modelValue + methods: { + toggleManualEntry () { + this.manualEntry = !this.manualEntry + } }, computed: { present () { - return typeof this.lValue !== 'undefined' - }, - dValue () { - return this.lValue || this.fallback || {} - }, - family: { - get () { - return this.dValue.family - }, - set (v) { - set(this.lValue, 'family', v) - this.$emit('update:modelValue', this.lValue) - } + return typeof this.modelValue !== 'undefined' }, - isCustom () { - return this.preset === 'custom' + localFontsList () { + return this.$store.state.interface.localFonts }, - preset: { - get () { - if (this.family === 'serif' || - this.family === 'sans-serif' || - this.family === 'monospace' || - this.family === 'inherit') { - return this.family - } else { - return 'custom' - } - }, - set (v) { - this.family = v === 'custom' ? '' : v - } + localFontsSize () { + return this.$store.state.interface.localFonts?.length } } } diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index d2d1b3884..fca3b360b 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -1,6 +1,6 @@ @@ -54,21 +132,15 @@ diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 6e134ef2b..76a90d3e5 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -129,7 +129,7 @@ .mobile-nav { display: grid; line-height: var(--navbar-height); - grid-template-rows: 50px; + grid-template-rows: var(--navbar-height); grid-template-columns: 2fr auto; width: 100%; box-sizing: border-box; @@ -190,8 +190,8 @@ justify-content: space-between; z-index: calc(var(--ZI_navbar) + 100); width: 100%; - height: 50px; - line-height: 50px; + height: 3.5em; + line-height: 3.5em; position: absolute; box-shadow: var(--shadow); @@ -214,7 +214,7 @@ } .mobile-notifications { - margin-top: 50px; + margin-top: 3.5em; width: 100vw; height: calc(100vh - var(--navbar-height)); overflow-x: hidden; diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue index 36eb1ebe2..decd1c04b 100644 --- a/src/components/navigation/navigation_pins.vue +++ b/src/components/navigation/navigation_pins.vue @@ -49,6 +49,7 @@ } &.toggled { + margin-bottom: -4px; border-bottom: 4px solid; } } diff --git a/src/components/notification/notification.style.js b/src/components/notification/notification.style.js index 0d36760a3..c6d317d1c 100644 --- a/src/components/notification/notification.style.js +++ b/src/components/notification/notification.style.js @@ -11,7 +11,8 @@ export default { 'RichContent', 'Input', 'Avatar', - 'Attachment' + 'Attachment', + 'PollGraph' ], defaultRules: [] } diff --git a/src/components/panel.style.js b/src/components/panel.style.js index ad16c18ff..1bba4766d 100644 --- a/src/components/panel.style.js +++ b/src/components/panel.style.js @@ -20,6 +20,16 @@ export default { 'Tab', 'ListItem' ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'Input', + 'PanelHeader', + 'Alert' + ], defaultRules: [ { directives: { diff --git a/src/components/root.style.js b/src/components/root.style.js index f9bdf16e8..4bd735aa5 100644 --- a/src/components/root.style.js +++ b/src/components/root.style.js @@ -12,6 +12,11 @@ export default { 'Alert', 'Button' // mobile post button ], + validInnerComponentsLite: [ + 'Underlay', + 'Scrollbar', + 'ScrollbarElement' + ], defaultRules: [ { directives: { diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue index 23c1a5dd2..32dc6f83f 100644 --- a/src/components/settings_modal/helpers/number_setting.vue +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -15,6 +15,7 @@ + {{ ' ' }} this.$store.dispatch('pushAdminSetting', { path: k, value: v }) default: - return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) + if (this.timedApplyMode) { + return (k, v) => this.$store.dispatch('setOptionTemporarily', { name: k, value: v }) + } else { + return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) + } } }, defaultState () { diff --git a/src/components/settings_modal/helpers/unit_setting.js b/src/components/settings_modal/helpers/unit_setting.js index c9c23cb0b..daeddd813 100644 --- a/src/components/settings_modal/helpers/unit_setting.js +++ b/src/components/settings_modal/helpers/unit_setting.js @@ -21,15 +21,23 @@ export default { unitSet: { type: String, default: 'none' + }, + step: { + type: Number, + default: 1 + }, + resetDefault: { + type: Object, + default: null } }, computed: { ...Setting.computed, stateUnit () { - return this.state.replace(/\d+/, '') + return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : '' }, stateValue () { - return this.state.replace(/\D+/, '') + return typeof this.state === 'string' ? this.state.replace(/[^0-9,.]+/, '') : '' } }, methods: { @@ -39,10 +47,18 @@ export default { return this.$t(['settings', 'units', this.unitSet, value].join('.')) }, updateValue (e) { - this.configSink(this.path, parseInt(e.target.value) + this.stateUnit) + this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit) }, updateUnit (e) { - this.configSink(this.path, this.stateValue + e.target.value) + let value = this.stateValue + const newUnit = e.target.value + if (this.resetDefault) { + const replaceValue = this.resetDefault[newUnit] + if (replaceValue != null) { + value = replaceValue + } + } + this.configSink(this.path, value + newUnit) } } } diff --git a/src/components/settings_modal/helpers/unit_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue index 68f52b1cd..40ab68801 100644 --- a/src/components/settings_modal/helpers/unit_setting.vue +++ b/src/components/settings_modal/helpers/unit_setting.vue @@ -9,11 +9,12 @@ > + {{ ' ' }} import('./settings_modal_user_content.vue'), { @@ -165,6 +167,7 @@ const SettingsModal = { }, computed: { currentSaveStateNotice () { + console.log(this.$store.state.interface.settings.currentSaveStateNotice) return this.$store.state.interface.settings.currentSaveStateNotice }, modalActivated () { diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 50859c94c..90dbbde0a 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -147,6 +147,18 @@ + + + {{ $t('settings.confirm_new_question') }} + + diff --git a/src/components/settings_modal/settings_modal_user_content.js b/src/components/settings_modal/settings_modal_user_content.js index 9ac0301f6..ebd5329f5 100644 --- a/src/components/settings_modal/settings_modal_user_content.js +++ b/src/components/settings_modal/settings_modal_user_content.js @@ -7,6 +7,7 @@ import FilteringTab from './tabs/filtering_tab.vue' import SecurityTab from './tabs/security_tab/security_tab.vue' import ProfileTab from './tabs/profile_tab.vue' import GeneralTab from './tabs/general_tab.vue' +import AppearanceTab from './tabs/appearance_tab.vue' import VersionTab from './tabs/version_tab.vue' import ThemeTab from './tabs/theme_tab/theme_tab.vue' @@ -19,7 +20,8 @@ import { faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore } from '@fortawesome/free-solid-svg-icons' library.add( @@ -30,7 +32,8 @@ library.add( faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore ) const SettingsModalContent = { @@ -44,6 +47,7 @@ const SettingsModalContent = { SecurityTab, ProfileTab, GeneralTab, + AppearanceTab, VersionTab, ThemeTab }, diff --git a/src/components/settings_modal/settings_modal_user_content.vue b/src/components/settings_modal/settings_modal_user_content.vue index 0221cccb6..1441d892d 100644 --- a/src/components/settings_modal/settings_modal_user_content.vue +++ b/src/components/settings_modal/settings_modal_user_content.vue @@ -13,6 +13,20 @@ > +
+ +
+
+ +
+
+ +
-
- -
- +
-
- -
({ + key: mode, + value: mode, + label: this.$t(`settings.third_column_mode_${mode}`) + })), + forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({ + key: mode, + value: i - 1, + label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`) + })), + underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({ + key: mode, + value: mode, + label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`) + })) + } + }, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + FloatSetting, + UnitSetting, + ProfileSettingIndicator, + FontControl, + Preview + }, + mounted () { + getThemes() + .then((promises) => { + return Promise.all( + Object.entries(promises) + .map(([k, v]) => v.then(res => [k, res])) + ) + }) + .then(themes => themes.reduce((acc, [k, v]) => { + if (v) { + return [ + ...acc, + { + name: v.name || v[0], + key: k, + data: v + } + ] + } else { + return acc + } + }, [])) + .then((themesComplete) => { + this.availableStyles = themesComplete + }) + + if (window.IntersectionObserver) { + this.intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(({ target, isIntersecting }) => { + if (!isIntersecting) return + const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey) + this.$nextTick(() => { + if (theme) theme.ready = true + }) + observer.unobserve(target) + }) + }, { + root: this.$refs.themeList + }) + } + }, + updated () { + this.$nextTick(() => { + this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => { + this.intersectionObserver.observe(node) + }) + }) + }, + computed: { + noIntersectionObserver () { + return !window.IntersectionObserver + }, + horizontalUnits () { + return defaultHorizontalUnits + }, + fontsOverride () { + return this.$store.getters.mergedConfig.fontsOverride + }, + columns () { + const mode = this.$store.getters.mergedConfig.thirdColumnMode + + const notif = mode === 'none' ? [] : ['notifs'] + + if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') { + return [...notif, 'content', 'sidebar'] + } else { + return ['sidebar', 'content', ...notif] + } + }, + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, + instanceWallpaperUsed () { + return this.$store.state.instance.background && + !this.$store.state.users.currentUser.background_image + }, + instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, + language: { + get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, + set: function (val) { + this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) + } + }, + isCustomThemeUsed () { + const { theme } = this.mergedConfig + return theme === 'custom' || theme === null + }, + ...SharedComputedObject() + }, + methods: { + updateFont (key, value) { + console.log(key, value) + this.$store.dispatch('setOption', { + name: 'theme3hacks', + value: { + ...this.mergedConfig.theme3hacks, + fonts: { + ...this.mergedConfig.theme3hacks.fonts, + [key]: value + } + } + }) + }, + isThemeActive (key) { + const { theme } = this.mergedConfig + return key === theme + }, + setTheme (name) { + this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true }) + }, + previewTheme (key, input) { + const style = normalizeThemeData(input) + const x = 2 + if (x === 1) return + const theme2 = convertTheme2To3(style) + const theme3 = init({ + inputRuleset: theme2, + ultimateBackgroundColor: '#000000', + liteMode: true, + debug: true, + onlyNormalState: true + }) + + return getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview-' + key + ).join('\n') + } + } +} + +export default AppearanceTab diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue new file mode 100644 index 000000000..55f0d1bee --- /dev/null +++ b/src/components/settings_modal/tabs/appearance_tab.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 7d701d34f..96caab071 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -3,7 +3,7 @@ import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_setting.vue' import FloatSetting from '../helpers/float_setting.vue' -import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue' +import UnitSetting from '../helpers/unit_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' @@ -40,11 +40,6 @@ const GeneralTab = { value: mode, label: this.$t(`settings.mention_link_display_${mode}`) })), - thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({ - key: mode, - value: mode, - label: this.$t(`settings.third_column_mode_${mode}`) - })), userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({ key: mode, value: mode, @@ -70,9 +65,6 @@ const GeneralTab = { ProfileSettingIndicator }, computed: { - horizontalUnits () { - return defaultHorizontalUnits - }, postFormats () { return this.$store.state.instance.postFormats || [] }, @@ -83,29 +75,6 @@ const GeneralTab = { label: this.$t(`post_status.content_type["${format}"]`) })) }, - columns () { - const mode = this.$store.getters.mergedConfig.thirdColumnMode - - const notif = mode === 'none' ? [] : ['notifs'] - - if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') { - return [...notif, 'content', 'sidebar'] - } else { - return ['sidebar', 'content', ...notif] - } - }, - instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, - instanceWallpaperUsed () { - return this.$store.state.instance.background && - !this.$store.state.users.currentUser.background_image - }, - instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, - language: { - get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, - set: function (val) { - this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) - } - }, ...SharedComputedObject() }, methods: { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 208c49ee3..4ece6cf41 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -15,11 +15,6 @@ {{ $t('settings.hide_isp') }} -
  • - - {{ $t('settings.hide_wallpaper') }} - -
  • {{ $t('settings.stop_gifs') }} @@ -98,53 +93,6 @@ {{ $t('settings.hide_shoutbox') }}
  • -
  • -

    {{ $t('settings.columns') }}

    -
  • -
  • - - {{ $t('settings.disable_sticky_headers') }} - -
  • -
  • - - {{ $t('settings.show_scrollbars') }} - -
  • -
  • - - {{ $t('settings.right_sidebar') }} - -
  • -
  • - - {{ $t('settings.navbar_column_stretch') }} - -
  • -
  • - - {{ $t('settings.third_column_mode') }} - -
  • -
  • - {{ $t('settings.column_sizes') }} -
    - - {{ $t('settings.column_sizes_' + column) }} - -
    -
  • {{ $t('settings.confirm_dialogs') }}
      @@ -200,14 +148,6 @@

      {{ $t('settings.post_look_feel') }}

        -
      • - - {{ $t('settings.force_theme_recompilation_debug') }} - -
      • -
      • - - {{ $t('settings.emoji_reactions_scale') }} - -
      • {{ $t('settings.attachments') }}

      • - - diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue index 1837620f6..3fb0558b4 100644 --- a/src/components/settings_modal/tabs/theme_tab/preview.vue +++ b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -99,15 +99,9 @@ >
        - - - - + + {{ $t('settings.style.preview.checkbox') }} + @@ -118,6 +112,7 @@ + diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 11c90b034..39dc372ef 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -1,7 +1,8 @@ import { rgb2hex, hex2rgb, - getContrastRatioLayers + getContrastRatioLayers, + relativeLuminance } from 'src/services/color_convert/color_convert.js' import { getThemes @@ -23,10 +24,17 @@ import { generateShadows, generateRadii, generateFonts, - composePreset, shadows2to3, colors2to3 } from 'src/services/theme_data/theme_data.service.js' + +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' +import { init } from 'src/services/theme_data/theme_data_3.service.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' + import ColorInput from 'src/components/color_input/color_input.vue' import RangeInput from 'src/components/range_input/range_input.vue' import OpacityInput from 'src/components/opacity_input/opacity_input.vue' @@ -62,6 +70,7 @@ const colorConvert = (color) => { export default { data () { return { + themeV3Preview: [], themeImporter: newImporter({ validator: this.importValidator, onImport: this.onImport, @@ -78,10 +87,7 @@ export default { tempImportFile: undefined, engineVersion: 0, - previewShadows: {}, - previewColors: {}, - previewRadii: {}, - previewFonts: {}, + previewTheme: {}, shadowsInvalid: true, colorsInvalid: true, @@ -232,13 +238,6 @@ export default { chatMessage: this.chatMessageRadiusLocal } }, - preview () { - return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts) - }, - previewTheme () { - if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} } - return this.preview.theme - }, // This needs optimization maybe previewContrast () { try { @@ -306,14 +305,6 @@ export default { return {} } }, - previewRules () { - if (!this.preview.rules) return '' - return [ - ...Object.values(this.preview.rules), - 'color: var(--text)', - 'font-family: var(--interfaceFont, sans-serif)' - ].join(';') - }, shadowsAvailable () { return Object.keys(DEFAULT_SHADOWS).sort() }, @@ -511,17 +502,14 @@ export default { } }, setCustomTheme () { - this.$store.dispatch('setOption', { - name: 'customTheme', - value: { + this.$store.dispatch('setThemeV2', { + customTheme: { + ignore: true, themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, ...this.previewTheme - } - }) - this.$store.dispatch('setOption', { - name: 'customThemeSource', - value: { + }, + customThemeSource: { themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, shadows: this.shadowsLocal, @@ -532,16 +520,24 @@ export default { } }) }, - updatePreviewColorsAndShadows () { - this.previewColors = generateColors({ + updatePreviewColors () { + const result = generateColors({ opacity: this.currentOpacity, colors: this.currentColors }) - this.previewShadows = generateShadows( - { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion }, - this.previewColors.theme.colors, - this.previewColors.mod - ) + this.previewTheme.colors = result.theme.colors + this.previewTheme.opacity = result.theme.opacity + }, + updatePreviewShadows () { + this.previewTheme.shadows = generateShadows( + { + shadows: this.shadowsLocal, + opacity: this.previewTheme.opacity, + themeEngineVersion: this.engineVersion + }, + this.previewTheme.colors, + relativeLuminance(this.previewTheme.colors.bg) < 0.5 ? 1 : -1 + ).theme.shadows }, importTheme () { this.themeImporter.importData() }, exportTheme () { this.themeExporter.exportData() }, @@ -610,7 +606,7 @@ export default { normalizeLocalState (theme, version = 0, source, forceSource = false) { let input if (typeof source !== 'undefined') { - if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { + if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) { input = source version = source.themeEngineVersion } else { @@ -692,6 +688,8 @@ export default { } else { this.shadowsLocal = shadows } + this.updatePreviewColors() + this.updatePreviewShadows() this.shadowSelected = this.shadowsAvailable[0] } @@ -699,12 +697,25 @@ export default { this.clearFonts() this.fontsLocal = fonts } + }, + updateTheme3Preview () { + const theme2 = convertTheme2To3(this.previewTheme) + const theme3 = init({ + inputRuleset: theme2, + ultimateBackgroundColor: '#000000', + liteMode: true + }) + + this.themeV3Preview = getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview' + ).join('\n') } }, watch: { currentRadii () { try { - this.previewRadii = generateRadii({ radii: this.currentRadii }) + this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii this.radiiInvalid = false } catch (e) { this.radiiInvalid = true @@ -713,9 +724,8 @@ export default { }, shadowsLocal: { handler () { - if (Object.getOwnPropertyNames(this.previewColors).length === 1) return try { - this.updatePreviewColorsAndShadows() + this.updatePreviewShadows() this.shadowsInvalid = false } catch (e) { this.shadowsInvalid = true @@ -727,7 +737,7 @@ export default { fontsLocal: { handler () { try { - this.previewFonts = generateFonts({ fonts: this.fontsLocal }) + this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts this.fontsInvalid = false } catch (e) { this.fontsInvalid = true @@ -738,18 +748,16 @@ export default { }, currentColors () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() this.colorsInvalid = false - this.shadowsInvalid = false } catch (e) { this.colorsInvalid = true - this.shadowsInvalid = true console.warn(e) } }, currentOpacity () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() } catch (e) { console.warn(e) } diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss index 5e6331204..84933fb8e 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -1,4 +1,9 @@ .theme-tab { + .deprecation-warning { + padding: 0.5em; + margin: 2em; + } + padding-bottom: 2em; .preset-switcher { @@ -10,6 +15,10 @@ margin-right: 0.25em; } + .btn-group .btn { + margin: 0; + } + .style-control { display: flex; align-items: baseline; @@ -157,107 +166,6 @@ } } - .preview-container { - border-top: 1px dashed; - border-bottom: 1px dashed; - border-color: var(--border); - margin: 1em 0; - padding: 1em; - background-color: var(--wallpaper); - background-image: var(--body-background-image); - background-size: cover; - background-position: 50% 50%; - - .dummy { - .post { - font-family: var(--postFont); - display: flex; - - .content { - flex: 1; - - h4 { - margin-bottom: 0.25em; - } - - .icons { - margin-top: 0.5em; - display: flex; - - i { - margin-right: 1em; - } - } - } - } - - .after-post { - margin-top: 1em; - display: flex; - align-items: center; - } - - .avatar, - .avatar-alt { - background: - linear-gradient( - 135deg, - #b8e1fc 0%, - #a9d2f3 10%, - #90bae4 25%, - #90bcea 37%, - #90bff0 50%, - #6ba8e5 51%, - #a2daf5 83%, - #bdf3fd 100% - ); - color: black; - font-family: sans-serif; - text-align: center; - margin-right: 1em; - } - - .avatar-alt { - flex: 0 auto; - margin-left: 28px; - font-size: 12px; - min-width: 20px; - min-height: 20px; - line-height: 20px; - } - - .avatar { - flex: 0 auto; - width: 48px; - height: 48px; - font-size: 14px; - line-height: 48px; - } - - .actions { - display: flex; - align-items: baseline; - - .checkbox { - display: inline-flex; - align-items: baseline; - margin-right: 1em; - flex: 1; - } - } - - .separator { - margin: 1em; - border-bottom: 1px solid; - border-color: var(--border); - } - - .btn { - min-width: 3em; - } - } - } - .radius-item { flex-basis: auto; } @@ -310,10 +218,6 @@ max-width: 50em; } - .theme-preview-content { - padding: 20px; - } - .theme-warning { display: flex; align-items: baseline; diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue index ff2fece9c..4498c1434 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -1,5 +1,8 @@