Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions app/Http/Controllers/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

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
{
Expand Down Expand Up @@ -41,4 +43,97 @@ public function index(Request $request)

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;
}
}
2,174 changes: 2,172 additions & 2 deletions public/css/app.css

Large diffs are not rendered by default.

4,636 changes: 4,635 additions & 1 deletion public/js/app.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion public/js/dummy.js

This file was deleted.

5 changes: 2 additions & 3 deletions public/mix-manifest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 98 additions & 1 deletion resources/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
$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,
},
success(data) {
const inputElement = $("#search-container input[name=q]")[0];
showAutocomplete(data, inputElement);
},
error() {
hideAutocomplete();
},
complete() {
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
Expand All @@ -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 () {
Expand All @@ -147,9 +229,24 @@ $.when($.ready).then(() => {
} else {
$("#search-container button").show();
items.show();
hideAutocomplete();
}
});

// Hide autocomplete when clicking outside
$(document).on("click", (e) => {
if (!$(e.target).closest("#search-container").length) {
hideAutocomplete();
}
});

// Hide autocomplete on Escape key
$(document).on("keydown", (e) => {
if (e.key === "Escape") {
hideAutocomplete();
}
});

$("#search-container select[name=provider]").trigger("change");

$("#app")
Expand Down
31 changes: 30 additions & 1 deletion resources/assets/sass/_app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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%;
Expand Down
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
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();
Expand Down
3 changes: 3 additions & 0 deletions storage/app/searchproviders.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ bing:
method: get
target: _blank
query: q
autocomplete: https://api.bing.com/osjson.aspx?query={query}

ddg:
id: ddg
Expand All @@ -26,6 +27,7 @@ ddg:
method: get
target: _blank
query: q
autocomplete: https://duckduckgo.com/ac/?q={query}&type=list

google:
id: google
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion webpack.mix.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down