Compare commits

...

39 Commits

Author SHA1 Message Date
Jason
b166acafa0
Merge e9b539face into a70a5c6ab2 2024-11-21 09:00:40 +00:00
Gideon Wentink
a70a5c6ab2
Translated using Weblate (Afrikaans)
Currently translated at 31.9% (272 of 852 strings)

Translation: FreeTube/Translations
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/af/
2024-11-21 10:00:20 +01:00
Jason Henriquez
e9b539face Only enable page bookmarking on certain routes 2024-10-27 21:53:47 -05:00
Jason Henriquez
a2914fa6ff Remove ' - FreeTube' portion of default titles 2024-10-27 21:26:35 -05:00
Jason Henriquez
69ed872ee7 Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-10-27 19:55:35 -05:00
Jason Henriquez
2039016ac1 Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-10-15 21:23:27 -05:00
Jason Henriquez
abbd3493ce Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-06-30 19:14:43 -05:00
Jason
3504424eb1
Merge branch 'development' into feat/add-page-bookmarking 2024-06-19 00:29:41 +00:00
Jason Henriquez
13ba73e6f1 Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-06-15 08:43:39 -05:00
Jason Henriquez
b162507811 Update playlist route icons 2024-06-11 21:30:19 -05:00
Jason Henriquez
094b2c293b Add message when there are other page bookmarks with the same name 2024-06-11 18:03:39 -05:00
Jason Henriquez
e5b5b43621 Constrain visible ft-input results list length to 15 2024-06-11 16:43:44 -05:00
Jason Henriquez
a81a8b5873 Prevent search results from wrapping across to multiple rows 2024-06-11 14:43:24 -05:00
Jason Henriquez
85e46b5bc5 Add icons corresponding to bookmarked page route 2024-06-11 14:42:10 -05:00
Jason Henriquez
039fce110d Delete page bookmarks to user playlists when those user playlists are deleted 2024-06-11 13:42:49 -05:00
Jason Henriquez
4736723e02 Update to show the proper page bookmark name upon deletion 2024-06-11 09:20:57 -05:00
Jason Henriquez
33d2d50017 Have 'Cancel' button appear for already bookmarked page case 2024-06-11 08:47:46 -05:00
Jason Henriquez
7f6a4ad340 Add search history sync logic 2024-06-11 08:34:21 -05:00
Jason Henriquez
d6429d58bb Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-06-11 07:27:11 -05:00
Jason Henriquez
ede347a2d8 Fix page bookmark removal function call to pass the route, not the full bookmark 2024-06-04 00:32:23 -05:00
Jason Henriquez
d9d8c9cc1b Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-06-03 23:59:12 -05:00
Jason Henriquez
c07505890e Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-05-22 08:28:46 -05:00
Jason Henriquez
5597720028 Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-05-20 09:12:35 -05:00
Jason Henriquez
50dc04b70a Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-05-08 16:23:42 -05:00
Jason Henriquez
709caef699 Update styling of search results 2024-05-02 20:51:00 -05:00
Jason Henriquez
5ee7638398 Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-05-02 20:24:14 -05:00
Jason Henriquez
78d1b9c4cb Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking 2024-04-26 23:04:23 -05:00
Jason Henriquez
392fa75a87 Implement changes to make work re-usable for saving generic search history 2024-04-23 13:41:32 -05:00
Jason Henriquez
7896b7b79e Modify bookmark result star icon opacity
Reduces chance that the star is seen as a button or control.
2024-04-22 23:01:51 -05:00
Jason Henriquez
88ec2a82cb Fix bookmark search results when search suggestions are disabled 2024-04-22 22:21:38 -05:00
Jason Henriquez
0572d33cd2 Fix deletion call and disable bookmark icon while data is being loaded in 2024-04-22 21:52:52 -05:00
Jason Henriquez
f7b8f1ab55 Add aria-roledescription for page bookmark search results 2024-04-22 21:02:44 -05:00
Jason Henriquez
871e3307fc Update bookmark icon color to use secondary colors when top nav is colored 2024-04-22 21:01:34 -05:00
Jason Henriquez
c254875e2e Fix bookmark searching logic 2024-04-22 20:02:36 -05:00
Jason Henriquez
e60cd786e9 Improve default bookmark names for Playlist and Search Results routes 2024-04-22 19:55:41 -05:00
Jason Henriquez
a3057c606a Add 'Remove All Page Bookmarks' setting 2024-04-22 19:14:18 -05:00
Jason Henriquez
a12c4ea894 Fix bookmark search result styling and keyboard interactions 2024-04-22 19:02:28 -05:00
Jason Henriquez
808cadb0b9 Update search results with matching bookmarks 2024-04-22 10:58:47 -05:00
Jason Henriquez
920bf92fc2 Implement page bookmarking modal & call, icon, & handling/calls 2024-04-22 00:57:16 -05:00
30 changed files with 783 additions and 39 deletions

View File

@ -25,10 +25,12 @@ const IpcChannels = {
DB_HISTORY: 'db-history',
DB_PROFILES: 'db-profiles',
DB_PLAYLISTS: 'db-playlists',
DB_SEARCH_HISTORY: 'db-search-history',
DB_SUBSCRIPTION_CACHE: 'db-subscription-cache',
SYNC_SETTINGS: 'sync-settings',
SYNC_HISTORY: 'sync-history',
SYNC_SEARCH_HISTORY: 'sync-search-history',
SYNC_PROFILES: 'sync-profiles',
SYNC_PLAYLISTS: 'sync-playlists',
SYNC_SUBSCRIPTION_CACHE: 'sync-subscription-cache',

View File

@ -211,6 +211,32 @@ class Playlists {
}
}
class SearchHistory {
static create(pageBookmark) {
return db.searchHistory.insertAsync(pageBookmark)
}
static find() {
return db.searchHistory.findAsync({})
}
static upsert(pageBookmark) {
return db.searchHistory.updateAsync({ _id: pageBookmark._id }, pageBookmark, { upsert: true })
}
static delete(_id) {
return db.searchHistory.removeAsync({ _id: _id })
}
static deleteMultiple(ids) {
return db.searchHistory.removeAsync({ _id: { $in: ids } })
}
static deleteAll() {
return db.searchHistory.removeAsync({}, { multi: true })
}
}
class SubscriptionCache {
static find() {
return db.subscriptionCache.findAsync({})
@ -296,6 +322,7 @@ function compactAllDatastores() {
db.history.compactDatafileAsync(),
db.profiles.compactDatafileAsync(),
db.playlists.compactDatafileAsync(),
db.searchHistory.compactDatafileAsync(),
db.subscriptionCache.compactDatafileAsync(),
])
}
@ -305,6 +332,7 @@ export {
History as history,
Profiles as profiles,
Playlists as playlists,
SearchHistory as searchHistory,
SubscriptionCache as subscriptionCache,
compactAllDatastores,

View File

@ -218,6 +218,50 @@ class Playlists {
}
}
class SearchHistory {
static create(pageBookmark) {
return ipcRenderer.invoke(
IpcChannels.DB_SEARCH_HISTORY,
{ action: DBActions.GENERAL.CREATE, data: pageBookmark }
)
}
static find() {
return ipcRenderer.invoke(
IpcChannels.DB_SEARCH_HISTORY,
{ action: DBActions.GENERAL.FIND }
)
}
static upsert(pageBookmark) {
return ipcRenderer.invoke(
IpcChannels.DB_SEARCH_HISTORY,
{ action: DBActions.GENERAL.UPSERT, data: pageBookmark }
)
}
static delete(_id) {
return ipcRenderer.invoke(
IpcChannels.DB_SEARCH_HISTORY,
{ action: DBActions.GENERAL.DELETE, data: _id }
)
}
static deleteMultiple(ids) {
return ipcRenderer.invoke(
IpcChannels.DB_SEARCH_HISTORY,
{ action: DBActions.GENERAL.DELETE_MULTIPLE, data: ids }
)
}
static deleteAll() {
return ipcRenderer.invoke(
IpcChannels.DB_SEARCH_HISTORY,
{ action: DBActions.GENERAL.DELETE_ALL }
)
}
}
class SubscriptionCache {
static find() {
return ipcRenderer.invoke(
@ -296,5 +340,6 @@ export {
History as history,
Profiles as profiles,
Playlists as playlists,
SearchHistory as searchHistory,
SubscriptionCache as subscriptionCache,
}

View File

@ -3,5 +3,6 @@ export {
history as DBHistoryHandlers,
profiles as DBProfileHandlers,
playlists as DBPlaylistHandlers,
searchHistory as DBSearchHistoryHandlers,
subscriptionCache as DBSubscriptionCacheHandlers,
} from 'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB'

View File

@ -122,6 +122,32 @@ class Playlists {
}
}
class SearchHistory {
static create(pageBookmark) {
return baseHandlers.searchHistory.create(pageBookmark)
}
static find() {
return baseHandlers.searchHistory.find()
}
static upsert(pageBookmark) {
return baseHandlers.searchHistory.upsert(pageBookmark)
}
static delete(_id) {
return baseHandlers.searchHistory.delete(_id)
}
static deleteMultiple(ids) {
return baseHandlers.searchHistory.deleteMultiple(ids)
}
static deleteAll() {
return baseHandlers.searchHistory.deleteAll()
}
}
class SubscriptionCache {
static find() {
return baseHandlers.subscriptionCache.find()
@ -180,5 +206,6 @@ export {
History as history,
Profiles as profiles,
Playlists as playlists,
SearchHistory as searchHistory,
SubscriptionCache as subscriptionCache,
}

View File

@ -26,4 +26,5 @@ export const settings = new Datastore({ filename: dbPath('settings'), autoload:
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 })
export const searchHistory = new Datastore({ filename: dbPath('search-history'), autoload: true })
export const subscriptionCache = new Datastore({ filename: dbPath('subscription-cache'), autoload: true })

View File

@ -1316,6 +1316,64 @@ function runApp() {
}
})
// ************** //
// Search History
ipcMain.handle(IpcChannels.DB_SEARCH_HISTORY, async (event, { action, data }) => {
try {
switch (action) {
case DBActions.GENERAL.CREATE: {
const pageBookmark = await baseHandlers.searchHistory.create(data)
syncOtherWindows(
IpcChannels.SYNC_SEARCH_HISTORY,
event,
{ event: SyncEvents.GENERAL.CREATE, data }
)
return pageBookmark
}
case DBActions.GENERAL.FIND:
return await baseHandlers.searchHistory.find()
case DBActions.GENERAL.UPSERT:
await baseHandlers.searchHistory.upsert(data)
syncOtherWindows(
IpcChannels.SYNC_SEARCH_HISTORY,
event,
{ event: SyncEvents.GENERAL.UPSERT, data }
)
return null
case DBActions.GENERAL.DELETE:
await baseHandlers.searchHistory.delete(data)
syncOtherWindows(
IpcChannels.SYNC_SEARCH_HISTORY,
event,
{ event: SyncEvents.GENERAL.DELETE, data }
)
return null
case DBActions.GENERAL.DELETE_MULTIPLE:
await baseHandlers.searchHistory.deleteMultiple(data)
syncOtherWindows(
IpcChannels.SYNC_SEARCH_HISTORY,
event,
{ event: SyncEvents.GENERAL.DELETE_MULTIPLE, data }
)
return null
case DBActions.GENERAL.DELETE_ALL:
await baseHandlers.searchHistory.deleteAll()
return null
default:
// eslint-disable-next-line no-throw-literal
throw 'invalid search history db action'
}
} catch (err) {
if (typeof err === 'string') throw err
else throw err.toString()
}
})
// *********** //
// *********** //

View File

@ -10,6 +10,7 @@ import FtToast from './components/ft-toast/ft-toast.vue'
import FtProgressBar from './components/FtProgressBar/FtProgressBar.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 PageBookmarkPrompt from './components/page-bookmark-prompt/page-bookmark-prompt.vue'
import FtSearchFilters from './components/ft-search-filters/ft-search-filters.vue'
import { marked } from 'marked'
import { IpcChannels } from '../constants'
@ -32,6 +33,7 @@ export default defineComponent({
FtProgressBar,
FtPlaylistAddVideoPrompt,
FtCreatePlaylistPrompt,
PageBookmarkPrompt,
FtSearchFilters
},
data: function () {
@ -40,6 +42,7 @@ export default defineComponent({
showUpdatesBanner: false,
showBlogBanner: false,
showReleaseNotes: false,
pageBookmarksAvailable: false,
updateBannerMessage: '',
blogBannerMessage: '',
latestBlogUrl: '',
@ -77,6 +80,9 @@ export default defineComponent({
showCreatePlaylistPrompt: function () {
return this.$store.getters.getShowCreatePlaylistPrompt
},
showPageBookmarkPrompt: function () {
return this.$store.getters.getShowPageBookmarkPrompt
},
showSearchFilters: function () {
return this.$store.getters.getShowSearchFilters
},
@ -142,7 +148,7 @@ export default defineComponent({
externalLinkHandling: function () {
return this.$store.getters.getExternalLinkHandling
}
},
},
watch: {
windowTitle: 'setWindowTitle',
@ -178,6 +184,9 @@ export default defineComponent({
this.grabAllProfiles(this.$t('Profile.All Channels')).then(async () => {
this.grabHistory()
this.grabAllPlaylists()
this.grabPageBookmarks().then(async () => {
this.pageBookmarksAvailable = true
})
this.grabAllSubscriptions()
if (process.env.IS_ELECTRON) {
@ -552,6 +561,7 @@ export default defineComponent({
'grabUserSettings',
'grabAllProfiles',
'grabHistory',
'grabPageBookmarks',
'grabAllPlaylists',
'grabAllSubscriptions',
'getYoutubeUrlInfo',

View File

@ -56,6 +56,9 @@
<ft-create-playlist-prompt
v-if="showCreatePlaylistPrompt"
/>
<page-bookmark-prompt
v-if="showPageBookmarkPrompt"
/>
<ft-toast />
<ft-progress-bar
v-if="showProgressBar"
@ -63,6 +66,7 @@
<top-nav
ref="topNav"
:inert="isPromptOpen"
:page-bookmarks-available="pageBookmarksAvailable"
/>
<side-nav
ref="sideNav"

View File

@ -202,8 +202,22 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp
.list li {
display: block;
padding-block: 0;
padding-inline: 15px;
line-height: 2rem;
padding-inline: 15px;
text-overflow: ellipsis;
overflow-x: hidden;
white-space: nowrap;
}
.bookmarkStarIcon {
color: var(--favorite-icon-color);
}
.searchResultIcon {
opacity: 0.6;
padding-inline-end: 10px;
inline-size: 16px;
block-size: 16px;
}
.hover {

View File

@ -2,7 +2,9 @@ import { defineComponent } from 'vue'
import { mapActions } from 'vuex'
import FtTooltip from '../ft-tooltip/ft-tooltip.vue'
import { isKeyboardEventKeyPrintableChar, isNullOrEmpty } from '../../helpers/strings'
import { getIconForRoute, isKeyboardEventKeyPrintableChar, isNullOrEmpty } from '../../helpers/strings'
const MAX_VISIBLE_LIST_ITEMS = 15
export default defineComponent({
name: 'FtInput',
@ -83,7 +85,7 @@ export default defineComponent({
isPointerInList: false,
keyboardSelectedOptionIndex: -1,
},
visibleDataList: this.dataList,
visibleDataList: this.dataList?.slice(0, MAX_VISIBLE_LIST_ITEMS),
// This button should be invisible on app start
// As the text input box should be empty
clearTextButtonExisting: false,
@ -116,8 +118,7 @@ export default defineComponent({
searchStateKeyboardSelectedOptionValue() {
if (this.searchState.keyboardSelectedOptionIndex === -1) { return null }
return this.visibleDataList[this.searchState.keyboardSelectedOptionIndex]
return this.getTextForArrayAtIndex(this.visibleDataList, this.searchState.keyboardSelectedOptionIndex)
},
},
watch: {
@ -143,6 +144,9 @@ export default defineComponent({
this.updateVisibleDataList()
},
methods: {
getTextForArrayAtIndex: function (array, index) {
return array[index].name ?? array[index]
},
handleClick: function (e) {
// No action if no input text
if (!this.inputDataPresent) {
@ -234,7 +238,11 @@ export default defineComponent({
handleOptionClick: function (index) {
this.searchState.showOptions = false
this.inputData = this.visibleDataList[index]
if (this.visibleDataList[index].route) {
this.inputData = `ft:${this.visibleDataList[index].route}`
} else {
this.inputData = this.visibleDataList[index]
}
this.$emit('input', this.inputData)
this.handleClick()
},
@ -248,9 +256,11 @@ export default defineComponent({
if (this.searchState.selectedOption !== -1) {
this.searchState.showOptions = false
event.preventDefault()
this.inputData = this.visibleDataList[this.searchState.selectedOption]
this.inputData = this.getTextForArrayAtIndex(this.visibleDataList, this.searchState.selectedOption)
this.handleOptionClick(this.searchState.selectedOption)
} else {
this.handleClick(event)
}
this.handleClick(event)
// Early return
return
}
@ -296,18 +306,21 @@ export default defineComponent({
},
updateVisibleDataList: function () {
if (this.dataList.length === 0) { return }
// Reset selected option before it's updated
this.searchState.selectedOption = -1
this.searchState.keyboardSelectedOptionIndex = -1
if (this.inputData === '') {
this.visibleDataList = this.dataList
this.visibleDataList = this.dataList?.slice(0, MAX_VISIBLE_LIST_ITEMS)
return
}
// get list of items that match input
const lowerCaseInputData = this.inputData.toLowerCase()
this.visibleDataList = this.dataList.filter(x => {
this.visibleDataList = this.dataList?.slice(0, MAX_VISIBLE_LIST_ITEMS).filter(x => {
if (x.name) {
return x.name.toLowerCase().indexOf(lowerCaseInputData) !== -1
}
return x.toLowerCase().indexOf(lowerCaseInputData) !== -1
})
},
@ -316,6 +329,10 @@ export default defineComponent({
this.inputData = text
},
iconForBookmarkedPage: (pageBookmark) => {
return getIconForRoute(pageBookmark.route) ?? ['fas', 'magnifying-glass']
},
focus() {
this.$refs.input.focus()
},

View File

@ -75,14 +75,25 @@
>
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
<li
v-for="(list, index) in visibleDataList"
v-for="(entry, index) in visibleDataList"
:key="index"
:class="searchState.selectedOption === index ? 'hover': ''"
:class="{ hover: searchState.selectedOption === index }"
:aria-roledescription="entry.isBookmark ? $t('Role Descriptions.bookmark') : null"
@click="handleOptionClick(index)"
@mouseenter="searchState.selectedOption = index"
@mouseleave="searchState.selectedOption = -1"
>
{{ list }}
<font-awesome-icon
v-if="entry.isBookmark"
:icon="iconForBookmarkedPage(entry)"
class="searchResultIcon bookmarkStarIcon"
/>
<font-awesome-icon
v-else-if="isSearch"
:icon="['fas', 'magnifying-glass']"
class="searchResultIcon"
/>
{{ entry.name ?? entry }}
</li>
<!-- skipped -->
</ul>

View File

@ -0,0 +1,9 @@
.heading {
text-align: center;
}
.pageBookmarkNameInput {
inline-size: 80%;
max-inline-size: 600px;
margin-inline: auto;
}

View File

@ -0,0 +1,87 @@
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'
import FtButton from '../ft-button/ft-button.vue'
import FtInput from '../../components/ft-input/ft-input.vue'
import { showToast } from '../../helpers/utils'
import { defaultBookmarkNameForRoute } from '../../helpers/strings'
export default defineComponent({
name: 'PageBookmarkPrompt',
components: {
'ft-flex-box': FtFlexBox,
'ft-prompt': FtPrompt,
'ft-button': FtButton,
'ft-input': FtInput
},
data: function () {
return {
name: ''
}
},
computed: {
isBookmarkBeingCreated: function () {
return this.pageBookmark == null
},
pageBookmarks: function () {
return this.$store.getters.getPageBookmarks
},
duplicateNameCount: function () {
const currentBookmarkAdjustment = this.name === this.pageBookmark?.name ? -1 : 0
return currentBookmarkAdjustment + this.pageBookmarks.filter((pageBookmark) => pageBookmark.name === this.name).length
},
duplicateNameMessage: function () {
return this.$tc('Page Bookmark["There is {count} other bookmark with the same name."]', this.duplicateNameCount, { count: this.duplicateNameCount })
},
pageBookmark: function () {
return this.$store.getters.getPageBookmarkWithRoute(this.$router.currentRoute.fullPath)
},
title: function () {
return this.isBookmarkBeingCreated ? this.$t('Page Bookmark.Create Bookmark') : this.$t('Page Bookmark.Edit Bookmark')
}
},
mounted: function () {
nextTick(() => {
this.name = this.pageBookmark?.name ?? defaultBookmarkNameForRoute(this.$router.currentRoute)
this.$refs.pageBookmarkNameInput?.focus()
})
},
methods: {
hide: function () {
this.hidePageBookmarkPrompt()
},
removeBookmark: function () {
const pageBookmark = this.pageBookmark
this.removePageBookmark(pageBookmark._id)
showToast(this.$t('Page Bookmark.Removed page bookmark', { name: pageBookmark.name }))
this.hide()
},
save: function () {
const pageBookmark = {
route: this.$router.currentRoute.fullPath,
name: this.name,
isBookmark: true
}
if (this.isBookmarkBeingCreated) {
this.createPageBookmark(pageBookmark)
showToast(this.$t('Page Bookmark.Created page bookmark', { name: this.name }))
} else if (this.pageBookmark.name !== pageBookmark.name) {
this.updatePageBookmark(pageBookmark)
showToast(this.$t('Page Bookmark.Updated page bookmark', { name: this.name }))
}
this.hide()
},
...mapActions([
'hidePageBookmarkPrompt',
'createPageBookmark',
'removePageBookmark',
'updatePageBookmark'
])
}
})

View File

@ -0,0 +1,56 @@
<template>
<ft-prompt
:label="title"
@click="hide"
>
<h2 class="heading">
{{ title }}
</h2>
<div
class="pageBookmarkDetails"
>
<ft-input
ref="pageBookmarkNameInput"
class="pageBookmarkNameInput"
:placeholder="$t('Name')"
:value="name"
:show-clear-text-button="true"
:show-action-button="false"
@input="e => name = e"
@clear="e => name = ''"
@keydown.enter.native="save"
/>
<ft-flex-box v-if="duplicateNameCount > 0">
<p>{{ duplicateNameMessage }}</p>
</ft-flex-box>
</div>
<div class="actions-container">
<ft-flex-box>
<ft-button
v-if="!isBookmarkBeingCreated"
:label="$t('Page Bookmark.Remove Bookmark')"
:icon="['fas', 'trash']"
text-color="var(--destructive-text-color)"
background-color="var(--destructive-color)"
@click="removeBookmark"
/>
<ft-button
:label="$t('Save')"
:disabled="name === ''"
text-color="var(--text-with-accent-color)"
background-color="var(--accent-color)"
@click="save"
/>
<ft-button
:label="$t('Cancel')"
:text-color="null"
:background-color="null"
@click="hide"
/>
</ft-flex-box>
</div>
</ft-prompt>
</template>
<script src="./page-bookmark-prompt.js" />
<style scoped src="./page-bookmark-prompt.css" />

View File

@ -23,6 +23,7 @@ export default defineComponent({
showRemoveHistoryPrompt: false,
showRemoveSubscriptionsPrompt: false,
showRemovePlaylistsPrompt: false,
showRemovePageBookmarksPrompt: false,
promptValues: [
'delete',
'cancel'
@ -46,6 +47,9 @@ export default defineComponent({
removeSubscriptionsPromptMessage: function () {
return this.$t('Settings.Privacy Settings["Are you sure you want to remove all subscriptions and profiles? This cannot be undone."]')
},
removePageBookmarksPromptMessage: function () {
return this.$t('Settings.Privacy Settings["Are you sure you want to remove all page bookmarks? This cannot be undone."]')
},
promptNames: function () {
return [
this.$t('Yes, Delete'),
@ -115,6 +119,14 @@ export default defineComponent({
showToast(this.$t('Settings.Privacy Settings.All playlists have been removed'))
},
handleRemovePageBookmarks: function (option) {
this.showRemovePageBookmarksPrompt = false
if (option !== 'delete') { return }
this.removeAllPageBookmarks()
showToast(this.$t('Settings.Privacy Settings.All page bookmarks have been removed'))
},
...mapActions([
'updateRememberHistory',
'removeAllHistory',
@ -128,6 +140,7 @@ export default defineComponent({
'updateAllSubscriptionsList',
'updateProfileSubscriptions',
'removeAllPlaylists',
'removeAllPageBookmarks',
'updateQuickBookmarkTargetPlaylistId',
])
}

View File

@ -60,6 +60,13 @@
:icon="['fas', 'trash']"
@click="showRemovePlaylistsPrompt = true"
/>
<ft-button
:label="$t('Settings.Privacy Settings.Remove All Page Bookmarks')"
text-color="var(--destructive-text-color)"
background-color="var(--destructive-color)"
:icon="['fas', 'trash']"
@click="showRemovePageBookmarksPrompt = true"
/>
</ft-flex-box>
<ft-prompt
v-if="showSearchCachePrompt"
@ -93,6 +100,14 @@
:is-first-option-destructive="true"
@click="handleRemovePlaylists"
/>
<ft-prompt
v-if="showRemovePageBookmarksPrompt"
:label="removePageBookmarksPromptMessage"
:option-names="promptNames"
:option-values="promptValues"
:is-first-option-destructive="true"
@click="handleRemovePageBookmarks"
/>
</ft-settings-section>
</template>

View File

@ -16,6 +16,12 @@ export default defineComponent({
FtInput,
FtProfileSelector
},
props: {
pageBookmarksAvailable: {
type: Boolean,
default: false
}
},
data: () => {
let isArrowBackwardDisabled = true
let isArrowForwardDisabled = true
@ -28,10 +34,20 @@ export default defineComponent({
}
return {
isRouteBookmarkable: false,
showSearchContainer: true,
isArrowBackwardDisabled,
isArrowForwardDisabled,
currentRouteFullPath: '',
searchSuggestionsDataList: [],
allowedPageBookmarkRouteMetaTitles: [
'Search Results',
'Playlist',
'Channel',
'Watch',
'Hashtag',
'Settings' // for linkable settings sections
],
lastSuggestionQuery: ''
}
},
@ -96,14 +112,33 @@ export default defineComponent({
newWindowText: function () {
return this.$t('Open New Window')
},
isPageBookmarked: function () {
return this.pageBookmarksAvailable && this.$store.getters.getPageBookmarkWithRoute(this.currentRouteFullPath) != null
},
matchingBookmarksDataList: function () {
return this.$store.getters.getPageBookmarksMatchingQuery(this.lastSuggestionQuery, this.currentRouteFullPath)
},
pageBookmarkIconTitle: function () {
return this.isPageBookmarked ? this.$t('Edit bookmark for this page') : this.$t('Bookmark this page')
},
pageBookmarkIconTheme: function () {
return this.isPageBookmarked ? 'base favorite' : 'base'
}
},
watch: {
$route: function () {
$route: function (to, from) {
if ('navigation' in window) {
this.isArrowForwardDisabled = !window.navigation.canGoForward
this.isArrowBackwardDisabled = !window.navigation.canGoBack
}
this.currentRouteFullPath = to.fullPath
this.isRouteBookmarkable = this.allowedPageBookmarkRouteMetaTitles.includes(to.meta.title)
}
},
mounted: function () {
@ -111,7 +146,7 @@ export default defineComponent({
if (window.innerWidth <= MOBILE_WIDTH_THRESHOLD) {
this.showSearchContainer = false
}
this.currentRouteFullPath = this.$router.currentRoute.fullPath
// Store is not up-to-date when the component mounts, so we use timeout.
setTimeout(() => {
if (this.expandSideBar) {
@ -143,6 +178,17 @@ export default defineComponent({
clearLocalSearchSuggestionsSession()
if (queryText.startsWith('ft:')) {
this.$refs.searchInput.handleClearTextClick()
const adjustedQuery = queryText.substring(3)
openInternalPath({
path: adjustedQuery,
adjustedQuery,
doCreateNewWindow
})
return
}
this.getYoutubeUrlInfo(queryText).then((result) => {
switch (result.urlType) {
case 'video': {
@ -264,12 +310,15 @@ export default defineComponent({
},
getSearchSuggestionsDebounce: function (query) {
const trimmedQuery = query.trim()
if (trimmedQuery === this.lastSuggestionQuery) {
return
}
this.lastSuggestionQuery = trimmedQuery
if (this.enableSearchSuggestions) {
const trimmedQuery = query.trim()
if (trimmedQuery !== this.lastSuggestionQuery) {
this.lastSuggestionQuery = trimmedQuery
this.debounceSearchResults(trimmedQuery)
}
this.debounceSearchResults(trimmedQuery)
}
},
@ -354,6 +403,7 @@ export default defineComponent({
},
...mapActions([
'getYoutubeUrlInfo',
'showPageBookmarkPrompt',
'showSearchFilters'
])
}

View File

@ -21,9 +21,9 @@
inline-size: 100%;
z-index: 4;
@media only screen and (width >= 961px) {
@media only screen and (width >= 1162px) {
display: grid;
grid-template-columns: 1fr 440px 1fr;
grid-template-columns: 1fr 720px 0.5fr 0.5fr;
}
@include top-nav-is-colored {
@ -60,7 +60,8 @@
}
&.arrowBackwardDisabled,
&.arrowForwardDisabled {
&.arrowForwardDisabled,
&.disabled {
color: #808080;
opacity: 0.5;
pointer-events: none;
@ -104,6 +105,16 @@
}
}
.pageBookmarkIcon {
&.favorite {
color: var(--favorite-icon-color);
@include top-nav-is-colored {
color: var(--accent-color);
}
}
}
.side {
align-items: center;
display: flex;

View File

@ -84,7 +84,7 @@
:placeholder="$t('Search / Go to URL')"
class="searchInput"
:is-search="true"
:data-list="searchSuggestionsDataList"
:data-list="[...matchingBookmarksDataList, ...searchSuggestionsDataList]"
:spellcheck="false"
:show-clear-text-button="true"
@input="getSearchSuggestionsDebounce"
@ -102,6 +102,18 @@
/>
</div>
</div>
<font-awesome-icon
class="pageBookmarkIcon navIcon"
:icon="['fas', 'star']"
:title="pageBookmarkIconTitle"
:active="isPageBookmarked"
:class="{ [pageBookmarkIconTheme]: true, disabled: !pageBookmarksAvailable || !isRouteBookmarkable }"
:aria-disabled="!pageBookmarksAvailable || !isRouteBookmarkable"
role="button"
tabindex="0"
@click="showPageBookmarkPrompt"
@keydown.enter.prevent="showPageBookmarkPrompt"
/>
<ft-profile-selector class="side profiles" />
</div>
</template>

View File

@ -1,4 +1,5 @@
import i18n from '../i18n/index'
import packageDetails from '../../../package.json'
/**
* This will return true if a string is null, undefined or empty.
@ -58,6 +59,55 @@ export function translateWindowTitle(title) {
}
}
export function getIconForRoute(route) {
const routeSlashIndex = route.indexOf('/', 2)
const truncatedRoute = (routeSlashIndex === -1) ? route : route.substring(0, routeSlashIndex)
switch (truncatedRoute) {
case '/subscriptions':
return ['fas', 'rss']
case '/subscribedchannels':
case '/channel':
return ['fas', 'list']
case '/trending':
return ['fas', 'fire']
case '/popular':
return ['fas', 'users']
case '/userplaylists':
return ['fas', 'bookmark']
case '/history':
return ['fas', 'history']
case '/settings':
return ['fas', 'sliders-h']
case '/about':
return ['fas', 'info-circle']
case '/search':
case '/hashtag':
return ['fas', 'magnifying-glass']
case '/playlist': {
const solidOrRegular = route.includes('?playlistType=user') ? 'fas' : 'far'
return [solidOrRegular, 'bookmark']
} case '/watch':
return ['fas', 'play']
default:
return null
}
}
/**
* Returns an appropriate default bookmark name
* for a given route.
* @param {import('vue-router').Route} route
* @returns {string}
*/
export function defaultBookmarkNameForRoute(route) {
if (route.meta.title === 'Search Results') {
// Use the inputted search query over 'Search Results'
return route.params.query
}
// Remove unnecessary " - FreeTube" appendage
return document.title.replace(` - ${packageDetails.productName}`, '')
}
/**
* Returns the first user-perceived character,
* respecting language specific rules and

View File

@ -93,6 +93,7 @@ import {
faSortAlphaDown,
faSortAlphaDownAlt,
faSortDown,
faStar,
faStepBackward,
faStepForward,
faSync,
@ -206,6 +207,7 @@ library.add(
faSortAlphaDown,
faSortAlphaDownAlt,
faSortDown,
faStar,
faStepBackward,
faStepForward,
faSync,

View File

@ -8,6 +8,7 @@ import invidious from './invidious'
import playlists from './playlists'
import profiles from './profiles'
import settings from './settings'
import searchHistory from './search-history'
import subscriptionCache from './subscription-cache'
import utils from './utils'
import player from './player'
@ -18,6 +19,7 @@ export default {
playlists,
profiles,
settings,
searchHistory,
subscriptionCache,
utils,
player,

View File

@ -369,8 +369,10 @@ const actions = {
}
},
async removeAllPlaylists({ commit }) {
async removeAllPlaylists({ commit, dispatch, state }) {
try {
const playlistIds = state.playlists.map((playlist) => playlist._id)
dispatch('removeUserPlaylistPageBookmarks', playlistIds, { root: true })
await DBPlaylistHandlers.deleteAll()
commit('removeAllPlaylists')
} catch (errMessage) {
@ -387,8 +389,9 @@ const actions = {
}
},
async removePlaylist({ commit }, playlistId) {
async removePlaylist({ commit, dispatch }, playlistId) {
try {
dispatch('removeUserPlaylistPageBookmarks', [playlistId], { root: true })
await DBPlaylistHandlers.delete(playlistId)
commit('removePlaylist', playlistId)
} catch (errMessage) {
@ -396,8 +399,9 @@ const actions = {
}
},
async removePlaylists({ commit }, playlistIds) {
async removePlaylists({ commit, dispatch }, playlistIds) {
try {
dispatch('removeUserPlaylistPageBookmarks', playlistIds, { root: true })
await DBPlaylistHandlers.deleteMultiple(playlistIds)
commit('removePlaylists', playlistIds)
} catch (errMessage) {

View File

@ -0,0 +1,151 @@
import { DBSearchHistoryHandlers } from '../../../datastores/handlers/index'
const state = {
pageBookmarks: []
}
const getters = {
getPageBookmarks: (state) => {
return state.pageBookmarks
},
getPageBookmarkWithRoute: (state) => (route) => {
const pageBookmark = state.pageBookmarks.find(p => p.route === route)
return pageBookmark
},
getPageBookmarksMatchingQuery: (state) => (query, routeToExclude) => {
if (query === '') {
return []
}
const queryToLower = query.toLowerCase()
return state.pageBookmarks.filter((pageBookmark) =>
pageBookmark.name.toLowerCase().includes(queryToLower) && pageBookmark.route !== routeToExclude
)
},
getPageBookmarkIdsForMatchingUserPlaylistIds: (state) => (playlistIds) => {
const pageBookmarkIds = []
const allPageBookmarks = state.pageBookmarks
const pageBookmarkLimitedRoutesMap = new Map()
allPageBookmarks.forEach((pageBookmark) => {
pageBookmarkLimitedRoutesMap.set(pageBookmark.route, pageBookmark._id)
})
playlistIds.forEach((playlistId) => {
const route = `/playlist/${playlistId}?playlistType=user&searchQueryText=`
if (!pageBookmarkLimitedRoutesMap.has(route)) {
return
}
pageBookmarkIds.push(pageBookmarkLimitedRoutesMap.get(route))
})
return pageBookmarkIds
}
}
const actions = {
async grabPageBookmarks({ commit }) {
try {
const results = await DBSearchHistoryHandlers.find()
commit('setPageBookmarks', results)
} catch (errMessage) {
console.error(errMessage)
}
},
async createPageBookmark({ commit }, pageBookmark) {
try {
const newPageBookmark = await DBSearchHistoryHandlers.create(pageBookmark)
commit('addPageBookmarkToList', newPageBookmark)
} catch (errMessage) {
console.error(errMessage)
}
},
async updatePageBookmark({ commit }, pageBookmark) {
try {
await DBSearchHistoryHandlers.upsert(pageBookmark)
commit('upsertPageBookmarkToList', pageBookmark)
} catch (errMessage) {
console.error(errMessage)
}
},
async removePageBookmark({ commit }, _id) {
try {
await DBSearchHistoryHandlers.delete(_id)
commit('removePageBookmarkFromList', _id)
} catch (errMessage) {
console.error(errMessage)
}
},
async removePageBookmarks({ commit }, ids) {
try {
await DBSearchHistoryHandlers.deleteMultiple(ids)
commit('removePageBookmarksFromList', ids)
} catch (errMessage) {
console.error(errMessage)
}
},
async removeUserPlaylistPageBookmarks({ dispatch, getters }, userPlaylistIds) {
const pageBookmarkIds = getters.getPageBookmarkIdsForMatchingUserPlaylistIds(userPlaylistIds)
if (pageBookmarkIds.length === 0) {
return
}
dispatch('removePageBookmarks', pageBookmarkIds)
},
async removeAllPageBookmarks({ commit }) {
try {
await DBSearchHistoryHandlers.deleteAll()
commit('setPageBookmarks', [])
} catch (errMessage) {
console.error(errMessage)
}
},
}
const mutations = {
addPageBookmarkToList(state, pageBookmark) {
state.pageBookmarks.push(pageBookmark)
},
setPageBookmarks(state, pageBookmarks) {
state.pageBookmarks = pageBookmarks
},
upsertPageBookmarkToList(state, updatedPageBookmark) {
const i = state.pageBookmarks.findIndex((p) => {
return p.route === updatedPageBookmark.route
})
if (i === -1) {
state.pageBookmarks.push(updatedPageBookmark)
} else {
state.pageBookmarks.splice(i, 1, updatedPageBookmark)
}
},
removePageBookmarkFromList(state, _id) {
const i = state.pageBookmarks.findIndex((pageBookmark) => {
return pageBookmark._id === _id
})
state.pageBookmarks.splice(i, 1)
},
removePageBookmarksFromList(state, ids) {
state.pageBookmarks = state.pageBookmarks.filter((pageBookmark) => !ids.includes(pageBookmark._id))
}
}
export default {
state,
getters,
actions,
mutations
}

View File

@ -507,6 +507,29 @@ const customActions = {
}
})
ipcRenderer.on(IpcChannels.SYNC_SEARCH_HISTORY, (_, { event, data }) => {
switch (event) {
case SyncEvents.GENERAL.CREATE:
commit('addPageBookmarkToList', data)
break
case SyncEvents.GENERAL.UPSERT:
commit('upsertPageBookmarkToList', data)
break
case SyncEvents.GENERAL.DELETE:
commit('removePageBookmarkFromList', data)
break
case SyncEvents.GENERAL.DELETE_MULTIPLE:
commit('removePageBookmarksFromList', data)
break
default:
console.error('search history: invalid sync event received')
}
})
ipcRenderer.on(IpcChannels.SYNC_PROFILES, (_, { event, data }) => {
switch (event) {
case SyncEvents.GENERAL.CREATE:

View File

@ -33,6 +33,7 @@ const state = {
showProgressBar: false,
showAddToPlaylistPrompt: false,
showCreatePlaylistPrompt: false,
showPageBookmarkPrompt: false,
showSearchFilters: false,
searchFilterValueChanged: false,
progressBarPercentage: 0,
@ -107,6 +108,10 @@ const getters = {
return state.showCreatePlaylistPrompt
},
getShowPageBookmarkPrompt(state) {
return state.showPageBookmarkPrompt
},
getShowSearchFilters(state) {
return state.showSearchFilters
},
@ -384,6 +389,14 @@ const actions = {
commit('setShowCreatePlaylistPrompt', false)
},
showPageBookmarkPrompt ({ commit }) {
commit('setShowPageBookmarkPrompt', true)
},
hidePageBookmarkPrompt ({ commit }) {
commit('setShowPageBookmarkPrompt', false)
},
showSearchFilters ({ commit }) {
commit('setShowSearchFilters', true)
},
@ -861,6 +874,10 @@ const mutations = {
state.showCreatePlaylistPrompt = payload
},
setShowPageBookmarkPrompt (state, payload) {
state.showPageBookmarkPrompt = payload
},
setShowSearchFilters (state, payload) {
state.showSearchFilters = payload
},

View File

@ -5,6 +5,7 @@ body {
background-color: var(--bg-color);
--primary-input-color: rgb(0 0 0 / 50%);
--side-nav-hover-text-color: var(--primary-text-color);
--link-color: var(--accent-color);
--link-visited-color: var(--accent-color-visited);
--instance-menu-color: var(--search-bar-color);

View File

@ -686,20 +686,23 @@ Channel:
Shorts:
This channel does not currently have any shorts: ''
Live:
Live: ''
This channel does not currently have any live streams: ''
Live: 'Regstreeks'
This channel does not currently have any live streams: 'Hierdie kanaal het tans
geen rekstreekse stromings nie'
Playlists:
Playlists: ''
This channel does not currently have any playlists: ''
Playlists: 'Afspeellyste'
This channel does not currently have any playlists: 'Hierdie kanaal het tans geen
afspeellyste nie'
Sort Types:
Last Video Added: ''
Newest: ''
Oldest: ''
Last Video Added: 'Laaste toegevoegde video'
Newest: 'Nuutste'
Oldest: 'Oudste'
Podcasts:
Podcasts: ''
This channel does not currently have any podcasts: ''
Podcasts: 'Podsendings'
This channel does not currently have any podcasts: 'Hierdie kanaal het tans geen
podsendings nie'
Releases:
Releases: ''
Releases: 'Vrystellings'
This channel does not currently have any releases: 'Hierdie kanaal het nie tans
enige vrystellings nie'
About:

View File

@ -32,6 +32,10 @@ Forward: Forward
Open New Window: Open New Window
Go to page: Go to {page}
Close Banner: Close Banner
Bookmark this page: Bookmark this page
Edit bookmark for this page: Edit bookmark for this page
Save: Save
Name: Name
Version {versionNumber} is now available! Click for more details: Version {versionNumber} is now available! Click
for more details
@ -469,6 +473,10 @@ Settings:
Remove All Playlists: Remove All Playlists
All playlists have been removed: All playlists have been removed
Are you sure you want to remove all your playlists?: Are you sure you want to remove all your playlists?
Remove All Page Bookmarks: Remove All Page Bookmarks
Are you sure you want to remove all page bookmarks? This cannot be undone.: Are
you sure you want to remove all page bookmarks? This cannot be undone.
All page bookmarks have been removed: All page bookmarks have been removed
Subscription Settings:
Subscription Settings: Subscription
Hide Videos on Watch: Hide Videos on Watch
@ -1080,6 +1088,18 @@ Hashtag:
Hashtag: Hashtag
This hashtag does not currently have any videos: This hashtag does not currently
have any videos
Page Bookmark:
Bookmark this page: Bookmark this page
Create Bookmark: Create Bookmark
Edit Bookmark: Edit Bookmark
Remove Bookmark: Remove Bookmark
Created page bookmark: Created page bookmark "{name}"
Updated page bookmark: Updated page bookmark "{name}"
Removed page bookmark: Removed page bookmark "{name}"
There is {count} other bookmark with the same name.: There is {count} other page bookmark with the same name. | There are {count} other page bookmarks with the same name.
This page cannot be bookmarked.: This page cannot be bookmarked.
Role Descriptions:
bookmark: bookmark
Moments Ago: moments ago
Yes: Yes
No: No