commit
3bf111603f
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
|
||||
abstract class AApiController |
||||
{ |
||||
abstract public function process(array $args); |
||||
} |
@ -0,0 +1,77 @@ |
||||
<?php |
||||
|
||||
require_once "a_api_controller.php"; |
||||
require_once "tags_controller.php"; |
||||
|
||||
class ApiImagesController extends AApiController |
||||
{ |
||||
public function process(array $args) |
||||
{ |
||||
$fourth_arg = @$args[3]; |
||||
|
||||
if (!is_numeric($fourth_arg)) { |
||||
// 4th arg should always be a number |
||||
return; |
||||
} |
||||
$id = intval($fourth_arg); |
||||
|
||||
$image = Image::by_id($id); |
||||
|
||||
if ($image===null) { |
||||
throw new SCoreException("Image $id not found"); |
||||
} |
||||
|
||||
if (@$args[4]==="tags") { |
||||
switch ($_SERVER['REQUEST_METHOD']) { |
||||
case "GET": |
||||
ApiTagsController::get_for_image_id($id); |
||||
break; |
||||
case "POST": |
||||
if (empty($_GET["tags"])) { |
||||
return; |
||||
} |
||||
$this->set_tags($image, json_decode($_GET["tags"]), false); |
||||
ApiTagsController::get_for_image_id($id); |
||||
break; |
||||
} |
||||
} else { |
||||
$this->search_tags(); |
||||
} |
||||
} |
||||
|
||||
private function set_tags(Image $image, array $tags, bool $replace) |
||||
{ |
||||
global $user; |
||||
if ($user->can(Permissions::EDIT_IMAGE_TAG)) { |
||||
$tags = Tag::explode(implode(" ", $tags)); |
||||
|
||||
$tags = Tag::sanitize_array($tags); |
||||
|
||||
$pos_tag_array = []; |
||||
$neg_tag_array = []; |
||||
foreach ($tags as $new_tag) { |
||||
if (strpos($new_tag, '-') === 0) { |
||||
$neg_tag_array[] = substr($new_tag, 1); |
||||
} else { |
||||
$pos_tag_array[] = $new_tag; |
||||
} |
||||
} |
||||
|
||||
if ($replace) { |
||||
send_event(new TagSetEvent($image, $tags)); |
||||
} else { |
||||
$img_tags = array_map("strtolower", $image->get_tag_array()); |
||||
|
||||
if (!empty($neg_tag_array)) { |
||||
$neg_tag_array = array_map("strtolower", $neg_tag_array); |
||||
|
||||
$img_tags = array_merge($pos_tag_array, $img_tags); |
||||
$img_tags = array_diff($img_tags, $neg_tag_array); |
||||
} else { |
||||
$img_tags = array_merge($tags, $img_tags); |
||||
} |
||||
send_event(new TagSetEvent($image, $img_tags)); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,120 @@ |
||||
<?php |
||||
|
||||
require_once "a_api_controller.php"; |
||||
|
||||
class ApiTagsController extends AApiController |
||||
{ |
||||
public function process(array $args) |
||||
{ |
||||
if (@$args[3]==="by_image") { |
||||
$this->get_for_image(); |
||||
} else { |
||||
$this->search_tags(); |
||||
} |
||||
} |
||||
|
||||
public static function prepare_output(PDOStatement $input): string |
||||
{ |
||||
$output = []; |
||||
$output["tags"] = []; |
||||
foreach ($input as $item) { |
||||
$output["tags"][] = ["id"=>$item["id"], "count"=>$item["count"], "tag"=>$item["tag"]]; |
||||
} |
||||
return json_encode($output); |
||||
} |
||||
|
||||
private function get_for_image() |
||||
{ |
||||
if (!isset($_GET["id"])) { |
||||
return; |
||||
} |
||||
$id = intval($_GET["id"]); |
||||
self::get_for_image_id($id); |
||||
} |
||||
|
||||
public static function get_for_image_id($id) |
||||
{ |
||||
global $database, $page, $cache; |
||||
|
||||
$page->set_mode(PageMode::DATA); |
||||
$page->set_mime(MimeType::JSON); |
||||
|
||||
$cache_key = "api_tags_by_image-$id"; |
||||
$SQLarr = ["id"=>$id]; |
||||
$res = $cache->get($cache_key); |
||||
if (!$res) { |
||||
$res = $database->get_all_iterable( |
||||
$database->scoreql_to_sql(" |
||||
SELECT * |
||||
FROM tags t |
||||
INNER JOIN image_tags it ON t.id = it.tag_id AND it.image_id = :id |
||||
ORDER BY count DESC"), |
||||
$SQLarr |
||||
); |
||||
$cache->set($cache_key, $res, 600); |
||||
} |
||||
|
||||
$page->set_data(self::prepare_output($res)); |
||||
} |
||||
|
||||
private function search_tags() |
||||
{ |
||||
global $database, $page, $cache; |
||||
if (!isset($_GET["query"])) { |
||||
return; |
||||
} |
||||
|
||||
$page->set_mode(PageMode::DATA); |
||||
$page->set_mime(MimeType::JSON); |
||||
|
||||
$s = strtolower($_GET["query"]); |
||||
if ( |
||||
$s == '' || |
||||
$s[0] == '_' || |
||||
$s[0] == '%' || |
||||
strlen($s) > 32 |
||||
) { |
||||
$page->set_data("{}"); |
||||
return; |
||||
} |
||||
|
||||
|
||||
|
||||
//$limit = 0; |
||||
$cache_key = "api_tags_search-$s"; |
||||
$limitSQL = ""; |
||||
$searchSQL = "LOWER(tag) LIKE LOWER(:search)"; |
||||
$s = str_replace('_', '\_', $s); |
||||
$s = str_replace('%', '\%', $s); |
||||
$SQLarr = ["search"=>"$s%"]; |
||||
|
||||
if (isset($_GET["search_categories"]) && $_GET["search_categories"] == "true") { |
||||
$searchSQL .= " OR LOWER(tag) LIKE LOWER(:cat_search)"; |
||||
$SQLarr['cat_search'] = "%:$s%"; |
||||
$cache_key .= "+cat"; |
||||
} |
||||
|
||||
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) { |
||||
$limitSQL = "LIMIT :limit"; |
||||
$SQLarr['limit'] = $_GET["limit"]; |
||||
$cache_key .= "-" . $_GET["limit"]; |
||||
} |
||||
|
||||
$res = $cache->get($cache_key); |
||||
if (!$res) { |
||||
$res = $database->get_all_iterable( |
||||
" |
||||
SELECT * |
||||
FROM tags |
||||
WHERE $searchSQL |
||||
AND count > 0 |
||||
ORDER BY count DESC |
||||
$limitSQL", |
||||
$SQLarr |
||||
); |
||||
$cache->set($cache_key, $res, 600); |
||||
} |
||||
|
||||
$page->set_data(self::prepare_output($res)); |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
<?php declare(strict_types=1); |
||||
|
||||
|
||||
/* |
||||
* Name: Autocomplete |
||||
* Author: Daku <admin@codeanimu.net> |
||||
* Description: Adds autocomplete to search & tagging. |
||||
*/ |
||||
|
||||
class ApiInternalInfo extends ExtensionInfo |
||||
{ |
||||
public const KEY = "api_internal"; |
||||
|
||||
public $key = self::KEY; |
||||
public $name = "Internal API"; |
||||
public $authors = ["Daku"=>"admin@codeanimu.net", "Matthew Barbour"=>"matthew@darkholme.net"]; |
||||
public $description = "Dependency extension used to provide a standardized source for performing operations via an API"; |
||||
public $visibility = self::VISIBLE_HIDDEN; |
||||
} |
@ -0,0 +1,53 @@ |
||||
<?php |
||||
|
||||
require_once "controllers/tags_controller.php"; |
||||
require_once "controllers/images_controller.php"; |
||||
|
||||
class ApiInternal extends Extension |
||||
{ |
||||
private const INTERNAL_API = "api/internal/"; |
||||
|
||||
public const TAGS_API_PATH = self::INTERNAL_API."tags/"; |
||||
public const TAGS_BY_IMAGE_PATH = self::TAGS_API_PATH."by_image"; |
||||
|
||||
public const IMAGE_API_PATH = self::INTERNAL_API."image/"; |
||||
|
||||
|
||||
private $tags; |
||||
private $images; |
||||
|
||||
public function __construct($class = null) |
||||
{ |
||||
parent::__construct(); |
||||
|
||||
$this->tags = new ApiTagsController(); |
||||
$this->images = new ApiImagesController(); |
||||
} |
||||
|
||||
public function get_priority(): int |
||||
{ |
||||
return 30; |
||||
} // before Home |
||||
|
||||
public function onPageRequest(PageRequestEvent $event) |
||||
{ |
||||
if ($event->args[0]==="api"&&$event->args[1]==="internal") { |
||||
$controller_name = $event->args[2]; |
||||
$controller = null; |
||||
switch ($controller_name) { |
||||
case "tags": |
||||
$controller = $this->tags; |
||||
break; |
||||
case "image": |
||||
$controller = $this->images; |
||||
break; |
||||
} |
||||
|
||||
if ($controller!==null) { |
||||
$controller->process($event->args); |
||||
} else { |
||||
throw new SCoreException("Controller not found for $controller_name"); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,81 +1,87 @@ |
||||
document.addEventListener('DOMContentLoaded', () => { |
||||
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename']; |
||||
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename']; |
||||
|
||||
$('[name="search"]').tagit({ |
||||
singleFieldDelimiter: ' ', |
||||
beforeTagAdded: function(event, ui) { |
||||
if(metatags.indexOf(ui.tagLabel) !== -1) { |
||||
ui.tag.addClass('tag-metatag'); |
||||
} else { |
||||
console.log(ui.tagLabel); |
||||
// give special class to negative tags
|
||||
if(ui.tagLabel[0] === '-') { |
||||
ui.tag.addClass('tag-negative'); |
||||
}else{ |
||||
ui.tag.addClass('tag-positive'); |
||||
} |
||||
} |
||||
}, |
||||
autocomplete : ({ |
||||
source: function (request, response) { |
||||
var ac_metatags = $.map( |
||||
$.grep(metatags, function(s) { |
||||
// Only show metatags for strings longer than one character
|
||||
return (request.term.length > 1 && s.indexOf(request.term) === 0); |
||||
}), |
||||
function(item) { |
||||
return { |
||||
label : item + ' [metatag]', |
||||
value : item |
||||
}; |
||||
} |
||||
); |
||||
function enableTagAutoComplete(element, limit, search_categories) { |
||||
$(element).tagit({ |
||||
singleFieldDelimiter: ' ', |
||||
beforeTagAdded: function(event, ui) { |
||||
if(metatags.indexOf(ui.tagLabel) !== -1) { |
||||
ui.tag.addClass('tag-metatag'); |
||||
} else { |
||||
console.log(ui.tagLabel); |
||||
// give special class to negative tags
|
||||
if(ui.tagLabel[0] === '-') { |
||||
ui.tag.addClass('tag-negative'); |
||||
}else{ |
||||
ui.tag.addClass('tag-positive'); |
||||
} |
||||
} |
||||
}, |
||||
autocomplete : ({ |
||||
source: function (request, response) { |
||||
var ac_metatags = $.map( |
||||
$.grep(metatags, function(s) { |
||||
// Only show metatags for strings longer than one character
|
||||
return (request.term.length > 1 && s.indexOf(request.term) === 0); |
||||
}), |
||||
function(item) { |
||||
return { |
||||
label : item + ' [metatag]', |
||||
value : item |
||||
}; |
||||
} |
||||
); |
||||
|
||||
var isNegative = (request.term[0] === '-'); |
||||
$.ajax({ |
||||
url: base_href + '/api/internal/autocomplete', |
||||
data: {'s': (isNegative ? request.term.substring(1) : request.term)}, |
||||
dataType : 'json', |
||||
type : 'GET', |
||||
success : function (data) { |
||||
response( |
||||
$.merge(ac_metatags, |
||||
$.map(data, function (count, item) { |
||||
item = (isNegative ? '-'+item : item); |
||||
return { |
||||
label : item + ' ('+count+')', |
||||
value : item |
||||
}; |
||||
}) |
||||
) |
||||
); |
||||
}, |
||||
error : function (request, status, error) { |
||||
console.log(error); |
||||
} |
||||
}); |
||||
}, |
||||
minLength: 1 |
||||
}) |
||||
}); |
||||
var isNegative = (request.term[0] === '-'); |
||||
var requestData = { |
||||
'query': (isNegative ? request.term.substring(1) : request.term), |
||||
'limit': limit, |
||||
'search_categories': search_categories |
||||
}; |
||||
|
||||
$('.ui-autocomplete-input').keydown(function(e) { |
||||
var keyCode = e.keyCode || e.which; |
||||
$.ajax({ |
||||
url: base_href + '/api/internal/tags/search', |
||||
data: requestData, |
||||
dataType : 'json', |
||||
type : 'GET', |
||||
success : function (data) { |
||||
var output = $.merge(ac_metatags, |
||||
$.map(data.tags, function (item, i) { |
||||
console.log(item); |
||||
var tag = (isNegative ? '-'+item.tag : item.tag); |
||||
return { |
||||
label : tag + ' ('+item.count+')', |
||||
value : tag |
||||
}; |
||||
}) |
||||
); |
||||
|
||||
//Stop tags containing space.
|
||||
if(keyCode == 32) { |
||||
e.preventDefault(); |
||||
response(output); |
||||
}, |
||||
error : function (request, status, error) { |
||||
console.log(error); |
||||
} |
||||
}); |
||||
}, |
||||
minLength: 1 |
||||
}) |
||||
}); |
||||
$(element).find('.ui-autocomplete-input').keydown(function(e) { |
||||
var keyCode = e.keyCode || e.which; |
||||
|
||||
$('.autocomplete_tags').tagit('createTag', $(this).val()); |
||||
$(this).autocomplete('close'); |
||||
} else if (keyCode == 9) { |
||||
e.preventDefault(); |
||||
//Stop tags containing space.
|
||||
if(keyCode == 32) { |
||||
e.preventDefault(); |
||||
|
||||
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first(); |
||||
if(tag.length){ |
||||
$(tag).click(); |
||||
$('.ui-autocomplete-input').val(''); //If tag already exists, make sure to remove duplicate.
|
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
$('.autocomplete_tags').tagit('createTag', $(this).val()); |
||||
$(this).autocomplete('close'); |
||||
} else if (keyCode == 9) { |
||||
e.preventDefault(); |
||||
|
||||
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first(); |
||||
if(tag.length){ |
||||
$(tag).click(); |
||||
$('.ui-autocomplete-input').val(''); //If tag already exists, make sure to remove duplicate.
|
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
Loading…
Reference in new issue