mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 19:29:15 +00:00
Add date filter to timeline + fix #302
This commit is contained in:
22
api/graphql/models/actions/media_actions.go
Normal file
22
api/graphql/models/actions/media_actions.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MyMedia(db *gorm.DB, user *models.User, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
|
||||
if err := user.FillAlbums(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := db.Where("media.album_id IN (SELECT user_albums.album_id FROM user_albums WHERE user_albums.user_id = ?)", user.ID)
|
||||
query = models.FormatSQL(query, order, paginate)
|
||||
|
||||
var media []*models.Media
|
||||
if err := query.Find(&media).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return media, nil
|
||||
}
|
||||
88
api/graphql/models/actions/media_actions_test.go
Normal file
88
api/graphql/models/actions/media_actions_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMyMedia(t *testing.T) {
|
||||
db := test_utils.DatabaseTest(t)
|
||||
|
||||
password := "1234"
|
||||
user, err := models.RegisterUser(db, "user", &password, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rootAlbum := models.Album{
|
||||
Title: "root",
|
||||
Path: "/photos",
|
||||
}
|
||||
|
||||
assert.NoError(t, db.Save(&rootAlbum).Error)
|
||||
|
||||
childAlbum := models.Album{
|
||||
Title: "subalbum",
|
||||
Path: "/photos/subalbum",
|
||||
ParentAlbumID: &rootAlbum.ID,
|
||||
}
|
||||
|
||||
assert.NoError(t, db.Save(&childAlbum).Error)
|
||||
|
||||
assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum))
|
||||
assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum))
|
||||
|
||||
media := []models.Media{
|
||||
{
|
||||
Title: "pic1",
|
||||
Path: "/photos/pic1",
|
||||
AlbumID: rootAlbum.ID,
|
||||
},
|
||||
{
|
||||
Title: "pic2",
|
||||
Path: "/photos/pic2",
|
||||
AlbumID: rootAlbum.ID,
|
||||
},
|
||||
{
|
||||
Title: "pic3",
|
||||
Path: "/photos/subalbum/pic3",
|
||||
AlbumID: childAlbum.ID,
|
||||
},
|
||||
{
|
||||
Title: "pic4",
|
||||
Path: "/photos/subalbum/pic4",
|
||||
AlbumID: childAlbum.ID,
|
||||
},
|
||||
}
|
||||
|
||||
assert.NoError(t, db.Save(&media).Error)
|
||||
|
||||
anotherUser, err := models.RegisterUser(db, "user2", &password, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
anotherAlbum := models.Album{
|
||||
Title: "AnotherAlbum",
|
||||
Path: "/another",
|
||||
}
|
||||
|
||||
assert.NoError(t, db.Save(&anotherAlbum).Error)
|
||||
|
||||
anotherMedia := models.Media{
|
||||
Title: "anotherPic",
|
||||
Path: "/another/anotherPic",
|
||||
AlbumID: anotherAlbum.ID,
|
||||
}
|
||||
|
||||
assert.NoError(t, db.Save(&anotherMedia).Error)
|
||||
|
||||
assert.NoError(t, db.Model(&anotherUser).Association("Albums").Append(&anotherAlbum))
|
||||
|
||||
t.Run("Simple query", func(t *testing.T) {
|
||||
myMedia, err := actions.MyMedia(db, user, nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, myMedia, 4)
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
api "github.com/photoview/photoview/api/graphql"
|
||||
"github.com/photoview/photoview/api/graphql/auth"
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||
"github.com/photoview/photoview/api/scanner/face_detection"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -19,29 +20,7 @@ func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, pag
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
if err := user.FillAlbums(r.Database); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userAlbumIDs := make([]int, len(user.Albums))
|
||||
for i, album := range user.Albums {
|
||||
userAlbumIDs[i] = album.ID
|
||||
}
|
||||
|
||||
var media []*models.Media
|
||||
|
||||
query := r.Database.
|
||||
Joins("Album").
|
||||
Where("albums.id IN (?)", userAlbumIDs).
|
||||
Where("media.id IN (?)", r.Database.Model(&models.MediaURL{}).Select("id").Where("media_url.media_id = media.id"))
|
||||
|
||||
query = models.FormatSQL(query, order, paginate)
|
||||
|
||||
if err := query.Find(&media).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return media, nil
|
||||
return actions.MyMedia(r.Database, user, order, paginate)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) {
|
||||
|
||||
@@ -26,6 +26,10 @@ func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Paginat
|
||||
query = query.Where("media.date_shot < ?", fromDate)
|
||||
}
|
||||
|
||||
if onlyFavorites != nil && *onlyFavorites == true {
|
||||
query = query.Where("media.id IN (?)", r.Database.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
|
||||
}
|
||||
|
||||
query = models.FormatSQL(query, nil, paginate)
|
||||
|
||||
var media []*models.Media
|
||||
@@ -35,127 +39,3 @@ func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Paginat
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
// func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool) ([]*models.TimelineGroup, error) {
|
||||
// user := auth.UserFromContext(ctx)
|
||||
// if user == nil {
|
||||
// return nil, auth.ErrUnauthorized
|
||||
// }
|
||||
|
||||
// var timelineGroups []*models.TimelineGroup
|
||||
|
||||
// transactionError := r.Database.Transaction(func(tx *gorm.DB) error {
|
||||
// // album_id, year, month, day
|
||||
// daysQuery := tx.Select(
|
||||
// "albums.id AS album_id",
|
||||
// fmt.Sprintf("%s AS year", database.DateExtract(tx, database.DateCompYear, "media.date_shot")),
|
||||
// fmt.Sprintf("%s AS month", database.DateExtract(tx, database.DateCompMonth, "media.date_shot")),
|
||||
// fmt.Sprintf("%s AS day", database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
|
||||
// ).
|
||||
// Table("media").
|
||||
// Joins("JOIN albums ON media.album_id = albums.id").
|
||||
// Where("albums.id IN (?)", tx.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID))
|
||||
|
||||
// if onlyFavorites != nil && *onlyFavorites == true {
|
||||
// daysQuery.Where("media.id IN (?)", tx.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
|
||||
// }
|
||||
|
||||
// if paginate != nil {
|
||||
// if paginate.Limit != nil {
|
||||
// daysQuery.Limit(*paginate.Limit)
|
||||
// }
|
||||
|
||||
// if paginate.Offset != nil {
|
||||
// daysQuery.Offset(*paginate.Offset)
|
||||
// }
|
||||
// }
|
||||
|
||||
// rows, err := daysQuery.Group("albums.id").Group(
|
||||
// fmt.Sprintf("%s, %s, %s",
|
||||
// database.DateExtract(tx, database.DateCompYear, "media.date_shot"),
|
||||
// database.DateExtract(tx, database.DateCompMonth, "media.date_shot"),
|
||||
// database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
|
||||
// ).
|
||||
// Order(
|
||||
// fmt.Sprintf("%s DESC, %s DESC, %s DESC",
|
||||
// database.DateExtract(tx, database.DateCompYear, "media.date_shot"),
|
||||
// database.DateExtract(tx, database.DateCompMonth, "media.date_shot"),
|
||||
// database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
|
||||
// ).Rows()
|
||||
|
||||
// defer rows.Close()
|
||||
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// type group struct {
|
||||
// albumID int
|
||||
// year int
|
||||
// month int
|
||||
// day int
|
||||
// }
|
||||
|
||||
// dbGroups := make([]group, 0)
|
||||
|
||||
// for rows.Next() {
|
||||
// var g group
|
||||
// rows.Scan(&g.albumID, &g.year, &g.month, &g.day)
|
||||
// dbGroups = append(dbGroups, g)
|
||||
// }
|
||||
|
||||
// timelineGroups = make([]*models.TimelineGroup, len(dbGroups))
|
||||
|
||||
// for i, group := range dbGroups {
|
||||
|
||||
// // Fill album
|
||||
// var groupAlbum models.Album
|
||||
// if err := tx.First(&groupAlbum, group.albumID).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // Fill media
|
||||
// var groupMedia []*models.Media
|
||||
// mediaQuery := tx.Model(&models.Media{}).
|
||||
// Where("album_id = ?", group.albumID).
|
||||
// Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompYear, "media.date_shot")), group.year).
|
||||
// Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompMonth, "media.date_shot")), group.month).
|
||||
// Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompDay, "media.date_shot")), group.day).
|
||||
// Order("date_shot DESC")
|
||||
|
||||
// if onlyFavorites != nil && *onlyFavorites == true {
|
||||
// mediaQuery.Where("media.id IN (?)", tx.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
|
||||
// }
|
||||
|
||||
// if err := mediaQuery.Limit(5).Find(&groupMedia).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // Get total media count
|
||||
// var totalMedia int64
|
||||
// if err := mediaQuery.Count(&totalMedia).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// var date time.Time = groupMedia[0].DateShot
|
||||
// date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
|
||||
// timelineGroup := models.TimelineGroup{
|
||||
// Album: &groupAlbum,
|
||||
// Media: groupMedia,
|
||||
// MediaTotal: int(totalMedia),
|
||||
// Date: date,
|
||||
// }
|
||||
|
||||
// timelineGroups[i] = &timelineGroup
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// })
|
||||
|
||||
// if transactionError != nil {
|
||||
// return nil, transactionError
|
||||
// }
|
||||
|
||||
// return timelineGroups, nil
|
||||
// }
|
||||
|
||||
@@ -3,16 +3,16 @@ import Layout from '../../components/layout/Layout'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TimelineGallery from '../../components/timelineGallery/TimelineGallery'
|
||||
|
||||
const PhotosPage = () => {
|
||||
const TimelinePage = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout title={t('photos_page.title', 'Photos')}>
|
||||
<Layout title={t('photos_page.title', 'Timeline')}>
|
||||
<TimelineGallery />
|
||||
</Layout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PhotosPage
|
||||
export default TimelinePage
|
||||
@@ -10,7 +10,7 @@ import { ReactComponent as DirectionIcon } from './icons/direction-arrow.svg'
|
||||
|
||||
import Dropdown from '../../primitives/form/Dropdown'
|
||||
|
||||
type FavoriteCheckboxProps = {
|
||||
export type FavoriteCheckboxProps = {
|
||||
onlyFavorites: boolean
|
||||
setOnlyFavorites(favorites: boolean): void
|
||||
}
|
||||
@@ -79,7 +79,7 @@ const SortingOptions = ({ setOrdering, ordering }: SortingOptionsProps) => {
|
||||
<fieldset>
|
||||
<legend id="filter_group_sort-label" className="inline-block mb-1">
|
||||
<SortingIcon
|
||||
className="inline-block align-baseline mr-1"
|
||||
className="inline-block align-baseline mr-1 mt-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{t('album_filter.sort', 'Sort')}</span>
|
||||
|
||||
@@ -80,7 +80,7 @@ export const MainMenu = () => {
|
||||
<div className="fixed w-full bottom-0 lg:bottom-auto lg:top-[84px] z-30 bg-white shadow-separator lg:shadow-none lg:w-[240px] lg:ml-8 lg:mr-5 flex-shrink-0">
|
||||
<ul className="flex justify-around py-2 px-2 max-w-lg mx-auto lg:flex-col lg:p-0">
|
||||
<MenuButton
|
||||
to="/photos"
|
||||
to="/timeline"
|
||||
exact
|
||||
label={t('sidemenu.photos', 'Timeline')}
|
||||
background="#8ac5f4"
|
||||
|
||||
@@ -12,7 +12,9 @@ const AlbumsPage = React.lazy(
|
||||
() => import('../../Pages/AllAlbumsPage/AlbumsPage')
|
||||
)
|
||||
const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage'))
|
||||
const PhotosPage = React.lazy(() => import('../../Pages/PhotosPage/PhotosPage'))
|
||||
const TimelinePage = React.lazy(
|
||||
() => import('../../Pages/TimelinePage/TimelinePage')
|
||||
)
|
||||
const PlacesPage = React.lazy(() => import('../../Pages/PlacesPage/PlacesPage'))
|
||||
const SharePage = React.lazy(() => import('../../Pages/SharePage/SharePage'))
|
||||
const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage'))
|
||||
@@ -49,11 +51,17 @@ const Routes = () => {
|
||||
<Route path="/share" component={SharePage} />
|
||||
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
||||
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
|
||||
<AuthorizedRoute path="/photos" component={PhotosPage} />
|
||||
<AuthorizedRoute path="/timeline" component={TimelinePage} />
|
||||
<AuthorizedRoute path="/places" component={PlacesPage} />
|
||||
<AuthorizedRoute path="/people/:person?" component={PeoplePage} />
|
||||
<AuthorizedRoute path="/settings" component={SettingsPage} />
|
||||
<Route path="/" exact render={() => <Redirect to="/photos" />} />
|
||||
<Route path="/" exact render={() => <Redirect to="/timeline" />} />
|
||||
{/* For backwards compatibility */}
|
||||
<Route
|
||||
path="/photos"
|
||||
exact
|
||||
render={() => <Redirect to="/timeline" />}
|
||||
/>
|
||||
<Route
|
||||
render={() => (
|
||||
<div>{t('routes.page_not_found', 'Page not found')}</div>
|
||||
|
||||
103
ui/src/components/timelineGallery/TimelineFilters.tsx
Normal file
103
ui/src/components/timelineGallery/TimelineFilters.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useQuery } from '@apollo/client'
|
||||
import gql from 'graphql-tag'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown'
|
||||
import { FavoriteCheckboxProps, FavoritesCheckbox } from '../album/AlbumFilter'
|
||||
|
||||
import { ReactComponent as DateIcon } from './icons/date.svg'
|
||||
import { earliestMedia } from './__generated__/earliestMedia'
|
||||
|
||||
const EARLIEST_MEDIA_QUERY = gql`
|
||||
query earliestMedia {
|
||||
myMedia(
|
||||
order: { order_by: "date_shot", order_direction: ASC }
|
||||
paginate: { limit: 1 }
|
||||
) {
|
||||
id
|
||||
date
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type DateSelectorProps = {
|
||||
filterDate: string | null
|
||||
setFilterDate(date: string | null): void
|
||||
}
|
||||
|
||||
const DateSelector = ({ filterDate, setFilterDate }: DateSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data, loading } = useQuery<earliestMedia>(EARLIEST_MEDIA_QUERY)
|
||||
|
||||
let items: DropdownItem[] = [
|
||||
{
|
||||
value: 'all',
|
||||
label: t('timeline_filter.date.dropdown_all', 'From today'),
|
||||
},
|
||||
]
|
||||
|
||||
if (data) {
|
||||
const dateStr = data.myMedia[0].date
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
|
||||
const currentYear = now.getFullYear()
|
||||
const earliestYear = date.getFullYear()
|
||||
|
||||
const years: number[] = []
|
||||
for (let i = currentYear - 1; i >= earliestYear; i--) {
|
||||
years.push(i)
|
||||
}
|
||||
|
||||
const yearItems = years.map<DropdownItem>(x => ({
|
||||
value: `${x}`,
|
||||
label: `${x} and earlier`,
|
||||
}))
|
||||
items = [...items, ...yearItems]
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend id="filter_group_date-label" className="inline-block mb-1">
|
||||
<DateIcon
|
||||
className="inline-block align-baseline mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{t('timeline_filter.date.label', 'Date')}</span>
|
||||
</legend>
|
||||
<div>
|
||||
<Dropdown
|
||||
aria-labelledby="filter_group_date-label"
|
||||
setSelected={date =>
|
||||
date == 'all' ? setFilterDate(null) : setFilterDate(date)
|
||||
}
|
||||
value={filterDate || 'all'}
|
||||
items={items}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
type TimelineFiltersProps = DateSelectorProps & FavoriteCheckboxProps
|
||||
|
||||
const TimelineFilters = ({
|
||||
onlyFavorites,
|
||||
setOnlyFavorites,
|
||||
filterDate,
|
||||
setFilterDate,
|
||||
}: TimelineFiltersProps) => {
|
||||
return (
|
||||
<div className="flex items-end gap-4 flex-wrap mb-4">
|
||||
<DateSelector filterDate={filterDate} setFilterDate={setFilterDate} />
|
||||
<FavoritesCheckbox
|
||||
onlyFavorites={onlyFavorites}
|
||||
setOnlyFavorites={setOnlyFavorites}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineFilters
|
||||
@@ -4,7 +4,6 @@ import { useQuery, gql } from '@apollo/client'
|
||||
import TimelineGroupDate from './TimelineGroupDate'
|
||||
import PresentView from '../photoGallery/presentView/PresentView'
|
||||
import useURLParameters from '../../hooks/useURLParameters'
|
||||
import { FavoritesCheckbox } from '../album/AlbumFilter'
|
||||
import useScrollPagination from '../../hooks/useScrollPagination'
|
||||
import PaginateLoader from '../PaginateLoader'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -20,11 +19,19 @@ import {
|
||||
import MediaSidebar from '../sidebar/MediaSidebar'
|
||||
import { SidebarContext } from '../sidebar/Sidebar'
|
||||
import { urlPresentModeSetupHook } from '../photoGallery/photoGalleryReducer'
|
||||
import TimelineFilters from './TimelineFilters'
|
||||
import client from '../../apolloClient'
|
||||
|
||||
const MY_TIMELINE_QUERY = gql`
|
||||
query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) {
|
||||
query myTimeline(
|
||||
$onlyFavorites: Boolean
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
$fromDate: Time
|
||||
) {
|
||||
myTimeline(
|
||||
onlyFavorites: $onlyFavorites
|
||||
fromDate: $fromDate
|
||||
paginate: { limit: $limit, offset: $offset }
|
||||
) {
|
||||
id
|
||||
@@ -77,7 +84,10 @@ const TimelineGallery = () => {
|
||||
|
||||
const onlyFavorites = getParam('favorites') == '1' ? true : false
|
||||
const setOnlyFavorites = (favorites: boolean) =>
|
||||
setParam('favorites', favorites ? '1' : '0')
|
||||
setParam('favorites', favorites ? '1' : null)
|
||||
|
||||
const filterDate = getParam('date')
|
||||
const setFilterDate = (x: string) => setParam('date', x)
|
||||
|
||||
const favoritesNeedsRefresh = useRef(false)
|
||||
|
||||
@@ -97,6 +107,9 @@ const TimelineGallery = () => {
|
||||
>(MY_TIMELINE_QUERY, {
|
||||
variables: {
|
||||
onlyFavorites,
|
||||
fromDate: filterDate
|
||||
? `${parseInt(filterDate) + 1}-01-01T00:00:00Z`
|
||||
: undefined,
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
},
|
||||
@@ -126,6 +139,20 @@ const TimelineGallery = () => {
|
||||
}
|
||||
}, [mediaState.activeIndex])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
await client.resetStore()
|
||||
await refetch({
|
||||
onlyFavorites,
|
||||
fromDate: filterDate
|
||||
? `${parseInt(filterDate) + 1}-01-01T00:00:00Z`
|
||||
: undefined,
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
})
|
||||
})()
|
||||
}, [filterDate])
|
||||
|
||||
urlPresentModeSetupHook({
|
||||
dispatchMedia,
|
||||
openPresentMode: event => {
|
||||
@@ -158,12 +185,12 @@ const TimelineGallery = () => {
|
||||
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<div className="mb-2">
|
||||
<FavoritesCheckbox
|
||||
onlyFavorites={onlyFavorites}
|
||||
setOnlyFavorites={setOnlyFavorites}
|
||||
/>
|
||||
</div>
|
||||
<TimelineFilters
|
||||
onlyFavorites={onlyFavorites}
|
||||
setOnlyFavorites={setOnlyFavorites}
|
||||
filterDate={filterDate}
|
||||
setFilterDate={setFilterDate}
|
||||
/>
|
||||
<div className="-mx-3 flex flex-wrap" ref={containerElem}>
|
||||
{timelineGroups}
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@ const TimelineGroupAlbum = ({
|
||||
<Link to={`/album/${albumID}`} className="hover:underline">
|
||||
{albumTitle}
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center relative -mx-1 pr-4 overflow-hidden">
|
||||
<div className="flex flex-wrap items-center relative -mx-1 overflow-hidden">
|
||||
{mediaElms}
|
||||
<PhotoFiller />
|
||||
</div>
|
||||
|
||||
24
ui/src/components/timelineGallery/__generated__/earliestMedia.ts
generated
Normal file
24
ui/src/components/timelineGallery/__generated__/earliestMedia.ts
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: earliestMedia
|
||||
// ====================================================
|
||||
|
||||
export interface earliestMedia_myMedia {
|
||||
__typename: 'Media'
|
||||
id: string
|
||||
/**
|
||||
* The date the image was shot or the date it was imported as a fallback
|
||||
*/
|
||||
date: any
|
||||
}
|
||||
|
||||
export interface earliestMedia {
|
||||
/**
|
||||
* List of media owned by the logged in user
|
||||
*/
|
||||
myMedia: earliestMedia_myMedia[]
|
||||
}
|
||||
@@ -3,95 +3,96 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MediaType } from "./../../../__generated__/globalTypes";
|
||||
import { MediaType } from './../../../__generated__/globalTypes'
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: myTimeline
|
||||
// ====================================================
|
||||
|
||||
export interface myTimeline_myTimeline_thumbnail {
|
||||
__typename: "MediaURL";
|
||||
__typename: 'MediaURL'
|
||||
/**
|
||||
* URL for previewing the image
|
||||
*/
|
||||
url: string;
|
||||
url: string
|
||||
/**
|
||||
* Width of the image in pixels
|
||||
*/
|
||||
width: number;
|
||||
width: number
|
||||
/**
|
||||
* Height of the image in pixels
|
||||
*/
|
||||
height: number;
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface myTimeline_myTimeline_highRes {
|
||||
__typename: "MediaURL";
|
||||
__typename: 'MediaURL'
|
||||
/**
|
||||
* URL for previewing the image
|
||||
*/
|
||||
url: string;
|
||||
url: string
|
||||
/**
|
||||
* Width of the image in pixels
|
||||
*/
|
||||
width: number;
|
||||
width: number
|
||||
/**
|
||||
* Height of the image in pixels
|
||||
*/
|
||||
height: number;
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface myTimeline_myTimeline_videoWeb {
|
||||
__typename: "MediaURL";
|
||||
__typename: 'MediaURL'
|
||||
/**
|
||||
* URL for previewing the image
|
||||
*/
|
||||
url: string;
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface myTimeline_myTimeline_album {
|
||||
__typename: "Album";
|
||||
id: string;
|
||||
title: string;
|
||||
__typename: 'Album'
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface myTimeline_myTimeline {
|
||||
__typename: "Media";
|
||||
id: string;
|
||||
title: string;
|
||||
type: MediaType;
|
||||
__typename: 'Media'
|
||||
id: string
|
||||
title: string
|
||||
type: MediaType
|
||||
/**
|
||||
* URL to display the media in a smaller resolution
|
||||
*/
|
||||
thumbnail: myTimeline_myTimeline_thumbnail | null;
|
||||
thumbnail: myTimeline_myTimeline_thumbnail | null
|
||||
/**
|
||||
* URL to display the photo in full resolution, will be null for videos
|
||||
*/
|
||||
highRes: myTimeline_myTimeline_highRes | null;
|
||||
highRes: myTimeline_myTimeline_highRes | null
|
||||
/**
|
||||
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
||||
*/
|
||||
videoWeb: myTimeline_myTimeline_videoWeb | null;
|
||||
favorite: boolean;
|
||||
videoWeb: myTimeline_myTimeline_videoWeb | null
|
||||
favorite: boolean
|
||||
/**
|
||||
* The album that holds the media
|
||||
*/
|
||||
album: myTimeline_myTimeline_album;
|
||||
album: myTimeline_myTimeline_album
|
||||
/**
|
||||
* The date the image was shot or the date it was imported as a fallback
|
||||
*/
|
||||
date: any;
|
||||
date: any
|
||||
}
|
||||
|
||||
export interface myTimeline {
|
||||
/**
|
||||
* Get a list of media, ordered first by day, then by album if multiple media was found for the same day.
|
||||
*/
|
||||
myTimeline: myTimeline_myTimeline[];
|
||||
myTimeline: myTimeline_myTimeline[]
|
||||
}
|
||||
|
||||
export interface myTimelineVariables {
|
||||
onlyFavorites?: boolean | null;
|
||||
limit?: number | null;
|
||||
offset?: number | null;
|
||||
onlyFavorites?: boolean | null
|
||||
limit?: number | null
|
||||
offset?: number | null
|
||||
fromDate?: any | null
|
||||
}
|
||||
|
||||
6
ui/src/components/timelineGallery/icons/date.svg
Normal file
6
ui/src/components/timelineGallery/icons/date.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="13px" height="15px" viewBox="0 0 13 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M9.16666667,5.68434189e-14 C9.41979718,5.68434189e-14 9.62899397,0.188102588 9.66210226,0.432152962 L9.66666667,0.5 L9.666,1.333 L11.1666667,1.33333333 C12.1285626,1.33333333 12.9174396,2.07411552 12.9939226,3.01630483 L13,3.16666667 L13,12.5 C13,13.512522 12.1791887,14.3333333 11.1666667,14.3333333 L11.1666667,14.3333333 L1.83333333,14.3333333 C0.820811292,14.3333333 0,13.512522 0,12.5 L0,12.5 L0,3.16666667 C0,2.15414463 0.820811292,1.33333333 1.83333333,1.33333333 L1.83333333,1.33333333 L3.333,1.333 L3.33333333,0.5 C3.33333333,0.223857625 3.55719096,5.68434189e-14 3.83333333,5.68434189e-14 C4.08646384,5.68434189e-14 4.29566064,0.188102588 4.32876892,0.432152962 L4.33333333,0.5 L4.333,1.333 L8.666,1.333 L8.66666667,0.5 C8.66666667,0.223857625 8.89052429,5.68434189e-14 9.16666667,5.68434189e-14 Z M12,6.333 L1,6.333 L1,12.5 C1,12.9248344 1.31790432,13.2754183 1.72880177,13.3268405 L1.83333333,13.3333333 L11.1666667,13.3333333 C11.626904,13.3333333 12,12.9602373 12,12.5 L12,12.5 L12,6.333 Z M3.333,2.333 L1.83333333,2.33333333 C1.37309604,2.33333333 1,2.70642938 1,3.16666667 L1,3.16666667 L1,5.333 L12,5.333 L12,3.16666667 C12,2.74183224 11.6820957,2.39124835 11.2711982,2.33982618 L11.1666667,2.33333333 L9.666,2.333 L9.66666667,3.16666667 C9.66666667,3.44280904 9.44280904,3.66666667 9.16666667,3.66666667 C8.91353616,3.66666667 8.70433936,3.47856408 8.67123108,3.2345137 L8.66666667,3.16666667 L8.666,2.333 L4.333,2.333 L4.33333333,3.16666667 C4.33333333,3.44280904 4.10947571,3.66666667 3.83333333,3.66666667 C3.58020282,3.66666667 3.37100603,3.47856408 3.33789774,3.2345137 L3.33333333,3.16666667 L3.333,2.333 Z" fill="currentColor" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -90,6 +90,10 @@ function useScrollPagination<D>({
|
||||
reconfigureIntersectionObserver()
|
||||
}, [fetchMore, data, finished])
|
||||
|
||||
useEffect(() => {
|
||||
setFinished(false)
|
||||
}, [data])
|
||||
|
||||
return {
|
||||
containerElem,
|
||||
finished,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export type UrlKeyValuePair = { key: string; value: string }
|
||||
export type UrlKeyValuePair = { key: string; value: string | null }
|
||||
|
||||
export type UrlParams = {
|
||||
getParam(key: string, defaultValue?: string | null): string | null
|
||||
setParam(key: string, value: string): void
|
||||
setParam(key: string, value: string | null): void
|
||||
setParams(pairs: UrlKeyValuePair[]): void
|
||||
}
|
||||
|
||||
@@ -19,18 +19,30 @@ function useURLParameters(): UrlParams {
|
||||
}
|
||||
|
||||
const updateParams = () => {
|
||||
history.replaceState({}, '', url.pathname + '?' + params.toString())
|
||||
if (params.toString()) {
|
||||
history.replaceState({}, '', url.pathname + '?' + params.toString())
|
||||
} else {
|
||||
history.replaceState({}, '', url.pathname)
|
||||
}
|
||||
setUrlString(document.location.href)
|
||||
}
|
||||
|
||||
const setParam = (key: string, value: string) => {
|
||||
params.set(key, value)
|
||||
const setParam = (key: string, value: string | null) => {
|
||||
if (value) {
|
||||
params.set(key, value)
|
||||
} else {
|
||||
params.delete(key)
|
||||
}
|
||||
updateParams()
|
||||
}
|
||||
|
||||
const setParams = (pairs: UrlKeyValuePair[]) => {
|
||||
for (const pair of pairs) {
|
||||
params.set(pair.key, pair.value)
|
||||
if (pair.value) {
|
||||
params.set(pair.key, pair.value)
|
||||
} else {
|
||||
params.delete(pair.key)
|
||||
}
|
||||
}
|
||||
updateParams()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user