diff --git a/.eslintrc.js b/.eslintrc.js index 1846e7791..f342b62dd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -112,6 +112,8 @@ module.exports = { ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube'] } ], + 'vue/require-explicit-emits': 'error', + 'vue/no-unused-emit-declarations': 'error', }, settings: { 'vue-i18n': { diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index a10f2f3e2..962a5f55d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -27,3 +27,5 @@ jobs: - run: yarn run lint # let's verify that webpack is able to package the project - run: yarn run pack + # verify that webpack is able to package the project using the web config + - run: yarn run pack:web diff --git a/_scripts/dev-runner.js b/_scripts/dev-runner.js index 0a4678040..fe25a836f 100644 --- a/_scripts/dev-runner.js +++ b/_scripts/dev-runner.js @@ -111,13 +111,14 @@ function startRenderer(callback) { const server = new WebpackDevServer({ static: { - directory: path.join(process.cwd(), 'static'), + directory: path.resolve(__dirname, '..', 'static'), watch: { ignored: [ /(dashFiles|storyboards)\/*/, '/**/.DS_Store', ] - } + }, + publicPath: '/static' }, port }, compiler) diff --git a/_scripts/webpack.renderer.config.js b/_scripts/webpack.renderer.config.js index 18d778c89..fcd9b0e58 100644 --- a/_scripts/webpack.renderer.config.js +++ b/_scripts/webpack.renderer.config.js @@ -111,8 +111,8 @@ const config = { ] }, node: { - __dirname: isDevMode, - __filename: isDevMode + __dirname: false, + __filename: false }, plugins: [ processLocalesPlugin, @@ -151,6 +151,8 @@ const config = { alias: { vue$: 'vue/dist/vue.runtime.esm.js', + 'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$': path.resolve(__dirname, '../src/datastores/handlers/electron.js'), + 'youtubei.js$': 'youtubei.js/web', // video.js's mpd-parser uses @xmldom/xmldom so that it can support both node and web browsers diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index c06679910..04f4b9803 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -108,8 +108,8 @@ const config = { ] }, node: { - __dirname: true, - __filename: isDevMode, + __dirname: false, + __filename: false }, plugins: [ new webpack.DefinePlugin({ @@ -160,6 +160,8 @@ const config = { alias: { vue$: 'vue/dist/vue.runtime.esm.js', + 'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$': path.resolve(__dirname, '../src/datastores/handlers/web.js'), + // video.js's mpd-parser uses @xmldom/xmldom so that it can support both node and web browsers // As FreeTube only runs in electron and web browsers, we can use the native DOMParser class, instead of the "polyfill" // https://caniuse.com/mdn-api_domparser diff --git a/jsconfig.json b/jsconfig.json index 6efa53c23..3dd46670d 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -3,6 +3,13 @@ "target": 2.7 }, "compilerOptions": { - "strictNullChecks": true + "strictNullChecks": true, + "baseUrl": "./", + "paths": { + "DB_HANDLERS_ELECTRON_RENDERER_OR_WEB": [ + "src/datastores/handlers/electron", + "src/datastores/handlers/web" + ] + } } } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index e2bff72ef..7c2eb4ec0 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -1,4 +1,4 @@ -import db from '../index' +import * as db from '../index' class Settings { static find() { @@ -192,13 +192,11 @@ function compactAllDatastores() { ]) } -const baseHandlers = { - settings: Settings, - history: History, - profiles: Profiles, - playlists: Playlists, +export { + Settings as settings, + History as history, + Profiles as profiles, + Playlists as playlists, compactAllDatastores, } - -export default baseHandlers diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index 9bf2526cf..31e8c9630 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -205,11 +205,9 @@ class Playlists { } } -const handlers = { - settings: Settings, - history: History, - profiles: Profiles, - playlists: Playlists +export { + Settings as settings, + History as history, + Profiles as profiles, + Playlists as playlists } - -export default handlers diff --git a/src/datastores/handlers/index.js b/src/datastores/handlers/index.js index e96565d1f..df6ebffd9 100644 --- a/src/datastores/handlers/index.js +++ b/src/datastores/handlers/index.js @@ -1,18 +1,6 @@ -let handlers -if (process.env.IS_ELECTRON) { - handlers = require('./electron').default -} else { - handlers = require('./web').default -} - -const DBSettingHandlers = handlers.settings -const DBHistoryHandlers = handlers.history -const DBProfileHandlers = handlers.profiles -const DBPlaylistHandlers = handlers.playlists - export { - DBSettingHandlers, - DBHistoryHandlers, - DBProfileHandlers, - DBPlaylistHandlers -} + settings as DBSettingHandlers, + history as DBHistoryHandlers, + profiles as DBProfileHandlers, + playlists as DBPlaylistHandlers +} from 'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB' diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index e8618beab..d5feccc99 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -1,4 +1,4 @@ -import baseHandlers from './base' +import * as baseHandlers from './base' // TODO: Syncing // Syncing on the web would involve a different implementation @@ -118,11 +118,9 @@ class Playlists { } } -const handlers = { - settings: Settings, - history: History, - profiles: Profiles, - playlists: Playlists +export { + Settings as settings, + History as history, + Profiles as profiles, + Playlists as playlists } - -export default handlers diff --git a/src/datastores/index.js b/src/datastores/index.js index a7038fec9..442fed649 100644 --- a/src/datastores/index.js +++ b/src/datastores/index.js @@ -22,10 +22,7 @@ if (process.env.IS_ELECTRON_MAIN) { dbPath = (dbName) => `${dbName}.db` } -const db = {} -db.settings = new Datastore({ filename: dbPath('settings'), autoload: true }) -db.profiles = new Datastore({ filename: dbPath('profiles'), autoload: true }) -db.playlists = new Datastore({ filename: dbPath('playlists'), autoload: true }) -db.history = new Datastore({ filename: dbPath('history'), autoload: true }) - -export default db +export const settings = new Datastore({ filename: dbPath('settings'), autoload: true }) +export const profiles = new Datastore({ filename: dbPath('profiles'), autoload: true }) +export const playlists = new Datastore({ filename: dbPath('playlists'), autoload: true }) +export const history = new Datastore({ filename: dbPath('history'), autoload: true }) diff --git a/src/main/index.js b/src/main/index.js index aa3b241a3..928dfd7b6 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -7,7 +7,7 @@ import path from 'path' import cp from 'child_process' import { IpcChannels, DBActions, SyncEvents } from '../constants' -import baseHandlers from '../datastores/handlers/base' +import * as baseHandlers from '../datastores/handlers/base' import { extractExpiryTimestamp, ImageCache } from './ImageCache' import { existsSync } from 'fs' import asyncFs from 'fs/promises' diff --git a/src/renderer/App.css b/src/renderer/App.css index 24b765ca8..036435314 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -18,10 +18,14 @@ .banner { inline-size: 85%; - margin-block: 20px; + margin-block: 40px 0; margin-inline: auto; } +.banner + .banner { + margin-block: 20px; +} + .banner-wrapper { margin-block: 0; margin-inline: 10px; @@ -53,8 +57,8 @@ } .banner { - inline-size: 80%; - margin-block-start: 20px; + inline-size: 90%; + margin-block: 60px 0; } .flexBox { diff --git a/src/renderer/App.js b/src/renderer/App.js index a80cebcf1..b208fe8e4 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -287,10 +287,7 @@ export default defineComponent({ }, checkExternalPlayer: async function () { - const payload = { - externalPlayer: this.externalPlayer - } - this.getExternalPlayerCmdArgumentsData(payload) + this.getExternalPlayerCmdArgumentsData() }, handleUpdateBannerClick: function (response) { diff --git a/src/renderer/components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.js b/src/renderer/components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.js index aca042948..4b26e8866 100644 --- a/src/renderer/components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.js +++ b/src/renderer/components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.js @@ -2,6 +2,7 @@ import { defineComponent } from 'vue' export default defineComponent({ name: 'FtAutoLoadNextPageWrapper', + emits: ['load-next-page'], computed: { generalAutoLoadMorePaginatedItemsEnabled() { return this.$store.getters.getGeneralAutoLoadMorePaginatedItemsEnabled diff --git a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js index c9854b34d..dbff93d28 100644 --- a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js +++ b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js @@ -20,6 +20,7 @@ export default defineComponent({ default: false } }, + emits: ['click'], data: function () { return { selected: false diff --git a/src/renderer/components/ft-icon-button/ft-icon-button.js b/src/renderer/components/ft-icon-button/ft-icon-button.js index fb6b4a3a9..cba8cfb34 100644 --- a/src/renderer/components/ft-icon-button/ft-icon-button.js +++ b/src/renderer/components/ft-icon-button/ft-icon-button.js @@ -16,6 +16,10 @@ export default defineComponent({ type: Array, default: () => ['fas', 'ellipsis-v'] }, + disabled: { + type: Boolean, + default: false + }, theme: { type: String, default: 'base' @@ -61,6 +65,7 @@ export default defineComponent({ default: false } }, + emits: ['click'], data: function () { return { dropdownShown: false, @@ -87,6 +92,7 @@ export default defineComponent({ }, handleIconClick: function () { + if (this.disabled) { return } if (this.forceDropdown || (this.dropdownOptions.length > 0)) { this.dropdownShown = !this.dropdownShown @@ -103,6 +109,7 @@ export default defineComponent({ }, handleIconMouseDown: function () { + if (this.disabled) { return } if (this.dropdownShown) { this.mouseDownOnIcon = true } diff --git a/src/renderer/components/ft-icon-button/ft-icon-button.scss b/src/renderer/components/ft-icon-button/ft-icon-button.scss index c76e9333f..6e4f3acb2 100644 --- a/src/renderer/components/ft-icon-button/ft-icon-button.scss +++ b/src/renderer/components/ft-icon-button/ft-icon-button.scss @@ -79,6 +79,12 @@ } } +.disabled { + opacity: 0.5; + pointer-events: none; + user-select: none; +} + .iconDropdown { background-color: var(--side-nav-color); box-shadow: 0 1px 2px rgb(0 0 0 / 50%); diff --git a/src/renderer/components/ft-icon-button/ft-icon-button.vue b/src/renderer/components/ft-icon-button/ft-icon-button.vue index afc4c68f4..8f4d2bfc7 100644 --- a/src/renderer/components/ft-icon-button/ft-icon-button.vue +++ b/src/renderer/components/ft-icon-button/ft-icon-button.vue @@ -7,7 +7,8 @@ :icon="icon" :class="{ [theme]: true, - shadow: useShadow + shadow: useShadow, + disabled }" :style="{ padding: padding + 'px', diff --git a/src/renderer/components/ft-input-tags/ft-input-tags.js b/src/renderer/components/ft-input-tags/ft-input-tags.js index e139db3b7..281b35171 100644 --- a/src/renderer/components/ft-input-tags/ft-input-tags.js +++ b/src/renderer/components/ft-input-tags/ft-input-tags.js @@ -53,6 +53,7 @@ export default defineComponent({ default: (_) => ({ preferredName: '', icon: '' }), } }, + emits: ['already-exists', 'change', 'error-find-tag-info', 'invalid-name'], methods: { updateTags: async function (text, _e) { if (this.areChannelTags) { diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index dc4c945c2..9c3bfc197 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -64,6 +64,7 @@ export default defineComponent({ default: '' } }, + emits: ['clear', 'click', 'input'], data: function () { let actionIcon = ['fas', 'search'] if (this.forceActionButtonIconName !== null) { diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js index b602727b6..8e34e17f2 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -5,6 +5,7 @@ import { copyToClipboard, formatDurationAsTimestamp, formatNumber, + getRelativeTimeFromDate, openExternalLink, showToast, toDistractionFreeTitle, @@ -84,6 +85,7 @@ export default defineComponent({ default: false, }, }, + emits: ['pause-player'], data: function () { return { id: '', @@ -365,6 +367,10 @@ export default defineComponent({ return this.historyEntryExists && !this.inHistory }, + currentLocale: function () { + return this.$i18n.locale.replace('_', '-') + }, + externalPlayer: function () { return this.$store.getters.getExternalPlayer }, @@ -482,14 +488,6 @@ export default defineComponent({ return query }, - currentLocale: function () { - return this.$i18n.locale.replace('_', '-') - }, - - showAddToPlaylistPrompt: function () { - return this.$store.getters.getShowAddToPlaylistPrompt - }, - useDeArrowTitles: function () { return this.$store.getters.getUseDeArrowTitles }, @@ -688,48 +686,8 @@ export default defineComponent({ if (this.inHistory) { this.uploadedTime = new Date(this.data.published).toLocaleDateString([this.currentLocale, 'en']) } else { - const now = new Date().getTime() - // Convert from ms to second - // For easier code interpretation the value is made to be positive - let timeDiffFromNow = ((now - this.data.published) / 1000) - let timeUnit = 'second' - - if (timeDiffFromNow >= 60) { - timeDiffFromNow /= 60 - timeUnit = 'minute' - } - - if (timeUnit === 'minute' && timeDiffFromNow >= 60) { - timeDiffFromNow /= 60 - timeUnit = 'hour' - } - - if (timeUnit === 'hour' && timeDiffFromNow >= 24) { - timeDiffFromNow /= 24 - timeUnit = 'day' - } - - const timeDiffFromNowDays = timeDiffFromNow - - if (timeUnit === 'day' && timeDiffFromNow >= 7) { - timeDiffFromNow /= 7 - timeUnit = 'week' - } - // Use 30 days per month, just like calculatePublishedDate - if (timeUnit === 'week' && timeDiffFromNowDays >= 30) { - timeDiffFromNow = timeDiffFromNowDays / 30 - timeUnit = 'month' - } - - if (timeUnit === 'month' && timeDiffFromNow >= 12) { - timeDiffFromNow /= 12 - timeUnit = 'year' - } - - // Using `Math.ceil` so that -1.x days ago displayed as 1 day ago - // Notice that the value is turned to negative to be displayed as "ago" - this.uploadedTime = new Intl.RelativeTimeFormat([this.currentLocale, 'en']).format(Math.ceil(-timeDiffFromNow), timeUnit) + this.uploadedTime = getRelativeTimeFromDate(new Date(this.data.published).toDateString(), false) } } diff --git a/src/renderer/components/ft-notification-banner/ft-notification-banner.css b/src/renderer/components/ft-notification-banner/ft-notification-banner.css index 2f8464be3..6efe9d4f9 100644 --- a/src/renderer/components/ft-notification-banner/ft-notification-banner.css +++ b/src/renderer/components/ft-notification-banner/ft-notification-banner.css @@ -30,3 +30,11 @@ inset-inline-end: 10px; cursor: pointer; } + +@media only screen and (width <= 680px) { + .bannerIcon { + inset-block-start: 27%; + block-size: 25px; + inline-size: 25px; + } +} diff --git a/src/renderer/components/ft-notification-banner/ft-notification-banner.js b/src/renderer/components/ft-notification-banner/ft-notification-banner.js index 9973993cb..75c7c4c87 100644 --- a/src/renderer/components/ft-notification-banner/ft-notification-banner.js +++ b/src/renderer/components/ft-notification-banner/ft-notification-banner.js @@ -8,6 +8,7 @@ export default defineComponent({ required: true } }, + emits: ['click'], methods: { handleClick: function (response) { this.$emit('click', response) diff --git a/src/renderer/components/ft-notification-banner/ft-notification-banner.vue b/src/renderer/components/ft-notification-banner/ft-notification-banner.vue index 02feed760..4b3ff2eb0 100644 --- a/src/renderer/components/ft-notification-banner/ft-notification-banner.vue +++ b/src/renderer/components/ft-notification-banner/ft-notification-banner.vue @@ -24,7 +24,7 @@ tabindex="0" :title="$t('Close Banner')" @click.stop="handleClose" - @keydown.enter.stop.prevent="handleClose" + @keydown.enter.space.stop.prevent="handleClose" /> diff --git a/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js b/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js index 61c2551f2..dbb64c271 100644 --- a/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js +++ b/src/renderer/components/ft-playlist-selector/ft-playlist-selector.js @@ -25,6 +25,7 @@ export default defineComponent({ required: true, }, }, + emits: ['selected'], data: function () { return { title: '', diff --git a/src/renderer/components/ft-profile-edit/ft-profile-edit.js b/src/renderer/components/ft-profile-edit/ft-profile-edit.js index 4cf9029c9..f89b449f1 100644 --- a/src/renderer/components/ft-profile-edit/ft-profile-edit.js +++ b/src/renderer/components/ft-profile-edit/ft-profile-edit.js @@ -32,6 +32,7 @@ export default defineComponent({ required: true } }, + emits: ['new-profile-created', 'profile-deleted'], data: function () { return { showDeletePrompt: false, diff --git a/src/renderer/components/ft-prompt/ft-prompt.js b/src/renderer/components/ft-prompt/ft-prompt.js index 060a4a006..45296bbfd 100644 --- a/src/renderer/components/ft-prompt/ft-prompt.js +++ b/src/renderer/components/ft-prompt/ft-prompt.js @@ -38,6 +38,7 @@ export default defineComponent({ default: false } }, + emits: ['click'], data: function () { return { promptButtons: [], diff --git a/src/renderer/components/ft-refresh-widget/ft-refresh-widget.css b/src/renderer/components/ft-refresh-widget/ft-refresh-widget.css new file mode 100644 index 000000000..3c9b49909 --- /dev/null +++ b/src/renderer/components/ft-refresh-widget/ft-refresh-widget.css @@ -0,0 +1,36 @@ +.floatingRefreshSection { + position: fixed; + inset-block-start: 60px; + inset-inline-end: 0; + box-sizing: border-box; + inline-size: calc(100% - 80px); + padding-block: 5px; + padding-inline: 10px; + box-shadow: 0 2px 1px 0 var(--primary-shadow-color); + background-color: var(--card-bg-color); + border-inline-start: 2px solid var(--primary-color); + display: flex; + align-items: center; + gap: 5px; + justify-content: flex-end; +} + +.floatingRefreshSection:has(.lastRefreshTimestamp + .refreshButton) { + justify-content: space-between; +} + +.floatingRefreshSection.sideNavOpen { + inline-size: calc(100% - 200px); +} + +.lastRefreshTimestamp { + margin-block: 0; + text-align: center; + font-size: 16px; +} + +@media only screen and (width <= 680px) { + .floatingRefreshSection, .floatingRefreshSection.sideNavOpen { + inline-size: 100%; + } +} diff --git a/src/renderer/components/ft-refresh-widget/ft-refresh-widget.js b/src/renderer/components/ft-refresh-widget/ft-refresh-widget.js new file mode 100644 index 000000000..24ca4e1f8 --- /dev/null +++ b/src/renderer/components/ft-refresh-widget/ft-refresh-widget.js @@ -0,0 +1,29 @@ +import { defineComponent } from 'vue' + +import FtIconButton from '../ft-icon-button/ft-icon-button.vue' + +export default defineComponent({ + name: 'FtRefreshWidget', + components: { + 'ft-icon-button': FtIconButton, + }, + props: { + disableRefresh: { + type: Boolean, + default: false + }, + lastRefreshTimestamp: { + type: String, + default: '' + }, + title: { + type: String, + required: true + } + }, + computed: { + isSideNavOpen: function () { + return this.$store.getters.getIsSideNavOpen + } + } +}) diff --git a/src/renderer/components/ft-refresh-widget/ft-refresh-widget.vue b/src/renderer/components/ft-refresh-widget/ft-refresh-widget.vue new file mode 100644 index 000000000..564f1cae8 --- /dev/null +++ b/src/renderer/components/ft-refresh-widget/ft-refresh-widget.vue @@ -0,0 +1,27 @@ + + +