2020-02-16 19:30:00 +01:00
|
|
|
import IsEqual from 'lodash.isequal'
|
2020-07-04 17:44:35 +02:00
|
|
|
import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
2020-10-22 20:56:49 +02:00
|
|
|
import fs from 'fs'
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
|
|
|
|
import { IpcChannels } from '../../../constants'
|
2022-01-30 18:49:16 +01:00
|
|
|
import { ipcRenderer } from 'electron'
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
|
2020-02-16 19:30:00 +01:00
|
|
|
const state = {
|
|
|
|
isSideNavOpen: false,
|
|
|
|
sessionSearchHistory: [],
|
2020-08-18 17:51:56 +02:00
|
|
|
popularCache: null,
|
2021-08-21 23:08:38 +02:00
|
|
|
trendingCache: {
|
|
|
|
default: null,
|
|
|
|
music: null,
|
|
|
|
gaming: null,
|
|
|
|
movies: null
|
|
|
|
},
|
2020-08-31 23:35:22 +02:00
|
|
|
showProgressBar: false,
|
|
|
|
progressBarPercentage: 0,
|
2020-10-22 20:56:49 +02:00
|
|
|
regionNames: [],
|
|
|
|
regionValues: [],
|
2020-09-20 20:22:39 +02:00
|
|
|
recentBlogPosts: [],
|
2020-02-16 19:30:00 +01:00
|
|
|
searchSettings: {
|
|
|
|
sortBy: 'relevance',
|
|
|
|
time: '',
|
|
|
|
type: 'all',
|
|
|
|
duration: ''
|
2020-05-23 23:29:42 +02:00
|
|
|
},
|
|
|
|
colorClasses: [
|
|
|
|
'mainRed',
|
|
|
|
'mainPink',
|
|
|
|
'mainPurple',
|
|
|
|
'mainDeepPurple',
|
|
|
|
'mainIndigo',
|
|
|
|
'mainBlue',
|
|
|
|
'mainLightBlue',
|
|
|
|
'mainCyan',
|
|
|
|
'mainTeal',
|
|
|
|
'mainGreen',
|
|
|
|
'mainLightGreen',
|
|
|
|
'mainLime',
|
|
|
|
'mainYellow',
|
|
|
|
'mainAmber',
|
|
|
|
'mainOrange',
|
2021-11-11 21:54:08 +01:00
|
|
|
'mainDeepOrange',
|
|
|
|
'mainDraculaCyan',
|
|
|
|
'mainDraculaGreen',
|
|
|
|
'mainDraculaOrange',
|
|
|
|
'mainDraculaPink',
|
|
|
|
'mainDraculaPurple',
|
|
|
|
'mainDraculaRed',
|
|
|
|
'mainDraculaYellow'
|
2020-08-23 21:07:29 +02:00
|
|
|
],
|
|
|
|
colorValues: [
|
|
|
|
'#d50000',
|
|
|
|
'#C51162',
|
|
|
|
'#AA00FF',
|
|
|
|
'#6200EA',
|
|
|
|
'#304FFE',
|
|
|
|
'#2962FF',
|
|
|
|
'#0091EA',
|
|
|
|
'#00B8D4',
|
|
|
|
'#00BFA5',
|
|
|
|
'#00C853',
|
|
|
|
'#64DD17',
|
|
|
|
'#AEEA00',
|
|
|
|
'#FFD600',
|
|
|
|
'#FFAB00',
|
|
|
|
'#FF6D00',
|
2021-11-11 21:54:08 +01:00
|
|
|
'#DD2C00',
|
|
|
|
'#8BE9FD',
|
|
|
|
'#50FA7B',
|
|
|
|
'#FFB86C',
|
|
|
|
'#FF79C6',
|
|
|
|
'#BD93F9',
|
|
|
|
'#FF5555',
|
|
|
|
'#F1FA8C'
|
2021-06-13 17:31:43 +02:00
|
|
|
],
|
|
|
|
externalPlayerNames: [],
|
|
|
|
externalPlayerValues: [],
|
|
|
|
externalPlayerCmdArguments: {}
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const getters = {
|
2020-03-01 04:37:02 +01:00
|
|
|
getIsSideNavOpen () {
|
|
|
|
return state.isSideNavOpen
|
|
|
|
},
|
|
|
|
|
|
|
|
getCurrentVolume () {
|
|
|
|
return state.currentVolume
|
|
|
|
},
|
|
|
|
|
2020-02-16 19:30:00 +01:00
|
|
|
getSessionSearchHistory () {
|
|
|
|
return state.sessionSearchHistory
|
|
|
|
},
|
|
|
|
|
2020-08-13 16:26:20 +02:00
|
|
|
getPopularCache () {
|
|
|
|
return state.popularCache
|
|
|
|
},
|
|
|
|
|
2020-08-22 22:37:09 +02:00
|
|
|
getTrendingCache () {
|
|
|
|
return state.trendingCache
|
|
|
|
},
|
|
|
|
|
2020-02-16 19:30:00 +01:00
|
|
|
getSearchSettings () {
|
|
|
|
return state.searchSettings
|
2020-08-24 04:56:33 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
getColorValues () {
|
|
|
|
return state.colorValues
|
2020-08-31 23:35:22 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
getShowProgressBar () {
|
|
|
|
return state.showProgressBar
|
|
|
|
},
|
|
|
|
|
|
|
|
getProgressBarPercentage () {
|
|
|
|
return state.progressBarPercentage
|
2020-09-20 20:22:39 +02:00
|
|
|
},
|
|
|
|
|
2020-10-22 20:56:49 +02:00
|
|
|
getRegionNames () {
|
|
|
|
return state.regionNames
|
|
|
|
},
|
|
|
|
|
|
|
|
getRegionValues () {
|
|
|
|
return state.regionValues
|
|
|
|
},
|
|
|
|
|
2020-09-20 20:22:39 +02:00
|
|
|
getRecentBlogPosts () {
|
|
|
|
return state.recentBlogPosts
|
2021-06-13 17:31:43 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
getExternalPlayerNames () {
|
|
|
|
return state.externalPlayerNames
|
|
|
|
},
|
|
|
|
|
|
|
|
getExternalPlayerValues () {
|
|
|
|
return state.externalPlayerValues
|
|
|
|
},
|
|
|
|
|
|
|
|
getExternalPlayerCmdArguments () {
|
|
|
|
return state.externalPlayerCmdArguments
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
/**
|
|
|
|
* Wrapper function that calls `ipcRenderer.invoke(IRCtype, payload)` if the user is
|
|
|
|
* using Electron or a provided custom callback otherwise.
|
|
|
|
* @param {Object} context Object
|
|
|
|
* @param {String} IRCtype String
|
|
|
|
* @param {Function} webCbk Function
|
|
|
|
* @param {Object} payload any (default: null)
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function invokeIRC(context, IRCtype, webCbk, payload = null) {
|
|
|
|
let response = null
|
|
|
|
const usingElectron = context.rootState.settings.usingElectron
|
|
|
|
if (usingElectron) {
|
|
|
|
const { ipcRenderer } = require('electron')
|
|
|
|
response = await ipcRenderer.invoke(IRCtype, payload)
|
|
|
|
} else if (webCbk) {
|
|
|
|
response = await webCbk()
|
|
|
|
}
|
|
|
|
|
|
|
|
return response
|
|
|
|
}
|
|
|
|
|
2020-05-23 23:29:42 +02:00
|
|
|
const actions = {
|
2021-05-22 01:49:48 +02:00
|
|
|
openExternalLink ({ rootState }, url) {
|
|
|
|
const usingElectron = rootState.settings.usingElectron
|
|
|
|
if (usingElectron) {
|
|
|
|
const ipcRenderer = require('electron').ipcRenderer
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
ipcRenderer.send(IpcChannels.OPEN_EXTERNAL_LINK, url)
|
2021-05-22 01:49:48 +02:00
|
|
|
} else {
|
|
|
|
// Web placeholder
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-01-30 18:49:16 +01:00
|
|
|
async downloadMedia({ rootState, dispatch }, { url, title, extension, folderPath, fallingBackPath }) {
|
|
|
|
const usingElectron = rootState.settings.usingElectron
|
|
|
|
const askFolderPath = folderPath === ''
|
|
|
|
let filePathSelected
|
|
|
|
const successMsg = 'Downloading has completed'
|
|
|
|
|
|
|
|
if (askFolderPath && usingElectron) {
|
|
|
|
const resp = await ipcRenderer.invoke(
|
|
|
|
IpcChannels.SHOW_SAVE_DIALOG,
|
|
|
|
{ defaultPath: `${title}.${extension}` }
|
|
|
|
)
|
|
|
|
filePathSelected = resp.filePath
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fallingBackPath !== undefined) {
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: 'Download folder does not exist',
|
|
|
|
translate: true,
|
|
|
|
formatArgs: [fallingBackPath]
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: 'Starting download', translate: true, formatArgs: [title]
|
|
|
|
})
|
|
|
|
|
|
|
|
const response = await fetch(url)
|
|
|
|
// mechanism to show the download progress reference https://javascript.info/fetch-progress
|
|
|
|
const reader = response.body.getReader()
|
|
|
|
|
|
|
|
const contentLength = response.headers.get('Content-Length')
|
|
|
|
|
|
|
|
let receivedLength = 0
|
|
|
|
const chunks = []
|
|
|
|
// manage frequency notifications to the user
|
|
|
|
const intervalPercentageNotification = 0.2
|
|
|
|
let lastPercentageNotification = 0
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const { done, value } = await reader.read()
|
|
|
|
|
|
|
|
if (done) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
chunks.push(value)
|
|
|
|
receivedLength += value.length
|
|
|
|
const percentage = receivedLength / contentLength
|
|
|
|
if (percentage > (lastPercentageNotification + intervalPercentageNotification)) {
|
|
|
|
// mechanism kept for an upcoming download page
|
|
|
|
lastPercentageNotification = percentage
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const chunksAll = new Uint8Array(receivedLength)
|
|
|
|
let position = 0
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
chunksAll.set(chunk, position)
|
|
|
|
position += chunk.length
|
|
|
|
}
|
|
|
|
|
|
|
|
// write the file into the hardrive
|
|
|
|
if (!response.ok) {
|
|
|
|
console.error(`"Unable to download ${title}, return status code ${response.status}`)
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: 'Downloading failed', translate: true, formatArgs: [title, response.status]
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const blobFile = new Blob(chunks)
|
|
|
|
const buffer = await blobFile.arrayBuffer()
|
|
|
|
|
|
|
|
if (usingElectron && !askFolderPath) {
|
|
|
|
fs.writeFile(`${folderPath}/${title}.${extension}`, new DataView(buffer), (err) => {
|
|
|
|
if (err) {
|
|
|
|
console.error(err)
|
|
|
|
dispatch('updateDownloadFolderPath', '')
|
|
|
|
dispatch('downloadMedia', { url: url, title: title, extension: extension, folderPath: '', fallingBackPath: folderPath })
|
|
|
|
} else {
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: successMsg, translate: true, formatArgs: [title]
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else if (usingElectron) {
|
|
|
|
fs.writeFile(filePathSelected, new DataView(buffer), (err) => {
|
|
|
|
if (err) {
|
|
|
|
console.error(err)
|
|
|
|
if (filePathSelected === '') {
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: 'Downloading canceled',
|
|
|
|
translate: true
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: err
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: successMsg, translate: true, formatArgs: [title]
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// Web placeholder
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-06-20 03:42:11 +02:00
|
|
|
async getSystemLocale (context) {
|
2021-05-22 01:49:48 +02:00
|
|
|
const webCbk = () => {
|
|
|
|
if (navigator && navigator.language) {
|
|
|
|
return navigator.language
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
return (await invokeIRC(context, IpcChannels.GET_SYSTEM_LOCALE, webCbk)) || 'en-US'
|
2021-05-22 01:49:48 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
async showOpenDialog (context, options) {
|
|
|
|
// TODO: implement showOpenDialog web compatible callback
|
|
|
|
const webCbk = () => null
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
return await invokeIRC(context, IpcChannels.SHOW_OPEN_DIALOG, webCbk, options)
|
2021-05-22 01:49:48 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
async showSaveDialog (context, options) {
|
|
|
|
// TODO: implement showSaveDialog web compatible callback
|
|
|
|
const webCbk = () => null
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
return await invokeIRC(context, IpcChannels.SHOW_SAVE_DIALOG, webCbk, options)
|
2021-05-22 01:49:48 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
async getUserDataPath (context) {
|
|
|
|
// TODO: implement getUserDataPath web compatible callback
|
|
|
|
const webCbk = () => null
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
return await invokeIRC(context, IpcChannels.GET_USER_DATA_PATH, webCbk)
|
2021-05-22 01:49:48 +02:00
|
|
|
},
|
|
|
|
|
2020-08-31 23:35:22 +02:00
|
|
|
updateShowProgressBar ({ commit }, value) {
|
|
|
|
commit('setShowProgressBar', value)
|
|
|
|
},
|
|
|
|
|
2020-05-23 23:29:42 +02:00
|
|
|
getRandomColorClass () {
|
|
|
|
const randomInt = Math.floor(Math.random() * state.colorClasses.length)
|
|
|
|
return state.colorClasses[randomInt]
|
2020-06-01 05:13:03 +02:00
|
|
|
},
|
|
|
|
|
2020-08-23 21:07:29 +02:00
|
|
|
getRandomColor () {
|
|
|
|
const randomInt = Math.floor(Math.random() * state.colorValues.length)
|
|
|
|
return state.colorValues[randomInt]
|
|
|
|
},
|
|
|
|
|
2020-10-22 20:56:49 +02:00
|
|
|
getRegionData ({ commit }, payload) {
|
|
|
|
let fileData
|
2020-10-27 18:47:40 +01:00
|
|
|
/* eslint-disable-next-line */
|
2020-10-22 20:56:49 +02:00
|
|
|
const fileLocation = payload.isDev ? './static/geolocations/' : `${__dirname}/static/geolocations/`
|
|
|
|
if (fs.existsSync(`${fileLocation}${payload.locale}`)) {
|
|
|
|
fileData = fs.readFileSync(`${fileLocation}${payload.locale}/countries.json`)
|
|
|
|
} else {
|
|
|
|
fileData = fs.readFileSync(`${fileLocation}en-US/countries.json`)
|
|
|
|
}
|
|
|
|
const countries = JSON.parse(fileData).map((entry) => { return { id: entry.id, name: entry.name, code: entry.alpha2 } })
|
|
|
|
countries.sort((a, b) => { return a.id - b.id })
|
|
|
|
|
|
|
|
const regionNames = countries.map((entry) => { return entry.name })
|
|
|
|
const regionValues = countries.map((entry) => { return entry.code })
|
|
|
|
|
|
|
|
commit('setRegionNames', regionNames)
|
|
|
|
commit('setRegionValues', regionValues)
|
|
|
|
},
|
|
|
|
|
2020-08-23 21:07:29 +02:00
|
|
|
calculateColorLuminance (_, colorValue) {
|
|
|
|
const cutHex = colorValue.substring(1, 7)
|
|
|
|
const colorValueR = parseInt(cutHex.substring(0, 2), 16)
|
|
|
|
const colorValueG = parseInt(cutHex.substring(2, 4), 16)
|
|
|
|
const colorValueB = parseInt(cutHex.substring(4, 6), 16)
|
|
|
|
|
|
|
|
const luminance = (0.299 * colorValueR + 0.587 * colorValueG + 0.114 * colorValueB) / 255
|
|
|
|
|
|
|
|
if (luminance > 0.5) {
|
|
|
|
return '#000000'
|
|
|
|
} else {
|
|
|
|
return '#FFFFFF'
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-08-31 23:35:22 +02:00
|
|
|
calculatePublishedDate(_, publishedText) {
|
|
|
|
const date = new Date()
|
|
|
|
|
2020-09-03 03:00:24 +02:00
|
|
|
if (publishedText === 'Live') {
|
|
|
|
return publishedText
|
|
|
|
}
|
|
|
|
|
2020-08-31 23:35:22 +02:00
|
|
|
const textSplit = publishedText.split(' ')
|
2020-09-03 03:00:24 +02:00
|
|
|
|
|
|
|
if (textSplit[0].toLowerCase() === 'streamed') {
|
|
|
|
textSplit.shift()
|
|
|
|
}
|
|
|
|
|
2020-08-31 23:35:22 +02:00
|
|
|
const timeFrame = textSplit[1]
|
|
|
|
const timeAmount = parseInt(textSplit[0])
|
|
|
|
let timeSpan = null
|
|
|
|
|
|
|
|
if (timeFrame.indexOf('second') > -1) {
|
|
|
|
timeSpan = timeAmount * 1000
|
|
|
|
} else if (timeFrame.indexOf('minute') > -1) {
|
|
|
|
timeSpan = timeAmount * 60000
|
|
|
|
} else if (timeFrame.indexOf('hour') > -1) {
|
|
|
|
timeSpan = timeAmount * 3600000
|
|
|
|
} else if (timeFrame.indexOf('day') > -1) {
|
|
|
|
timeSpan = timeAmount * 86400000
|
|
|
|
} else if (timeFrame.indexOf('week') > -1) {
|
|
|
|
timeSpan = timeAmount * 604800000
|
|
|
|
} else if (timeFrame.indexOf('month') > -1) {
|
|
|
|
timeSpan = timeAmount * 2592000000
|
|
|
|
} else if (timeFrame.indexOf('year') > -1) {
|
|
|
|
timeSpan = timeAmount * 31556952000
|
|
|
|
}
|
|
|
|
|
|
|
|
return date.getTime() - timeSpan
|
|
|
|
},
|
|
|
|
|
2021-03-06 20:03:40 +01:00
|
|
|
getVideoParamsFromUrl (_, url) {
|
2020-06-20 14:52:32 +02:00
|
|
|
/** @type {URL} */
|
|
|
|
let urlObject
|
2021-05-31 13:23:35 +02:00
|
|
|
const paramsObject = { videoId: null, timestamp: null, playlistId: null }
|
2020-06-20 14:52:32 +02:00
|
|
|
try {
|
|
|
|
urlObject = new URL(url)
|
|
|
|
} catch (e) {
|
2021-03-06 20:03:40 +01:00
|
|
|
return paramsObject
|
|
|
|
}
|
|
|
|
|
|
|
|
function extractParams(videoId) {
|
|
|
|
paramsObject.videoId = videoId
|
|
|
|
paramsObject.timestamp = urlObject.searchParams.get('t')
|
2020-06-01 05:13:03 +02:00
|
|
|
}
|
2020-06-20 14:52:32 +02:00
|
|
|
|
|
|
|
const extractors = [
|
|
|
|
// anything with /watch?v=
|
|
|
|
function() {
|
2020-06-20 15:05:36 +02:00
|
|
|
if (urlObject.pathname === '/watch' && urlObject.searchParams.has('v')) {
|
2021-03-06 20:03:40 +01:00
|
|
|
extractParams(urlObject.searchParams.get('v'))
|
2021-05-31 13:23:35 +02:00
|
|
|
paramsObject.playlistId = urlObject.searchParams.get('list')
|
2021-03-06 20:03:40 +01:00
|
|
|
return paramsObject
|
2020-06-20 14:52:32 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
// youtu.be
|
|
|
|
function() {
|
2020-06-20 15:05:36 +02:00
|
|
|
if (urlObject.host === 'youtu.be' && urlObject.pathname.match(/^\/[A-Za-z0-9_-]+$/)) {
|
2021-03-06 20:03:40 +01:00
|
|
|
extractParams(urlObject.pathname.slice(1))
|
|
|
|
return paramsObject
|
2020-06-20 14:52:32 +02:00
|
|
|
}
|
|
|
|
},
|
2021-01-15 04:37:52 +01:00
|
|
|
// youtube.com/embed
|
|
|
|
function() {
|
|
|
|
if (urlObject.pathname.match(/^\/embed\/[A-Za-z0-9_-]+$/)) {
|
2021-03-06 20:03:40 +01:00
|
|
|
extractParams(urlObject.pathname.replace('/embed/', ''))
|
|
|
|
return paramsObject
|
2021-01-15 04:37:52 +01:00
|
|
|
}
|
|
|
|
},
|
2021-08-22 18:25:14 +02:00
|
|
|
// youtube.com/shorts
|
|
|
|
function() {
|
|
|
|
if (urlObject.pathname.match(/^\/shorts\/[A-Za-z0-9_-]+$/)) {
|
|
|
|
extractParams(urlObject.pathname.replace('/shorts/', ''))
|
|
|
|
return paramsObject
|
|
|
|
}
|
|
|
|
},
|
2020-06-20 14:52:32 +02:00
|
|
|
// cloudtube
|
|
|
|
function() {
|
|
|
|
if (urlObject.host.match(/^cadence\.(gq|moe)$/) && urlObject.pathname.match(/^\/cloudtube\/video\/[A-Za-z0-9_-]+$/)) {
|
2021-03-06 20:03:40 +01:00
|
|
|
extractParams(urlObject.pathname.slice('/cloudtube/video/'.length))
|
|
|
|
return paramsObject
|
2020-06-20 14:52:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
2021-03-06 20:03:40 +01:00
|
|
|
return extractors.reduce((a, c) => a || c(), null) || paramsObject
|
2020-07-04 17:44:35 +02:00
|
|
|
},
|
|
|
|
|
2021-05-11 17:28:26 +02:00
|
|
|
getYoutubeUrlInfo ({ state }, urlStr) {
|
2021-04-28 19:21:16 +02:00
|
|
|
// Returns
|
|
|
|
// - urlType [String] `video`, `playlist`
|
|
|
|
//
|
|
|
|
// If `urlType` is "video"
|
|
|
|
// - videoId [String]
|
|
|
|
// - timestamp [String]
|
|
|
|
//
|
|
|
|
// If `urlType` is "playlist"
|
|
|
|
// - playlistId [String]
|
|
|
|
// - query [Object]
|
|
|
|
//
|
|
|
|
// If `urlType` is "search"
|
|
|
|
// - searchQuery [String]
|
|
|
|
// - query [Object]
|
|
|
|
//
|
|
|
|
// If `urlType` is "hashtag"
|
|
|
|
// Nothing else
|
|
|
|
//
|
|
|
|
// If `urlType` is "channel"
|
|
|
|
// - channelId [String]
|
|
|
|
//
|
|
|
|
// If `urlType` is "unknown"
|
|
|
|
// Nothing else
|
|
|
|
//
|
|
|
|
// If `urlType` is "invalid_url"
|
|
|
|
// Nothing else
|
2021-05-31 13:23:35 +02:00
|
|
|
const { videoId, timestamp, playlistId } = actions.getVideoParamsFromUrl(null, urlStr)
|
2021-04-28 19:21:16 +02:00
|
|
|
if (videoId) {
|
|
|
|
return {
|
|
|
|
urlType: 'video',
|
|
|
|
videoId,
|
2021-05-31 13:23:35 +02:00
|
|
|
playlistId,
|
2021-04-28 19:21:16 +02:00
|
|
|
timestamp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let url
|
2021-01-15 17:34:44 +01:00
|
|
|
try {
|
2021-04-28 19:21:16 +02:00
|
|
|
url = new URL(urlStr)
|
|
|
|
} catch {
|
|
|
|
return {
|
|
|
|
urlType: 'invalid_url'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let urlType = 'unknown'
|
|
|
|
|
|
|
|
const channelPattern =
|
2021-11-02 12:45:50 +01:00
|
|
|
/^\/(?:(c|channel|user)\/)?(?<channelId>[^/]+)(?:\/(join|featured|videos|playlists|about|community|channels))?\/?$/
|
2021-04-28 19:21:16 +02:00
|
|
|
|
|
|
|
const typePatterns = new Map([
|
|
|
|
['playlist', /^\/playlist\/?$/],
|
|
|
|
['search', /^\/results\/?$/],
|
|
|
|
['hashtag', /^\/hashtag\/([^/?&#]+)$/],
|
|
|
|
['channel', channelPattern]
|
|
|
|
])
|
|
|
|
|
|
|
|
for (const [type, pattern] of typePatterns) {
|
|
|
|
const matchFound = pattern.test(url.pathname)
|
|
|
|
if (matchFound) {
|
|
|
|
urlType = type
|
|
|
|
break
|
|
|
|
}
|
2021-01-15 17:34:44 +01:00
|
|
|
}
|
|
|
|
|
2021-04-28 19:21:16 +02:00
|
|
|
switch (urlType) {
|
|
|
|
case 'playlist': {
|
|
|
|
if (!url.searchParams.has('list')) {
|
|
|
|
throw new Error('Playlist: "list" field not found')
|
|
|
|
}
|
|
|
|
|
|
|
|
const playlistId = url.searchParams.get('list')
|
|
|
|
url.searchParams.delete('list')
|
|
|
|
|
|
|
|
const query = {}
|
|
|
|
for (const [param, value] of url.searchParams) {
|
|
|
|
query[param] = value
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
urlType: 'playlist',
|
|
|
|
playlistId,
|
|
|
|
query
|
2021-01-15 17:34:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 19:21:16 +02:00
|
|
|
case 'search': {
|
|
|
|
if (!url.searchParams.has('search_query')) {
|
|
|
|
throw new Error('Search: "search_query" field not found')
|
|
|
|
}
|
|
|
|
|
|
|
|
const searchQuery = url.searchParams.get('search_query')
|
|
|
|
url.searchParams.delete('search_query')
|
|
|
|
|
2021-05-11 17:28:26 +02:00
|
|
|
const searchSettings = state.searchSettings
|
2021-04-28 19:21:16 +02:00
|
|
|
const query = {
|
2021-05-11 17:28:26 +02:00
|
|
|
sortBy: searchSettings.sortBy,
|
|
|
|
time: searchSettings.time,
|
|
|
|
type: searchSettings.type,
|
|
|
|
duration: searchSettings.duration
|
2021-04-28 19:21:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const [param, value] of url.searchParams) {
|
|
|
|
query[param] = value
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
urlType: 'search',
|
|
|
|
searchQuery,
|
|
|
|
query
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'hashtag': {
|
|
|
|
return {
|
|
|
|
urlType: 'hashtag'
|
|
|
|
}
|
|
|
|
}
|
2021-11-02 12:45:50 +01:00
|
|
|
/*
|
|
|
|
Using RegExp named capture groups from ES2018
|
|
|
|
To avoid access to specific captured value broken
|
2021-04-28 19:21:16 +02:00
|
|
|
|
2021-11-02 12:45:50 +01:00
|
|
|
Channel URL (ID-based)
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/about
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/channels
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/community
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/featured
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/join
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/playlists
|
|
|
|
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/videos
|
|
|
|
|
|
|
|
Custom URL
|
|
|
|
|
|
|
|
https://www.youtube.com/c/YouTubeCreators
|
|
|
|
https://www.youtube.com/c/YouTubeCreators/about
|
|
|
|
etc.
|
|
|
|
|
|
|
|
Legacy Username URL
|
|
|
|
|
|
|
|
https://www.youtube.com/user/ufoludek
|
|
|
|
https://www.youtube.com/user/ufoludek/about
|
|
|
|
etc.
|
|
|
|
|
|
|
|
*/
|
2021-04-28 19:21:16 +02:00
|
|
|
case 'channel': {
|
2021-11-02 12:45:50 +01:00
|
|
|
const channelId = url.pathname.match(channelPattern).groups.channelId
|
2021-04-28 19:21:16 +02:00
|
|
|
if (!channelId) {
|
|
|
|
throw new Error('Channel: could not extract id')
|
|
|
|
}
|
|
|
|
|
2021-11-02 12:45:50 +01:00
|
|
|
let subPath = null
|
|
|
|
switch (url.pathname.split('/').filter(i => i)[2]) {
|
|
|
|
case 'playlists':
|
|
|
|
subPath = 'playlists'
|
|
|
|
break
|
|
|
|
case 'channels':
|
|
|
|
case 'about':
|
|
|
|
subPath = 'about'
|
|
|
|
break
|
|
|
|
case 'community':
|
|
|
|
default:
|
|
|
|
subPath = 'videos'
|
|
|
|
break
|
|
|
|
}
|
2021-04-28 19:21:16 +02:00
|
|
|
return {
|
|
|
|
urlType: 'channel',
|
2021-11-02 12:45:50 +01:00
|
|
|
channelId,
|
|
|
|
subPath
|
2021-04-28 19:21:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
default: {
|
|
|
|
// Unknown URL type
|
|
|
|
return {
|
|
|
|
urlType: 'unknown'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-01-15 17:34:44 +01:00
|
|
|
},
|
|
|
|
|
2020-08-17 00:11:44 +02:00
|
|
|
padNumberWithLeadingZeros(_, payload) {
|
|
|
|
let numberString = payload.number.toString()
|
|
|
|
while (numberString.length < payload.length) {
|
|
|
|
numberString = '0' + numberString
|
|
|
|
}
|
|
|
|
return numberString
|
|
|
|
},
|
|
|
|
|
|
|
|
async buildVTTFileLocally ({ dispatch }, Storyboard) {
|
|
|
|
let vttString = 'WEBVTT\n\n'
|
|
|
|
// how many images are in one image
|
|
|
|
const numberOfSubImagesPerImage = Storyboard.sWidth * Storyboard.sHeight
|
|
|
|
// the number of storyboard images
|
|
|
|
const numberOfImages = Math.ceil(Storyboard.count / numberOfSubImagesPerImage)
|
|
|
|
const intervalInSeconds = Storyboard.interval / 1000
|
|
|
|
let currentUrl = Storyboard.url
|
|
|
|
let startHours = 0
|
|
|
|
let startMinutes = 0
|
|
|
|
let startSeconds = 0
|
|
|
|
let endHours = 0
|
|
|
|
let endMinutes = 0
|
|
|
|
let endSeconds = intervalInSeconds
|
|
|
|
for (let i = 0; i < numberOfImages; i++) {
|
|
|
|
let xCoord = 0
|
|
|
|
let yCoord = 0
|
|
|
|
for (let j = 0; j < numberOfSubImagesPerImage; j++) {
|
|
|
|
// add the timestamp information
|
|
|
|
const paddedStartHours = await dispatch('padNumberWithLeadingZeros', {
|
|
|
|
number: startHours,
|
|
|
|
length: 2
|
|
|
|
})
|
|
|
|
const paddedStartMinutes = await dispatch('padNumberWithLeadingZeros', {
|
|
|
|
number: startMinutes,
|
|
|
|
length: 2
|
|
|
|
})
|
|
|
|
const paddedStartSeconds = await dispatch('padNumberWithLeadingZeros', {
|
|
|
|
number: startSeconds,
|
|
|
|
length: 2
|
|
|
|
})
|
|
|
|
const paddedEndHours = await dispatch('padNumberWithLeadingZeros', {
|
|
|
|
number: endHours,
|
|
|
|
length: 2
|
|
|
|
})
|
|
|
|
const paddedEndMinutes = await dispatch('padNumberWithLeadingZeros', {
|
|
|
|
number: endMinutes,
|
|
|
|
length: 2
|
|
|
|
})
|
|
|
|
const paddedEndSeconds = await dispatch('padNumberWithLeadingZeros', {
|
|
|
|
number: endSeconds,
|
|
|
|
length: 2
|
|
|
|
})
|
|
|
|
vttString += `${paddedStartHours}:${paddedStartMinutes}:${paddedStartSeconds}.000 --> ${paddedEndHours}:${paddedEndMinutes}:${paddedEndSeconds}.000\n`
|
|
|
|
// add the current image url as well as the x, y, width, height information
|
|
|
|
vttString += currentUrl + `#xywh=${xCoord},${yCoord},${Storyboard.width},${Storyboard.height}\n\n`
|
|
|
|
// update the variables
|
|
|
|
startHours = endHours
|
|
|
|
startMinutes = endMinutes
|
|
|
|
startSeconds = endSeconds
|
|
|
|
endSeconds += intervalInSeconds
|
|
|
|
if (endSeconds >= 60) {
|
|
|
|
endSeconds -= 60
|
|
|
|
endMinutes += 1
|
|
|
|
}
|
|
|
|
if (endMinutes >= 60) {
|
|
|
|
endMinutes -= 60
|
|
|
|
endHours += 1
|
|
|
|
}
|
|
|
|
// x coordinate can only be smaller than the width of one subimage * the number of subimages per row
|
|
|
|
xCoord = (xCoord + Storyboard.width) % (Storyboard.width * Storyboard.sWidth)
|
|
|
|
// only if the x coordinate is , so in a new row, we have to update the y coordinate
|
|
|
|
if (xCoord === 0) {
|
|
|
|
yCoord += Storyboard.height
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// make sure that there is no value like M0 or M1 in the parameters that gets replaced
|
|
|
|
currentUrl = currentUrl.replace('M' + i.toString() + '.jpg', 'M' + (i + 1).toString() + '.jpg')
|
|
|
|
}
|
|
|
|
return vttString
|
|
|
|
},
|
|
|
|
|
2020-08-18 21:37:43 +02:00
|
|
|
toLocalePublicationString ({ dispatch }, payload) {
|
|
|
|
if (payload.isLive) {
|
|
|
|
return '0' + payload.liveStreamString
|
|
|
|
} else if (payload.isUpcoming || payload.publishText === null) {
|
|
|
|
// the check for null is currently just an inferring of knowledge, because there is no other possibility left
|
2020-10-09 03:53:34 +02:00
|
|
|
return `${payload.upcomingString}: ${payload.publishText}`
|
2020-09-02 05:20:21 +02:00
|
|
|
} else if (payload.isRSS) {
|
|
|
|
return payload.publishText
|
2020-08-18 21:37:43 +02:00
|
|
|
}
|
2020-08-18 20:32:01 +02:00
|
|
|
const strings = payload.publishText.split(' ')
|
2020-09-02 22:59:16 +02:00
|
|
|
// filters out the streamed x hours ago and removes the streamed in order to keep the rest of the code working
|
|
|
|
if (strings[0].toLowerCase() === 'streamed') {
|
|
|
|
strings.shift()
|
|
|
|
}
|
2020-08-18 20:32:01 +02:00
|
|
|
const singular = (strings[0] === '1')
|
|
|
|
let publicationString = payload.templateString.replace('$', strings[0])
|
|
|
|
switch (strings[1].substring(0, 2)) {
|
|
|
|
case 'se':
|
|
|
|
if (singular) {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Second)
|
|
|
|
} else {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Seconds)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'mi':
|
|
|
|
if (singular) {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Minute)
|
|
|
|
} else {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Minutes)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'ho':
|
|
|
|
if (singular) {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Hour)
|
|
|
|
} else {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Hours)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'da':
|
|
|
|
if (singular) {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Day)
|
|
|
|
} else {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Days)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'we':
|
|
|
|
if (singular) {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Week)
|
|
|
|
} else {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Weeks)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'mo':
|
|
|
|
if (singular) {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Month)
|
|
|
|
} else {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Months)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'ye':
|
|
|
|
if (singular) {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Year)
|
|
|
|
} else {
|
|
|
|
publicationString = publicationString.replace('%', payload.timeStrings.Years)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return publicationString
|
|
|
|
},
|
|
|
|
|
2020-08-22 22:51:04 +02:00
|
|
|
clearSessionSearchHistory ({ commit }) {
|
|
|
|
commit('setSessionSearchHistory', [])
|
|
|
|
},
|
|
|
|
|
2020-08-05 05:44:34 +02:00
|
|
|
showToast (_, payload) {
|
2022-01-30 18:49:16 +01:00
|
|
|
const formatArgs = 'formatArgs' in payload ? payload.formatArgs : []
|
|
|
|
const translate = 'translate' in payload ? payload.translate : false
|
|
|
|
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time, translate, formatArgs)
|
2021-06-13 17:31:43 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
showExternalPlayerUnsupportedActionToast: function ({ dispatch }, payload) {
|
|
|
|
if (!payload.ignoreWarnings) {
|
|
|
|
const toastMessage = payload.template
|
|
|
|
.replace('$', payload.externalPlayer)
|
|
|
|
.replace('%', payload.action)
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: toastMessage
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
getExternalPlayerCmdArgumentsData ({ commit }, payload) {
|
|
|
|
const fileName = 'external-player-map.json'
|
|
|
|
let fileData
|
|
|
|
/* eslint-disable-next-line */
|
|
|
|
const fileLocation = payload.isDev ? './static/' : `${__dirname}/static/`
|
|
|
|
|
|
|
|
if (fs.existsSync(`${fileLocation}${fileName}`)) {
|
|
|
|
fileData = fs.readFileSync(`${fileLocation}${fileName}`)
|
|
|
|
} else {
|
|
|
|
fileData = '[{"name":"None","value":"","cmdArguments":null}]'
|
|
|
|
}
|
|
|
|
|
|
|
|
const externalPlayerMap = JSON.parse(fileData).map((entry) => {
|
|
|
|
return { name: entry.name, value: entry.value, cmdArguments: entry.cmdArguments }
|
|
|
|
})
|
|
|
|
|
|
|
|
const externalPlayerNames = externalPlayerMap.map((entry) => { return entry.name })
|
|
|
|
const externalPlayerValues = externalPlayerMap.map((entry) => { return entry.value })
|
|
|
|
const externalPlayerCmdArguments = externalPlayerMap.reduce((result, item) => {
|
|
|
|
result[item.value] = item.cmdArguments
|
|
|
|
return result
|
|
|
|
}, {})
|
|
|
|
|
|
|
|
commit('setExternalPlayerNames', externalPlayerNames)
|
|
|
|
commit('setExternalPlayerValues', externalPlayerValues)
|
|
|
|
commit('setExternalPlayerCmdArguments', externalPlayerCmdArguments)
|
|
|
|
},
|
|
|
|
|
|
|
|
openInExternalPlayer ({ dispatch, state, rootState }, payload) {
|
|
|
|
const args = []
|
|
|
|
const externalPlayer = rootState.settings.externalPlayer
|
|
|
|
const cmdArgs = state.externalPlayerCmdArguments[externalPlayer]
|
|
|
|
const executable = rootState.settings.externalPlayerExecutable !== ''
|
|
|
|
? rootState.settings.externalPlayerExecutable
|
|
|
|
: cmdArgs.defaultExecutable
|
|
|
|
const ignoreWarnings = rootState.settings.externalPlayerIgnoreWarnings
|
|
|
|
const customArgs = rootState.settings.externalPlayerCustomArgs
|
|
|
|
|
2021-10-07 08:29:43 +02:00
|
|
|
// Append custom user-defined arguments,
|
|
|
|
// or use the default ones specified for the external player.
|
|
|
|
if (typeof customArgs === 'string' && customArgs !== '') {
|
|
|
|
const custom = customArgs.split(';')
|
|
|
|
args.push(...custom)
|
|
|
|
} else if (typeof cmdArgs.defaultCustomArguments === 'string' && cmdArgs.defaultCustomArguments !== '') {
|
|
|
|
const defaultCustomArguments = cmdArgs.defaultCustomArguments.split(';')
|
|
|
|
args.push(...defaultCustomArguments)
|
|
|
|
}
|
|
|
|
|
2021-06-13 17:31:43 +02:00
|
|
|
if (payload.watchProgress > 0) {
|
|
|
|
if (typeof cmdArgs.startOffset === 'string') {
|
|
|
|
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
|
|
|
|
} else {
|
|
|
|
dispatch('showExternalPlayerUnsupportedActionToast', {
|
|
|
|
ignoreWarnings,
|
|
|
|
externalPlayer,
|
|
|
|
template: payload.strings.UnsupportedActionTemplate,
|
|
|
|
action: payload.strings['Unsupported Actions']['starting video at offset']
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.playbackRate !== null) {
|
|
|
|
if (typeof cmdArgs.playbackRate === 'string') {
|
|
|
|
args.push(`${cmdArgs.playbackRate}${payload.playbackRate}`)
|
|
|
|
} else {
|
|
|
|
dispatch('showExternalPlayerUnsupportedActionToast', {
|
|
|
|
ignoreWarnings,
|
|
|
|
externalPlayer,
|
|
|
|
template: payload.strings.UnsupportedActionTemplate,
|
|
|
|
action: payload.strings['Unsupported Actions']['setting a playback rate']
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check whether the video is in a playlist
|
|
|
|
if (typeof cmdArgs.playlistUrl === 'string' && payload.playlistId !== null && payload.playlistId !== '') {
|
|
|
|
if (payload.playlistIndex !== null) {
|
|
|
|
if (typeof cmdArgs.playlistIndex === 'string') {
|
|
|
|
args.push(`${cmdArgs.playlistIndex}${payload.playlistIndex}`)
|
|
|
|
} else {
|
|
|
|
dispatch('showExternalPlayerUnsupportedActionToast', {
|
|
|
|
ignoreWarnings,
|
|
|
|
externalPlayer,
|
|
|
|
template: payload.strings.UnsupportedActionTemplate,
|
|
|
|
action: payload.strings['Unsupported Actions']['opening specific video in a playlist (falling back to opening the video)']
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.playlistReverse) {
|
|
|
|
if (typeof cmdArgs.playlistReverse === 'string') {
|
|
|
|
args.push(cmdArgs.playlistReverse)
|
|
|
|
} else {
|
|
|
|
dispatch('showExternalPlayerUnsupportedActionToast', {
|
|
|
|
ignoreWarnings,
|
|
|
|
externalPlayer,
|
|
|
|
template: payload.strings.UnsupportedActionTemplate,
|
|
|
|
action: payload.strings['Unsupported Actions']['reversing playlists']
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.playlistShuffle) {
|
|
|
|
if (typeof cmdArgs.playlistShuffle === 'string') {
|
|
|
|
args.push(cmdArgs.playlistShuffle)
|
|
|
|
} else {
|
|
|
|
dispatch('showExternalPlayerUnsupportedActionToast', {
|
|
|
|
ignoreWarnings,
|
|
|
|
externalPlayer,
|
|
|
|
template: payload.strings.UnsupportedActionTemplate,
|
|
|
|
action: payload.strings['Unsupported Actions']['shuffling playlists']
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.playlistLoop) {
|
|
|
|
if (typeof cmdArgs.playlistLoop === 'string') {
|
|
|
|
args.push(cmdArgs.playlistLoop)
|
|
|
|
} else {
|
|
|
|
dispatch('showExternalPlayerUnsupportedActionToast', {
|
|
|
|
ignoreWarnings,
|
|
|
|
externalPlayer,
|
|
|
|
template: payload.strings.UnsupportedActionTemplate,
|
|
|
|
action: payload.strings['Unsupported Actions']['looping playlists']
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (cmdArgs.supportsYtdlProtocol) {
|
|
|
|
args.push(`${cmdArgs.playlistUrl}ytdl://${payload.playlistId}`)
|
|
|
|
} else {
|
|
|
|
args.push(`${cmdArgs.playlistUrl}https://youtube.com/playlist?list=${payload.playlistId}`)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (payload.playlistId !== null && payload.playlistId !== '') {
|
|
|
|
dispatch('showExternalPlayerUnsupportedActionToast', {
|
|
|
|
ignoreWarnings,
|
|
|
|
externalPlayer,
|
|
|
|
template: payload.strings.UnsupportedActionTemplate,
|
|
|
|
action: payload.strings['Unsupported Actions']['opening playlists']
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if (payload.videoId !== null) {
|
|
|
|
if (cmdArgs.supportsYtdlProtocol) {
|
|
|
|
args.push(`${cmdArgs.videoUrl}ytdl://${payload.videoId}`)
|
|
|
|
} else {
|
|
|
|
args.push(`${cmdArgs.videoUrl}https://www.youtube.com/watch?v=${payload.videoId}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const openingToast = payload.strings.OpeningTemplate
|
|
|
|
.replace('$', payload.playlistId === null || payload.playlistId === ''
|
|
|
|
? payload.strings.video
|
|
|
|
: payload.strings.playlist)
|
|
|
|
.replace('%', externalPlayer)
|
|
|
|
dispatch('showToast', {
|
|
|
|
message: openingToast
|
|
|
|
})
|
|
|
|
|
|
|
|
console.log(executable, args)
|
|
|
|
|
|
|
|
const { ipcRenderer } = require('electron')
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
ipcRenderer.send(IpcChannels.OPEN_IN_EXTERNAL_PLAYER, { executable, args })
|
2020-05-23 23:29:42 +02:00
|
|
|
}
|
|
|
|
}
|
2020-02-16 19:30:00 +01:00
|
|
|
|
|
|
|
const mutations = {
|
|
|
|
toggleSideNav (state) {
|
|
|
|
state.isSideNavOpen = !state.isSideNavOpen
|
|
|
|
},
|
|
|
|
|
2020-08-31 23:35:22 +02:00
|
|
|
setShowProgressBar (state, value) {
|
|
|
|
state.showProgressBar = value
|
|
|
|
},
|
|
|
|
|
|
|
|
setProgressBarPercentage (state, value) {
|
|
|
|
state.progressBarPercentage = value
|
|
|
|
},
|
|
|
|
|
2020-02-16 19:30:00 +01:00
|
|
|
setSessionSearchHistory (state, history) {
|
|
|
|
state.sessionSearchHistory = history
|
|
|
|
},
|
|
|
|
|
|
|
|
addToSessionSearchHistory (state, payload) {
|
|
|
|
const sameSearch = state.sessionSearchHistory.findIndex((search) => {
|
|
|
|
return search.query === payload.query && IsEqual(payload.searchSettings, search.searchSettings)
|
|
|
|
})
|
|
|
|
|
|
|
|
if (sameSearch !== -1) {
|
|
|
|
state.sessionSearchHistory[sameSearch].data = state.sessionSearchHistory[sameSearch].data.concat(payload.data)
|
|
|
|
state.sessionSearchHistory[sameSearch].nextPageRef = payload.nextPageRef
|
|
|
|
} else {
|
|
|
|
state.sessionSearchHistory.push(payload)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-08-13 16:26:20 +02:00
|
|
|
setPopularCache (state, value) {
|
|
|
|
state.popularCache = value
|
|
|
|
},
|
|
|
|
|
2021-08-21 23:08:38 +02:00
|
|
|
setTrendingCache (state, value, page) {
|
|
|
|
state.trendingCache[page] = value
|
2020-08-22 22:37:09 +02:00
|
|
|
},
|
|
|
|
|
2020-02-16 19:30:00 +01:00
|
|
|
setSearchSortBy (state, value) {
|
|
|
|
state.searchSettings.sortBy = value
|
|
|
|
},
|
|
|
|
|
|
|
|
setSearchTime (state, value) {
|
|
|
|
state.searchSettings.time = value
|
|
|
|
},
|
|
|
|
|
|
|
|
setSearchType (state, value) {
|
|
|
|
state.searchSettings.type = value
|
|
|
|
},
|
|
|
|
|
|
|
|
setSearchDuration (state, value) {
|
|
|
|
state.searchSettings.duration = value
|
2020-09-20 20:22:39 +02:00
|
|
|
},
|
|
|
|
|
2020-10-22 20:56:49 +02:00
|
|
|
setRegionNames (state, value) {
|
|
|
|
state.regionNames = value
|
|
|
|
},
|
|
|
|
|
|
|
|
setRegionValues (state, value) {
|
|
|
|
state.regionValues = value
|
|
|
|
},
|
|
|
|
|
2020-09-20 20:22:39 +02:00
|
|
|
setRecentBlogPosts (state, value) {
|
|
|
|
state.recentBlogPosts = value
|
2021-06-13 17:31:43 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
setExternalPlayerNames (state, value) {
|
|
|
|
state.externalPlayerNames = value
|
|
|
|
},
|
|
|
|
|
|
|
|
setExternalPlayerValues (state, value) {
|
|
|
|
state.externalPlayerValues = value
|
|
|
|
},
|
|
|
|
|
|
|
|
setExternalPlayerCmdArguments (state, value) {
|
|
|
|
state.externalPlayerCmdArguments = value
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default {
|
|
|
|
state,
|
|
|
|
getters,
|
|
|
|
actions,
|
|
|
|
mutations
|
|
|
|
}
|