diff --git a/src/renderer/components/DataSettings/DataSettings.vue b/src/renderer/components/DataSettings/DataSettings.vue
index 9228ded2fd876..99e102eb71d58 100644
--- a/src/renderer/components/DataSettings/DataSettings.vue
+++ b/src/renderer/components/DataSettings/DataSettings.vue
@@ -65,6 +65,24 @@
@click="showExportSearchHistoryPrompt = true"
/>
+
+ {{ t('Settings.Settings') }}
+
+
+
+
+
+
} */
+const transferableSettings = computed(() => {
+ return store.getters.getTransferableSettings
+})
+
+async function importSettings() {
+ let response
+ try {
+ response = await readFileWithPicker(
+ t('Settings.Data Settings.Settings File'),
+ {
+ 'application/x-freetube-db': '.db',
+ 'application/json': '.json'
+ },
+ IMPORT_DIRECTORY_ID,
+ START_IN_DIRECTORY
+ )
+ } catch (err) {
+ const message = t('Settings.Data Settings.Unable to read file')
+ showToast(`${message}: ${err}`)
+ return
+ }
+
+ if (response === null) {
+ return
+ }
+
+ const { content } = response
+ const importedSettings = JSON.parse(content)
+ const currentTransferableSettings = transferableSettings.value
+ const currentSettings = store.state.settings
+
+ for (const [importedKey, importedValue] of Object.entries(importedSettings)) {
+ if (!Object.hasOwn(currentSettings, importedKey)) {
+ const message = `${t('Settings.Data Settings.Unknown setting key')}: ${importedKey}`
+ showToast(message)
+ continue
+ }
+
+ if (!Object.hasOwn(currentTransferableSettings, importedKey)) {
+ const message = `${t('Settings.Data Settings.Non-transferable setting key')}: ${importedKey}`
+ showToast(message)
+ continue
+ }
+
+ const currentValue = currentTransferableSettings[importedKey]
+ const areValuesEqual = currentValue === importedValue ||
+ (typeof importedValue === 'object' && JSON.stringify(currentValue) === JSON.stringify(importedValue))
+ if (areValuesEqual) {
+ continue
+ }
+
+ const updaterId = defaultUpdaterId(importedKey)
+ await store.dispatch(updaterId, importedValue)
+ }
+
+ showToast(t('Settings.Data Settings.All settings have been successfully imported'))
+}
+
+async function exportSettings() {
+ const dateStr = getTodayDateStrLocalTimezone()
+ const exportFileName = `freetube-settings-${dateStr}.db`
+ const settingsContent = JSON.stringify(transferableSettings.value)
+
+ await promptAndWriteToFile(
+ exportFileName,
+ settingsContent,
+ t('Settings.Data Settings.Settings File'),
+ 'application/x-freetube-db',
+ '.db',
+ t('Settings.Data Settings.All settings have been successfully exported')
+ )
+}
+
+// #endregion settings
+
diff --git a/src/renderer/components/FtSettingsSection/FtSettingsSection.scss b/src/renderer/components/FtSettingsSection/FtSettingsSection.scss
index 9d9fad66dba36..c723cdcbceb5d 100644
--- a/src/renderer/components/FtSettingsSection/FtSettingsSection.scss
+++ b/src/renderer/components/FtSettingsSection/FtSettingsSection.scss
@@ -97,7 +97,7 @@
padding-inline: 10px;
}
- :deep(:not(.select, .selectLabel) > .tooltip) {
+ :deep(:not(.select, .selectLabel, .groupTitle) > .tooltip) {
display: inline-block;
position: absolute;
inset-inline-end: -25px;
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index 99affa66401d0..4560c38420f4c 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -121,6 +121,15 @@ import { getSystemLocale, showToast } from '../../helpers/utils'
* to evaluate if it is truly necessary
* and to ensure that the implementation works as intended.
*
+ ***
+ * `settingsNotTransferable`
+ * This set contains setting keys
+ * that should not be exported when a user chooses to "Export settings".
+ *
+ * When adding a new setting, it should be considered
+ * whether this setting can be exported or not. For example, settings
+ * that are OS or user specific like paths should not be exported.
+ *
****
* ENDING NOTES
*
@@ -141,10 +150,10 @@ import { getSystemLocale, showToast } from '../../helpers/utils'
// HELPERS
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)
-const defaultGetterId = settingId => 'get' + capitalize(settingId)
-const defaultMutationId = settingId => 'set' + capitalize(settingId)
-const defaultUpdaterId = settingId => 'update' + capitalize(settingId)
-const defaultSideEffectsTriggerId = settingId =>
+export const defaultGetterId = settingId => 'get' + capitalize(settingId)
+export const defaultMutationId = settingId => 'set' + capitalize(settingId)
+export const defaultUpdaterId = settingId => 'update' + capitalize(settingId)
+export const defaultSideEffectsTriggerId = settingId =>
'trigger' + capitalize(settingId) + 'SideEffects'
/*****/
@@ -414,10 +423,47 @@ const sideEffectHandlers = {
const settingsWithSideEffects = Object.keys(sideEffectHandlers)
+const settingsNotTransferable = new Set([
+ /* Depends on process.env.IS_ELECTRON */
+ // ProxySettings
+ 'useProxy',
+ 'proxyProtocol',
+ 'proxyHostname',
+ 'proxyPort',
+ 'proxyUsername',
+ 'proxyPassword',
+ // ExternalPlayerSettings
+ 'externalPlayer',
+ 'externalPlayerExecutable',
+ 'externalPlayerIgnoreWarnings',
+ 'externalPlayerIgnoreDefaultArgs',
+ 'externalPlayerCustomArgs',
+ 'showAddedExternalPlayerCustomArgs',
+ // Others
+ 'disableSmoothScrolling',
+ 'hideToTrayOnMinimize',
+ 'screenshotAskPath',
+ 'screenshotFolderPath',
+
+ /* Depends on process.env.SUPPORTS_LOCAL_API */
+ 'backendFallback',
+ 'backendPreference',
+ 'proxyVideos',
+])
+
const customState = {
}
const customGetters = {
+ getTransferableSettings: (state) => {
+ const transferableSettings = {}
+ for (const [key, value] of Object.entries(state)) {
+ if (!settingsNotTransferable.has(key)) {
+ transferableSettings[key] = value
+ }
+ }
+ return transferableSettings
+ }
}
const customMutations = {}
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index 95aedd31f9d06..411550d232494 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -589,6 +589,7 @@ Settings:
History File: History File
Playlist File: Playlist File
Search history file: Search history file
+ Settings File: Settings File
Export Subscriptions: Export Subscriptions
Export FreeTube: Export FreeTube
Export YouTube: Export YouTube
@@ -600,6 +601,9 @@ Settings:
Search history: Search history
Import search history: Import search history
Export search history: Export search history
+ Import Settings: Import Settings
+ Export Settings: Export Settings
+ Settings Tooltip: Settings that are OS/user-specific or experimental cannot be exported or imported (e.g., proxy, external player, screenshot folder...)
Profile object has insufficient data, skipping item: Profile object has insufficient
data, skipping item
All subscriptions and profiles have been successfully imported: All subscriptions
@@ -625,9 +629,15 @@ Settings:
successfully imported
All search history has been successfully exported: All search history has been
successfully exported
+ All settings have been successfully imported: All settings have been
+ successfully imported
+ All settings have been successfully exported: All settings have been
+ successfully exported
Unable to read file: Unable to read file
Unable to write file: Unable to write file
Unknown data key: Unknown data key
+ Unknown setting key: Unknown setting key
+ Non-transferable setting key: Non-transferable setting key
How do I import my subscriptions?: How do I import my subscriptions?
Manage Subscriptions: Manage Subscriptions
Proxy Settings: