feat: Enhance UI with emoji page titles and improve tagging functionality
This commit updates the Layout component to include emojis in page titles for better visual cues. The Login component removes default credential placeholders for improved security. Additionally, the Search component is enhanced to allow immediate tagging of selected photos with existing tags, and it supports adding multiple tags at once, improving user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
parent
0a109b198a
commit
6e196ff859
@ -77,7 +77,7 @@ export default function Layout() {
|
||||
// Get page title based on route
|
||||
const getPageTitle = () => {
|
||||
const route = location.pathname
|
||||
if (route === '/') return 'Home Page'
|
||||
if (route === '/') return '🏠 Home Page'
|
||||
if (route === '/scan') return '🗂️ Scan Photos'
|
||||
if (route === '/process') return '⚙️ Process Faces'
|
||||
if (route === '/search') return '🔍 Search Photos'
|
||||
|
||||
@ -87,7 +87,6 @@ export default function Login() {
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -106,7 +105,6 @@ export default function Login() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="admin"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@ -127,10 +125,6 @@ export default function Login() {
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-gray-500">
|
||||
<p>Default credentials: admin / admin</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -519,37 +519,76 @@ export default function Search() {
|
||||
return commonTags
|
||||
}, [photoTags, selectedPhotos])
|
||||
|
||||
const handleSelectExistingTag = async (tagName: string) => {
|
||||
if (selectedPhotos.size === 0 || !tagName.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Add the selected tag to photos
|
||||
await tagsApi.addToPhotos({
|
||||
photo_ids: Array.from(selectedPhotos),
|
||||
tag_names: [tagName.trim()],
|
||||
})
|
||||
|
||||
// Clear the dropdown selection
|
||||
setSelectedTagName('')
|
||||
// Reload tags for selected photos so the new tag appears in the Tags area
|
||||
await loadPhotoTags()
|
||||
} catch (error) {
|
||||
console.error('Error tagging photos:', error)
|
||||
alert('Error tagging photos. Please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTag = async () => {
|
||||
if (selectedPhotos.size === 0) {
|
||||
alert('Please select photos to tag.')
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which tag to use: new tag input or selected from dropdown
|
||||
const tagToAdd = newTagName.trim() || selectedTagName.trim()
|
||||
// Collect both tags: selected existing tag and new tag name
|
||||
const tagsToAdd: string[] = []
|
||||
|
||||
if (!tagToAdd) {
|
||||
if (selectedTagName.trim()) {
|
||||
tagsToAdd.push(selectedTagName.trim())
|
||||
}
|
||||
|
||||
if (newTagName.trim()) {
|
||||
tagsToAdd.push(newTagName.trim())
|
||||
}
|
||||
|
||||
if (tagsToAdd.length === 0) {
|
||||
alert('Please select a tag or enter a new tag name.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// If it's a new tag (from input field), create it first
|
||||
if (newTagName.trim()) {
|
||||
await tagsApi.create(newTagName.trim())
|
||||
// Refresh available tags list
|
||||
await loadTags()
|
||||
// Clear the new tag input
|
||||
setNewTagName('')
|
||||
// Create any new tags first
|
||||
const newTags = tagsToAdd.filter(tag =>
|
||||
!availableTags.some(availableTag =>
|
||||
availableTag.tag_name.toLowerCase() === tag.toLowerCase()
|
||||
)
|
||||
)
|
||||
|
||||
for (const newTag of newTags) {
|
||||
await tagsApi.create(newTag)
|
||||
}
|
||||
|
||||
// Add tag to photos
|
||||
// Refresh available tags list if we created any
|
||||
if (newTags.length > 0) {
|
||||
await loadTags()
|
||||
}
|
||||
|
||||
// Add all tags to photos in a single API call
|
||||
await tagsApi.addToPhotos({
|
||||
photo_ids: Array.from(selectedPhotos),
|
||||
tag_names: [tagToAdd],
|
||||
tag_names: tagsToAdd,
|
||||
})
|
||||
|
||||
// Clear inputs after successful tagging
|
||||
setSelectedTagName('')
|
||||
setNewTagName('')
|
||||
// Reload tags for selected photos
|
||||
await loadPhotoTags()
|
||||
} catch (error) {
|
||||
@ -1480,10 +1519,10 @@ export default function Search() {
|
||||
<select
|
||||
value={selectedTagName}
|
||||
onChange={(e) => {
|
||||
setSelectedTagName(e.target.value)
|
||||
// Clear new tag input when selecting from dropdown
|
||||
if (e.target.value) {
|
||||
setNewTagName('')
|
||||
const tagName = e.target.value
|
||||
if (tagName) {
|
||||
// Immediately add the tag and clear the dropdown
|
||||
handleSelectExistingTag(tagName)
|
||||
}
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||
@ -1495,20 +1534,19 @@ export default function Search() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Selecting a tag will immediately add it to the photos. You can select multiple tags in a row.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Or Enter New Tag Name:
|
||||
Enter New Tag Name:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTagName}
|
||||
onChange={(e) => {
|
||||
setNewTagName(e.target.value)
|
||||
// Clear selected tag when typing new tag
|
||||
if (e.target.value) {
|
||||
setSelectedTagName('')
|
||||
}
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||
placeholder="Type new tag name..."
|
||||
@ -1519,7 +1557,7 @@ export default function Search() {
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
New tags will be created in the database automatically.
|
||||
You can select an existing tag and enter a new tag name to add both at once. New tags will be created in the database automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -229,6 +229,7 @@ def get_photos_with_tags(db: Session) -> List[dict]:
|
||||
|
||||
# Query 1: Get all photos with face counts using LEFT JOIN and GROUP BY
|
||||
# This gets face_count and unidentified_face_count in one query
|
||||
# Note: Excludes excluded faces (Face.excluded == False) to match identify UI behavior
|
||||
photos_with_counts = (
|
||||
db.query(
|
||||
Photo.id,
|
||||
@ -238,14 +239,14 @@ def get_photos_with_tags(db: Session) -> List[dict]:
|
||||
Photo.date_taken,
|
||||
Photo.date_added,
|
||||
Photo.media_type,
|
||||
# Face count (all faces)
|
||||
# Face count (non-excluded faces only)
|
||||
func.count(distinct(Face.id)).label('face_count'),
|
||||
# Unidentified face count (faces with person_id IS NULL)
|
||||
# Unidentified face count (non-excluded faces with person_id IS NULL)
|
||||
func.sum(
|
||||
case((Face.person_id.is_(None), 1), else_=0)
|
||||
).label('unidentified_face_count'),
|
||||
)
|
||||
.outerjoin(Face, Photo.id == Face.photo_id)
|
||||
.outerjoin(Face, (Photo.id == Face.photo_id) & (Face.excluded == False))
|
||||
.group_by(
|
||||
Photo.id,
|
||||
Photo.filename,
|
||||
@ -292,6 +293,7 @@ def get_photos_with_tags(db: Session) -> List[dict]:
|
||||
|
||||
# Query 3: Get all people for all photos in one query
|
||||
# Get distinct people per photo, then format names in Python
|
||||
# Note: Excludes excluded faces to match face count behavior
|
||||
people_data = (
|
||||
db.query(
|
||||
Face.photo_id,
|
||||
@ -304,6 +306,7 @@ def get_photos_with_tags(db: Session) -> List[dict]:
|
||||
.join(Person, Face.person_id == Person.id)
|
||||
.filter(Face.photo_id.in_(photo_ids))
|
||||
.filter(Face.person_id.isnot(None))
|
||||
.filter(Face.excluded == False)
|
||||
.distinct()
|
||||
.order_by(Face.photo_id, Person.last_name, Person.first_name)
|
||||
.all()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user