Hacky tag-based filtering.

master
Sander Vocke 6 years ago
parent 9601aa35f8
commit 705d88a222
  1. 115
      src/browser.js
  2. 2
      src/debuggingpage.js
  3. 14
      src/main.js
  4. 15
      src/media.js
  5. 74
      src/queries.js

@ -8,46 +8,47 @@ import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore'; import ExpandMore from '@material-ui/icons/ExpandMore';
import Collapse from '@material-ui/core/Collapse'; 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 { class NavTreeItem {
name = false; display_name = false;
relative_path = false; data = false;
children = []; children = [];
constructor(name, relative_path, children) { constructor(display_name, data, children) {
this.name = name; this.display_name = display_name;
this.relative_path = relative_path; this.data = data;
this.children = children; this.children = children;
} }
} }
export function split_tree_item_location(treeitem) { export function split_relative_path(path) {
var r = treeitem.relative_path.split("/"); var r = path.split("/");
r.shift(); r.shift();
return r; return r;
} }
export function insert_into_album_tree(treebaseitem, treeitem) { 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; var current_item = treebaseitem;
for (var i = 0; i < parts.length; i++) { for (var i = 0; i < parts.length; i++) {
var part = parts[i]; var part = parts[i];
var subitem = false; 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++) { for (var j = 0; j < current_item.children.length; j++) {
var child = current_item.children[j]; var child = current_item.children[j];
if (child.name == part) { if (child.data == required_relative_path) {
subitem = child; subitem = child;
break; break;
} }
} }
if (!subitem) { if (!subitem) {
var new_path = (current_item.relative_path == "/") ? var new_sub = new NavTreeItem(
current_item.relative_path + part :
current_item.relative_path + "/" + part;
var new_sub = new AlbumTreeItem(
part, part,
new_path, required_relative_path,
[] []
); );
current_item.children.push(new_sub); 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) { 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++) { for (var i = 0; i < all_db_albums.length; i++) {
var album = all_db_albums[i]; var album = all_db_albums[i];
if (album.state.relative_path != "/") { // we already made the base if (album.state.relative_path != "/") { // we already made the base, skip that one
var item = new AlbumTreeItem( var item = new NavTreeItem(
album.state.name, album.state.name,
album.state.relative_path, album.state.relative_path,
[] []
@ -73,6 +74,24 @@ export function build_albums_tree(all_db_albums) {
return tree; 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 => ({ const useStyles = makeStyles(theme => ({
root: { root: {
width: '100%', width: '100%',
@ -84,40 +103,39 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
export function AlbumListItem(props) { export function NavListItem(props) {
const classes = useStyles(); const classes = useStyles();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const { album, onNewQuery } = props; const { navitem, onSelect } = props;
const handleExpandClick = () => { const handleExpandClick = () => {
setOpen(!open); setOpen(!open);
} }
const handleClick = () => { const handleClick = () => {
if (onNewQuery) { if (onSelect) {
var query = user_query_from_browsed_album(album.relative_path); onSelect(navitem);
onNewQuery(query);
} }
} }
if (album.children.length == 0) { if (navitem.children.length == 0) {
return ( return (
<ListItem button onClick={handleClick}> <ListItem button onClick={handleClick}>
<ListItemText primary={album.name} /> <ListItemText primary={navitem.display_name} />
</ListItem> </ListItem>
) )
} else { } else {
return ( return (
<> <>
<ListItem button > <ListItem button >
<ListItemText primary={album.name} onClick={handleClick} /> <ListItemText primary={navitem.display_name} onClick={handleClick} />
{open ? <ExpandLess onClick={handleExpandClick} /> : <ExpandMore onClick={handleExpandClick} />} {open ? <ExpandLess onClick={handleExpandClick} /> : <ExpandMore onClick={handleExpandClick} />}
</ListItem> </ListItem>
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={open} timeout="auto" unmountOnExit>
<List component="nav" className={classes.nested}> <List component="nav" className={classes.nested}>
{ {
album.children.map(elem => { navitem.children.map(elem => {
return <AlbumListItem album={elem} onNewQuery={onNewQuery}/> return <NavListItem navitem={elem} onSelect={onSelect} />
}) })
} }
</List> </List>
@ -129,7 +147,7 @@ export function AlbumListItem(props) {
export function Browser(props) { export function Browser(props) {
const classes = useStyles(); const classes = useStyles();
const { albums, onNewQuery } = props; const { albums, tags, onNewQuery } = props;
const propagateQuery = (query) => { const propagateQuery = (query) => {
if (onNewQuery) { 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 ( return (
<> <>
@ -146,14 +177,30 @@ export function Browser(props) {
aria-labelledby="nested-list-subheader" aria-labelledby="nested-list-subheader"
subheader={ subheader={
<ListSubheader component="div" id="nested-list-subheader"> <ListSubheader component="div" id="nested-list-subheader">
Albums Album Navigation
</ListSubheader>
}
className={classes.root}
>
{
albums_tree.children.map(elem => {
return <NavListItem navitem={elem} onSelect={propagateAlbumQuery} />
})
}
</List>
<List
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
Tag Navigation
</ListSubheader> </ListSubheader>
} }
className={classes.root} className={classes.root}
> >
{ {
tree.children.map(elem => { tags_tree.children.map(elem => {
return <AlbumListItem album={elem} onNewQuery={propagateQuery} /> return <NavListItem navitem={elem} onSelect={propagateTagQuery} />
}) })
} }
</List> </List>

@ -103,11 +103,9 @@ export class TestDBStringQuery extends React.Component {
var sql_album_query = maybe_album_query(q); 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 }); 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 => { 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 }); this.setState({ done: true, photos: photos });
}); });
do_album_query(sql_album_query, this.props.db, "/test_photos", "/test_photos_thumbs").then(albums => { 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 }); this.setState({ done: true, albums: albums });
}); });
} }

@ -9,7 +9,7 @@ import { InternalErrorPage } from './error.js';
import { LoadingPage } from './loading.js'; import { LoadingPage } from './loading.js';
import { ProvideDB, DBTypeEnum, DBSourceEnum } from './database.js'; import { ProvideDB, DBTypeEnum, DBSourceEnum } from './database.js';
import { GridGallery } from './gridgallery.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'; import { Browser } from './browser.js';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
@ -42,15 +42,23 @@ export function LoadedMainPage(props) {
const [gallery_user_query, setGalleryUserQuery] = React.useState(false); const [gallery_user_query, setGalleryUserQuery] = React.useState(false);
const [photos, setPhotos] = React.useState(false); const [photos, setPhotos] = React.useState(false);
const [albums, setAlbums] = React.useState(false); const [albums, setAlbums] = React.useState(false);
const [tags, setTags] = React.useState(false);
useEffect(() => { 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 blank_user_query = new UserQuery;
var sql_album_query = maybe_album_query(blank_user_query); 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) do_album_query(sql_album_query, props.database)
.then(albums => { .then(albums => {
setAlbums(albums); setAlbums(albums);
}); });
do_tag_query(sql_tag_query, props.database)
.then(tags => {
setTags(tags);
});
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -83,7 +91,7 @@ export function LoadedMainPage(props) {
<> <>
<Box className={classes.root}> <Box className={classes.root}>
<Box className={classes.navigator}> <Box className={classes.navigator}>
<Box className={classes.margined}>{albums && <Browser albums={albums} onNewQuery={onBrowser} />}</Box> <Box className={classes.margined}>{albums && <Browser albums={albums} tags={tags} onNewQuery={onBrowser} />}</Box>
</Box> </Box>
<Box className={classes.searchandview}> <Box className={classes.searchandview}>
<Box className={classes.margined}><SearchBar onSubmit={onSearch} /></Box> <Box className={classes.margined}><SearchBar onSubmit={onSearch} /></Box>

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import './TableLine.css'; import './TableLine.css';
import { findAllByDisplayValue } from '@testing-library/dom';
export class Media { } 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) { export function create_photo(maybe_id, maybe_name, maybe_path, maybe_thumbnail_path) {
var p = new Photo(); var p = new Photo();
if (maybe_id) { p.state.id = maybe_id; } 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; 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 }) => <img src={photo.state.path ? photo.state.path : "/resources/no_image_available.png"} alt="" />; export const PhotoView = ({ photo }) => <img src={photo.state.path ? photo.state.path : "/resources/no_image_available.png"} alt="" />;
export class PhotoThumbView extends React.Component { export class PhotoThumbView extends React.Component {

@ -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) { export function escape_regex(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}; };
export function do_image_query(query, database, collection_path, collection_thumbs_path) { export function do_image_query(query, database, collection_path, collection_thumbs_path) {
console.log("Doing image query:", query);
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var queries = []; var queries = [];
var ids = []; // TODO: this is for always uniquifying because of GROUP BY apparent bug in AlaSQL
queries.push(query); queries.push(query);
database.queries_async(queries).then(res => { database.queries_async(queries).then(res => {
var photos = []; var photos = [];
if (res && Array.isArray(res)) { if (res && Array.isArray(res)) {
res.forEach(row => { res.forEach(row => {
if (!ids.includes(row["id"])) { //uniquify
ids.push(row["id"]); //uniquify
var imagepath = process.env.PUBLIC_URL + collection_path + "/" + row["relativePath"] + "/" + row["name"]; var imagepath = process.env.PUBLIC_URL + collection_path + "/" + row["relativePath"] + "/" + row["name"];
var thumbpath = process.env.PUBLIC_URL + collection_thumbs_path + "/" + row["uniqueHash"] + ".jpg"; var thumbpath = process.env.PUBLIC_URL + collection_thumbs_path + "/" + row["uniqueHash"] + ".jpg";
photos.push(create_photo(row["id"], row["name"], imagepath, thumbpath)); photos.push(create_photo(row["id"], row["name"], imagepath, thumbpath));
}
}); });
} }
resolve(photos); 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 = { export const MatchTypeEnum = {
MATCH_EQUALS: 1, MATCH_EQUALS: 1,
MATCH_REGEXP_CASEINSENSITIVE: 2, MATCH_REGEXP_CASEINSENSITIVE: 2,
@ -48,13 +67,15 @@ export const MatchTypeEnum = {
export const MatchAgainstEnum = { export const MatchAgainstEnum = {
MATCH_RESULT_NAME: 1, // Match on the name of whatever object type we are querying for 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_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, 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 = { export const ResultTypeEnum = {
IMAGE: 1, IMAGE: 1,
ALBUM: 2, ALBUM: 2,
TAG: 3,
} }
export class ResultFilter { export class ResultFilter {
@ -143,11 +164,14 @@ export class LogicalOperatorFilter extends ResultFilter {
export class UserQuery { export class UserQuery {
image_filter = new ConstFilter(ResultTypeEnum.IMAGE, true); image_filter = new ConstFilter(ResultTypeEnum.IMAGE, true);
album_filter = new ConstFilter(ResultTypeEnum.ALBUM, 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. // 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) { 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. // 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 : "") + ";"; 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) { export function maybe_image_query(user_query) {
var where = false; var where = false;
if (user_query.image_filter) { if (user_query.image_filter) {
@ -171,6 +200,14 @@ export function maybe_album_query(user_query) {
return album_query_with_where(where); 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) { function filter_from_text_segment(result_type, segment) {
var filter = false; var filter = false;
@ -205,6 +242,16 @@ function filter_from_text_segment(result_type, segment) {
segment['negated'] segment['negated']
); );
filter = name_filter; 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; return filter;
@ -220,11 +267,12 @@ export function user_query_from_search_string(search_string) {
var r = new UserQuery(); var r = new UserQuery();
texts.forEach(text => { texts.forEach(text => {
console.log(text);
r.image_filter = new LogicalOperatorFilter(ResultTypeEnum.IMAGE, r.image_filter, r.image_filter = new LogicalOperatorFilter(ResultTypeEnum.IMAGE, r.image_filter,
filter_from_text_segment(ResultTypeEnum.IMAGE, text), LogicalOperatorEnum.AND); filter_from_text_segment(ResultTypeEnum.IMAGE, text), LogicalOperatorEnum.AND);
r.album_filter = new LogicalOperatorFilter(ResultTypeEnum.ALBUM, r.album_filter, r.album_filter = new LogicalOperatorFilter(ResultTypeEnum.ALBUM, r.album_filter,
filter_from_text_segment(ResultTypeEnum.ALBUM, text), LogicalOperatorEnum.AND); 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; return r;
@ -235,8 +283,20 @@ export function user_query_from_browsed_album(album_path) {
var match_type = MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE; var match_type = MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE;
var match_text = '"' + escape_regex(album_path) + '(\/[^\/]+)*' + '"'; var match_text = '"' + escape_regex(album_path) + '(\/[^\/]+)*' + '"';
var match_against = "Albums.relativePath"; 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.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false);
r.tag_filter = new ConstFilter(ResultTypeEnum.TAG, false);
return r; return r;
} }
Loading…
Cancel
Save