Bookmark folders
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
a8092de638
commit
9e45228823
28 changed files with 464 additions and 31 deletions
16
src/components/bookmark_folder_card/bookmark_folder_card.js
Normal file
16
src/components/bookmark_folder_card/bookmark_folder_card.js
Normal 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
|
||||
38
src/components/bookmark_folder_card/bookmark_folder_card.vue
Normal file
38
src/components/bookmark_folder_card/bookmark_folder_card.vue
Normal 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>
|
||||
27
src/components/bookmark_folders/bookmark_folders.js
Normal file
27
src/components/bookmark_folders/bookmark_folders.js
Normal 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
|
||||
33
src/components/bookmark_folders/bookmark_folders.vue
Normal file
33
src/components/bookmark_folders/bookmark_folders.vue
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
:title="$t('nav.bookmarks')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'bookmarks'"
|
||||
:bookmark-folder-id="bookmarkFolderId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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' } },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue