forked from AkkomaGang/akkoma-fe
Merge branch 'settings-import-export' into 'develop'
Settings backup/restore + small fixes See merge request pleroma/pleroma-fe!1372
This commit is contained in:
commit
8b96ea9377
14 changed files with 396 additions and 194 deletions
24
src/App.scss
24
src/App.scss
|
@ -547,9 +547,21 @@ main-router {
|
||||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
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 $fallback--panelRadius $fallback--panelRadius;
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--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 {
|
.faint {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
|
@ -871,16 +883,10 @@ nav {
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-status-notification {
|
.new-status-notification {
|
||||||
position:relative;
|
position: relative;
|
||||||
margin-top: -1px;
|
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
border-width: 1px 0 0 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--border, $fallback--border);
|
|
||||||
padding: 10px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: $fallback--fg;
|
flex: 1;
|
||||||
background-color: var(--panel, $fallback--fg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-layout {
|
.chat-layout {
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,6 +1,6 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.notifications {
|
.Notifications {
|
||||||
&:not(.minimal) {
|
&:not(.minimal) {
|
||||||
// a bit of a hack to allow scrolling below notifications
|
// a bit of a hack to allow scrolling below notifications
|
||||||
padding-bottom: 15em;
|
padding-bottom: 15em;
|
||||||
|
@ -11,6 +11,10 @@
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notifications-footer {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -82,7 +86,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.follow-text, .move-text {
|
.follow-text, .move-text {
|
||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="{ minimal: minimalMode }"
|
:class="{ minimal: minimalMode }"
|
||||||
class="notifications"
|
class="Notifications"
|
||||||
>
|
>
|
||||||
<div :class="mainClass">
|
<div :class="mainClass">
|
||||||
<div
|
<div
|
||||||
|
@ -35,10 +35,10 @@
|
||||||
<notification :notification="notification" />
|
<notification :notification="notification" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer notifications-footer">
|
||||||
<div
|
<div
|
||||||
v-if="bottomedOut"
|
v-if="bottomedOut"
|
||||||
class="new-status-notification text-center panel-footer faint"
|
class="new-status-notification text-center faint"
|
||||||
>
|
>
|
||||||
{{ $t('notifications.no_more_notifications') }}
|
{{ $t('notifications.no_more_notifications') }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,13 +47,13 @@
|
||||||
class="button-unstyled -link -fullwidth"
|
class="button-unstyled -link -fullwidth"
|
||||||
@click.prevent="fetchOlderNotifications()"
|
@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') }}
|
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="new-status-notification text-center panel-footer"
|
class="new-status-notification text-center"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
icon="circle-notch"
|
icon="circle-notch"
|
||||||
|
|
|
@ -2,10 +2,55 @@ import Modal from 'src/components/modal/modal.vue'
|
||||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
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 = {
|
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: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
|
Popover,
|
||||||
SettingsModalContent: getResettableAsyncComponent(
|
SettingsModalContent: getResettableAsyncComponent(
|
||||||
() => import('./settings_modal_content.vue'),
|
() => import('./settings_modal_content.vue'),
|
||||||
{
|
{
|
||||||
|
@ -21,6 +66,85 @@ const SettingsModal = {
|
||||||
},
|
},
|
||||||
peekModal () {
|
peekModal () {
|
||||||
this.$store.dispatch('togglePeekSettingsModal')
|
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: {
|
computed: {
|
||||||
|
|
|
@ -31,20 +31,86 @@
|
||||||
</transition>
|
</transition>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
|
:title="$t('general.peek')"
|
||||||
@click="peekModal"
|
@click="peekModal"
|
||||||
>
|
>
|
||||||
{{ $t('general.peek') }}
|
<FAIcon
|
||||||
|
:icon="['far', 'window-minimize']"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
|
:title="$t('general.close')"
|
||||||
@click="closeModal"
|
@click="closeModal"
|
||||||
>
|
>
|
||||||
{{ $t('general.close') }}
|
<FAIcon
|
||||||
|
icon="times"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -15,6 +15,10 @@ import {
|
||||||
shadows2to3,
|
shadows2to3,
|
||||||
colors2to3
|
colors2to3
|
||||||
} from 'src/services/style_setter/style_setter.js'
|
} from 'src/services/style_setter/style_setter.js'
|
||||||
|
import {
|
||||||
|
newImporter,
|
||||||
|
newExporter
|
||||||
|
} from 'src/services/export_import/export_import.js'
|
||||||
import {
|
import {
|
||||||
SLOT_INHERITANCE
|
SLOT_INHERITANCE
|
||||||
} from 'src/services/theme_data/pleromafe.js'
|
} from 'src/services/theme_data/pleromafe.js'
|
||||||
|
@ -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 FontControl from 'src/components/font_control/font_control.vue'
|
||||||
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
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 Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
import Preview from './preview.vue'
|
import Preview from './preview.vue'
|
||||||
|
@ -67,6 +70,15 @@ const colorConvert = (color) => {
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
themeImporter: newImporter({
|
||||||
|
validator: this.importValidator,
|
||||||
|
onImport: this.onImport,
|
||||||
|
onImportFailure: this.onImportFailure
|
||||||
|
}),
|
||||||
|
themeExporter: newExporter({
|
||||||
|
filename: 'pleroma_theme',
|
||||||
|
getExportedObject: () => this.exportedTheme
|
||||||
|
}),
|
||||||
availableStyles: [],
|
availableStyles: [],
|
||||||
selected: this.$store.getters.mergedConfig.theme,
|
selected: this.$store.getters.mergedConfig.theme,
|
||||||
themeWarning: undefined,
|
themeWarning: undefined,
|
||||||
|
@ -383,7 +395,6 @@ export default {
|
||||||
FontControl,
|
FontControl,
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
Preview,
|
Preview,
|
||||||
ExportImport,
|
|
||||||
Checkbox
|
Checkbox
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -528,10 +539,15 @@ export default {
|
||||||
this.previewColors.mod
|
this.previewColors.mod
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
importTheme () { this.themeImporter.importData() },
|
||||||
|
exportTheme () { this.themeExporter.exportData() },
|
||||||
onImport (parsed, forceSource = false) {
|
onImport (parsed, forceSource = false) {
|
||||||
this.tempImportFile = parsed
|
this.tempImportFile = parsed
|
||||||
this.loadTheme(parsed, 'file', forceSource)
|
this.loadTheme(parsed, 'file', forceSource)
|
||||||
},
|
},
|
||||||
|
onImportFailure (result) {
|
||||||
|
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||||
|
},
|
||||||
importValidator (parsed) {
|
importValidator (parsed) {
|
||||||
const version = parsed._pleroma_theme_version
|
const version = parsed._pleroma_theme_version
|
||||||
return version >= 1 || version <= 2
|
return version >= 1 || version <= 2
|
||||||
|
|
|
@ -48,46 +48,51 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ExportImport
|
<div class="top">
|
||||||
:export-object="exportedTheme"
|
<div class="presets">
|
||||||
:export-label="$t("settings.export_theme")"
|
{{ $t('settings.presets') }}
|
||||||
:import-label="$t("settings.import_theme")"
|
<label
|
||||||
:import-failed-text="$t("settings.invalid_theme_imported")"
|
for="preset-switcher"
|
||||||
:on-import="onImport"
|
class="select"
|
||||||
:validator="importValidator"
|
>
|
||||||
>
|
<select
|
||||||
<template slot="before">
|
id="preset-switcher"
|
||||||
<div class="presets">
|
v-model="selected"
|
||||||
{{ $t('settings.presets') }}
|
class="preset-switcher"
|
||||||
<label
|
|
||||||
for="preset-switcher"
|
|
||||||
class="select"
|
|
||||||
>
|
>
|
||||||
<select
|
<option
|
||||||
id="preset-switcher"
|
v-for="style in availableStyles"
|
||||||
v-model="selected"
|
:key="style.name"
|
||||||
class="preset-switcher"
|
:value="style"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: style[1] || (style.theme || style.source).colors.bg,
|
||||||
|
color: style[3] || (style.theme || style.source).colors.text
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<option
|
{{ style[0] || style.name }}
|
||||||
v-for="style in availableStyles"
|
</option>
|
||||||
:key="style.name"
|
</select>
|
||||||
:value="style"
|
<FAIcon
|
||||||
:style="{
|
class="select-down-icon"
|
||||||
backgroundColor: style[1] || (style.theme || style.source).colors.bg,
|
icon="chevron-down"
|
||||||
color: style[3] || (style.theme || style.source).colors.text
|
/>
|
||||||
}"
|
</label>
|
||||||
>
|
</div>
|
||||||
{{ style[0] || style.name }}
|
<div class="export-import">
|
||||||
</option>
|
<button
|
||||||
</select>
|
class="btn button-default"
|
||||||
<FAIcon
|
@click="importTheme"
|
||||||
class="select-down-icon"
|
>
|
||||||
icon="chevron-down"
|
{{ $t("settings.import_theme") }}
|
||||||
/>
|
</button>
|
||||||
</label>
|
<button
|
||||||
</div>
|
class="btn button-default"
|
||||||
</template>
|
@click="exportTheme"
|
||||||
</ExportImport>
|
>
|
||||||
|
{{ $t("settings.export_theme") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="save-load-options">
|
<div class="save-load-options">
|
||||||
<span class="keep-option">
|
<span class="keep-option">
|
||||||
|
|
31
src/components/timeline/timeline.scss
Normal file
31
src/components/timeline/timeline.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,13 +52,13 @@
|
||||||
<div :class="classes.footer">
|
<div :class="classes.footer">
|
||||||
<div
|
<div
|
||||||
v-if="count===0"
|
v-if="count===0"
|
||||||
class="new-status-notification text-center panel-footer faint"
|
class="new-status-notification text-center faint"
|
||||||
>
|
>
|
||||||
{{ $t('timeline.no_statuses') }}
|
{{ $t('timeline.no_statuses') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="bottomedOut"
|
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') }}
|
{{ $t('timeline.no_more_statuses') }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,13 +67,13 @@
|
||||||
class="button-unstyled -link -fullwidth"
|
class="button-unstyled -link -fullwidth"
|
||||||
@click.prevent="fetchOlderStatuses()"
|
@click.prevent="fetchOlderStatuses()"
|
||||||
>
|
>
|
||||||
<div class="new-status-notification text-center panel-footer">
|
<div class="new-status-notification text-center">
|
||||||
{{ $t('timeline.load_older') }}
|
{{ $t('timeline.load_older') }}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="new-status-notification text-center panel-footer"
|
class="new-status-notification text-center"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
icon="circle-notch"
|
icon="circle-notch"
|
||||||
|
@ -87,32 +87,4 @@
|
||||||
|
|
||||||
<script src="./timeline.js"></script>
|
<script src="./timeline.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style src="./timeline.scss" lang="scss"> </style>
|
||||||
@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>
|
|
||||||
|
|
|
@ -368,6 +368,18 @@
|
||||||
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||||
"mutes_tab": "Mutes",
|
"mutes_tab": "Mutes",
|
||||||
"play_videos_in_modal": "Play videos in a popup frame",
|
"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": {
|
"profile_fields": {
|
||||||
"label": "Profile metadata",
|
"label": "Profile metadata",
|
||||||
"add_field": "Add field",
|
"add_field": "Add field",
|
||||||
|
|
|
@ -110,6 +110,20 @@ const config = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
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 }) {
|
setHighlight ({ commit, dispatch }, { user, color, type }) {
|
||||||
commit('setHighlight', { user, color, type })
|
commit('setHighlight', { user, color, type })
|
||||||
},
|
},
|
||||||
|
|
55
src/services/export_import/export_import.js
Normal file
55
src/services/export_import/export_import.js
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
|
@ -380,7 +380,7 @@ export const colors2to3 = (colors) => {
|
||||||
*/
|
*/
|
||||||
export const shadows2to3 = (shadows, opacity) => {
|
export const shadows2to3 = (shadows, opacity) => {
|
||||||
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
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 getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
|
||||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||||
...shadowAcc,
|
...shadowAcc,
|
||||||
|
|
Loading…
Reference in a new issue