From 705d88a2226b43a059ab4a10c1f0e61c31f9e103 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Wed, 22 Jan 2020 14:59:20 +0100 Subject: [PATCH] Hacky tag-based filtering. --- src/browser.js | 115 ++++++++++++++++++++++++++++++------------- src/database.js | 4 +- src/debuggingpage.js | 2 - src/main.js | 14 ++++-- src/media.js | 15 +++++- src/queries.js | 80 ++++++++++++++++++++++++++---- 6 files changed, 178 insertions(+), 52 deletions(-) diff --git a/src/browser.js b/src/browser.js index 6d6ddc0..89220e7 100644 --- a/src/browser.js +++ b/src/browser.js @@ -8,46 +8,47 @@ import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; import Collapse from '@material-ui/core/Collapse'; -import { user_query_from_browsed_album } from './queries'; +import { user_query_from_browsed_album, user_query_from_browsed_tag } from './queries'; -class AlbumTreeItem { - name = false; - relative_path = false; +class NavTreeItem { + display_name = false; + data = false; children = []; - constructor(name, relative_path, children) { - this.name = name; - this.relative_path = relative_path; + constructor(display_name, data, children) { + this.display_name = display_name; + this.data = data; this.children = children; } } -export function split_tree_item_location(treeitem) { - var r = treeitem.relative_path.split("/"); +export function split_relative_path(path) { + var r = path.split("/"); r.shift(); return r; } export function insert_into_album_tree(treebaseitem, treeitem) { - var parts = split_tree_item_location(treeitem); + var parts = split_relative_path(treeitem.data); var current_item = treebaseitem; for (var i = 0; i < parts.length; i++) { var part = parts[i]; var subitem = false; + var required_relative_path = (current_item.data == "/") ? + current_item.data + part : + current_item.data + "/" + part; + for (var j = 0; j < current_item.children.length; j++) { var child = current_item.children[j]; - if (child.name == part) { + if (child.data == required_relative_path) { subitem = child; break; } } if (!subitem) { - var new_path = (current_item.relative_path == "/") ? - current_item.relative_path + part : - current_item.relative_path + "/" + part; - var new_sub = new AlbumTreeItem( + var new_sub = new NavTreeItem( part, - new_path, + required_relative_path, [] ); current_item.children.push(new_sub); @@ -58,11 +59,11 @@ export function insert_into_album_tree(treebaseitem, treeitem) { } export function build_albums_tree(all_db_albums) { - var tree = new AlbumTreeItem("", "/", []); + var tree = new NavTreeItem("", "/", []); for (var i = 0; i < all_db_albums.length; i++) { var album = all_db_albums[i]; - if (album.state.relative_path != "/") { // we already made the base - var item = new AlbumTreeItem( + if (album.state.relative_path != "/") { // we already made the base, skip that one + var item = new NavTreeItem( album.state.name, album.state.relative_path, [] @@ -73,6 +74,24 @@ export function build_albums_tree(all_db_albums) { return tree; } +export function insert_into_tag_tree(treebaseitem, treeitem) { + treebaseitem.children.push(treeitem); +} + +export function build_tags_tree(all_db_tags) { + var tree = new NavTreeItem("", "", []); + for (var i = 0; i < all_db_tags.length; i++) { + var tag = all_db_tags[i]; + var item = new NavTreeItem( + tag.state.name, + tag.state.name, + [] + ); + insert_into_tag_tree(tree, item); + }; + return tree; +} + const useStyles = makeStyles(theme => ({ root: { width: '100%', @@ -84,40 +103,39 @@ const useStyles = makeStyles(theme => ({ }, })); -export function AlbumListItem(props) { +export function NavListItem(props) { const classes = useStyles(); const [open, setOpen] = React.useState(false); - const { album, onNewQuery } = props; + const { navitem, onSelect } = props; const handleExpandClick = () => { setOpen(!open); } const handleClick = () => { - if (onNewQuery) { - var query = user_query_from_browsed_album(album.relative_path); - onNewQuery(query); + if (onSelect) { + onSelect(navitem); } } - if (album.children.length == 0) { + if (navitem.children.length == 0) { return ( - + ) } else { return ( <> - + {open ? : } { - album.children.map(elem => { - return + navitem.children.map(elem => { + return }) } @@ -129,7 +147,7 @@ export function AlbumListItem(props) { export function Browser(props) { const classes = useStyles(); - const { albums, onNewQuery } = props; + const { albums, tags, onNewQuery } = props; const propagateQuery = (query) => { if (onNewQuery) { @@ -137,7 +155,20 @@ export function Browser(props) { } } - const tree = build_albums_tree(albums); + const propagateAlbumQuery = (navitem) => { + propagateQuery( + user_query_from_browsed_album(navitem.data) + ); + } + + const propagateTagQuery = (navitem) => { + propagateQuery( + user_query_from_browsed_tag(navitem.data) + ); + } + + const albums_tree = build_albums_tree(albums); + const tags_tree = build_tags_tree(tags); return ( <> @@ -146,14 +177,30 @@ export function Browser(props) { aria-labelledby="nested-list-subheader" subheader={ - Albums + Album Navigation + + } + className={classes.root} + > + { + albums_tree.children.map(elem => { + return + }) + } + + + Tag Navigation } className={classes.root} > { - tree.children.map(elem => { - return + tags_tree.children.map(elem => { + return }) } diff --git a/src/database.js b/src/database.js index cb3a64a..b9e2bdc 100644 --- a/src/database.js +++ b/src/database.js @@ -16,7 +16,7 @@ export function alasql_async_queries(alasql_object, queries) { var p = Promise.resolve(null); for (let i = 0; i < queries.length; i++) { p = p.then(() => { - return alasql_object.promise(queries[i]); + return alasql_object.promise(queries[i]); }); } @@ -296,7 +296,7 @@ export class ProvideDB extends React.Component { export class DBQueryBar extends React.Component { onChangeHandler = (event) => { - if(this.props.onChange) { this.props.onChange(event.target.value) }; + if (this.props.onChange) { this.props.onChange(event.target.value) }; } render() { diff --git a/src/debuggingpage.js b/src/debuggingpage.js index afa9348..ce1228b 100644 --- a/src/debuggingpage.js +++ b/src/debuggingpage.js @@ -103,11 +103,9 @@ export class TestDBStringQuery extends React.Component { var sql_album_query = maybe_album_query(q); this.setState({ query: q, sql_image_query: sql_image_query, sql_album_query: sql_album_query, photos: false, albums: false }); do_image_query(sql_image_query, this.props.db, "/test_photos", "/test_photos_thumbs").then(photos => { - console.log(photos); this.setState({ done: true, photos: photos }); }); do_album_query(sql_album_query, this.props.db, "/test_photos", "/test_photos_thumbs").then(albums => { - console.log(albums); this.setState({ done: true, albums: albums }); }); } diff --git a/src/main.js b/src/main.js index 52dde08..0c6d219 100644 --- a/src/main.js +++ b/src/main.js @@ -9,7 +9,7 @@ import { InternalErrorPage } from './error.js'; import { LoadingPage } from './loading.js'; import { ProvideDB, DBTypeEnum, DBSourceEnum } from './database.js'; import { GridGallery } from './gridgallery.js'; -import { UserQuery, user_query_from_search_string, maybe_image_query, do_image_query, maybe_album_query, do_album_query } from './queries.js'; +import { UserQuery, user_query_from_search_string, maybe_image_query, do_image_query, maybe_album_query, do_album_query, maybe_tag_query, do_tag_query } from './queries.js'; import { Browser } from './browser.js'; const useStyles = makeStyles(theme => ({ @@ -42,15 +42,23 @@ export function LoadedMainPage(props) { const [gallery_user_query, setGalleryUserQuery] = React.useState(false); const [photos, setPhotos] = React.useState(false); const [albums, setAlbums] = React.useState(false); + const [tags, setTags] = React.useState(false); useEffect(() => { - // Single-fire effect to start retrieving the albums list. + // Single-fire effect to start retrieving the albums and tags lists. var blank_user_query = new UserQuery; var sql_album_query = maybe_album_query(blank_user_query); + var sql_tag_query = maybe_tag_query(blank_user_query); + do_album_query(sql_album_query, props.database) .then(albums => { setAlbums(albums); }); + + do_tag_query(sql_tag_query, props.database) + .then(tags => { + setTags(tags); + }); }, []); useEffect(() => { @@ -83,7 +91,7 @@ export function LoadedMainPage(props) { <> - {albums && } + {albums && } diff --git a/src/media.js b/src/media.js index caebc73..926011c 100644 --- a/src/media.js +++ b/src/media.js @@ -1,6 +1,5 @@ import React from 'react'; import './TableLine.css'; -import { findAllByDisplayValue } from '@testing-library/dom'; export class Media { } @@ -38,6 +37,13 @@ export class Album extends Media { } } +export class Tag extends Media { + state = { + id: false, + name: false + } +} + export function create_photo(maybe_id, maybe_name, maybe_path, maybe_thumbnail_path) { var p = new Photo(); if (maybe_id) { p.state.id = maybe_id; } @@ -55,6 +61,13 @@ export function create_album(maybe_id, maybe_name, maybe_relative_path) { return a; } +export function create_tag(maybe_id, maybe_name) { + var t = new Tag(); + if(maybe_id) { t.state.id = maybe_id; } + if(maybe_name) { t.state.name = maybe_name; } + return t; +} + export const PhotoView = ({ photo }) => ; export class PhotoThumbView extends React.Component { diff --git a/src/queries.js b/src/queries.js index dd0eb5a..6ac9764 100644 --- a/src/queries.js +++ b/src/queries.js @@ -1,22 +1,25 @@ -import { create_photo, create_album } from './media.js'; +import { create_photo, create_album, create_tag } from './media.js'; export function escape_regex(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); }; export function do_image_query(query, database, collection_path, collection_thumbs_path) { - console.log("Doing image query:", query); return new Promise(function (resolve, reject) { var queries = []; + var ids = []; // TODO: this is for always uniquifying because of GROUP BY apparent bug in AlaSQL queries.push(query); database.queries_async(queries).then(res => { var photos = []; if (res && Array.isArray(res)) { res.forEach(row => { - var imagepath = process.env.PUBLIC_URL + collection_path + "/" + row["relativePath"] + "/" + row["name"]; - var thumbpath = process.env.PUBLIC_URL + collection_thumbs_path + "/" + row["uniqueHash"] + ".jpg"; - photos.push(create_photo(row["id"], row["name"], imagepath, thumbpath)); + if (!ids.includes(row["id"])) { //uniquify + ids.push(row["id"]); //uniquify + var imagepath = process.env.PUBLIC_URL + collection_path + "/" + row["relativePath"] + "/" + row["name"]; + var thumbpath = process.env.PUBLIC_URL + collection_thumbs_path + "/" + row["uniqueHash"] + ".jpg"; + photos.push(create_photo(row["id"], row["name"], imagepath, thumbpath)); + } }); } resolve(photos); @@ -41,6 +44,22 @@ export function do_album_query(query, database) { }); } +export function do_tag_query(query, database) { + return new Promise(function (resolve, reject) { + var queries = []; + queries.push(query); + database.queries_async(queries).then(res => { + var tags = []; + if (res && Array.isArray(res)) { + res.forEach(row => { + tags.push(create_tag(row["id"], row["name"])); + }); + } + resolve(tags); + }); + }); +} + export const MatchTypeEnum = { MATCH_EQUALS: 1, MATCH_REGEXP_CASEINSENSITIVE: 2, @@ -48,13 +67,15 @@ export const MatchTypeEnum = { export const MatchAgainstEnum = { MATCH_RESULT_NAME: 1, // Match on the name of whatever object type we are querying for - MATCH_IMAGE_NAME: 2, // Match on the name of the relevant image, if any - MATCH_ALBUM_NAME: 3, // Match on the name of the relevant album, if any + MATCH_IMAGE_NAME: 2, // Match on the name of the relevant image(s), if any + MATCH_ALBUM_NAME: 3, // Match on the name of the relevant album(s), if any + MATCH_TAG_NAME: 4, // Match on the name of the relevant tag(s), if any } export const ResultTypeEnum = { IMAGE: 1, ALBUM: 2, + TAG: 3, } export class ResultFilter { @@ -143,11 +164,14 @@ export class LogicalOperatorFilter extends ResultFilter { export class UserQuery { image_filter = new ConstFilter(ResultTypeEnum.IMAGE, true); album_filter = new ConstFilter(ResultTypeEnum.ALBUM, true); + tag_filter = new ConstFilter(ResultTypeEnum.TAG, true); } // This query will return database entries with the fields "id", "uniqueHash", "relativePath" (of the album) and "name" for each matching image. export function image_query_with_where(maybe_where) { - return "SELECT Images.id, Images.name, Images.uniqueHash, Albums.relativePath FROM Images INNER JOIN Albums ON Images.album=Albums.id " + (maybe_where ? maybe_where : "") + ";"; + return "SELECT Images.id, Images.name, Images.uniqueHash, Albums.relativePath FROM Images INNER JOIN Albums ON Images.album=Albums.id LEFT JOIN ImageTags ON Images.id=ImageTags.imageid LEFT JOIN Tags ON ImageTags.tagid=Tags.id " + (maybe_where ? maybe_where : ""); + // TODO: the following for some reason breaks the query: + //+ " GROUP BY Images.id;"; } // This query will return database entries with the fields "id" and "relativePath" for each matching album. @@ -155,6 +179,11 @@ export function album_query_with_where(maybe_where) { return "SELECT Albums.id, Albums.relativePath FROM Albums " + (maybe_where ? maybe_where : "") + ";"; } +// This query will return database entries with the fields "id" and "name" for each matching tag. +export function tag_query_with_where(maybe_where) { + return "SELECT Tags.id, Tags.name FROM Tags " + (maybe_where ? maybe_where : "") + ";"; +} + export function maybe_image_query(user_query) { var where = false; if (user_query.image_filter) { @@ -171,6 +200,14 @@ export function maybe_album_query(user_query) { return album_query_with_where(where); } +export function maybe_tag_query(user_query) { + var where = false; + if (user_query.tag_filter) { + where = "WHERE (" + user_query.tag_filter.to_sql_where() + " AND Tags.pid = 0 AND NOT Tags.name=\"_Digikam_Internal_Tags_\")"; // TODO this way of doing the pid is hacky + } + return tag_query_with_where(where); +} + function filter_from_text_segment(result_type, segment) { var filter = false; @@ -205,6 +242,16 @@ function filter_from_text_segment(result_type, segment) { segment['negated'] ); filter = name_filter; + } else if (result_type == ResultTypeEnum.TAG) { + // Match against the tag name. + var name_filter = new MatchingFilter( + result_type, + "Tags.name", + '"' + '.*' + escape_regex(segment['text']) + '.*' + '"', + MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE, + segment['negated'] + ); + filter = name_filter; } return filter; @@ -220,11 +267,12 @@ export function user_query_from_search_string(search_string) { var r = new UserQuery(); texts.forEach(text => { - console.log(text); r.image_filter = new LogicalOperatorFilter(ResultTypeEnum.IMAGE, r.image_filter, filter_from_text_segment(ResultTypeEnum.IMAGE, text), LogicalOperatorEnum.AND); r.album_filter = new LogicalOperatorFilter(ResultTypeEnum.ALBUM, r.album_filter, filter_from_text_segment(ResultTypeEnum.ALBUM, text), LogicalOperatorEnum.AND); + r.tag_filter = new LogicalOperatorFilter(ResultTypeEnum.TAG, r.tag_filter, + filter_from_text_segment(ResultTypeEnum.TAG, text), LogicalOperatorEnum.AND); }); return r; @@ -235,8 +283,20 @@ export function user_query_from_browsed_album(album_path) { var match_type = MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE; var match_text = '"' + escape_regex(album_path) + '(\/[^\/]+)*' + '"'; var match_against = "Albums.relativePath"; - r.image_filter = new MatchingFilter(ResultTypeEnum.ALBUM, match_against, match_text, match_type, false); + r.image_filter = new MatchingFilter(ResultTypeEnum.IMAGE, match_against, match_text, match_type, false); + r.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false); + + return r; +} + +export function user_query_from_browsed_tag(name) { + var r = new UserQuery(); + var match_type = MatchTypeEnum.MATCH_EQUALS; + var match_text = '"' + name + '"'; + var match_against = "Tags.name"; + r.image_filter = new MatchingFilter(ResultTypeEnum.IMAGE, match_against, match_text, match_type, false); r.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false); + r.tag_filter = new ConstFilter(ResultTypeEnum.TAG, false); return r; } \ No newline at end of file