+
+
+
+
+
+
+
+
+
+ {{ $t('Settings.Player Settings.Screenshot.Folder Label') }}
+
+
+
+
+
+
+ {{ $t('Settings.Player Settings.Screenshot.File Name Label') }}
+
+
+
+
+
+
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index 7a8db63e..693813d1 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -254,7 +254,13 @@ const state = {
videoVolumeMouseScroll: false,
videoPlaybackRateMouseScroll: false,
videoPlaybackRateInterval: 0.25,
- downloadFolderPath: ''
+ downloadFolderPath: '',
+ enableScreenshot: false,
+ screenshotFormat: 'png',
+ screenshotQuality: 95,
+ screenshotAskPath: false,
+ screenshotFolderPath: '',
+ screenshotFilenamePattern: '%Y%M%D-%H%N%S'
}
const stateWithSideEffects = {
diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js
index 40a21aa2..a0884a85 100644
--- a/src/renderer/store/modules/utils.js
+++ b/src/renderer/store/modules/utils.js
@@ -200,11 +200,12 @@ const actions = {
defaultPath: fileName,
filters: [
{
+ name: extension.toUpperCase(),
extensions: [extension]
}
]
}
- const response = await dispatch('showSaveDialog', options)
+ const response = await dispatch('showSaveDialog', { options })
if (response.canceled || response.filePath === '') {
// User canceled the save dialog
@@ -283,10 +284,10 @@ const actions = {
return await invokeIRC(context, IpcChannels.SHOW_OPEN_DIALOG, webCbk, options)
},
- async showSaveDialog (context, options) {
+ async showSaveDialog (context, { options, useModal = false }) {
// TODO: implement showSaveDialog web compatible callback
const webCbk = () => null
- return await invokeIRC(context, IpcChannels.SHOW_SAVE_DIALOG, webCbk, options)
+ return await invokeIRC(context, IpcChannels.SHOW_SAVE_DIALOG, webCbk, { options, useModal })
},
async getUserDataPath (context) {
@@ -295,6 +296,66 @@ const actions = {
return await invokeIRC(context, IpcChannels.GET_USER_DATA_PATH, webCbk)
},
+ async getPicturesPath (context) {
+ const webCbk = () => null
+ return await invokeIRC(context, IpcChannels.GET_PICTURES_PATH, webCbk)
+ },
+
+ parseScreenshotCustomFileName: function({ rootState }, payload) {
+ return new Promise((resolve, reject) => {
+ const { pattern = rootState.settings.screenshotFilenamePattern, date, playerTime, videoId } = payload
+ const keywords = [
+ ['%Y', date.getFullYear()], // year 4 digits
+ ['%M', (date.getMonth() + 1).toString().padStart(2, '0')], // month 2 digits
+ ['%D', date.getDate().toString().padStart(2, '0')], // day 2 digits
+ ['%H', date.getHours().toString().padStart(2, '0')], // hour 2 digits
+ ['%N', date.getMinutes().toString().padStart(2, '0')], // minute 2 digits
+ ['%S', date.getSeconds().toString().padStart(2, '0')], // second 2 digits
+ ['%T', date.getMilliseconds().toString().padStart(3, '0')], // millisecond 3 digits
+ ['%s', parseInt(playerTime)], // video position second n digits
+ ['%t', (playerTime % 1).toString().slice(2, 5) || '000'], // video position millisecond 3 digits
+ ['%i', videoId] // video id
+ ]
+
+ let parsedString = pattern
+ for (const [key, value] of keywords) {
+ parsedString = parsedString.replaceAll(key, value)
+ }
+
+ const platform = process.platform
+ if (platform === 'win32') {
+ // https://www.boost.org/doc/libs/1_78_0/libs/filesystem/doc/portability_guide.htm
+ // https://stackoverflow.com/questions/1976007/
+ const noForbiddenChars = ['<', '>', ':', '"', '/', '|'].every(char => {
+ return parsedString.indexOf(char) === -1
+ })
+ if (!noForbiddenChars) {
+ reject(new Error('Forbidden Characters')) // use message as translation key
+ }
+ } else if (platform === 'darwin') {
+ // https://superuser.com/questions/204287/
+ if (parsedString.indexOf(':') !== -1) {
+ reject(new Error('Forbidden Characters'))
+ }
+ }
+
+ const dirChar = platform === 'win32' ? '\\' : '/'
+ let filename
+ if (parsedString.indexOf(dirChar) !== -1) {
+ const lastIndex = parsedString.lastIndexOf(dirChar)
+ filename = parsedString.substring(lastIndex + 1)
+ } else {
+ filename = parsedString
+ }
+
+ if (!filename) {
+ reject(new Error('Empty File Name'))
+ }
+
+ resolve(parsedString)
+ })
+ },
+
updateShowProgressBar ({ commit }, value) {
commit('setShowProgressBar', value)
},
diff --git a/src/renderer/videoJS.css b/src/renderer/videoJS.css
index 9b1f59ad..8a3f4831 100644
--- a/src/renderer/videoJS.css
+++ b/src/renderer/videoJS.css
@@ -490,6 +490,16 @@ body.vjs-full-window {
content: url(assets/img/close_theatre.svg)
}
+.vjs-icon-screenshot {
+ margin-top: 3px;
+ padding-top: 3px;
+ cursor: pointer;
+}
+
+.vjs-icon-screenshot::before {
+ content: url(assets/img/camera.svg)
+}
+
@media only screen and (max-width: 1350px) {
.videoPlayer .vjs-button-theatre {
display: none
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index c9abfa6d..1e29973a 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -232,6 +232,20 @@ Settings:
1440p: 1440p
4k: 4k
8k: 8k
+ Screenshot:
+ Enable: Enable Screenshot
+ Format Label: Screenshot Format
+ Quality Label: Screenshot Quality
+ Ask Path: Ask for Save Folder
+ Folder Label: Screenshot Folder
+ Folder Button: Select Folder
+ File Name Label: Filename Pattern
+ File Name Tooltip: You can use variables below. %Y Year 4 digits. %M Month 2 digits.
+ %D Day 2 digits. %H Hour 2 digits. %N Minute 2 digits. %S Second 2 digits. %T Millisecond 3 digits.
+ %s Video Second. %t Video Millisecond 3 digits. %i Video ID. You can also use "\" or "/" to create subfolders.
+ Error:
+ Forbidden Characters: Forbidden Characters
+ Empty File Name: Empty File Name
External Player Settings:
External Player Settings: External Player Settings
External Player: External Player
@@ -740,6 +754,8 @@ External link opening has been disabled in the general settings: 'External link
Downloading has completed: '"$" has finished downloading'
Starting download: 'Starting download of "$"'
Downloading failed: 'There was an issue downloading "$"'
+Screenshot Success: Saved screenshot as "$"
+Screenshot Error: Screenshot failed. $
Yes: Yes
No: No