-
Notifications
You must be signed in to change notification settings - Fork 201
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #424 from osmandapp/fix17949
update photo gallery
- Loading branch information
Showing
12 changed files
with
667 additions
and
87 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { AppBar, Box, IconButton, ImageList, Toolbar, Typography } from '@mui/material'; | ||
import React, { useContext, useState, useCallback } from 'react'; | ||
import headerStyles from '../../../menu/trackfavmenu.module.css'; | ||
import styles from '../../infoblock.module.css'; | ||
import AppContext from '../../../context/AppContext'; | ||
import { ReactComponent as BackIcon } from '../../../assets/icons/ic_arrow_back.svg'; | ||
import ImageItem from '../../../menu/search/ImageItem'; | ||
|
||
export default function WptPhotoList({ photos }) { | ||
const ctx = useContext(AppContext); | ||
const [loadedImages, setLoadedImages] = useState({}); | ||
|
||
const handleImageLoad = useCallback((index) => { | ||
setLoadedImages((prevState) => ({ | ||
...prevState, | ||
[index]: true, | ||
})); | ||
}, []); | ||
|
||
return ( | ||
<Box minWidth={ctx.infoBlockWidth} maxWidth={ctx.infoBlockWidth} sx={{ height: 'auto', overflowX: 'hidden' }}> | ||
<AppBar position="sticky" sx={{ top: 0, width: '100%' }} className={headerStyles.appbar}> | ||
<Toolbar className={headerStyles.toolbar}> | ||
<IconButton | ||
variant="contained" | ||
type="button" | ||
className={styles.closeIcon} | ||
onClick={() => ctx.setPhotoGallery(null)} | ||
> | ||
<BackIcon /> | ||
</IconButton> | ||
<Typography component="div" className={headerStyles.title}> | ||
Photos | ||
</Typography> | ||
</Toolbar> | ||
</AppBar> | ||
<ImageList sx={{ pr: '16px', pl: '16px', overflowX: 'hidden' }} gap={16} cols={2}> | ||
{photos.map((photo, index) => ( | ||
<ImageItem | ||
key={index} | ||
photo={photo} | ||
index={index} | ||
handleImageLoad={handleImageLoad} | ||
isLoaded={!!loadedImages[index]} | ||
/> | ||
))} | ||
</ImageList> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,165 @@ | ||
import { apiGet } from '../util/HttpApi'; | ||
|
||
export const WIKI_IMAGE_BASE_URL = 'https://commons.wikimedia.org/wiki/Special:FilePath/'; | ||
|
||
export async function fetchPhotoProperties(photo) { | ||
if (!photo.properties.date || !photo.properties.author || !photo.properties.license) { | ||
const imageTitle = photo.properties.imageTitle; | ||
const url = `/wiki/File:${imageTitle}?action=raw`; | ||
try { | ||
const response = await apiGet(url, { | ||
apiCache: true, | ||
}); | ||
const data = response.data; | ||
|
||
const parseWikiText = (wikiText) => { | ||
const lines = wikiText.split('\n'); | ||
let author = 'Unknown'; | ||
let date = 'Unknown'; | ||
let license = 'Unknown'; | ||
let inInformationBlock = false; | ||
|
||
lines.forEach((line) => { | ||
if (line === '{{Information') { | ||
inInformationBlock = true; | ||
} else if (line === '}}' && inInformationBlock) { | ||
inInformationBlock = false; | ||
} | ||
// Parse date | ||
if (line.toLowerCase().includes('|date={{')) { | ||
const parts = line.split('|date={{')[1]?.split('|') || line.split('|Date={{')[1].split('|'); | ||
for (let part of parts) { | ||
const potentialDate = part.split('}}')[0].trim(); | ||
if (/^\d{4}-\d{2}-\d{2}$/.test(potentialDate)) { | ||
date = potentialDate; | ||
break; | ||
} | ||
} | ||
} else if (line.toLowerCase().includes('|date=')) { | ||
date = line.split('=')[1].trim().split(' ')[0]; | ||
} else if (line.includes('| Date = ')) { | ||
date = line.split('| Date = ')[1].split(' ')[0]; | ||
} | ||
|
||
// Parse author (priority for {{Information block) | ||
if (inInformationBlock && line.toLowerCase().includes('|author=')) { | ||
if (line.includes('[https://')) { | ||
author = line.split('[https://')[1].split(']')[0].split(' ')[1]; | ||
} else if (line.includes('|author={{')) { | ||
author = line.split('|author={{')[1].split('}}')[0]; | ||
} else if (line.includes('|Author={{')) { | ||
author = line.split('|Author={{')[1].split('}}')[0]; | ||
} else if (line.includes('[[')) { | ||
author = line.split('[[')[1].split(']]')[0].split('|')[1]; | ||
} else if (line.includes('|author=')) { | ||
author = line.split('|author=')[1].trim(); | ||
} else { | ||
author = line.split('|Author=')[1].trim(); | ||
} | ||
} else if (!inInformationBlock && line.toLowerCase().includes('|author=') && author === 'Unknown') { | ||
if (line.includes('{{unknown|author}}')) { | ||
author = 'Unknown'; | ||
} else if (line.includes('[[')) { | ||
author = line.split('[[')[1].split(']]')[0].split('|')[1]; | ||
} else if (line.includes('{{Creator:')) { | ||
author = line.split('{{Creator:')[1].split('}}')[0]; | ||
} else if (line.includes('{{creator:')) { | ||
author = line.split('{{creator:')[1].split('}}')[0]; | ||
} else if (line.includes('{{user at project|')) { | ||
author = line.split('{{user at project|')[1].split('|')[0]; | ||
} else if (line.includes('[https://')) { | ||
author = line.split('[https://')[1].split(']')[0].split(' ')[1]; | ||
} else { | ||
author = line.split('|author=')[1].trim(); | ||
} | ||
} else if (line.includes('| Author = ')) { | ||
author = line.split('| Author = ')[1].split(']')[0].split(' ')[1]; | ||
} | ||
if (author === '-') { | ||
author = 'Unknown'; | ||
} | ||
|
||
// Parse license | ||
if (line.includes('|license=')) { | ||
license = line.split('|license=')[1].split('|')[0].split('}')[0]; | ||
} else if (line.includes('|permission=')) { | ||
if (line.includes('{{')) { | ||
license = line.split('{{')[1].split('}}')[0].split('|')[0]; | ||
} else { | ||
license = line.split('|permission=')[1].split('|')[0].split('}')[0]; | ||
} | ||
} else if (line.includes('{{Self|')) { | ||
const parts = line.split('{{Self|')[1].split('|'); | ||
license = parts.find((part) => part.startsWith('CC-BY') || part.startsWith('cc-by')); | ||
} else if (line.includes('{{cc-by-')) { | ||
license = line.split('{{')[1].split('}}')[0].split('|')[0]; | ||
} else if (line.includes('{{RCE-license}}')) { | ||
license = 'RCE-license'; | ||
} else if (line.includes('{{RCE license}}')) { | ||
license = 'RCE license'; | ||
} else if (line.includes('No known copyright restrictions')) { | ||
license = 'No known copyright restrictions'; | ||
} | ||
}); | ||
|
||
return { | ||
author, | ||
date, | ||
license, | ||
}; | ||
}; | ||
|
||
const parsedData = parseWikiText(data); | ||
|
||
return { | ||
...photo, | ||
properties: { | ||
...photo.properties, | ||
date: parsedData.date !== 'Unknown' ? parsedData.date : photo.properties.date, | ||
author: parsedData.author !== 'Unknown' ? parsedData.author : photo.properties.author, | ||
license: parsedData.license !== 'Unknown' ? parsedData.license : photo.properties.license, | ||
}, | ||
}; | ||
} catch (error) { | ||
console.error('Failed to fetch photo properties:', error); | ||
return photo; | ||
} | ||
} | ||
return photo; | ||
} | ||
|
||
/** | ||
* Examples: | ||
* Date: | ||
* |date={{Original upload date|2015-04-15}} => 2015-04-15 | ||
* |Date={{original upload date|2006-11-05}} => 2006-11-05 | ||
* |date=2011-10-08 => 2011-10-08 | ||
* |Date=2009-12-06 23:11 => 2009-12-06 | ||
* |date=1940-05 => 1940-05 | ||
* | Date = 2018-04-06 12:25 => 2018-04-06 | ||
* |date={{Taken on|2014-03-09|location=Netherlands}} => 2014-03-09 | ||
* |date={{According to Exif data|2021-07-17}} => 2021-07-17 | ||
* | ||
* Author: | ||
* |author=[https://web.archive.org/web/20161031223609/http://www.panoramio.com/user/4678999?with_photo_id=118704129 Ben Bender] => Ben Bender | ||
* |author={{Creator:Johannes Petrus Albertus Antonietti}} => Johannes Petrus Albertus Antonietti | ||
* |author={{creator:Johannes Petrus Albertus Antonietti}} => Johannes Petrus Albertus Antonietti | ||
* |author=[[User:PersianDutchNetwork|PersianDutchNetwork]] => PersianDutchNetwork | ||
* |Author={{user at project|MelvinvdC|wikipedia|nl}} => MelvinvdC | ||
* |Author=[https://www.flickr.com/people/13088710@N02 Jos van Zetten] from Amsterdam, the Netherlands => Jos van Zetten | ||
* |author={{unknown|author}} => 'Unknown' | ||
* | Author = [https://www.flickr.com/people/141420435@N08 Nanda Sluijsmans] from Den Haag, Nederland => Nanda Sluijsmans | ||
* | Author = Coalition for the ICC / Credit: UN => Coalition for the ICC | ||
* | Author = - => 'Unknown' | ||
* | ||
* License: | ||
* |license={{cc-by-sa-3.0|Author Name}} => cc-by-sa-3.0 | ||
* |permission={{cc-by-sa-3.0|ekstijn}} => cc-by-sa-3.0 | ||
* == {{int:license-header}} == | ||
* {{Self|author={{user at project|MelvinvdC|wikipedia|nl}}|GFDL|CC-BY-SA-2.5|migration=relicense}} => CC-BY-SA-2.5 | ||
* {{self|cc-by-sa-3.0}} => cc-by-sa-3.0 | ||
* {{cc-by-2.0}} => cc-by-2.0 | ||
* {{RCE-license}} => RCE-license | ||
* {{RCE license}} => RCE license | ||
* {{User:FlickreviewR/reviewed-pass|Nationaal Archief|https://flickr.com/photos/29998366@N02/2949392968|2016-11-27 10:53:09|No known copyright restrictions|}} => No known copyright restrictions | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { useInView } from 'react-intersection-observer'; | ||
import React, { useCallback, useContext, useEffect, useState, useRef } from 'react'; | ||
import { ImageListItem, Skeleton } from '@mui/material'; | ||
import { WIKI_IMAGE_BASE_URL } from '../../manager/SearchManager'; | ||
import AppContext from '../../context/AppContext'; | ||
import styles from '../search/search.module.css'; | ||
|
||
export default function ImageItem({ photo, index, handleImageLoad, isLoaded }) { | ||
const ctx = useContext(AppContext); | ||
const [isSelected, setIsSelected] = useState(false); | ||
const [isHovered, setIsHovered] = useState(false); | ||
const itemRef = useRef(null); | ||
const { ref, inView } = useInView({ | ||
triggerOnce: true, | ||
threshold: 0.1, | ||
}); | ||
|
||
const handleMouseEnter = () => setIsHovered(true); | ||
const handleMouseLeave = () => setIsHovered(false); | ||
|
||
const combinedRef = (node) => { | ||
ref(node); // assign the node to useInView ref | ||
itemRef.current = node; // assign the node to the scroll ref | ||
}; | ||
|
||
const handlePhotoClick = useCallback((index) => { | ||
ctx.setSelectedPhotoInd(index); | ||
}, []); | ||
|
||
useEffect(() => { | ||
setIsSelected(ctx.selectedPhotoInd === index); | ||
}, [ctx.selectedPhotoInd, index]); | ||
|
||
useEffect(() => { | ||
if (isSelected && itemRef.current) { | ||
itemRef.current.scrollIntoView({ | ||
behavior: 'smooth', | ||
block: 'center', | ||
}); | ||
} | ||
}, [isSelected]); | ||
|
||
return ( | ||
<ImageListItem | ||
ref={combinedRef} | ||
className={styles.imageItem} | ||
onClick={() => handlePhotoClick(index)} | ||
onMouseOver={handleMouseEnter} | ||
onMouseOut={handleMouseLeave} | ||
> | ||
{!isLoaded && <Skeleton className={styles.skeleton} />} | ||
{inView && ( | ||
<img | ||
src={`${WIKI_IMAGE_BASE_URL}${photo.properties.imageTitle}?width=300`} | ||
alt={photo.properties.imageTitle} | ||
className={styles.image} | ||
onLoad={() => handleImageLoad(index)} | ||
/> | ||
)} | ||
{(isSelected || isHovered) && <div className={styles.selectedImage} />} | ||
</ImageListItem> | ||
); | ||
} |
Oops, something went wrong.