Bookmark folders

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-09-23 23:10:32 +02:00
commit 9e45228823
28 changed files with 464 additions and 31 deletions

View file

@ -0,0 +1,16 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faEllipsisH
)
const BookmarkFolderCard = {
props: [
'folder'
]
}
export default BookmarkFolderCard

View file

@ -0,0 +1,38 @@
<template>
<div class="bookmark-folder-card">
<router-link
:to="{ name: 'bookmark-folder', params: { id: folder.id } }"
class="bookmark-folder-name"
>
{{ folder.name }}
</router-link>
<router-link
:to="{ name: 'bookmark-folder' /* -edit' */, params: { id: folder.id } }"
class="button-folder-edit"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="ellipsis-h"
/>
</router-link>
</div>
</template>
<script src="./bookmark_folder_card.js"></script>
<style lang="scss">
.bookmark-folder-card {
display: flex;
}
.bookmark-folder-name {
flex-grow: 1;
}
.bookmark-folder-name,
.button-folder-edit {
margin: 0;
padding: 1em;
color: var(--link);
}
</style>

View file

@ -0,0 +1,27 @@
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
const BookmarkFolders = {
data () {
return {
isNew: false
}
},
components: {
BookmarkFolderCard
},
computed: {
bookmarkFolders () {
return this.$store.state.bookmarkFolders.allFolders
}
},
methods: {
cancelNewFolder () {
this.isNew = false
},
newFolder () {
this.isNew = true
}
}
}
export default BookmarkFolders

View file

@ -0,0 +1,33 @@
<template>
<div class="Bookmark-folders panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('nav.bookmark_folders') }}
</div>
<router-link
:to="{ name: 'bookmark-folders' /* 'new' */ }"
class="button-default btn new-folder-button"
>
{{ $t("bookmark_folders.new") }}
</router-link>
</div>
<div class="panel-body">
<BookmarkFolderCard
v-for="folder in bookmarkFolders.slice().reverse()"
:key="folder"
:folder="folder"
class="bookmark-folder-item"
/>
</div>
</div>
</template>
<script src="./bookmark_folders.js"></script>
<style lang="scss">
.Bookmark-folders {
.new-folder-button {
padding: 0 0.5em;
}
}
</style>

View file

@ -0,0 +1,16 @@
import { mapState } from 'vuex'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
export const BookmarkFoldersMenuContent = {
components: {
NavigationEntry
},
computed: {
...mapState({
folders: getBookmarkFolderEntries
})
}
}
export default BookmarkFoldersMenuContent

View file

@ -0,0 +1,19 @@
<template>
<ul>
<NavigationEntry
:item="{
name: 'bookmarks',
routeObject: { name: 'bookmarks' },
label: 'nav.all_bookmarks',
icon: 'bookmark'
}"
/>
<NavigationEntry
v-for="item in folders"
:key="item.id"
:item="item"
/>
</ul>
</template>
<script src="./bookmark_folders_menu_content.js"></script>

View file

@ -1,16 +1,31 @@
import Timeline from '../timeline/timeline.vue'
const Bookmarks = {
computed: {
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
created () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
},
components: {
Timeline
},
computed: {
folderId () {
return this.$route.params.id
},
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
},
watch: {
folderId () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
}
},
unmounted () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
}
}

View file

@ -3,6 +3,7 @@
:title="$t('nav.bookmarks')"
:timeline="timeline"
:timeline-name="'bookmarks'"
:bookmark-folder-id="bookmarkFolderId"
/>
</template>

View file

@ -1,3 +1,4 @@
import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue'
import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
import { mapState, mapGetters } from 'vuex'
import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js'
@ -41,6 +42,7 @@ const NavPanel = {
created () {
},
components: {
BookmarkFoldersMenuContent,
ListsMenuContent,
NavigationEntry,
NavigationPins,
@ -51,6 +53,7 @@ const NavPanel = {
editMode: false,
showTimelines: false,
showLists: false,
showBookmarkFolders: false,
timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })),
rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k }))
}
@ -62,6 +65,9 @@ const NavPanel = {
toggleLists () {
this.showLists = !this.showLists
},
toggleBookmarkFolders () {
this.showBookmarkFolders = !this.showBookmarkFolders
},
toggleEditMode () {
this.editMode = !this.editMode
},
@ -90,7 +96,8 @@ const NavPanel = {
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
supportsAnnouncements: state => state.announcements.supportsAnnouncements,
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav,
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
}),
timelinesItems () {
return filterNavigation(
@ -102,7 +109,8 @@ const NavPanel = {
hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
currentUser: this.currentUser,
supportsBookmarkFolders: this.bookmarkFolders
}
)
},
@ -116,7 +124,8 @@ const NavPanel = {
hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
currentUser: this.currentUser,
supportsBookmarkFolders: this.bookmarkFolders
}
)
},

View file

@ -83,6 +83,39 @@
class="timelines"
/>
</div>
<NavigationEntry
v-if="currentUser && bookmarkFolders"
:show-pin="false"
:item="{ icon: 'bookmark', label: 'nav.bookmarks' }"
:aria-expanded="showBookmarkFolders ? 'true' : 'false'"
@click="toggleBookmarkFolders"
>
<router-link
:title="$t('bookmarks.manage_bookmark_folders')"
class="button-unstyled extra-button"
:to="{ name: 'bookmark-folders' }"
@click.stop
>
<FAIcon
fixed-width
icon="wrench"
/>
</router-link>
<FAIcon
class="timelines-chevron"
fixed-width
:icon="showBookmarkFolders ? 'chevron-up' : 'chevron-down'"
/>
</NavigationEntry>
<div
v-show="showBookmarkFolders"
class="timelines-background menu-item-collapsible"
:class="{ '-expanded': showBookmarkFolders }"
>
<BookmarkFoldersMenuContent
class="timelines"
/>
</div>
<NavigationEntry
v-for="item in rootItems"
:key="item.name"

View file

@ -1,4 +1,4 @@
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser }) => {
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser, supportsBookmarkFolders }) => {
return list.filter(({ criteria, anon, anonRoute }) => {
const set = new Set(criteria || [])
if (!isFederating && set.has('federating')) return false
@ -7,6 +7,7 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
if (!hasChats && set.has('chats')) return false
if (!hasAnnouncements && set.has('announcements')) return false
if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false
return true
})
}
@ -17,3 +18,12 @@ export const getListEntries = state => state.lists.allLists.map(list => ({
labelRaw: list.title,
iconLetter: list.title[0]
}))
export const getBookmarkFolderEntries = state => state.bookmarkFolders.allFolders.map(folder => ({
name: 'bookmark-folder-' + folder.id,
routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
labelRaw: folder.name,
iconEmoji: folder.emoji,
iconEmojiUrl: folder.emoji_url,
iconLetter: folder.name[0]
}))

View file

@ -32,7 +32,8 @@ export const TIMELINES = {
bookmarks: {
route: 'bookmarks',
icon: 'bookmark',
label: 'nav.bookmarks'
label: 'nav.bookmarks',
criteria: ['!supportsBookmarkFolders']
},
favorites: {
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },

View file

@ -22,11 +22,25 @@
:icon="item.icon"
/>
</span>
<img
v-if="item.iconEmojiUrl"
class="menu-icon iconEmoji iconEmoji-image"
:src="item.iconEmojiUrl"
:alt="item.iconEmoji"
:title="item.iconEmoji"
>
<span
v-if="item.iconLetter"
class="icon iconLetter fa-scale-110 menu-icon"
>{{ item.iconLetter }}
v-else-if="item.iconEmoji"
class="menu-icon iconEmoji"
>
<span>
{{ item.iconEmoji }}
</span>
</span>
<span
v-else-if="item.iconLetter"
class="icon iconLetter fa-scale-110 menu-icon"
>{{ item.iconLetter }}</span>
<span class="label">
{{ item.labelRaw || $t(item.label) }}
</span>
@ -110,5 +124,18 @@
.badge {
margin: 0 var(--__horizontal-gap);
}
.iconEmoji {
display: inline-block;
text-align: center;
object-fit: contain;
vertical-align: middle;
height: var(--__line-height);
width: var(--__line-height);
> span {
font-size: 1.5rem;
}
}
}
</style>

View file

@ -26,6 +26,7 @@ const Timeline = {
'userId',
'listId',
'statusId',
'bookmarkFolderId',
'tag',
'embedded',
'count',
@ -123,6 +124,7 @@ const Timeline = {
userId: this.userId,
listId: this.listId,
statusId: this.statusId,
bookmarkFolderId: this.bookmarkFolderId,
tag: this.tag
})
},
@ -186,6 +188,7 @@ const Timeline = {
userId: this.userId,
listId: this.listId,
statusId: this.statusId,
bookmarkFolderId: this.bookmarkFolderId,
tag: this.tag
}).then(({ statuses }) => {
if (statuses && statuses.length === 0) {

View file

@ -2,6 +2,7 @@ import Popover from '../popover/popover.vue'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { mapState } from 'vuex'
import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue'
import { BookmarkFoldersMenuContent } from '../bookmark_folders_menu/bookmark_folders_menu_content.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { TIMELINES } from 'src/components/navigation/navigation.js'
import { filterNavigation } from 'src/components/navigation/filter.js'
@ -13,10 +14,10 @@ library.add(faChevronDown)
// Route -> i18n key mapping, exported and not in the computed
// because nav panel benefits from the same information.
export const timelineNames = () => {
export const timelineNames = (supportsBookmarkFolders) => {
return {
friends: 'nav.home_timeline',
bookmarks: 'nav.bookmarks',
bookmarks: supportsBookmarkFolders ? 'nav.all_bookmarks' : 'nav.bookmarks',
dms: 'nav.dms',
'public-timeline': 'nav.public_tl',
'public-external-timeline': 'nav.twkn',
@ -28,7 +29,8 @@ const TimelineMenu = {
components: {
Popover,
NavigationEntry,
ListsMenuContent
ListsMenuContent,
BookmarkFoldersMenuContent
},
data () {
return {
@ -36,7 +38,7 @@ const TimelineMenu = {
}
},
created () {
if (timelineNames()[this.$route.name]) {
if (timelineNames(this.bookmarkFolders)[this.$route.name]) {
this.$store.dispatch('setLastTimeline', this.$route.name)
}
},
@ -45,10 +47,15 @@ const TimelineMenu = {
const route = this.$route.name
return route === 'lists-timeline'
},
useBookmarkFoldersMenu () {
const route = this.$route.name
return this.bookmarkFolders && (route === 'bookmark-folder' || route === 'bookmarks')
},
...mapState({
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
federating: state => state.instance.federating,
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
}),
timelinesList () {
return filterNavigation(
@ -57,7 +64,8 @@ const TimelineMenu = {
hasChats: this.pleromaChatMessagesAvailable,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
currentUser: this.currentUser,
supportsBookmarkFolders: this.bookmarkFolders
}
)
}
@ -89,7 +97,10 @@ const TimelineMenu = {
if (route === 'lists-timeline') {
return this.$store.getters.findListTitle(this.$route.params.id)
}
const i18nkey = timelineNames()[this.$route.name]
if (route === 'bookmark-folder') {
return this.$store.getters.findBookmarkFolderName(this.$route.params.id)
}
const i18nkey = timelineNames(this.bookmarkFolders)[this.$route.name]
return i18nkey ? this.$t(i18nkey) : route
}
}

View file

@ -15,6 +15,10 @@
:show-pin="false"
class="timelines"
/>
<BookmarkFoldersMenuContent
v-else-if="useBookmarkFoldersMenu"
class="timelines"
/>
<ul v-else>
<NavigationEntry
v-for="item in timelinesList"