logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 8b96ea93776fd1eb462a7c54822d4f8ad6a9e776
parent 4e3c4ec1dbbf88b09a84a80519f4ccced5a4dc2c
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date:   Wed,  7 Apr 2021 17:40:07 +0000

Merge branch 'settings-import-export' into 'develop'

Settings backup/restore + small fixes

See merge request pleroma/pleroma-fe!1372

Diffstat:

Msrc/App.scss24+++++++++++++++---------
Dsrc/components/export_import/export_import.vue102-------------------------------------------------------------------------------
Msrc/components/notifications/notifications.scss7+++++--
Msrc/components/notifications/notifications.vue10+++++-----
Msrc/components/settings_modal/settings_modal.js124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/settings_modal.vue70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js20++++++++++++++++++--
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.vue81++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Asrc/components/timeline/timeline.scss31+++++++++++++++++++++++++++++++
Msrc/components/timeline/timeline.vue38+++++---------------------------------
Msrc/i18n/en.json12++++++++++++
Msrc/modules/config.js14++++++++++++++
Asrc/services/export_import/export_import.js55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/services/style_setter/style_setter.js2+-
14 files changed, 396 insertions(+), 194 deletions(-)

diff --git a/src/App.scss b/src/App.scss @@ -547,9 +547,21 @@ main-router { border-radius: var(--panelRadius, $fallback--panelRadius); } -.panel-footer { +/* TODO Should remove timeline-footer from here when we refactor panels into + * separate component and utilize slots + */ +.panel-footer, .timeline-footer { + display: flex; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + flex: none; + padding: 0.6em 0.6em; + text-align: left; + line-height: 28px; + align-items: baseline; + border-width: 1px 0 0 0; + border-style: solid; + border-color: var(--border, $fallback--border); .faint { color: $fallback--faint; @@ -871,16 +883,10 @@ nav { } .new-status-notification { - position:relative; - margin-top: -1px; + position: relative; font-size: 1.1em; - border-width: 1px 0 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - padding: 10px; z-index: 1; - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + flex: 1; } .chat-layout { diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue @@ -1,102 +0,0 @@ -<template> - <div class="import-export-container"> - <slot name="before" /> - <button - class="btn button-default" - @click="exportData" - > - {{ exportLabel }} - </button> - <button - class="btn button-default" - @click="importData" - > - {{ importLabel }} - </button> - <slot name="afterButtons" /> - <p - v-if="importFailed" - class="alert error" - > - {{ importFailedText }} - </p> - <slot name="afterError" /> - </div> -</template> - -<script> -export default { - props: [ - 'exportObject', - 'importLabel', - 'exportLabel', - 'importFailedText', - 'validator', - 'onImport', - 'onImportFailure' - ], - data () { - return { - importFailed: false - } - }, - methods: { - exportData () { - const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces - - // Create an invisible link with a data url and simulate a click - const e = document.createElement('a') - e.setAttribute('download', 'pleroma_theme.json') - e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) - e.style.display = 'none' - - document.body.appendChild(e) - e.click() - document.body.removeChild(e) - }, - importData () { - this.importFailed = false - const filePicker = document.createElement('input') - filePicker.setAttribute('type', 'file') - filePicker.setAttribute('accept', '.json') - - filePicker.addEventListener('change', event => { - if (event.target.files[0]) { - // eslint-disable-next-line no-undef - const reader = new FileReader() - reader.onload = ({ target }) => { - try { - const parsed = JSON.parse(target.result) - const valid = this.validator(parsed) - if (valid) { - this.onImport(parsed) - } else { - this.importFailed = true - // this.onImportFailure(valid) - } - } catch (e) { - // This will happen both if there is a JSON syntax error or the theme is missing components - this.importFailed = true - // this.onImportFailure(e) - } - } - reader.readAsText(event.target.files[0]) - } - }) - - document.body.appendChild(filePicker) - filePicker.click() - document.body.removeChild(filePicker) - } - } -} -</script> - -<style lang="scss"> -.import-export-container { - display: flex; - flex-wrap: wrap; - align-items: baseline; - justify-content: center; -} -</style> diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -1,6 +1,6 @@ @import '../../_variables.scss'; -.notifications { +.Notifications { &:not(.minimal) { // a bit of a hack to allow scrolling below notifications padding-bottom: 15em; @@ -11,6 +11,10 @@ color: var(--text, $fallback--text); } + .notifications-footer { + border: none; + } + .notification { position: relative; @@ -82,7 +86,6 @@ } } - .follow-text, .move-text { padding: 0.5em 0; overflow-wrap: break-word; diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue @@ -1,7 +1,7 @@ <template> <div :class="{ minimal: minimalMode }" - class="notifications" + class="Notifications" > <div :class="mainClass"> <div @@ -35,10 +35,10 @@ <notification :notification="notification" /> </div> </div> - <div class="panel-footer"> + <div class="panel-footer notifications-footer"> <div v-if="bottomedOut" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('notifications.no_more_notifications') }} </div> @@ -47,13 +47,13 @@ class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderNotifications()" > - <div class="new-status-notification text-center panel-footer"> + <div class="new-status-notification text-center"> {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }} </div> </button> <div v-else - class="new-status-notification text-center panel-footer" + class="new-status-notification text-center" > <FAIcon icon="circle-notch" diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js @@ -2,10 +2,55 @@ import Modal from 'src/components/modal/modal.vue' import PanelLoading from 'src/components/panel_loading/panel_loading.vue' import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue' import getResettableAsyncComponent from 'src/services/resettable_async_component.js' +import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { cloneDeep } from 'lodash' +import { + newImporter, + newExporter +} from 'src/services/export_import/export_import.js' +import { + faTimes, + faFileUpload, + faFileDownload, + faChevronDown +} from '@fortawesome/free-solid-svg-icons' +import { + faWindowMinimize +} from '@fortawesome/free-regular-svg-icons' + +const PLEROMAFE_SETTINGS_MAJOR_VERSION = 1 +const PLEROMAFE_SETTINGS_MINOR_VERSION = 0 + +library.add( + faTimes, + faWindowMinimize, + faFileUpload, + faFileDownload, + faChevronDown +) const SettingsModal = { + data () { + return { + dataImporter: newImporter({ + validator: this.importValidator, + onImport: this.onImport, + onImportFailure: this.onImportFailure + }), + dataThemeExporter: newExporter({ + filename: 'pleromafe_settings.full', + getExportedObject: () => this.generateExport(true) + }), + dataExporter: newExporter({ + filename: 'pleromafe_settings', + getExportedObject: () => this.generateExport() + }) + } + }, components: { Modal, + Popover, SettingsModalContent: getResettableAsyncComponent( () => import('./settings_modal_content.vue'), { @@ -21,6 +66,85 @@ const SettingsModal = { }, peekModal () { this.$store.dispatch('togglePeekSettingsModal') + }, + importValidator (data) { + if (!Array.isArray(data._pleroma_settings_version)) { + return { + messageKey: 'settings.file_import_export.invalid_file' + } + } + + const [major, minor] = data._pleroma_settings_version + + if (major > PLEROMAFE_SETTINGS_MAJOR_VERSION) { + return { + messageKey: 'settings.file_export_import.errors.file_too_new', + messageArgs: { + fileMajor: major, + feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION + } + } + } + + if (major < PLEROMAFE_SETTINGS_MAJOR_VERSION) { + return { + messageKey: 'settings.file_export_import.errors.file_too_old', + messageArgs: { + fileMajor: major, + feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION + } + } + } + + if (minor > PLEROMAFE_SETTINGS_MINOR_VERSION) { + this.$store.dispatch('pushGlobalNotice', { + level: 'warning', + messageKey: 'settings.file_export_import.errors.file_slightly_new' + }) + } + + return true + }, + onImportFailure (result) { + if (result.error) { + this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_settings_imported', level: 'error' }) + } else { + this.$store.dispatch('pushGlobalNotice', { ...result.validationResult, level: 'error' }) + } + }, + onImport (data) { + if (data) { this.$store.dispatch('loadSettings', data) } + }, + restore () { + this.dataImporter.importData() + }, + backup () { + this.dataExporter.exportData() + }, + backupWithTheme () { + this.dataThemeExporter.exportData() + }, + generateExport (theme = false) { + const { config } = this.$store.state + let sample = config + if (!theme) { + const ignoreList = new Set([ + 'customTheme', + 'customThemeSource', + 'colors' + ]) + sample = Object.fromEntries( + Object + .entries(sample) + .filter(([key]) => !ignoreList.has(key)) + ) + } + const clone = cloneDeep(sample) + clone._pleroma_settings_version = [ + PLEROMAFE_SETTINGS_MAJOR_VERSION, + PLEROMAFE_SETTINGS_MINOR_VERSION + ] + return clone } }, computed: { diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -31,20 +31,86 @@ </transition> <button class="btn button-default" + :title="$t('general.peek')" @click="peekModal" > - {{ $t('general.peek') }} + <FAIcon + :icon="['far', 'window-minimize']" + fixed-width + /> </button> <button class="btn button-default" + :title="$t('general.close')" @click="closeModal" > - {{ $t('general.close') }} + <FAIcon + icon="times" + fixed-width + /> </button> </div> <div class="panel-body"> <SettingsModalContent v-if="modalOpenedOnce" /> </div> + <div class="panel-footer"> + <Popover + class="export" + trigger="click" + placement="top" + :offset="{ y: 5, x: 5 }" + :bound-to="{ x: 'container' }" + remove-padding + > + <button + slot="trigger" + class="btn button-default" + :title="$t('general.close')" + > + <span>{{ $t("settings.file_export_import.backup_restore") }}</span> + <FAIcon + icon="chevron-down" + /> + </button> + <div + slot="content" + slot-scope="{close}" + > + <div class="dropdown-menu"> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="backup" + @click="close" + > + <FAIcon + icon="file-download" + fixed-width + /><span>{{ $t("settings.file_export_import.backup_settings") }}</span> + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="backupWithTheme" + @click="close" + > + <FAIcon + icon="file-download" + fixed-width + /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span> + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="restore" + @click="close" + > + <FAIcon + icon="file-upload" + fixed-width + /><span>{{ $t("settings.file_export_import.restore_settings") }}</span> + </button> + </div> + </div> + </Popover> + </div> </div> </Modal> </template> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -16,6 +16,10 @@ import { colors2to3 } from 'src/services/style_setter/style_setter.js' import { + newImporter, + newExporter +} from 'src/services/export_import/export_import.js' +import { SLOT_INHERITANCE } from 'src/services/theme_data/pleromafe.js' import { @@ -31,7 +35,6 @@ import ShadowControl from 'src/components/shadow_control/shadow_control.vue' import FontControl from 'src/components/font_control/font_control.vue' import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' -import ExportImport from 'src/components/export_import/export_import.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import Preview from './preview.vue' @@ -67,6 +70,15 @@ const colorConvert = (color) => { export default { data () { return { + themeImporter: newImporter({ + validator: this.importValidator, + onImport: this.onImport, + onImportFailure: this.onImportFailure + }), + themeExporter: newExporter({ + filename: 'pleroma_theme', + getExportedObject: () => this.exportedTheme + }), availableStyles: [], selected: this.$store.getters.mergedConfig.theme, themeWarning: undefined, @@ -383,7 +395,6 @@ export default { FontControl, TabSwitcher, Preview, - ExportImport, Checkbox }, methods: { @@ -528,10 +539,15 @@ export default { this.previewColors.mod ) }, + importTheme () { this.themeImporter.importData() }, + exportTheme () { this.themeExporter.exportData() }, onImport (parsed, forceSource = false) { this.tempImportFile = parsed this.loadTheme(parsed, 'file', forceSource) }, + onImportFailure (result) { + this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' }) + }, importValidator (parsed) { const version = parsed._pleroma_theme_version return version >= 1 || version <= 2 diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -48,46 +48,51 @@ </template> </div> </div> - <ExportImport - :export-object="exportedTheme" - :export-label="$t(&quot;settings.export_theme&quot;)" - :import-label="$t(&quot;settings.import_theme&quot;)" - :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)" - :on-import="onImport" - :validator="importValidator" - > - <template slot="before"> - <div class="presets"> - {{ $t('settings.presets') }} - <label - for="preset-switcher" - class="select" + <div class="top"> + <div class="presets"> + {{ $t('settings.presets') }} + <label + for="preset-switcher" + class="select" + > + <select + id="preset-switcher" + v-model="selected" + class="preset-switcher" > - <select - id="preset-switcher" - v-model="selected" - class="preset-switcher" + <option + v-for="style in availableStyles" + :key="style.name" + :value="style" + :style="{ + backgroundColor: style[1] || (style.theme || style.source).colors.bg, + color: style[3] || (style.theme || style.source).colors.text + }" > - <option - v-for="style in availableStyles" - :key="style.name" - :value="style" - :style="{ - backgroundColor: style[1] || (style.theme || style.source).colors.bg, - color: style[3] || (style.theme || style.source).colors.text - }" - > - {{ style[0] || style.name }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> - </div> - </template> - </ExportImport> + {{ style[0] || style.name }} + </option> + </select> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> + </label> + </div> + <div class="export-import"> + <button + class="btn button-default" + @click="importTheme" + > + {{ $t(&quot;settings.import_theme&quot;) }} + </button> + <button + class="btn button-default" + @click="exportTheme" + > + {{ $t(&quot;settings.export_theme&quot;) }} + </button> + </div> + </div> </div> <div class="save-load-options"> <span class="keep-option"> diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss @@ -0,0 +1,31 @@ +@import '../../_variables.scss'; + +.Timeline { + .loadmore-text { + opacity: 1; + } + + &.-blocked { + cursor: progress; + } + + .timeline-heading { + max-width: 100%; + flex-wrap: nowrap; + align-items: center; + position: relative; + + .loadmore-button { + flex-shrink: 0; + } + + .loadmore-text { + flex-shrink: 0; + line-height: 1em; + } + } + + .timeline-footer { + border: none; + } +} diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -52,13 +52,13 @@ <div :class="classes.footer"> <div v-if="count===0" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('timeline.no_statuses') }} </div> <div v-else-if="bottomedOut" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('timeline.no_more_statuses') }} </div> @@ -67,13 +67,13 @@ class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderStatuses()" > - <div class="new-status-notification text-center panel-footer"> + <div class="new-status-notification text-center"> {{ $t('timeline.load_older') }} </div> </button> <div v-else - class="new-status-notification text-center panel-footer" + class="new-status-notification text-center" > <FAIcon icon="circle-notch" @@ -87,32 +87,4 @@ <script src="./timeline.js"></script> -<style lang="scss"> -@import '../../_variables.scss'; - -.Timeline { - .loadmore-text { - opacity: 1; - } - - &.-blocked { - cursor: progress; - } -} - -.timeline-heading { - max-width: 100%; - flex-wrap: nowrap; - align-items: center; - position: relative; - - .loadmore-button { - flex-shrink: 0; - } - - .loadmore-text { - flex-shrink: 0; - line-height: 1em; - } -} -</style> +<style src="./timeline.scss" lang="scss"> </style> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -368,6 +368,18 @@ "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "mutes_tab": "Mutes", "play_videos_in_modal": "Play videos in a popup frame", + "file_export_import": { + "backup_restore": "Settings backup", + "backup_settings": "Backup settings to file", + "backup_settings_theme": "Backup settings and theme to file", + "restore_settings": "Restore settings from file", + "errors": { + "invalid_file": "The selected file is not a supported Pleroma settings backup. No changes were made.", + "file_too_new": "Incompatile major version: {fileMajor}, this PleromaFE (settings ver {feMajor}) is too old to handle it", + "file_too_old": "Incompatile major version: {fileMajor}, file version is too old and not supported (min. set. ver. {feMajor})", + "file_slightly_new": "File minor version is different, some settings might not load" + } + }, "profile_fields": { "label": "Profile metadata", "add_field": "Add field", diff --git a/src/modules/config.js b/src/modules/config.js @@ -110,6 +110,20 @@ const config = { } }, actions: { + loadSettings ({ dispatch }, data) { + const knownKeys = new Set(Object.keys(defaultState)) + const presentKeys = new Set(Object.keys(data)) + const intersection = new Set() + for (let elem of presentKeys) { + if (knownKeys.has(elem)) { + intersection.add(elem) + } + } + + intersection.forEach( + name => dispatch('setOption', { name, value: data[name] }) + ) + }, setHighlight ({ commit, dispatch }, { user, color, type }) { commit('setHighlight', { user, color, type }) }, diff --git a/src/services/export_import/export_import.js b/src/services/export_import/export_import.js @@ -0,0 +1,55 @@ +export const newExporter = ({ + filename = 'data', + getExportedObject +}) => ({ + exportData () { + const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces + + // Create an invisible link with a data url and simulate a click + const e = document.createElement('a') + e.setAttribute('download', `${filename}.json`) + e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) + e.style.display = 'none' + + document.body.appendChild(e) + e.click() + document.body.removeChild(e) + } +}) + +export const newImporter = ({ + onImport, + onImportFailure, + validator = () => true +}) => ({ + importData () { + const filePicker = document.createElement('input') + filePicker.setAttribute('type', 'file') + filePicker.setAttribute('accept', '.json') + + filePicker.addEventListener('change', event => { + if (event.target.files[0]) { + // eslint-disable-next-line no-undef + const reader = new FileReader() + reader.onload = ({ target }) => { + try { + const parsed = JSON.parse(target.result) + const validationResult = validator(parsed) + if (validationResult === true) { + onImport(parsed) + } else { + onImportFailure({ validationResult }) + } + } catch (error) { + onImportFailure({ error }) + } + } + reader.readAsText(event.target.files[0]) + } + }) + + document.body.appendChild(filePicker) + filePicker.click() + document.body.removeChild(filePicker) + } +}) diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -380,7 +380,7 @@ export const colors2to3 = (colors) => { */ export const shadows2to3 = (shadows, opacity) => { return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { - const isDynamic = ({ color }) => color.startsWith('--') + const isDynamic = ({ color = '#000000' }) => color.startsWith('--') const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])] const newShadow = shadowDefs.reduce((shadowAcc, def) => [ ...shadowAcc,