diff --git a/_icons/iconSolarizedDarkSmall.svg b/_icons/iconSolarizedDarkSmall.svg new file mode 100644 index 000000000000..21624404d42b --- /dev/null +++ b/_icons/iconSolarizedDarkSmall.svg @@ -0,0 +1,42 @@ + + + + + diff --git a/_icons/iconSolarizedLightSmall.svg b/_icons/iconSolarizedLightSmall.svg new file mode 100644 index 000000000000..8d3bff1d90ef --- /dev/null +++ b/_icons/iconSolarizedLightSmall.svg @@ -0,0 +1,42 @@ + + + + + diff --git a/_icons/textSolarizedDarkSmall.svg b/_icons/textSolarizedDarkSmall.svg new file mode 100644 index 000000000000..f153a3f85185 --- /dev/null +++ b/_icons/textSolarizedDarkSmall.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + diff --git a/_icons/textSolarizedLightSmall.svg b/_icons/textSolarizedLightSmall.svg new file mode 100644 index 000000000000..3ddacfa53515 --- /dev/null +++ b/_icons/textSolarizedLightSmall.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + diff --git a/package.json b/package.json index 0ccc4e89beb7..c38614bcb211 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "lodash.debounce": "^4.0.8", "marked": "^12.0.2", "path-browserify": "^1.0.1", + "portal-vue": "^2.1.7", "process": "^0.11.10", "swiper": "^11.1.1", "video.js": "7.21.5", @@ -77,27 +78,27 @@ "vue-observe-visibility": "^1.0.0", "vue-router": "^3.6.5", "vuex": "^3.6.2", - "youtubei.js": "^9.3.0" + "youtubei.js": "^9.4.0" }, "devDependencies": { - "@babel/core": "^7.24.4", - "@babel/eslint-parser": "^7.24.1", + "@babel/core": "^7.24.5", + "@babel/eslint-parser": "^7.24.5", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.24.4", + "@babel/preset-env": "^7.24.5", "@double-great/stylelint-a11y": "^3.0.2", "@intlify/eslint-plugin-vue-i18n": "^2.0.0", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.1", "css-minimizer-webpack-plugin": "^6.0.0", - "electron": "^30.0.1", + "electron": "^30.0.2", "electron-builder": "^24.13.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsonc": "^2.15.1", - "eslint-plugin-n": "^17.3.1", + "eslint-plugin-n": "^17.4.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-unicorn": "^52.0.0", @@ -114,9 +115,9 @@ "postcss-scss": "^4.0.9", "prettier": "^2.8.8", "rimraf": "^5.0.5", - "sass": "^1.75.0", + "sass": "^1.76.0", "sass-loader": "^14.2.1", - "stylelint": "^16.4.0", + "stylelint": "^16.5.0", "stylelint-config-sass-guidelines": "^11.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-high-performance-animation": "^1.10.0", diff --git a/src/constants.js b/src/constants.js index 3224725035b5..bd888eb24ce9 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,8 +4,6 @@ const IpcChannels = { DISABLE_PROXY: 'disable-proxy', OPEN_EXTERNAL_LINK: 'open-external-link', GET_SYSTEM_LOCALE: 'get-system-locale', - GET_USER_DATA_PATH: 'get-user-data-path', - GET_USER_DATA_PATH_SYNC: 'get-user-data-path-sync', GET_PICTURES_PATH: 'get-pictures-path', SHOW_OPEN_DIALOG: 'show-open-dialog', SHOW_SAVE_DIALOG: 'show-save-dialog', @@ -26,7 +24,10 @@ const IpcChannels = { SYNC_PLAYLISTS: 'sync-playlists', GET_REPLACE_HTTP_CACHE: 'get-replace-http-cache', - TOGGLE_REPLACE_HTTP_CACHE: 'toggle-replace-http-cache' + TOGGLE_REPLACE_HTTP_CACHE: 'toggle-replace-http-cache', + + PLAYER_CACHE_GET: 'player-cache-get', + PLAYER_CACHE_SET: 'player-cache-set' } const DBActions = { @@ -76,9 +77,13 @@ const SyncEvents = { // Utils const MAIN_PROFILE_ID = 'allChannels' +// YouTube search character limit is 100 characters +const SEARCH_CHAR_LIMIT = 100 + export { IpcChannels, DBActions, SyncEvents, - MAIN_PROFILE_ID + MAIN_PROFILE_ID, + SEARCH_CHAR_LIMIT } diff --git a/src/main/index.js b/src/main/index.js index 709efeb8a0a8..7187f149ac23 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -14,6 +14,8 @@ import asyncFs from 'fs/promises' import { promisify } from 'util' import { brotliDecompress } from 'zlib' +import contextMenu from 'electron-context-menu' + import packageDetails from '../../package.json' const brotliDecompressAsync = promisify(brotliDecompress) @@ -43,7 +45,7 @@ function runApp() { }]) } - require('electron-context-menu')({ + contextMenu({ showSearchWithGoogle: false, showSaveImageAs: true, showCopyImageAddress: true, @@ -197,10 +199,12 @@ function runApp() { app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecodeLinuxGL') } + const userDataPath = app.getPath('userData') + // command line switches need to be added before the app ready event first // that means we can't use the normal settings system as that is asynchronous, // doing it synchronously ensures that we add it before the event fires - const REPLACE_HTTP_CACHE_PATH = `${app.getPath('userData')}/experiment-replace-http-cache` + const REPLACE_HTTP_CACHE_PATH = `${userDataPath}/experiment-replace-http-cache` const replaceHttpCache = existsSync(REPLACE_HTTP_CACHE_PATH) if (replaceHttpCache) { // the http cache causes excessive disk usage during video playback @@ -209,6 +213,8 @@ function runApp() { app.commandLine.appendSwitch('disable-http-cache') } + const PLAYER_CACHE_PATH = `${userDataPath}/player_cache` + // See: https://stackoverflow.com/questions/45570589/electron-protocol-handler-not-working-on-windows // remove so we can register each time as we run the app. app.removeAsDefaultProtocolClient('freetube') @@ -630,6 +636,10 @@ function runApp() { return '282828' case 'gruvbox-light': return 'fbf1c7' + case 'solarized-dark': + return '#002B36' + case 'solarized-light': + return '#fdf6e3' case 'system': default: return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1' @@ -864,14 +874,6 @@ function runApp() { return app.getSystemLocale() }) - ipcMain.handle(IpcChannels.GET_USER_DATA_PATH, () => { - return app.getPath('userData') - }) - - ipcMain.on(IpcChannels.GET_USER_DATA_PATH_SYNC, (event) => { - event.returnValue = app.getPath('userData') - }) - ipcMain.handle(IpcChannels.GET_PICTURES_PATH, () => { return app.getPath('pictures') }) @@ -936,6 +938,35 @@ function runApp() { relaunch() }) + function playerCachePathForKey(key) { + // Remove path separators and period characters, + // to prevent any files outside of the player_cache directory, + // from being read or written + const sanitizedKey = `${key}`.replaceAll(/[./\\]/g, '__') + + return path.join(PLAYER_CACHE_PATH, sanitizedKey) + } + + ipcMain.handle(IpcChannels.PLAYER_CACHE_GET, async (_, key) => { + const filePath = playerCachePathForKey(key) + + try { + const contents = await asyncFs.readFile(filePath) + return contents.buffer + } catch (e) { + console.error(e) + return undefined + } + }) + + ipcMain.handle(IpcChannels.PLAYER_CACHE_SET, async (_, key, value) => { + const filePath = playerCachePathForKey(key) + + await asyncFs.mkdir(PLAYER_CACHE_PATH, { recursive: true }) + + await asyncFs.writeFile(filePath, new Uint8Array(value)) + }) + // ************************************************* // // DB related IPC calls // *********** // diff --git a/src/renderer/App.js b/src/renderer/App.js index eade3f2166a9..2f58d730de73 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -1,6 +1,5 @@ -import Vue, { defineComponent } from 'vue' +import { defineComponent } from 'vue' import { mapActions, mapMutations } from 'vuex' -import { ObserveVisibility } from 'vue-observe-visibility' import FtFlexBox from './components/ft-flex-box/ft-flex-box.vue' import TopNav from './components/top-nav/top-nav.vue' import SideNav from './components/side-nav/side-nav.vue' @@ -11,6 +10,7 @@ import FtToast from './components/ft-toast/ft-toast.vue' import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue' import FtPlaylistAddVideoPrompt from './components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue' import FtCreatePlaylistPrompt from './components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue' +import FtSearchFilters from './components/ft-search-filters/ft-search-filters.vue' import { marked } from 'marked' import { IpcChannels } from '../constants' import packageDetails from '../../package.json' @@ -19,8 +19,6 @@ import { translateWindowTitle } from './helpers/strings' let ipcRenderer = null -Vue.directive('observe-visibility', ObserveVisibility) - export default defineComponent({ name: 'App', components: { @@ -34,6 +32,7 @@ export default defineComponent({ FtProgressBar, FtPlaylistAddVideoPrompt, FtCreatePlaylistPrompt, + FtSearchFilters }, data: function () { return { @@ -46,6 +45,7 @@ export default defineComponent({ latestBlogUrl: '', updateChangelog: '', changeLogTitle: '', + isPromptOpen: false, lastExternalLinkToBeOpened: '', showExternalLinkOpeningPrompt: false, externalLinkOpeningPromptValues: [ @@ -77,6 +77,9 @@ export default defineComponent({ showCreatePlaylistPrompt: function () { return this.$store.getters.getShowCreatePlaylistPrompt }, + showSearchFilters: function () { + return this.$store.getters.getShowSearchFilters + }, windowTitle: function () { const routePath = this.$route.path if (!routePath.startsWith('/channel/') && !routePath.startsWith('/watch/') && !routePath.startsWith('/hashtag/')) { @@ -124,7 +127,7 @@ export default defineComponent({ externalLinkOpeningPromptNames: function () { return [ - this.$t('Yes'), + this.$t('Yes, Open Link'), this.$t('No') ] }, @@ -143,12 +146,6 @@ export default defineComponent({ secColor: 'checkThemeSettings', locale: 'setLocale', - - $route () { - // react to route changes... - // Hide top nav filter panel on page change - this.$refs.topNav?.hideFilters() - } }, created () { this.checkThemeSettings() @@ -159,11 +156,17 @@ export default defineComponent({ this.grabUserSettings().then(async () => { this.checkThemeSettings() - await this.fetchInvidiousInstances() + await this.fetchInvidiousInstancesFromFile() if (this.defaultInvidiousInstance === '') { await this.setRandomCurrentInvidiousInstance() } + this.fetchInvidiousInstances().then(e => { + if (this.defaultInvidiousInstance === '') { + this.setRandomCurrentInvidiousInstance() + } + }) + this.grabAllProfiles(this.$t('Profile.All Channels')).then(async () => { this.grabHistory() this.grabAllPlaylists() @@ -295,6 +298,10 @@ export default defineComponent({ this.showBlogBanner = false }, + handlePromptPortalUpdate: function(newVal) { + this.isPromptOpen = newVal + }, + openDownloadsPage: function () { const url = 'https://freetubeapp.io#download' openExternalLink(url) @@ -538,13 +545,14 @@ export default defineComponent({ 'getYoutubeUrlInfo', 'getExternalPlayerCmdArgumentsData', 'fetchInvidiousInstances', + 'fetchInvidiousInstancesFromFile', 'setRandomCurrentInvidiousInstance', 'setupListenersToSyncWindows', 'updateBaseTheme', 'updateMainColor', 'updateSecColor', 'showOutlines', - 'hideOutlines' + 'hideOutlines', ]) } }) diff --git a/src/renderer/App.vue b/src/renderer/App.vue index fb49f342eec8..a428bcb383d8 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -8,45 +8,10 @@ isLocaleRightToLeft: isLocaleRightToLeft }" > - - - - - - - - - - - + @@ -75,6 +42,9 @@ :option-values="externalLinkOpeningPromptValues" @click="handleExternalLinkOpeningPromptAnswer" /> + @@ -85,6 +55,51 @@ + + + + + + + + + + diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index 9cda4bad1504..1671981f1cea 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -39,7 +39,8 @@ export default defineComponent({ 'youtubenew', 'youtube', 'youtubeold', - 'newpipe' + 'newpipe', + 'close' ], shouldExportPlaylistForOlderVersions: false, @@ -70,7 +71,8 @@ export default defineComponent({ `${exportYouTube} (.csv)`, `${exportYouTube} (.json)`, `${exportYouTube} (.opml)`, - `${exportNewPipe} (.json)` + `${exportNewPipe} (.json)`, + this.$t('Close') ] }, primaryProfile: function () { diff --git a/src/renderer/components/data-settings/data-settings.vue b/src/renderer/components/data-settings/data-settings.vue index 456c6d3585c5..81dcab91f609 100644 --- a/src/renderer/components/data-settings/data-settings.vue +++ b/src/renderer/components/data-settings/data-settings.vue @@ -67,7 +67,6 @@ :label="$t('Settings.Data Settings.Select Export Type')" :option-names="exportSubscriptionsPromptNames" :option-values="subscriptionsPromptValues" - :show-close="true" @click="exportSubscriptions" /> diff --git a/src/renderer/components/experimental-settings/experimental-settings.js b/src/renderer/components/experimental-settings/experimental-settings.js index 1c313c86688e..652c94f752b1 100644 --- a/src/renderer/components/experimental-settings/experimental-settings.js +++ b/src/renderer/components/experimental-settings/experimental-settings.js @@ -37,7 +37,7 @@ export default defineComponent({ handleReplaceHttpCache: function (value) { this.showRestartPrompt = false - if (value === null || value === 'no') { + if (value === null || value === 'cancel') { this.replaceHttpCache = !this.replaceHttpCache return } diff --git a/src/renderer/components/experimental-settings/experimental-settings.vue b/src/renderer/components/experimental-settings/experimental-settings.vue index 0e70671b8d4c..1f0d61f64ebb 100644 --- a/src/renderer/components/experimental-settings/experimental-settings.vue +++ b/src/renderer/components/experimental-settings/experimental-settings.vue @@ -19,8 +19,8 @@ diff --git a/src/renderer/components/ft-button/ft-button.css b/src/renderer/components/ft-button/ft-button.css index 4a0cc58edf2c..a2a9a5f4474b 100644 --- a/src/renderer/components/ft-button/ft-button.css +++ b/src/renderer/components/ft-button/ft-button.css @@ -7,7 +7,6 @@ block-size: fit-content; box-sizing: border-box; cursor: pointer; - display: inline-block; align-items: center; justify-content: center; text-align: center; @@ -17,6 +16,8 @@ white-space: nowrap; font-weight: 500; vertical-align: middle; + display: flex; + gap: 10px; margin: 5px; box-shadow: 0 1px 2px rgb(0 0 0 / 50%); } diff --git a/src/renderer/components/ft-button/ft-button.js b/src/renderer/components/ft-button/ft-button.js index 4aa1b12352ca..ecb2f45286ea 100644 --- a/src/renderer/components/ft-button/ft-button.js +++ b/src/renderer/components/ft-button/ft-button.js @@ -18,6 +18,10 @@ export default defineComponent({ id: { type: String, default: '' + }, + icon: { + type: Array, + default: null } }, emits: ['click'], diff --git a/src/renderer/components/ft-button/ft-button.vue b/src/renderer/components/ft-button/ft-button.vue index 945000bbff1a..5566ec1dce40 100644 --- a/src/renderer/components/ft-button/ft-button.vue +++ b/src/renderer/components/ft-button/ft-button.vue @@ -10,6 +10,10 @@ @click="click" > + {{ label }} diff --git a/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.js b/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.js index 1beac5852a38..1a01b59df898 100644 --- a/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.js +++ b/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.js @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue' +import { defineComponent, nextTick } from 'vue' import { mapActions } from 'vuex' import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' import FtPrompt from '../ft-prompt/ft-prompt.vue' @@ -24,6 +24,9 @@ export default defineComponent({ } }, computed: { + title: function () { + return this.$t('User Playlists.CreatePlaylistPrompt.New Playlist Name') + }, allPlaylists: function () { return this.$store.getters.getAllPlaylists }, @@ -34,7 +37,7 @@ export default defineComponent({ mounted: function () { this.playlistName = this.newPlaylistVideoObject.title // Faster to input required playlist name - this.$refs.playlistNameInput.focus() + nextTick(() => this.$refs.playlistNameInput.focus()) }, methods: { createNewPlaylist: function () { diff --git a/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue b/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue index f541d08892a3..5370f2fb0450 100644 --- a/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue +++ b/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue @@ -1,9 +1,10 @@