Skip to content

Commit 65889fa

Browse files
authored
Add import/export settings buttons (#9123)
* Add import/export settings buttons * Add exported settings exclusion list * Add documentation * Add tooltip to the data settings header * Consider settingsPassword to be exportable, could be discussed * Take into account settings that need restart * Simplify const access across store and vue script * Add documentation * Improve tooltip text * Fix tooltip on mobile screen * Add more items to settingsNotExportable set * Add backendPreference key to settingsNotExportable set because it is dependent of process.env * Set 'showRestartPrompt.value' once after for loop * Replace 'exportable' occurences by 'transferrable' * Add more settings to 'settingsNotTransferrable' array * Remove pending restart and update 'settingsNotTransferrable' * Fix typo * Differentiate between non-transferable and unknown settings * Fix localization key * Use same format as db file for import/export * Apply code suggestions * Apply code suggestions
1 parent 73ec29f commit 65889fa

4 files changed

Lines changed: 145 additions & 2 deletions

File tree

src/renderer/components/DataSettings/DataSettings.vue

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@
6565
@click="showExportSearchHistoryPrompt = true"
6666
/>
6767
</FtFlexBox>
68+
<h4 class="groupTitle">
69+
{{ t('Settings.Settings') }}
70+
<FtTooltip
71+
class="selectTooltip"
72+
position="top"
73+
:tooltip="t('Settings.Data Settings.Settings Tooltip')"
74+
/>
75+
</h4>
76+
<FtFlexBox class="box">
77+
<FtButton
78+
:label="t('Settings.Data Settings.Import Settings')"
79+
@click="importSettings"
80+
/>
81+
<FtButton
82+
:label="t('Settings.Data Settings.Export Settings')"
83+
@click="exportSettings"
84+
/>
85+
</FtFlexBox>
6886
<FtPrompt
6987
v-if="showExportSubscriptionsPrompt"
7088
:label="$t('Settings.Data Settings.Select Export Type')"
@@ -98,8 +116,10 @@ import FtButton from '../FtButton/FtButton.vue'
98116
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
99117
import FtPrompt from '../FtPrompt/FtPrompt.vue'
100118
import FtSettingsSection from '../FtSettingsSection/FtSettingsSection.vue'
119+
import FtTooltip from '../FtTooltip/FtTooltip.vue'
101120
102121
import store from '../../store/index'
122+
import { defaultUpdaterId, NON_TRANSFERABLE_SETTINGS } from '../../store/modules/settings'
103123
104124
import { MAIN_PROFILE_ID } from '../../../constants'
105125
import { calculateColorLuminance, getRandomColor } from '../../helpers/colors'
@@ -1465,6 +1485,81 @@ async function exportYouTubeSearchHistory() {
14651485
}
14661486
14671487
// #endregion search history
1488+
1489+
// #region settings
1490+
1491+
async function importSettings() {
1492+
let response
1493+
try {
1494+
response = await readFileWithPicker(
1495+
t('Settings.Data Settings.Settings File'),
1496+
{
1497+
'application/x-freetube-db': '.db',
1498+
'application/json': '.json'
1499+
},
1500+
IMPORT_DIRECTORY_ID,
1501+
START_IN_DIRECTORY
1502+
)
1503+
} catch (err) {
1504+
const message = t('Settings.Data Settings.Unable to read file')
1505+
showToast(`${message}: ${err}`)
1506+
return
1507+
}
1508+
1509+
if (response === null) {
1510+
return
1511+
}
1512+
1513+
const textDecode = response.content.split('\n')
1514+
textDecode.pop()
1515+
1516+
const currentSettings = store.state.settings
1517+
1518+
textDecode.forEach((rawEntry) => {
1519+
const entry = JSON.parse(rawEntry)
1520+
if (typeof entry._id !== 'string' || !Object.hasOwn(entry, 'value')) {
1521+
showToast(t('Settings.Data Settings.Setting object has insufficient data, skipping item'))
1522+
console.error('Missing keys:', entry)
1523+
} else if (!Object.hasOwn(currentSettings, entry._id)) {
1524+
const message = t('Settings.Data Settings.Unknown setting key', { key: entry._id })
1525+
showToast(message)
1526+
} else if (NON_TRANSFERABLE_SETTINGS.has(entry._id)) {
1527+
const message = t('Settings.Data Settings.Non-transferable setting key', { key: entry._id })
1528+
showToast(message)
1529+
} else {
1530+
const currentValue = currentSettings[entry._id]
1531+
const areValuesEqual = currentValue === entry.value ||
1532+
(typeof entry.value === 'object' && JSON.stringify(currentValue) === JSON.stringify(entry.value))
1533+
if (!areValuesEqual) {
1534+
const updaterId = defaultUpdaterId(entry._id)
1535+
store.dispatch(updaterId, entry.value)
1536+
}
1537+
}
1538+
})
1539+
1540+
showToast(t('Settings.Data Settings.All settings have been successfully imported'))
1541+
}
1542+
1543+
async function exportSettings() {
1544+
const settingDb = Object.entries(store.state.settings)
1545+
.filter(([_id]) => !NON_TRANSFERABLE_SETTINGS.has(_id))
1546+
.map(([_id, value]) => JSON.stringify({ _id, value }))
1547+
.join('\n') + '\n'
1548+
const dateStr = getTodayDateStrLocalTimezone()
1549+
const exportFileName = 'freetube-settings-' + dateStr + '.db'
1550+
1551+
await promptAndWriteToFile(
1552+
exportFileName,
1553+
settingDb,
1554+
t('Settings.Data Settings.Settings File'),
1555+
'application/x-freetube-db',
1556+
'.db',
1557+
t('Settings.Data Settings.All settings have been successfully exported')
1558+
)
1559+
}
1560+
1561+
// #endregion settings
1562+
14681563
</script>
14691564
14701565
<style scoped src="./DataSettings.css" />

src/renderer/components/FtSettingsSection/FtSettingsSection.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
padding-inline: 10px;
9898
}
9999

100-
:deep(:not(.select, .selectLabel) > .tooltip) {
100+
:deep(:not(.select, .selectLabel, .groupTitle) > .tooltip) {
101101
display: inline-block;
102102
position: absolute;
103103
inset-inline-end: -25px;

src/renderer/store/modules/settings.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ import { getSystemLocale, showToast } from '../../helpers/utils'
121121
* to evaluate if it is truly necessary
122122
* and to ensure that the implementation works as intended.
123123
*
124+
***
125+
* `NON_TRANSFERABLE_SETTINGS`
126+
* This set contains setting keys
127+
* that should not be exported when a user chooses to "Export settings".
128+
*
129+
* When adding a new setting, it should be considered
130+
* whether this setting can be exported or not. For example, settings
131+
* that are OS or user specific like paths should not be exported.
132+
*
124133
****
125134
* ENDING NOTES
126135
*
@@ -143,7 +152,7 @@ import { getSystemLocale, showToast } from '../../helpers/utils'
143152
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)
144153
const defaultGetterId = settingId => 'get' + capitalize(settingId)
145154
const defaultMutationId = settingId => 'set' + capitalize(settingId)
146-
const defaultUpdaterId = settingId => 'update' + capitalize(settingId)
155+
export const defaultUpdaterId = settingId => 'update' + capitalize(settingId)
147156
const defaultSideEffectsTriggerId = settingId =>
148157
'trigger' + capitalize(settingId) + 'SideEffects'
149158
/*****/
@@ -415,6 +424,34 @@ const sideEffectHandlers = {
415424

416425
const settingsWithSideEffects = Object.keys(sideEffectHandlers)
417426

427+
export const NON_TRANSFERABLE_SETTINGS = new Set([
428+
/* Depends on process.env.IS_ELECTRON */
429+
// ProxySettings
430+
'useProxy',
431+
'proxyProtocol',
432+
'proxyHostname',
433+
'proxyPort',
434+
'proxyUsername',
435+
'proxyPassword',
436+
// ExternalPlayerSettings
437+
'externalPlayer',
438+
'externalPlayerExecutable',
439+
'externalPlayerIgnoreWarnings',
440+
'externalPlayerIgnoreDefaultArgs',
441+
'externalPlayerCustomArgs',
442+
'showAddedExternalPlayerCustomArgs',
443+
// Others
444+
'disableSmoothScrolling',
445+
'hideToTrayOnMinimize',
446+
'screenshotAskPath',
447+
'screenshotFolderPath',
448+
449+
/* Depends on process.env.SUPPORTS_LOCAL_API */
450+
'backendFallback',
451+
'backendPreference',
452+
'proxyVideos',
453+
])
454+
418455
const customState = {
419456
}
420457

static/locales/en-US.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ Settings:
593593
History File: History File
594594
Playlist File: Playlist File
595595
Search history file: Search history file
596+
Settings File: Settings File
596597
Export Subscriptions: Export Subscriptions
597598
Export FreeTube: Export FreeTube
598599
Export YouTube: Export YouTube
@@ -604,6 +605,9 @@ Settings:
604605
Search history: Search history
605606
Import search history: Import search history
606607
Export search history: Export search history
608+
Import Settings: Import Settings
609+
Export Settings: Export Settings
610+
Settings Tooltip: Settings that are OS/user-specific or experimental cannot be exported or imported (e.g., proxy, external player, screenshot folder...)
607611
Profile object has insufficient data, skipping item: Profile object has insufficient
608612
data, skipping item
609613
All subscriptions and profiles have been successfully imported: All subscriptions
@@ -629,9 +633,16 @@ Settings:
629633
successfully imported
630634
All search history has been successfully exported: All search history has been
631635
successfully exported
636+
All settings have been successfully imported: All settings have been
637+
successfully imported
638+
All settings have been successfully exported: All settings have been
639+
successfully exported
632640
Unable to read file: Unable to read file
633641
Unable to write file: Unable to write file
634642
Unknown data key: Unknown data key
643+
Setting object has insufficient data, skipping item: Setting object has insufficient data, skipping item
644+
Unknown setting key: 'Unknown setting key: {key}'
645+
Non-transferable setting key: 'Non-transferable setting key: {key}'
635646
How do I import my subscriptions?: How do I import my subscriptions?
636647
Manage Subscriptions: Manage Subscriptions
637648
Proxy Settings:

0 commit comments

Comments
 (0)