Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/renderer/components/DataSettings/DataSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@
@click="showExportSearchHistoryPrompt = true"
/>
</FtFlexBox>
<h4 class="groupTitle">
{{ t('Settings') }}
<FtTooltip
class="selectTooltip"
position="top"
:tooltip="t('Settings.Data Settings.Settings Tooltip')"
/>
</h4>
<FtFlexBox class="box">
<FtButton
:label="t('Settings.Data Settings.Import Settings')"
@click="importSettings"
/>
<FtButton
:label="t('Settings.Data Settings.Export Settings')"
@click="exportSettings"
/>
</FtFlexBox>
<FtPrompt
v-if="showExportSubscriptionsPrompt"
:label="$t('Settings.Data Settings.Select Export Type')"
Expand All @@ -86,6 +104,13 @@
:option-values="WATCH_SEARCH_HISTORY_PROMPT_VALUES"
@click="exportSearchHistory"
/>
<FtPrompt
v-if="showRestartPrompt"
:label="$t('Settings[\'The app needs to restart for changes to take effect. Restart and apply change?\']')"
:option-names="[$t('Yes, Restart'), $t('Cancel')]"
:option-values="['restart', 'cancel']"
@click="handleRestart"
/>
</FtSettingsSection>
</template>

Expand All @@ -98,8 +123,10 @@ import FtButton from '../FtButton/FtButton.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtPrompt from '../FtPrompt/FtPrompt.vue'
import FtSettingsSection from '../FtSettingsSection/FtSettingsSection.vue'
import FtTooltip from '../FtTooltip/FtTooltip.vue'

import store from '../../store/index'
import { defaultUpdaterId, settingsNeedingRestart } from '../../store/modules/settings'

import { MAIN_PROFILE_ID } from '../../../constants'
import { calculateColorLuminance, getRandomColor } from '../../helpers/colors'
Expand Down Expand Up @@ -1458,6 +1485,110 @@ async function exportYouTubeSearchHistory() {
}

// #endregion search history

// #region settings

/** @type {import('vue').ComputedRef<object>} */
const exportableSettings = computed(() => {
return store.getters.getExportableSettings
})

const showRestartPrompt = ref(false)

const pendingSettings = new Map()

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 currentSettings = exportableSettings.value

for (const [importedKey, importedValue] of Object.entries(importedSettings)) {
Comment thread
Shadorc marked this conversation as resolved.
if (!Object.hasOwn(currentSettings, importedKey)) {
const message = `${t('Settings.Data Settings.Unknown setting key')}: ${importedKey}`
showToast(message)
continue
}

const currentValue = currentSettings[importedKey]
const areValuesEqual = currentValue === importedValue ||
(typeof importedValue === 'object' && JSON.stringify(currentValue) === JSON.stringify(importedValue))
if (areValuesEqual) {
continue
}

if (settingsNeedingRestart.has(importedKey)) {
pendingSettings.set(importedKey, importedValue)
showRestartPrompt.value = true
Comment thread
Shadorc marked this conversation as resolved.
Outdated
} else {
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(exportableSettings.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')
)
}

/**
* @param {'restart' | 'cancel' | null} value
*/
function handleRestart(value) {
showRestartPrompt.value = false

if (value === null || value === 'cancel') {
pendingSettings.clear()
return
}

if (process.env.IS_ELECTRON) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean the prompt is shown for web build (even though not officially supported) and will do nothing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shadorc The difference is that in the other places it will never even get to that code, the electron check is just there to ensure that the electron only code gets removed properly during the build (as deadcode removal isn't guaranteed with Vue in the mix) and makes it clearer to people reading the code that it is electron only.

E.g. for the smooth scrolling switch is not shown outside of electron: https://github.com/FreeTubeApp/FreeTube/blob/development/src/renderer/components/ThemeSettings.vue#L20 and the entire experimental settings component is Electron only: https://github.com/FreeTubeApp/FreeTube/blob/development/src/renderer/views/Settings/Settings.vue#L174-L181

So you can either make the entire settings import and export feature electron only or write it in a way that also has a non-Electron restart and flexible enough that an Android specific restart could be added in downstream FreeTubeAndroid.

Copy link
Copy Markdown
Contributor Author

@Shadorc Shadorc May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification.
I had searched for settings depending on the IS_ELECTRON env var, but I missed disableSmoothScrolling and hideToTrayOnMinimize.

Concerning your suggestions, I see one more option.
Instead of making the entire feature electron-only, all electron-only settings could be marked as not transferrable.

It would not remove a lot of settings because, according to my searches, here are the settings depending on process.env:

/* 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',

If we remove settings mentioned here, we get:

  /* Depends on process.env.IS_ELECTRON */
  // Others
  'disableSmoothScrolling',
  'hideToTrayOnMinimize',

  /* Depends on process.env.SUPPORTS_LOCAL_API */
  'backendFallback',
  'backendPreference',
  'proxyVideos',

The last three depend on process.env.SUPPORTS_LOCAL_API, they should probably not be transferrable.
hideToTrayOnMinimize is OS-specific because it is not available on macOS.
The remaining one is disableSmoothScrolling.

So, making the whole feature electron-only would only be useful to support the smooth scrolling feature but would prevent users from importing and exporting settings on mobile.
It could still be possible to later implement your idea of having a flexible way to restart the app, no matter if it's electron or not, and add the missing support for disableSmoothScrolling.

I will update my MR to change the settingsNotExportable with all the settings mentioned above.
Let me know what you think, I've tried to imagine the best scenario for users, code maintainability and complexity.

Promise.all(
Array.from(pendingSettings, ([settingKey, settingValue]) => {
const updaterId = defaultUpdaterId(settingKey)
return store.dispatch(updaterId, settingValue)
})
).then(() => {
window.ftElectron.relaunch()
})
}
}

// #endregion settings

</script>

<style scoped src="./DataSettings.css" />
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 53 additions & 4 deletions src/renderer/store/modules/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,23 @@ import { getSystemLocale, showToast } from '../../helpers/utils'
* to evaluate if it is truly necessary
* and to ensure that the implementation works as intended.
*
***
* `settingsNotExportable`
* 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.
*
***
* `settingsNeedingRestart`
* This set contains setting keys
* that require an application restart to take effect.
*
* When adding a new setting that needs a restart for the
* change to apply, it should be added to this set.
*
****
* ENDING NOTES
*
Expand All @@ -141,10 +158,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'
/*****/

Expand Down Expand Up @@ -414,10 +431,42 @@ const sideEffectHandlers = {

const settingsWithSideEffects = Object.keys(sideEffectHandlers)

const settingsNotExportable = new Set([
Comment thread
Shadorc marked this conversation as resolved.
Outdated
'useProxy',
'proxyHostname',
'proxyPort',
'proxyUsername',
'proxyPassword',
'proxyProtocol',
'proxyVideos',
'externalPlayer',
'externalPlayerExecutable',
'externalPlayerIgnoreWarnings',
'externalPlayerIgnoreDefaultArgs',
'externalPlayerCustomArgs',
'showAddedExternalPlayerCustomArgs',
'screenshotAskPath',
'screenshotFolderPath',
'backendPreference'
])

export const settingsNeedingRestart = new Set([
'disableSmoothScrolling'
])

const customState = {
}

const customGetters = {
getExportableSettings: (state) => {
const exportableSettings = {}
for (const [key, value] of Object.entries(state)) {
if (!settingsNotExportable.has(key)) {
exportableSettings[key] = value
}
}
return exportableSettings
}
}

const customMutations = {}
Expand Down
9 changes: 9 additions & 0 deletions static/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -625,9 +629,14 @@ 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
How do I import my subscriptions?: How do I import my subscriptions?
Manage Subscriptions: Manage Subscriptions
Proxy Settings:
Expand Down
Loading