From 41d1851fccf857c2dd739b384c80782112c159e8 Mon Sep 17 00:00:00 2001 From: Dukobpa3 Date: Fri, 12 Apr 2024 14:31:06 +0300 Subject: [PATCH] Storing face-tags to events.db and database --- packages/events/src/apply-events.ts | 64 ++++++-- packages/events/src/models.ts | 23 +-- packages/events/src/taggable.ts | 1 + packages/webapp/src/api/ApiService.ts | 21 ++- packages/webapp/src/api/models.ts | 7 +- .../webapp/src/dialog/dialog-provider.tsx | 137 ++++++++++++++++++ .../src/dialog/face-tag-dialog-store.ts | 8 +- .../webapp/src/dialog/face-tag-dialog.tsx | 66 +++++++-- packages/webapp/src/dialog/face-tag-input.tsx | 2 +- .../webapp/src/dialog/face-tag-suggestion.ts | 58 ++++++++ .../webapp/src/dialog/suggestion-list.tsx | 2 +- .../webapp/src/dialog/tag-dialog-provider.tsx | 126 ---------------- .../webapp/src/dialog/tag-dialog-store.ts | 2 +- packages/webapp/src/dialog/tag-input.tsx | 2 +- .../{suggestion.ts => tag-suggestion.ts} | 0 packages/webapp/src/dialog/use-tag-dialog.ts | 4 +- packages/webapp/src/list/List.tsx | 2 +- packages/webapp/src/navbar/NavBar.tsx | 8 +- packages/webapp/src/single/Details.tsx | 46 ++++-- packages/webapp/src/single/MediaView.tsx | 36 ++--- packages/webapp/src/store/event-store.ts | 3 +- 21 files changed, 400 insertions(+), 218 deletions(-) create mode 100644 packages/webapp/src/dialog/dialog-provider.tsx create mode 100644 packages/webapp/src/dialog/face-tag-suggestion.ts delete mode 100644 packages/webapp/src/dialog/tag-dialog-provider.tsx rename packages/webapp/src/dialog/{suggestion.ts => tag-suggestion.ts} (100%) diff --git a/packages/events/src/apply-events.ts b/packages/events/src/apply-events.ts index 29334121..bf24993e 100644 --- a/packages/events/src/apply-events.ts +++ b/packages/events/src/apply-events.ts @@ -1,7 +1,24 @@ -import { Event, EventAction } from './models'; +import { random } from 'lodash'; +import { Event, EventAction, FaceTag, Rect } from './models'; import { Taggable } from './taggable'; +const defaultRect: Rect = { + x: -1, + y: -1, + width: -1, + height: -1 +} + +const findFace = (faces: any[], rect: Rect) => { + return faces.findIndex((face) => + face.x == rect.x + && face.y == rect.y + && face.width == rect.width + && face.height == rect.height + ) +} + const applyEventAction = (data: T, action: EventAction): boolean => { let changed = false; switch (action.action) { @@ -9,8 +26,8 @@ const applyEventAction = (data: T, action: EventAction): boo if (!data.tags) { data.tags = []; } - if (data.tags.indexOf(action.value) < 0) { - data.tags.push(action.value); + if (data.tags.indexOf(action.value as string) < 0) { + data.tags.push(action.value as string); changed = true; } break; @@ -19,28 +36,45 @@ const applyEventAction = (data: T, action: EventAction): boo if (!data.tags || !data.tags.length) { return false; } - const index = data.tags.indexOf(action.value); + const index = data.tags.indexOf(action.value as string); if (index >= 0) { data.tags.splice(index, 1); changed = true; } break; } + + case 'addFaceTag': { + if (!data.faces || !data.faces.length) { + return false; + } + + const faceIdx = findFace(data.faces, (action.value as FaceTag).rect) + if (faceIdx >= 0) { + data.faces[faceIdx].faceTag = (action.value as FaceTag).name; + changed = true; + } + break; + } + case 'removeFaceTag': { + if (!data.faces || !data.faces.length) { + return false; + } + const faceIdx = findFace(data.faces, (action.value as FaceTag).rect) + if (faceIdx >= 0) { + data.faces[faceIdx].faceTag = `unknown (${random(0, 1000)})`; + changed = true; + } + break; + } } return changed; } -const isSubIdsValid = (event: Event) => { - if(!event.subtargetCoords) return true; - if(event.subtargetCoords.length == event.targetIds.length) return true; - return false; -} - const isValidEvent = (event: Event) => { - return event.type == 'userAction' - && event.targetIds?.length - && event.actions?.length - && isSubIdsValid(event) + return event.type == 'userAction' + && event.targetIds?.length + && event.actions?.length } const applyEventDate = (entry: Taggable, event: Event) => { @@ -51,7 +85,7 @@ const applyEventDate = (entry: Taggable, event: Event) => { } } -type EntryIdMap = {[key: string]: Taggable[]} +type EntryIdMap = { [key: string]: Taggable[] } const idMapReducer = (result: EntryIdMap, entry: Taggable) => { const id = entry.id diff --git a/packages/events/src/models.ts b/packages/events/src/models.ts index f04e4d9b..ee14cf39 100644 --- a/packages/events/src/models.ts +++ b/packages/events/src/models.ts @@ -3,28 +3,17 @@ export interface Event { id: string; date?: string; targetIds: string[]; - // means that can't be more than one same object in one file. - // made for faces, so seems useful - // for example we have file 02f3p with faces 0, 1, 2 - // in one event we can put only one faceTag. So in case we will make bulk renaming we will get three events: - // targetIds[02f3p], tagretSubIds[0] - // targetIds[02f3p], tagretSubIds[1] - // targetIds[02f3p], tagretSubIds[2] - // - // espesially important for multiedit of many files, for example files 02f3p, jhr65, 3lh7a - // they have same person, but it have different places in each file, then we will get ONE event: - // targetIds[02f3p, jhr65, 3lh7a], tagretSubIds[5, 0, 3] - // - // this approach is kind of trade off and we loose some benefits in single file/many faces edit - // but makes multifiles edit better - - subtargetCoords?: Rect[]; actions: EventAction[]; } export interface EventAction { action: string; - value: string; + value: string|FaceTag; +} + +export interface FaceTag { + name: string; + rect: Rect; } export interface Rect { diff --git a/packages/events/src/taggable.ts b/packages/events/src/taggable.ts index ab274573..2a0aab39 100644 --- a/packages/events/src/taggable.ts +++ b/packages/events/src/taggable.ts @@ -2,5 +2,6 @@ export interface Taggable { id: string; updated?: string; tags?: string[]; + faces?: any[]; appliedEventIds?: string[]; } \ No newline at end of file diff --git a/packages/webapp/src/api/ApiService.ts b/packages/webapp/src/api/ApiService.ts index 160e2136..5380dc72 100644 --- a/packages/webapp/src/api/ApiService.ts +++ b/packages/webapp/src/api/ApiService.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { Event, EventAction } from '@home-gallery/events' import { pushEvent as pushEventApi, eventStream as eventStreamApi, ServerEventListener, getTree, mapEntriesForBrowser } from './api'; import { UnsavedEventHandler } from './UnsavedEventHandler'; -import { Tag } from './models'; +import { FaceTag, Tag } from './models'; import { Entry } from "../store/entry"; import { OfflineDatabase } from '../offline' import { EventBus } from './EventBus'; @@ -18,12 +18,31 @@ const tagToAction = (tag: Tag): EventAction => { } } +const faceTagToAction = (tag: FaceTag): EventAction => { + if (tag.remove) { + return {action: 'removeFaceTag', value: {name:tag.name, rect:tag.rect}} + } else { + return {action: 'addFaceTag', value: {name:tag.name, rect:tag.rect}} + } +} + export const addTags = async (entryIds: string[], tags: Tag[]) => { const actions = tags.map(tagToAction); const event: Event = {type: 'userAction', id: uuidv4(), targetIds: entryIds, actions }; return pushEvent(event); } +export const addFaceTags = async (entryIds: string[], faceTags: FaceTag[]) => { + const actions = faceTags.map(faceTagToAction); + const event: Event = { + type: 'userAction', + id: uuidv4(), + targetIds: entryIds, + actions + }; + return pushEvent(event); +} + let eventStreamSubscribed = false; const unsavedEventHandler = new UnsavedEventHandler(); diff --git a/packages/webapp/src/api/models.ts b/packages/webapp/src/api/models.ts index 7fb42af9..ca32fefc 100644 --- a/packages/webapp/src/api/models.ts +++ b/packages/webapp/src/api/models.ts @@ -1,9 +1,10 @@ +import { Rect } from '@home-gallery/events' + export interface Tag { name: string; remove: boolean; } export interface FaceTag extends Tag { - descriptorIndex: number; -} - + rect: Rect; +} \ No newline at end of file diff --git a/packages/webapp/src/dialog/dialog-provider.tsx b/packages/webapp/src/dialog/dialog-provider.tsx new file mode 100644 index 00000000..3cded584 --- /dev/null +++ b/packages/webapp/src/dialog/dialog-provider.tsx @@ -0,0 +1,137 @@ +import * as React from "react"; + +import { Tag, FaceTag } from "../api/models"; + +import { MultiTagDialog, SingleTagDialog } from './tag-dialog' +import { MultiFaceTagDialog, SingleFaceTagDialog } from './face-tag-dialog' + +export type TagsConfig = { + initialTags?: Tag[]; + onTagsSubmit: ({ tags }: { tags: Tag[] }) => void; +} + +export type FaceTagsConfig = { + initialFaceTags?: FaceTag[]; + onFaceTagsSubmit: ({ faceTags }: { faceTags: FaceTag[] }) => void; +} + +export type DialogConfig = { + tagsConfig?: TagsConfig; + faceTagsConfig?: FaceTagsConfig; +} + +const initialTagsConfig: TagsConfig = { + initialTags: [], + onTagsSubmit: () => false, +} + +const initialFaceTagsConfig: FaceTagsConfig = { + initialFaceTags: [], + onFaceTagsSubmit: () => false +} + +export type DialogContextType = { + setTagsDialogVisible: (visible: boolean) => void; + openTagsDialog: ({ initialTags, onTagsSubmit }: TagsConfig) => void + + setFaceTagsDialogVisible: (visible: boolean) => void; + openFaceTagsDialog: ({ initialFaceTags, onFaceTagsSubmit }: FaceTagsConfig) => void +} + +const initialDialogContextValue: DialogContextType = { + setTagsDialogVisible: () => false, + openTagsDialog: () => false, + + setFaceTagsDialogVisible: () => false, + openFaceTagsDialog: () => false +} + +export const DialogContext = React.createContext(initialDialogContextValue) + +export const MultiTagDialogProvider = ({ children }) => { + const [tagsDialogVisible, setTagsDialogVisible] = React.useState(false); + const [facesDialogVisible, setFaceTagsDialogVisible] = React.useState(false); + + const [tagsConfig, setTagsConfig] = React.useState(initialTagsConfig); + const [faceTagsConfig, setFaceTagsConfig] = React.useState(initialFaceTagsConfig); + + const openTagsDialog = (tagsConfig: TagsConfig) => { + setTagsDialogVisible(true); + setFaceTagsDialogVisible(false); + + setTagsConfig((prev) => ({ ...prev, ...tagsConfig })); + }; + + const onTagsSubmit = ({ tags }) => { + tagsConfig.onTagsSubmit({ tags }) + } + + const openFaceTagsDialog = (faceTagsConfig: FaceTagsConfig) => { + setTagsDialogVisible(true); + setFaceTagsDialogVisible(false); + + setFaceTagsConfig((prev) => ({ ...prev, ...faceTagsConfig })); + }; + + const onFaceTagsSubmit = ({ faceTags }) => { + faceTagsConfig.onFaceTagsSubmit({ faceTags }) + } + + return ( + + {children} + {tagsDialogVisible && ( + setTagsDialogVisible(false)}> + )} + { facesDialogVisible && ( + setFaceTagsDialogVisible(false)}> + )} + + ) +} + +export const SingleTagDialogProvider = ({ children }) => { + const [tagsDialogVisible, setTagsDialogVisible] = React.useState(false); + const [facesDialogVisible, setFaceTagsDialogVisible] = React.useState(false); + + const [tagsConfig, setTagsConfig] = React.useState(initialTagsConfig); + const [faceTagsConfig, setFaceTagsConfig] = React.useState(initialFaceTagsConfig); + + + const openTagsDialog = ({ initialTags, onTagsSubmit }: TagsConfig) => { + setTagsDialogVisible(true); + setFaceTagsDialogVisible(false); + + setTagsConfig({ initialTags, onTagsSubmit }); + }; + + const openFaceTagsDialog = ({ initialFaceTags, onFaceTagsSubmit }: FaceTagsConfig) => { + setTagsDialogVisible(false); + setFaceTagsDialogVisible(true); + + setFaceTagsConfig({ initialFaceTags, onFaceTagsSubmit }); + }; + + const onTagsSubmit = ({ tags }) => { + tagsConfig.onTagsSubmit({ tags }) + } + + const onFaceTagsSubmit = ({ faceTags }) => { + faceTagsConfig.onFaceTagsSubmit({ faceTags }) + } + + + return ( + + {children} + {tagsDialogVisible && ( + setTagsDialogVisible(false)}> + )} + {facesDialogVisible && ( + setFaceTagsDialogVisible(false)}> + )} + + ) +} + + diff --git a/packages/webapp/src/dialog/face-tag-dialog-store.ts b/packages/webapp/src/dialog/face-tag-dialog-store.ts index 0fce63e1..5b1626bb 100644 --- a/packages/webapp/src/dialog/face-tag-dialog-store.ts +++ b/packages/webapp/src/dialog/face-tag-dialog-store.ts @@ -1,13 +1,13 @@ import { useReducer } from "react" import { FaceTag } from "../api/models" -import { TagSuggestion, getSuggestions } from "./suggestion" +import { FaceTagSuggestion, getSuggestions } from "./face-tag-suggestion" export interface FaceTagDialogState { current: number faceTags: FaceTag[] allTags: string[] - suggestions: TagSuggestion[] + suggestions: FaceTagSuggestion[] showSuggestions: boolean } @@ -45,10 +45,10 @@ export const reducer = (state: FaceTagDialogState, action: FaceTagAction): FaceT case 'addFaceTag': { let name = action.value.replace(/(^\s+|\s+$)/g, '') let remove = false - let descriptorIndex = state.faceTags[action.selectedId].descriptorIndex; + let rect = state.faceTags[action.selectedId].rect; const tailSuggestions = {inputValue: action.tail || '', suggestions: getSuggestions(state.allTags, action.tail), showSuggestions: !!action.tail} - const faceTags: FaceTag[] = [...state.faceTags, {descriptorIndex, name, remove}]; + const faceTags: FaceTag[] = [...state.faceTags, {rect, name, remove}]; return {...state, faceTags, ...tailSuggestions} } /* diff --git a/packages/webapp/src/dialog/face-tag-dialog.tsx b/packages/webapp/src/dialog/face-tag-dialog.tsx index 5febf434..e180d339 100644 --- a/packages/webapp/src/dialog/face-tag-dialog.tsx +++ b/packages/webapp/src/dialog/face-tag-dialog.tsx @@ -12,15 +12,14 @@ import { RecentTags } from "./recent-tags"; import { UsedTags } from "./used-tags"; import { MultiTagHelp, SingleTagHelp } from "./face-tag-dialog-help"; import { useFaceTagsDialogStore } from "./face-tag-dialog-store"; -import { stat } from "fs"; export type FaceTagDialogProps = { faceTags?: FaceTag[] onCancel: () => void; - onSubmit: ({tags}: {tags: FaceTag[]}) => void; + onSubmit: ({faceTags}: {faceTags: FaceTag[]}) => void; } -const useAllTags = () => { +const useAllFaceTags = () => { const allEntries = useEntryStore(state => state.allEntries) const selectedIds = useEditModeStore(state => state.selectedIds) @@ -78,32 +77,79 @@ const Dialog = ({title, submitText, onCancel, onSubmit, children}) => { ) } +export const MultiFaceTagDialog = ({onCancel, onSubmit}: FaceTagDialogProps) => { + const [state, dispatch] = useFaceTagsDialogStore() + const [showHelp, setShowHelp] = useState(false) + + const recentTags = useEventStore(state => state.recentFaceTags); + const [allFaceTags] = useAllFaceTags() + + const faceTag = state.faceTags[state.current]; + + useEffect(() => { + dispatch({type: 'setAllFaceTags', value: allFaceTags.map(t => t.name).sort(), selectedIds:[]}) + }, [allFaceTags]) + + + const getFinalTags = () => { + const tags = [...state.faceTags] + if (faceTag.name.length) { + tags.push({name: faceTag.name, remove: false, rect: faceTag.rect}) + } + return tags + } + + const submitHandler = (event) => { + event.preventDefault(); + onSubmit({ faceTags: getFinalTags() }); + } + + return ( + +
+ + + {state.faceTags.map((tag, i) =>( +
+ + +
+ ))} + + +
+
+ ) +} + export const SingleFaceTagDialog = ({faceTags, onCancel, onSubmit}: FaceTagDialogProps) => { const [state, dispatch] = useFaceTagsDialogStore({faceTags}) const [showHelp, setShowHelp] = useState(false) const recentTags = useEventStore(state => state.recentFaceTags); - const [allTags] = useAllTags() + const [allFaceTags] = useAllFaceTags() const faceTag = state.faceTags[state.current]; - /* useEffect(() => { - dispatch({type: 'setAllFaceTags', value: allTags}) - }, [allTags]) */ + dispatch({type: 'setAllFaceTags', value: allFaceTags.map(t => t.name).sort(), selectedIds:[]}) + }, [allFaceTags]) const getFinalTags = () => { const tags = [...state.faceTags] if (faceTag.name.length) { - tags.push({name: faceTag.name, remove: false, descriptorIndex: faceTag.descriptorIndex}) + tags.push({name: faceTag.name, remove: false, rect: faceTag.rect}) } return tags } const submitHandler = (event) => { event.preventDefault(); - onSubmit({ tags: getFinalTags() }); + onSubmit({ faceTags: getFinalTags() }); } return ( @@ -121,7 +167,7 @@ export const SingleFaceTagDialog = ({faceTags, onCancel, onSubmit}: FaceTagDialo ))} - + ) diff --git a/packages/webapp/src/dialog/face-tag-input.tsx b/packages/webapp/src/dialog/face-tag-input.tsx index b4eef72f..3e0213ef 100644 --- a/packages/webapp/src/dialog/face-tag-input.tsx +++ b/packages/webapp/src/dialog/face-tag-input.tsx @@ -5,7 +5,7 @@ import * as icons from '@fortawesome/free-solid-svg-icons' import { FaceTag } from "../api/models"; import { classNames } from '../utils/class-names' import { toKey } from "../utils/toKey"; -import { TagSuggestion } from "./suggestion"; +import { TagSuggestion } from "./tag-suggestion"; import { SuggestionList } from "./suggestion-list"; const TagView = ({tag, withRemove, dispatch}: {tag: FaceTag, withRemove: boolean, dispatch}) => { diff --git a/packages/webapp/src/dialog/face-tag-suggestion.ts b/packages/webapp/src/dialog/face-tag-suggestion.ts new file mode 100644 index 00000000..d82337b0 --- /dev/null +++ b/packages/webapp/src/dialog/face-tag-suggestion.ts @@ -0,0 +1,58 @@ +import { FaceTag } from "../api/models"; +import { TextMatches, TextMatchSpread, findMatches, getMatchSpreads, mergeMatches, sortSpreads } from "./find-text"; + +export interface SuggestionPart { + pos: number + text: string + isMatch: boolean +} + +export interface FaceTagSuggestion extends FaceTag { + active: boolean; + matches: TextMatches[] + spreads: TextMatchSpread[] + parts: SuggestionPart[] +} + +export const getParts = (text: string, matches: TextMatches[]) : SuggestionPart[] => { + const parts: SuggestionPart[] = [] + let lastPos = 0 + for (let i = 0; i < matches.length; i++) { + const pos = matches[i].pos + const len = matches[i].text.length + if (matches[i].pos > lastPos) { + parts.push({pos: lastPos, text: text.substring(lastPos, pos), isMatch: false}) + } + parts.push({pos: pos, text: text.substring(pos, pos + len), isMatch: true}) + lastPos = pos + len + } + if (lastPos < text.length) { + parts.push({pos: lastPos, text: text.substring(lastPos), isMatch: false}) + } + return parts +} + +export const getSuggestions = (allTags: string[], value: string = ''): FaceTagSuggestion[] => { + const active = false + let remove = false + let needle = value.replace(/(^\s+|\s+$)/g, '').toLowerCase() + + if (!needle) { + return allTags.map(name => ({name, remove, rect: 0, active, matches: [], spreads: [], parts: [{pos: 0, text: name, isMatch: false}]})) + } + if (needle.startsWith('-')) { + needle = needle.substring(1) + remove = true + } + + return allTags + .map(name => { + const matches = findMatches(name, needle) + const spreads = getMatchSpreads(matches) + const mergedMatches = mergeMatches(matches) + const parts = getParts(name, mergedMatches) + return {name, remove, rect: 0, active, matches: mergedMatches, spreads, parts } + }) + .filter(m => m.matches.length > 0) + .sort((a, b) => sortSpreads(a.spreads, b.spreads)) +} diff --git a/packages/webapp/src/dialog/suggestion-list.tsx b/packages/webapp/src/dialog/suggestion-list.tsx index e2bc8594..ef117096 100644 --- a/packages/webapp/src/dialog/suggestion-list.tsx +++ b/packages/webapp/src/dialog/suggestion-list.tsx @@ -4,7 +4,7 @@ import { useState, useRef, useEffect } from "react"; import { classNames } from "../utils/class-names"; import { toKey } from "../utils/toKey"; import { useClientRect } from "../utils/useClientRect"; -import { TagSuggestion } from "./suggestion"; +import { TagSuggestion } from "./tag-suggestion"; export const SuggestionList = ({suggestions, input, dispatch}: {suggestions: TagSuggestion[], input: any, dispatch: Function}) => { const [style, setStyle] = useState({}) diff --git a/packages/webapp/src/dialog/tag-dialog-provider.tsx b/packages/webapp/src/dialog/tag-dialog-provider.tsx deleted file mode 100644 index d8605e86..00000000 --- a/packages/webapp/src/dialog/tag-dialog-provider.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import * as React from "react"; - -import { Tag, FaceTag } from "../api/models"; - -import { MultiTagDialog, SingleTagDialog } from './tag-dialog' -import { SingleFaceTagDialog } from './face-tag-dialog' - -export type TagDialogConfig = { - initialTags?: Tag[]; - initialFaceTags?: FaceTag[]; - onTagsSubmit: ({tags}: {tags: Tag[]}) => void; - onFaceTagsSubmit: ({tags}: {tags: FaceTag[]}) => void; -} - -const initialTagDialogConfig: TagDialogConfig = { - initialTags: [], - initialFaceTags: [], - onTagsSubmit: () => false, - onFaceTagsSubmit: () => false -} - - -export type TagDialogContextType = { - setTagsDialogVisible: (visible: boolean) => void; - openTagsDialog: ({initialTags, onTagsSubmit}: TagDialogConfig) => void - - setFaceTagsDialogVisible: (visible: boolean) => void; - openFaceTagsDialog: ({initialTags, onFaceTagsSubmit}: TagDialogConfig) => void -} - -const initialTagDialogContextValue: TagDialogContextType = { - setTagsDialogVisible: () => false, - openTagsDialog: () => false, - - setFaceTagsDialogVisible: () => false, - openFaceTagsDialog: () => false -} - - -export const TagDialogContext = React.createContext(initialTagDialogContextValue) -export const FaceTagDialogContext = React.createContext(initialTagDialogContextValue) - -export const MultiTagDialogProvider = ({children}) => { - const [ dialogVisible, setTagsDialogVisible ] = React.useState(false); - const [ config, setTagsConfig ] = React.useState(initialTagDialogConfig); - - const [ faceDialogVisible, setFaceTagsDialogVisible ] = React.useState(false); - const [ facesConfig, setFacesConfig ] = React.useState(initialTagDialogConfig); - - const openTagsDialog = (dialgConfig: TagDialogConfig) => { - setTagsDialogVisible(true); - setFaceTagsDialogVisible(false); - setTagsConfig((prev) => ({...prev, ...dialgConfig})); - }; - - const onTagsSubmit = ({tags}) => { - config.onTagsSubmit({tags}) - } - - const openFaceTagsDialog = (dialgConfig: TagDialogConfig) => { - setTagsDialogVisible(true); - setFaceTagsDialogVisible(false); - - setFacesConfig((prev) => ({...prev, ...dialgConfig})); - }; - - // const onFaceTagsSubmit = ({tags}) => { - // facesConfig.onFaceTagsSubmit({tags}) - // } - - return ( - - {children} - { dialogVisible && ( - setTagsDialogVisible(false)}> - )} - {/* {children} - { faceDialogVisible && ( - setFaceTagsDialogVisible(false)}> - )} */} - - ) -} - -export const SingleTagDialogProvider = ({children}) => { - const [ tagsDialogVisible, setTagsDialogVisible ] = React.useState(false); - const [ facesDialogVisible, setFaceTagsDialogVisible ] = React.useState(false); - - const [ tagsConfig, setTagsConfig ] = React.useState(initialTagDialogConfig); - - - const openTagsDialog = ({ initialTags: tags, initialFaceTags: faceTags, onTagsSubmit, onFaceTagsSubmit }: TagDialogConfig) => { - setTagsDialogVisible(true); - setFaceTagsDialogVisible(false); - setTagsConfig({ initialTags: tags, initialFaceTags: faceTags, onTagsSubmit, onFaceTagsSubmit }); - }; - - const openFaceTagsDialog = ({ initialTags: tags, initialFaceTags: faceTags, onTagsSubmit, onFaceTagsSubmit }: TagDialogConfig) => { - setTagsDialogVisible(false); - setFaceTagsDialogVisible(true); - setTagsConfig({ initialTags: tags, initialFaceTags: faceTags, onTagsSubmit, onFaceTagsSubmit }); - }; - - const onTagsSubmit = ({tags}) => { - tagsConfig.onTagsSubmit({tags}) - } - - const onFaceTagsSubmit = ({tags}) => { - tagsConfig.onFaceTagsSubmit({tags}) - } - - - return ( - - {children} - { tagsDialogVisible && ( - setTagsDialogVisible(false)}> - )} - { facesDialogVisible && ( - setFaceTagsDialogVisible(false)}> - )} - - ) -} - - diff --git a/packages/webapp/src/dialog/tag-dialog-store.ts b/packages/webapp/src/dialog/tag-dialog-store.ts index e772f241..72fb63aa 100644 --- a/packages/webapp/src/dialog/tag-dialog-store.ts +++ b/packages/webapp/src/dialog/tag-dialog-store.ts @@ -1,7 +1,7 @@ import { useReducer } from "react" import { Tag } from "../api/models" -import { TagSuggestion, getSuggestions } from "./suggestion" +import { TagSuggestion, getSuggestions } from "./tag-suggestion" export interface TagDialogState { inputValue: string, diff --git a/packages/webapp/src/dialog/tag-input.tsx b/packages/webapp/src/dialog/tag-input.tsx index 4226e0bc..b454b256 100644 --- a/packages/webapp/src/dialog/tag-input.tsx +++ b/packages/webapp/src/dialog/tag-input.tsx @@ -5,7 +5,7 @@ import * as icons from '@fortawesome/free-solid-svg-icons' import { Tag } from "../api/models"; import { classNames } from '../utils/class-names' import { toKey } from "../utils/toKey"; -import { TagSuggestion } from "./suggestion"; +import { TagSuggestion } from "./tag-suggestion"; import { SuggestionList } from "./suggestion-list"; const TagList = ({tags, withRemove, dispatch}: {tags: Tag[], withRemove: boolean, dispatch}) => { diff --git a/packages/webapp/src/dialog/suggestion.ts b/packages/webapp/src/dialog/tag-suggestion.ts similarity index 100% rename from packages/webapp/src/dialog/suggestion.ts rename to packages/webapp/src/dialog/tag-suggestion.ts diff --git a/packages/webapp/src/dialog/use-tag-dialog.ts b/packages/webapp/src/dialog/use-tag-dialog.ts index f41de54d..f35627f8 100644 --- a/packages/webapp/src/dialog/use-tag-dialog.ts +++ b/packages/webapp/src/dialog/use-tag-dialog.ts @@ -1,9 +1,9 @@ import * as React from "react"; -import { TagDialogContext } from './tag-dialog-provider' +import { DialogContext } from './dialog-provider' export const useTagDialog = () => { - const context = React.useContext(TagDialogContext) + const context = React.useContext(DialogContext) if (context == undefined) { throw new Error('useTagDialog must be used within a UserProvider') } diff --git a/packages/webapp/src/list/List.tsx b/packages/webapp/src/list/List.tsx index 366a26cc..7bebf40b 100644 --- a/packages/webapp/src/list/List.tsx +++ b/packages/webapp/src/list/List.tsx @@ -11,7 +11,7 @@ import { Scrollbar } from "./scrollbar"; import useBodyDimensions from '../utils/useBodyDimensions'; import { useDeviceType, DeviceType } from "../utils/useDeviceType"; import { fluent } from "./fluent"; -import { MultiTagDialogProvider } from "../dialog/tag-dialog-provider"; +import { MultiTagDialogProvider } from "../dialog/dialog-provider"; const NAV_HEIGHT = 44 const BOTTOM_MARGIN = 4 diff --git a/packages/webapp/src/navbar/NavBar.tsx b/packages/webapp/src/navbar/NavBar.tsx index aeabb09b..5e0b369a 100644 --- a/packages/webapp/src/navbar/NavBar.tsx +++ b/packages/webapp/src/navbar/NavBar.tsx @@ -86,21 +86,21 @@ export const MobileNavBar = ({disableEdit = false, showDialog}) => { export const NavBar = ({disableEdit = false}) => { const [ deviceType ] = useDeviceType(); - const { setDialogVisible, openDialog } = useTagDialog() + const { setTagsDialogVisible, openTagsDialog } = useTagDialog() const selectedIds = useEditModeStore(state => state.selectedIds); - const onSubmit = ({tags} : {tags: Tag[]}) => { + const onTagsSubmit = ({tags} : {tags: Tag[]}) => { const entryIds = Object.entries(selectedIds).filter(([_, selected]) => selected).map(([id]) => id) addTags(entryIds, tags).then(() => { - setDialogVisible(false); + setTagsDialogVisible(false); }) return false; } const showDialog = () => { - openDialog({onSubmit}) + openTagsDialog({onTagsSubmit}) } return ( diff --git a/packages/webapp/src/single/Details.tsx b/packages/webapp/src/single/Details.tsx index 13ff9ff4..faaa2971 100644 --- a/packages/webapp/src/single/Details.tsx +++ b/packages/webapp/src/single/Details.tsx @@ -5,7 +5,7 @@ import * as icons from '@fortawesome/free-solid-svg-icons' import { humanizeDuration, humanizeBytes, formatDate } from "../utils/format"; import { useTagDialog } from "../dialog/use-tag-dialog"; import { useNavigate } from "react-router-dom"; -import { addTags } from '../api/ApiService'; +import { addTags, addFaceTags } from '../api/ApiService'; import { Tag, FaceTag } from "../api/models"; import { useAppConfig } from "../utils/useAppConfig"; import { classNames } from "../utils/class-names"; @@ -97,9 +97,17 @@ export const Details = ({ entry, dispatch }) => { ) } - const origTags: Tag[] = (entry.tags || []).map(name => ({ name, remove: false })) + const initialTags: Tag[] = (entry.tags || []).map(name => ({ name, remove: false })) - const origFaceTags: FaceTag[] = (entry.faces || []).map((face, i) => ({ name: face.faceTag, remove: false, descriptorIndex: i })) + const initialFaceTags: FaceTag[] = (entry.faces || []).map((face) => ({ + name: face.faceTag, + rect: { + x: +face.x, + y: +face.y, + width: +face.width, + height: +face.height + } + })) const onTagsSubmit = ({ tags }: { tags: Tag[] }) => { const tagNames = tags.map(({ name }) => name) @@ -117,13 +125,27 @@ export const Details = ({ entry, dispatch }) => { return false; } - const onFaceTagsSubmit = ({ tags }: { tags: FaceTag[] }) => { - const tagNames = tags.map(({ name }) => name) - const origTagNames = entry.tags || [] - const tagActions = origTagNames.filter(name => !tagNames.includes(name)).map(name => ({ name, remove: true })) - tagActions.push(...tagNames.filter(name => !origTagNames.includes(name)).map(name => ({ name, remove: false }))) - if (tagActions.length) { - addTags([entry.id], tagActions).then(() => { + const onFaceTagsSubmit = ({ faceTags }: { faceTags: FaceTag[] }) => { + const contains = (faceTag: FaceTag, allTags: FaceTag[]) => { + return allTags.filter(tag => tag.name == faceTag.name + && tag.rect.x == faceTag.rect.x + && tag.rect.x == faceTag.rect.x + && tag.rect.x == faceTag.rect.x + && tag.rect.x == faceTag.rect.x + ).length > 0; + } + + const faceTagActions = initialFaceTags.filter(tag => !contains(tag, faceTags)) + .map(tag => ({ name: tag.name, rect: tag.rect, remove: true })) + + faceTagActions.push(...faceTags.filter(tag => !contains(tag, initialFaceTags)) + .map(tag => ({ name: tag.name, rect: tag.rect, remove: false }))) + + console.log(initialFaceTags); + console.log(faceTags); + console.log(faceTagActions); + if (faceTagActions.length) { + addFaceTags([entry.id], faceTagActions).then(() => { setFaceTagsDialogVisible(false); }) } else { @@ -134,11 +156,11 @@ export const Details = ({ entry, dispatch }) => { } const editTags = () => { - openTagsDialog({ initialTags: origTags,initialFaceTags: origFaceTags, onTagsSubmit , onFaceTagsSubmit}) + openTagsDialog({ initialTags, onTagsSubmit }) } const editFaceTags = () => { - openFaceTagsDialog({ initialTags: origTags,initialFaceTags: origFaceTags, onTagsSubmit , onFaceTagsSubmit}) + openFaceTagsDialog({ initialFaceTags, onFaceTagsSubmit }) } return ( diff --git a/packages/webapp/src/single/MediaView.tsx b/packages/webapp/src/single/MediaView.tsx index 71967fb9..d902a4a8 100644 --- a/packages/webapp/src/single/MediaView.tsx +++ b/packages/webapp/src/single/MediaView.tsx @@ -21,7 +21,7 @@ import { Details } from './Details'; import { Zoomable } from "./Zoomable"; import useBodyDimensions from "../utils/useBodyDimensions"; import { classNames } from '../utils/class-names' -import { SingleTagDialogProvider } from "../dialog/tag-dialog-provider"; +import { SingleTagDialogProvider } from "../dialog/dialog-provider"; const findEntryIndex = (location, entries, id) => { if (location.state?.index && entries[location.state.index]?.id.startsWith(id)) { @@ -108,7 +108,7 @@ export const MediaView = () => { const viewEntry = (index: number) => { const { shortId } = entries[index] - navigate(`/view/${shortId}`, {state: {index, listLocation}, replace: true}); + navigate(`/view/${shortId}`, { state: { index, listLocation }, replace: true }); } const dispatch = (action: any) => { @@ -135,9 +135,9 @@ export const MediaView = () => { } else if (type == 'last' && entries.length) { viewEntry(entries.length - 1) } else if (type == 'list') { - navigate(`${listLocation.pathname}${listLocation.search ? encodeUrl(listLocation.search) : ''}`, {state: {id: current?.id}}); + navigate(`${listLocation.pathname}${listLocation.search ? encodeUrl(listLocation.search) : ''}`, { state: { id: current?.id } }); } else if (type == 'chronology') { - search({type: 'none'}); + search({ type: 'none' }); navigate('/'); } else if (type == 'play') { setHideNavigation(true); @@ -146,15 +146,15 @@ export const MediaView = () => { } else if (type == 'search') { navigate(`/search/${encodeUrl(action.query)}`); } else if (type == 'map') { - navigate(`/map?lat=${current.latitude.toFixed(5)}&lng=${current.longitude.toFixed(5)}&zoom=14`, {state: {listLocation}}) + navigate(`/map?lat=${current.latitude.toFixed(5)}&lng=${current.longitude.toFixed(5)}&zoom=14`, { state: { listLocation } }) } } const onSwipe = (ev) => { if (ev.direction === Hammer.DIRECTION_LEFT) { - dispatch({type: 'next'}) + dispatch({ type: 'next' }) } else if (ev.direction === Hammer.DIRECTION_RIGHT) { - dispatch({type: 'prev'}) + dispatch({ type: 'prev' }) } } @@ -164,7 +164,7 @@ export const MediaView = () => { const found = keys.find(key => handler.key == key) if (found) { console.log(`Catch hotkey ${found} for ${hotkeysToAction[hotkey]}`) - dispatch({type: hotkeysToAction[hotkey]}) + dispatch({ type: hotkeysToAction[hotkey] }) return true } }) @@ -175,44 +175,44 @@ export const MediaView = () => { const mediaVanishes = index < 0 && lastIndex >= 0 && entries.length > 0 if (mediaVanishes) { - dispatch({type: 'index', index: lastIndex}) + dispatch({ type: 'index', index: lastIndex }) } const listBecomesEmpty = entries.length == 0 && lastIndex >= 0 if (listBecomesEmpty) { - dispatch({type: 'list'}) + dispatch({ type: 'list' }) } console.log('Media object:', current, 'showDetails:', showDetails, 'showRects:', showRects); return ( <> - +
-
+
{!hideNavigation && - + } {isImage && - + } {isVideo && - + } {isUnknown && - + }
- { showDetails && + {showDetails &&
}
- + ) } diff --git a/packages/webapp/src/store/event-store.ts b/packages/webapp/src/store/event-store.ts index 0347b021..ca1ba856 100644 --- a/packages/webapp/src/store/event-store.ts +++ b/packages/webapp/src/store/event-store.ts @@ -4,6 +4,7 @@ import { applyEvents, Event, EventAction } from '@home-gallery/events' import { Entry } from './entry' import { useEntryStore } from './entry-store' +import { FaceTag } from '../api/models' const lruAdd = (list: string[], item: string, size: number = 50) => { @@ -29,7 +30,7 @@ const getEventFaceTags = events => events .filter(event => event.type == 'userAction') .map(event => event.actions.filter(a => a.action == 'addFaceTag')) .reduce((all, actions) => all.concat(actions), []) - .map(action => action.value) + .map(action => action.value.name) export interface EventStore {