mirror of
https://github.com/linuxserver/Heimdall.git
synced 2026-01-09 06:51:23 +08:00
Add autocomplete suggestions support and added to bing, duckduckgo, and google
This commit is contained in:
parent
045bdf0deb
commit
852c231724
@ -4,9 +4,11 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Search;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
@ -41,4 +43,97 @@ class SearchController extends Controller
|
||||
|
||||
abort(404, 'Provider type not supported');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get autocomplete suggestions for a search query
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function autocomplete(Request $request)
|
||||
{
|
||||
$requestprovider = $request->input('provider');
|
||||
$query = $request->input('q');
|
||||
|
||||
if (!$query || trim($query) === '') {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$provider = Search::providerDetails($requestprovider);
|
||||
|
||||
if (!$provider || !isset($provider->autocomplete)) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
// Replace {query} placeholder with actual query
|
||||
$autocompleteUrl = str_replace('{query}', urlencode($query), $provider->autocomplete);
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)->get($autocompleteUrl);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->body();
|
||||
|
||||
// Parse the response based on provider
|
||||
$suggestions = $this->parseAutocompleteResponse($data, $provider->id);
|
||||
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Return empty array on error
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse autocomplete response based on provider format
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $providerId
|
||||
* @return array
|
||||
*/
|
||||
private function parseAutocompleteResponse($data, $providerId)
|
||||
{
|
||||
$suggestions = [];
|
||||
|
||||
switch ($providerId) {
|
||||
case 'google':
|
||||
// Google returns XML format
|
||||
if (strpos($data, '<?xml') === 0) {
|
||||
$xml = simplexml_load_string($data);
|
||||
if ($xml && isset($xml->CompleteSuggestion)) {
|
||||
foreach ($xml->CompleteSuggestion as $suggestion) {
|
||||
if (isset($suggestion->suggestion['data'])) {
|
||||
$suggestions[] = (string) $suggestion->suggestion['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bing':
|
||||
case 'ddg':
|
||||
// Bing and DuckDuckGo return JSON array format
|
||||
$json = json_decode($data, true);
|
||||
if (is_array($json) && isset($json[1]) && is_array($json[1])) {
|
||||
$suggestions = $json[1];
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Try to parse as JSON array
|
||||
$json = json_decode($data, true);
|
||||
if (is_array($json)) {
|
||||
if (isset($json[1]) && is_array($json[1])) {
|
||||
$suggestions = $json[1];
|
||||
} else {
|
||||
$suggestions = $json;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
2174
public/css/app.css
vendored
2174
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
4636
public/js/app.js
vendored
4636
public/js/app.js
vendored
File diff suppressed because one or more lines are too long
1
public/js/dummy.js
vendored
1
public/js/dummy.js
vendored
File diff suppressed because one or more lines are too long
5
public/mix-manifest.json
generated
5
public/mix-manifest.json
generated
@ -1,5 +1,4 @@
|
||||
{
|
||||
"/js/dummy.js": "/js/dummy.js?id=daec5f3b283a510837bec36ca3868a54",
|
||||
"/css/app.css": "/css/app.css?id=8e5c9ae35dd160a37c9d33d663f996b9",
|
||||
"/js/app.js": "/js/app.js?id=19052619246fec368cad13937c62d850"
|
||||
"/css/app.css": "/css/app.css?id=18678c7bd2dc9b9f1d75e1aff3b7ea8e",
|
||||
"/js/app.js": "/js/app.js?id=43116113f2c9194304ab84d8205fc7f9"
|
||||
}
|
||||
|
||||
@ -108,11 +108,87 @@ $.when($.ready).then(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Autocomplete functionality
|
||||
let autocompleteTimeout = null;
|
||||
let currentAutocompleteRequest = null;
|
||||
|
||||
function hideAutocomplete() {
|
||||
$("#search-autocomplete").remove();
|
||||
}
|
||||
|
||||
function showAutocomplete(suggestions, inputElement) {
|
||||
hideAutocomplete();
|
||||
|
||||
if (!suggestions || suggestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $input = $(inputElement);
|
||||
const position = $input.position();
|
||||
const width = $input.outerWidth();
|
||||
|
||||
const $autocomplete = $('<div id="search-autocomplete"></div>');
|
||||
|
||||
suggestions.forEach((suggestion) => {
|
||||
const $item = $('<div class="autocomplete-item"></div>')
|
||||
.text(suggestion)
|
||||
.on('click', function() {
|
||||
$input.val(suggestion);
|
||||
hideAutocomplete();
|
||||
$input.closest('form').submit();
|
||||
});
|
||||
$autocomplete.append($item);
|
||||
});
|
||||
|
||||
$autocomplete.css({
|
||||
position: 'absolute',
|
||||
top: position.top + $input.outerHeight() + 'px',
|
||||
left: position.left + 'px',
|
||||
width: width + 'px'
|
||||
});
|
||||
|
||||
$input.closest('#search-container').append($autocomplete);
|
||||
}
|
||||
|
||||
function fetchAutocomplete(query, provider) {
|
||||
// Cancel previous request if any
|
||||
if (currentAutocompleteRequest) {
|
||||
currentAutocompleteRequest.abort();
|
||||
}
|
||||
|
||||
if (!query || query.trim().length < 2) {
|
||||
hideAutocomplete();
|
||||
return;
|
||||
}
|
||||
|
||||
currentAutocompleteRequest = $.ajax({
|
||||
url: base + 'search/autocomplete',
|
||||
method: 'GET',
|
||||
data: {
|
||||
q: query,
|
||||
provider: provider
|
||||
},
|
||||
success: function(data) {
|
||||
const inputElement = $("#search-container input[name=q]")[0];
|
||||
showAutocomplete(data, inputElement);
|
||||
},
|
||||
error: function() {
|
||||
hideAutocomplete();
|
||||
},
|
||||
complete: function() {
|
||||
currentAutocompleteRequest = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#search-container")
|
||||
.on("input", "input[name=q]", function () {
|
||||
const search = this.value;
|
||||
const items = $("#sortable").find(".item-container");
|
||||
if ($("#search-container select[name=provider]").val() === "tiles") {
|
||||
const provider = $("#search-container select[name=provider]").val();
|
||||
|
||||
if (provider === "tiles") {
|
||||
hideAutocomplete();
|
||||
if (search.length > 0) {
|
||||
items.hide();
|
||||
items
|
||||
@ -126,6 +202,12 @@ $.when($.ready).then(() => {
|
||||
}
|
||||
} else {
|
||||
items.show();
|
||||
|
||||
// Debounce autocomplete requests
|
||||
clearTimeout(autocompleteTimeout);
|
||||
autocompleteTimeout = setTimeout(() => {
|
||||
fetchAutocomplete(search, provider);
|
||||
}, 300);
|
||||
}
|
||||
})
|
||||
.on("change", "select[name=provider]", function () {
|
||||
@ -147,9 +229,24 @@ $.when($.ready).then(() => {
|
||||
} else {
|
||||
$("#search-container button").show();
|
||||
items.show();
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide autocomplete when clicking outside
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('#search-container').length) {
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide autocomplete on Escape key
|
||||
$(document).on('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
$("#search-container select[name=provider]").trigger("change");
|
||||
|
||||
$("#app")
|
||||
|
||||
@ -933,7 +933,6 @@ div.create {
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 5px 0 rgba(0,0,0,0.4);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@ -965,9 +964,39 @@ div.create {
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
border-right: 1px solid #ddd;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
#search-autocomplete {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
border-radius: 0 0 5px 5px;
|
||||
box-shadow: 0px 4px 8px 0 rgba(0,0,0,0.2);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
.autocomplete-item {
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
|
||||
@ -75,6 +75,7 @@ Route::post('test_config', [ItemController::class,'testConfig'])->name('test_con
|
||||
Route::get('get_stats/{id}', [ItemController::class,'getStats'])->name('get_stats');
|
||||
|
||||
Route::get('/search', [SearchController::class,'index'])->name('search');
|
||||
Route::get('/search/autocomplete', [SearchController::class,'autocomplete'])->name('search.autocomplete');
|
||||
|
||||
Route::get('view/{name_view}', function ($name_view) {
|
||||
return view('SupportedApps::'.$name_view)->render();
|
||||
|
||||
@ -18,6 +18,7 @@ bing:
|
||||
method: get
|
||||
target: _blank
|
||||
query: q
|
||||
autocomplete: https://api.bing.com/osjson.aspx?query={query}
|
||||
|
||||
ddg:
|
||||
id: ddg
|
||||
@ -26,6 +27,7 @@ ddg:
|
||||
method: get
|
||||
target: _blank
|
||||
query: q
|
||||
autocomplete: https://duckduckgo.com/ac/?q={query}&type=list
|
||||
|
||||
google:
|
||||
id: google
|
||||
@ -34,6 +36,7 @@ google:
|
||||
method: get
|
||||
target: _blank
|
||||
query: q
|
||||
autocomplete: https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q={query}
|
||||
|
||||
startpage:
|
||||
id: startpage
|
||||
|
||||
1
webpack.mix.js
vendored
1
webpack.mix.js
vendored
@ -12,7 +12,6 @@ const mix = require("laravel-mix");
|
||||
*/
|
||||
|
||||
mix
|
||||
.js("resources/assets/js/app.js", "public/js/dummy.js")
|
||||
.babel(
|
||||
[
|
||||
"node_modules/sortablejs/Sortable.min.js",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user