diff --git a/metager/app/Http/Controllers/SettingsController.php b/metager/app/Http/Controllers/SettingsController.php index 5b20e584282e79867777002990a05d1c6b2aa301..2636109a5f7a33217f6d6d158179371dad1e2a22 100644 --- a/metager/app/Http/Controllers/SettingsController.php +++ b/metager/app/Http/Controllers/SettingsController.php @@ -298,6 +298,15 @@ class SettingsController extends Controller } } + $tiles_startpage = $request->input('tiles_startpage', ''); + if (!empty($tiles_startpage)) { + if ($tiles_startpage === "off") { + Cookie::queue(Cookie::forever('tiles_startpage', 'off', '/', null, $secure, false)); + } elseif ($tiles_startpage === "on") { + Cookie::queue(Cookie::forget("tiles_startpage", "/")); + } + } + $quotes = $request->input('zitate', ''); if (!empty($quotes)) { if ($quotes === "off") { @@ -349,6 +358,7 @@ class SettingsController extends Controller "new_tab", "zitate", "self_advertisements", + "tiles_startpage", "suggestions", ]; diff --git a/metager/app/Http/Controllers/StartpageController.php b/metager/app/Http/Controllers/StartpageController.php index b294965987afa9dd6447188f078e43192e233d43..8a5b1bdd3fce697d1539dfc91e4be1f687e68ec2 100644 --- a/metager/app/Http/Controllers/StartpageController.php +++ b/metager/app/Http/Controllers/StartpageController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use Cache; use Illuminate\Http\Request; use Jenssegers\Agent\Agent; use Response; @@ -28,32 +29,17 @@ class StartpageController extends Controller return redirect(route("resultpage", ["eingabe" => $eingabe])); } - $optionParams = ['param_sprueche', 'param_newtab', 'param_maps', 'param_autocomplete', 'param_lang', 'param_key']; - $option_values = []; - - foreach ($optionParams as $param) { - $value = $request->input($param); - if ($value) { - $option_values[$param] = $value; - } - } - - $autocomplete = 'on'; - if (in_array('autocomplete', array_keys($option_values))) { - $autocomplete = $option_values['autocomplete']; - } + $ckey = hash_hmac("sha256", $request->ip() . now()->format("Y-m-d"), config("metager.taketiles.secret")); + Cache::put($ckey, "1", now()->addSeconds(TilesController::CACHE_DURATION_SECONDS)); + $tiles = TilesController::TILES($ckey); + $tiles_update_url = route('tiles', ["ckey" => $ckey]); return view('index') ->with('title', trans('titles.index')) - ->with('homeIcon') - ->with('agent', new Agent()) - ->with('navbarFocus', 'suche') ->with('focus', $request->input('focus', 'web')) - ->with('time', $request->input('param_time', '1500')) ->with('request', $request->input('request', 'GET')) - ->with('option_values', $option_values) - ->with('autocomplete', $autocomplete) - ->with('pluginmodal', $request->input('plugin-modal', 'off')) + ->with('tiles_update_url', $tiles_update_url) + ->with('tiles', $tiles) ->with('css', [mix('css/themes/startpage/light.css')]) ->with('js', [mix('js/startpage/app.js')]) ->with('darkcss', [mix('css/themes/startpage/dark.css')]); diff --git a/metager/app/Http/Controllers/StatisticsController.php b/metager/app/Http/Controllers/StatisticsController.php index e66c903cf3c17d5e83c47d65e3cc1b7ee6087d09..dc0c252c26c2e191b82ec8862aa512825140d6ca 100644 --- a/metager/app/Http/Controllers/StatisticsController.php +++ b/metager/app/Http/Controllers/StatisticsController.php @@ -10,9 +10,6 @@ class StatisticsController extends Controller { public function pageLoad(Request $request) { - if (!config("metager.matomo.enabled") || config("metager.matomo.url") === null) - return; - $params = [ "idsite" => config("metager.matomo.site_id"), "token_auth" => config("metager.matomo.token_auth"), @@ -30,6 +27,24 @@ class StatisticsController extends Controller $params["lang"] = $request->header("Accept-Language"); $params = array_merge($http_params, $params); // Merge arrays keeping our serverside defined options if key is set multiple times + self::LOG_STATISTICS($params); + } + + public static function LOG_STATISTICS(array $params) + { + if (!config("metager.matomo.enabled") || config("metager.matomo.url") === null) + return; + + $params = array_merge($params, [ + "idsite" => config("metager.matomo.site_id"), + "token_auth" => config("metager.matomo.token_auth"), + "rand" => md5(microtime(true)), + "rec" => "1", + "send_image" => "0", + "cip" => \Request::ip(), + "_id" => substr(md5(\Request::ip() . now()->format("Y-m-d")), 0, 16) + ]); + $url = config("metager.matomo.url") . "/matomo.php?" . http_build_query($params); // Submit fetch job to worker @@ -47,4 +62,5 @@ class StatisticsController extends Controller Redis::rpush(\App\MetaGer::FETCHQUEUE_KEY, $mission); } + } diff --git a/metager/app/Http/Controllers/TilesController.php b/metager/app/Http/Controllers/TilesController.php new file mode 100644 index 0000000000000000000000000000000000000000..c9268c22ad1762ca1a1ca314231f7dee936600c6 --- /dev/null +++ b/metager/app/Http/Controllers/TilesController.php @@ -0,0 +1,139 @@ +<?php + +namespace App\Http\Controllers; + +use App\Localization; +use App\Models\Authorization\Authorization; +use Exception; +use Illuminate\Support\Facades\Redis; +use App\Models\Tile; +use App\SearchSettings; +use Cache; +use Illuminate\Http\Request; +use Log; + +class TilesController extends Controller +{ + const CACHE_DURATION_SECONDS = 300; + + public function loadTakeTiles(Request $request) + { + if (!$request->filled("ckey") || !Cache::has($request->input("ckey"))) { + abort(404); + } + $ckey = $request->input("ckey"); + $count = $request->input("count", 4); + $tiles = []; + $tiles = self::TAKE_TILES($ckey, $count); + StatisticsController::LOG_STATISTICS([ + "e_c" => "Take Tiles", + "e_a" => "Load", + "e_n" => "Take Tiles", + "e_v" => sizeof($tiles), + ]); + return response()->json($tiles); + } + + /** + * Generate Tiles for a given request + * This includes static TIles and SUMA Tiles + * Take Tiles are generated asynchroniously + * + * @param string $ckey + * @return array + */ + public static function TILES(string $ckey): array + { + // Check if the user has disabled tiles + if (!app(SearchSettings::class)->tiles_startpage) + return []; + $tiles = self::STATIC_TILES(); + $tiles = array_merge($tiles, self::SUMA_TILES()); + return $tiles; + } + + /** + * Generates Static Tiles + * @return Tile[] + */ + private static function STATIC_TILES(): array + { + $tiles = []; + $tiles[] = new Tile(title: "Unser Trägerverein", image: "/img/tiles/sumaev.png", url: "https://suma-ev.de", image_alt: "SUMA_EV Logo"); + //$tiles[] = new Tile(title: "Maps", image: "/img/tiles/maps.png", url: "https://maps.metager.de", image_alt: "MetaGer Maps Logo"); + $tiles[] = new Tile(title: __('sidebar.nav28'), image: "/img/icon-settings.svg", url: route("settings", ["focus" => app(SearchSettings::class)->fokus, "url" => url()->full()]), image_alt: "Settings Logo", image_classes: "invert-dm"); + $tiles[] = new Tile(title: __('index.plugin'), image: "/img/svg-icons/plug-in.svg", url: route("plugin"), image_alt: "MetaGer Plugin Logo", classes: "orange"); + return $tiles; + } + + /** + * Generates dynamic Tiles booked through SUMA-EV + * + * @return array + */ + private static function SUMA_TILES(): array + { + $tiles = []; + return $tiles; + } + + /** + * Generates Tile ads from Takeads + * + * @return array + */ + private static function TAKE_TILES(string $ckey, int $count): array + { + $tiles = []; + $result_cache_key = "taketiles:fetch:$ckey:$count"; + + $result = Cache::get($result_cache_key); + if ($result === null) { + $supported_countries = ["US", "GB", "DE", "AT", "CH", "TR"]; + if (!config("metager.taketiles.enabled") || !in_array(Localization::getRegion(), $supported_countries)) { + return $tiles; + } + if (app(Authorization::class)->canDoAuthenticatedSearch(false)) + return $tiles; + + $endpoint = config("metager.taketiles.endpoint"); + $params = [ + "count" => $count, + "deviceId" => $ckey, + "countryCode" => Localization::getLanguage() + ]; + $mission = [ + "resulthash" => $result_cache_key, + "url" => $endpoint . "?" . http_build_query($params), + "useragent" => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0", + "headers" => [ + "Content-Type" => "application/json", + "Authorization" => "Bearer " . config("metager.taketiles.public_key"), + ], + "cacheDuration" => ceil(self::CACHE_DURATION_SECONDS / 60), + "name" => "Take Tiles", + ]; + $mission = json_encode($mission); + Redis::rpush(\App\MetaGer::FETCHQUEUE_KEY, $mission); + Cache::put($ckey, "1", now()->addSeconds(self::CACHE_DURATION_SECONDS)); + $result = Redis::blpop($result_cache_key, 0); + if (sizeof($result) === 2) { + $result = $result[1]; + } + } + + if ($result !== null) { + try { + $result = json_decode($result); + foreach ($result->data as $result_tile) { + $tiles[] = new Tile(title: $result_tile->title, image: $result_tile->image, image_alt: $result_tile->title . " Image", url: $result_tile->url, advertisement: true); + } + } catch (Exception $e) { + Log::error($e); + } + + } + + return $tiles; + } +} diff --git a/metager/app/Models/Tile.php b/metager/app/Models/Tile.php new file mode 100644 index 0000000000000000000000000000000000000000..54eb28758c95567f16ccc0e1b9cb77b7f7bab665 --- /dev/null +++ b/metager/app/Models/Tile.php @@ -0,0 +1,54 @@ +<?php + +namespace App\Models; + +use App\Http\Controllers\Pictureproxy; +use Exception; +use Request; + +/** + * Model to hold data which represents a tile as visible on the startpage + */ +class Tile implements \JsonSerializable +{ + public string $title; + public string $image; + public string $image_alt; + public string $url; + public string $classes = ""; + public string $image_classes = ""; + public bool $advertisement = false; + /** + * + * @param string $title Title to show for the Tile + * @param string $image URL to a image to show for the Tile + * @param string $image_alt Alt Text to show for the image + * @param string $url URL to link to when the user clicks on the Tile + * @param string $classes Additional css classes to append for the tile + * @param string $image_classes Additional css classes to append to the image of the tile + */ + public function __construct(string $title, string $image, string $image_alt, string $url, string $classes = "", string $image_classes = "", bool $advertisement = false) + { + $this->title = $title; + try { + $host = parse_url($image, PHP_URL_HOST); + if ($host !== null && Request::host() !== $host) { + $image = Pictureproxy::generateUrl($image); + } + } catch (Exception $e) { + } + $this->image = $image; + $this->image_alt = $image_alt; + $this->url = $url; + $this->classes = $classes; + $this->image_classes = $image_classes; + $this->advertisement = $advertisement; + } + + public function jsonSerialize() + { + $json = get_object_vars($this); + $json["html"] = view("parts.tile", ["tile" => $this])->render(); + return $json; + } +} \ No newline at end of file diff --git a/metager/app/SearchSettings.php b/metager/app/SearchSettings.php index 1bd7ef1ad1131533fa4f98e2b7df641eb6580c84..43086cc014c6e00ad659c67660e145e18353405a 100644 --- a/metager/app/SearchSettings.php +++ b/metager/app/SearchSettings.php @@ -30,6 +30,8 @@ class SearchSettings public $enableQuotes = true; /** @var bool */ public $self_advertisements; + /** @var bool */ + public $tiles_startpage; /** @var string */ public $suggestions = "bing"; public $external_image_search = "metager"; @@ -75,6 +77,9 @@ class SearchSettings $this->self_advertisements = $this->getSettingValue("self_advertisements", true); $this->self_advertisements = $this->self_advertisements !== "off" ? true : false; + $this->tiles_startpage = $this->getSettingValue("tiles_startpage", true); + $this->tiles_startpage = $this->tiles_startpage !== "off" ? true : false; + $suggestions = $this->getSettingValue("suggestions", "bing"); if ($suggestions === "off") { $this->suggestions = "off"; diff --git a/metager/config/metager/taketiles.php b/metager/config/metager/taketiles.php new file mode 100644 index 0000000000000000000000000000000000000000..8278726608c095aeb52377a4eeffe193a862ca21 --- /dev/null +++ b/metager/config/metager/taketiles.php @@ -0,0 +1,8 @@ +<?php +return [ + "enabled" => env("TAKE_TILES_ENABLED", false), + "secret" => env("TAKE_TILES_SECRET", "12345"), + "endpoint" => env("TAKE_TILES_ENDPOINT", ""), + "public_key" => env("TAKE_TILES_PUBLIC_KEY", ""), + +]; \ No newline at end of file diff --git a/metager/lang/de/index.php b/metager/lang/de/index.php index 5091418b23abb56eea780daf7d136fc5c55b05f8..d4320124af90774d2e93def52422534fe626098f 100644 --- a/metager/lang/de/index.php +++ b/metager/lang/de/index.php @@ -1,6 +1,6 @@ <?php return [ - 'plugin' => 'MetaGer installieren', + 'plugin' => 'MetaGer Installieren', 'plugin-title' => 'MetaGer zu Ihrem Browser hinzufügen', 'key' => [ 'placeholder' => 'Schlüssel für werbefreie Suche eingeben', diff --git a/metager/lang/de/settings.php b/metager/lang/de/settings.php index 46d875fb93afbe521f94fc42fd1a1aae5d399327..d8a7e0a8cedb441214d9034e91d634c400f3eea0 100644 --- a/metager/lang/de/settings.php +++ b/metager/lang/de/settings.php @@ -88,4 +88,7 @@ return [ 'self_advertisements' => [ 'label' => "Subtile Werbung für unseren eigenen Service", ], + 'tiles_startpage' => [ + 'label' => "Zeige Kacheln auf der Startseite", + ], ]; diff --git a/metager/lang/de/tiles.php b/metager/lang/de/tiles.php new file mode 100644 index 0000000000000000000000000000000000000000..e3f4774253e5e5d8fb6d9aa4cb01faddc1858a3e --- /dev/null +++ b/metager/lang/de/tiles.php @@ -0,0 +1,5 @@ +<?php + +return [ + "sponsored" => "Gesponsert" +]; \ No newline at end of file diff --git a/metager/lang/en/settings.php b/metager/lang/en/settings.php index 5f8aaea5050d84550080f74f211d0ee47027529d..ec48e3f697e3535406a2e590ec3d42fc05c220aa 100644 --- a/metager/lang/en/settings.php +++ b/metager/lang/en/settings.php @@ -37,6 +37,9 @@ return [ 'self_advertisements' => [ "label" => "Subtle advertisements for our own service", ], + 'tiles_startpage' => [ + 'label' => "Show tiles on the startpage", + ], 'system' => 'System Default', 'dark' => 'Dark', 'light' => 'Light', diff --git a/metager/lang/en/tiles.php b/metager/lang/en/tiles.php new file mode 100644 index 0000000000000000000000000000000000000000..1d11df8e5f427243ea06e352f4113857a0e4753a --- /dev/null +++ b/metager/lang/en/tiles.php @@ -0,0 +1,5 @@ +<?php + +return [ + "sponsored" => "Sponsored" +]; \ No newline at end of file diff --git a/metager/public/img/svg-icons/plug-in.svg b/metager/public/img/svg-icons/plug-in.svg index ea5c6c048f0f3174c6699d188e64d060a99669cb..662dc035f4349fdf16ce941da89191cf49b2f9c2 100644 --- a/metager/public/img/svg-icons/plug-in.svg +++ b/metager/public/img/svg-icons/plug-in.svg @@ -1,16 +1,39 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" id="svg8" version="1.1" viewBox="0 0 15.681789 15.681206" height="59.267548" - width="59.269756"> + width="59.269756" + sodipodi:docname="plug-in.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <sodipodi:namedview + id="namedview12" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="15.219121" + inkscape:cx="23.851574" + inkscape:cy="29.666627" + inkscape:window-width="2560" + inkscape:window-height="1371" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="layer1" /> <defs id="defs2"> <linearGradient @@ -18,11 +41,11 @@ <stop id="stop2456" offset="0" - style="stop-color:#666666;stop-opacity:1;" /> + style="stop-color:#d9d9d9;stop-opacity:1;" /> <stop id="stop2458" offset="1" - style="stop-color:#666666;stop-opacity:0.5" /> + style="stop-color:#e5e5e5;stop-opacity:0.43157893;" /> </linearGradient> <linearGradient gradientTransform="matrix(0.22994292,0,0,0.22994292,1.7319905,272.27393)" @@ -42,7 +65,7 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> + <dc:title /> </cc:Work> </rdf:RDF> </metadata> @@ -60,6 +83,6 @@ <path id="rect845" d="m 13.017158,277.28013 c -0.0053,0 -0.0099,0.005 -0.0099,0.01 v 6.25921 H 6.7480696 c -0.00536,0 -0.00988,0.005 -0.00988,0.01 v 0.42396 c 0,0.005 0.00452,0.01 0.00988,0.01 h 6.2592084 v 6.2592 c 0,0.005 0.0045,0.009 0.0099,0.009 h 0.423958 c 0.0053,0 0.0099,-0.004 0.0099,-0.009 v -6.2592 h 6.259208 c 0.0053,0 0.0099,-0.005 0.0099,-0.01 v -0.42396 c 0,-0.005 -0.0045,-0.01 -0.0099,-0.01 h -6.259208 v -6.25921 c 0,-0.005 -0.0045,-0.01 -0.0099,-0.01 z" - style="fill:none;fill-opacity:1;stroke:url(#linearGradient2462);stroke-width:2.69983554;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;fill-opacity:1;stroke:url(#linearGradient2462);stroke-width:2.69980833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> </g> </svg> diff --git a/metager/public/img/tiles/heart.svg b/metager/public/img/tiles/heart.svg new file mode 100644 index 0000000000000000000000000000000000000000..c2aeb55c23c856336066c073cfc7bcecb87009ae --- /dev/null +++ b/metager/public/img/tiles/heart.svg @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + id="svg10" + xml:space="preserve" + viewBox="0 0 176.29866 426.0522" + version="1.1" + height="80" + width="80" + sodipodi:docname="heart.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview + id="namedview16" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="13.724014" + inkscape:cx="37.379736" + inkscape:cy="28.67237" + inkscape:window-width="2560" + inkscape:window-height="1371" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg10" /><defs + id="defs14"><linearGradient + xlink:href="#linearGradient6206" + id="linearGradient6210" + gradientUnits="userSpaceOnUse" + x1="-293.63593" + y1="3.2192702" + x2="-258.68027" + y2="13.229791" + gradientTransform="matrix(0,-0.91412987,0.91412987,0,0.02226508,24.582359)" /><linearGradient + id="linearGradient6206"><stop + style="stop-color:#ffec00;stop-opacity:1" + offset="0" + id="stop6202" /><stop + style="stop-color:#ff0000;stop-opacity:1" + offset="1" + id="stop6204" /></linearGradient></defs><metadata + id="metadata2"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><g + id="g8" + transform="matrix(6.4131855,0,0,-6.4131855,-1935.3288,416.0147)"><g + id="layer1" + transform="matrix(2.0600602,0,0,-1.954417,280.47242,579.78551)"><g + id="g61" + transform="rotate(-45,12.388336,277.04149)"><g + id="g112"><g + id="g120"><g + id="g125"><path + id="path26-6" + style="fill:url(#linearGradient6210);fill-opacity:1;stroke-width:0.385146" + d="m 12.115996,264.57061 a 9.1509008,10.131353 0 0 1 9.1509,10.13136 9.1509008,10.131353 0 0 1 -9.1509,10.13135 9.1509008,10.131353 0 0 1 -9.1509012,-10.13135 9.1509008,10.131353 0 0 1 9.1509012,-10.13136 z m 19.282252,19.28223 a 10.131353,9.1508998 0 0 1 -10.131354,9.15089 10.131353,9.1508998 0 0 1 -10.131352,-9.15089 10.131353,9.1508998 0 0 1 10.131352,-9.15091 10.131353,9.1508998 0 0 1 10.131354,9.15091 z M 2.9650961,274.70197 H 21.266896 v 18.30177 H 2.9650961 Z" /></g></g></g></g></g></g></svg> diff --git a/metager/public/img/tiles/maps.png b/metager/public/img/tiles/maps.png new file mode 100644 index 0000000000000000000000000000000000000000..c45398faa74d8432ed3a4934e0a30f24f3921517 Binary files /dev/null and b/metager/public/img/tiles/maps.png differ diff --git a/metager/public/img/tiles/sumaev.png b/metager/public/img/tiles/sumaev.png new file mode 100644 index 0000000000000000000000000000000000000000..c0ceb801eaf3bb4e98e22352441ad2e17b0a16b5 Binary files /dev/null and b/metager/public/img/tiles/sumaev.png differ diff --git a/metager/resources/js/startpage/app.js b/metager/resources/js/startpage/app.js index 9241a27bb211cab0a725e778f4f24048e5384a96..a8845d071433244cc84e33858e36797784774ff4 100644 --- a/metager/resources/js/startpage/app.js +++ b/metager/resources/js/startpage/app.js @@ -1,5 +1,7 @@ +import "./tiles"; + // Register Keyboard listener for quicklinks on startpage -(() => { +(async () => { let sidebar_toggle = document.querySelector("#sidebarToggle"); let skip_links_container = document.querySelector(".skiplinks"); @@ -28,4 +30,4 @@ }, { once: true } ); -})(); +})(); \ No newline at end of file diff --git a/metager/resources/js/startpage/tiles.js b/metager/resources/js/startpage/tiles.js new file mode 100644 index 0000000000000000000000000000000000000000..891eb2821a5dafda358c0f8c5833cc98668f7cd2 --- /dev/null +++ b/metager/resources/js/startpage/tiles.js @@ -0,0 +1,97 @@ +import { statistics } from "../statistics"; + +(async () => { + let tile_container = document.querySelector("#tiles"); + let tile_count = tile_container.querySelectorAll("a").length; + + let advertisements = []; + let fetch_timeout = null; + fetchAdvertisements().then(() => udpateInterface()); + + async function fetchAdvertisements() { + let desired_tile_count = calculateDesiredTileCount(); + let regular_tile_count = getRegularTileCount(); + console.log(desired_tile_count, advertisements.length) + if (advertisements.length >= desired_tile_count - regular_tile_count) return; + let update_url = document.querySelector("meta[name=tiles-update-url]").content; + update_url += "&count=" + (desired_tile_count - tile_count); + return new Promise((resolve, reject) => { + if (fetch_timeout != null) return resolve(); + fetch_timeout = setTimeout(() => { + fetch(update_url).then(response => response.json()).then(response => { + advertisements = response; + fetch_timeout = null; + resolve(); + }).catch(e => reject(e)); + }, 500); + }); + } + function udpateInterface() { + let desired_tile_count = calculateDesiredTileCount(); + let regular_tile_count = getRegularTileCount(); + + if (document.querySelectorAll("#tiles > a").length == desired_tile_count) return; + document.querySelectorAll("#tiles >a.advertisement").forEach(element => { + console.log("remove"); + element.remove(); + }); + for (let i = 0; i < desired_tile_count - regular_tile_count; i++) { + if (advertisements.length < i + 1) continue; + let container = document.createElement("div"); + container.innerHTML = advertisements[i].html; + + container.firstChild.addEventListener("click", e => { + statistics.takeTilesClick(e.target.closest("a").href); + }); + + tile_container.appendChild(container.firstChild); + } + } + function getRegularTileCount() { + return tile_container.querySelectorAll("a:not(.advertisement)").length; + } + function calculateDesiredTileCount() { + let max_tiles = 8; + let native_tile_count = getRegularTileCount(); + let min_advertisements = 2; + + let tile_width = parseFloat(window.getComputedStyle(document.querySelector("#tiles")).getPropertyValue("--tile-width").replace("px", "")); + let tile_gap = parseFloat(window.getComputedStyle(document.querySelector("#tiles"))["column-gap"].replace("px", "")); + let client_width = document.querySelector("html").clientWidth; + let client_height = document.querySelector("html").clientHeight; + + let desired_tile_count = 8; + + if (client_width > 9 * tile_width + 8 * tile_gap) { + // Largest Screen Size => Up to 8 Tiles in one row + desired_tile_count = max_tiles; + } else if (client_width > 8 * tile_width + 7 * tile_gap) { + // Large Screen => Up to 7 Tiles in one row => Just Fill up + desired_tile_count = 7; + } else if (client_width > 6 * tile_width + 5 * tile_gap) { + desired_tile_count = 5; + } else if (client_width > 4 * tile_width + 3 * tile_gap) { + desired_tile_count = 3; + } else { + desired_tile_count = 2; + } + + console.log(client_width, tile_width, tile_gap, desired_tile_count); + console.log("Six Tiles", tile_width * 6 + 5 * tile_gap); + if (native_tile_count + min_advertisements > desired_tile_count) { + // Allow 2x3 Tiles on small displays + if (desired_tile_count == 2 && client_height > 850) { + desired_tile_count *= 3; + } else { + desired_tile_count *= 2; + } + } + + + return desired_tile_count; + } + window.addEventListener("resize", e => { + fetchAdvertisements().then(() => udpateInterface()); + }) +})(); + diff --git a/metager/resources/js/statistics.js b/metager/resources/js/statistics.js index 4b2f8df60f80003e83bbeb477b2ff6ee446aca87..02707a0a89d89d0aa7a47f9aaa6ebab07bee3ffa 100644 --- a/metager/resources/js/statistics.js +++ b/metager/resources/js/statistics.js @@ -7,6 +7,18 @@ class Statistics { #load_time = new Date(); constructor() { + + } + + #init() { + setTimeout(this.pageLoad.bind(this), 60000); + document.addEventListener("visibilitychange", this.pageLoad.bind(this)); + document.querySelectorAll("a").forEach(anchor => { + anchor.addEventListener("click", e => this.pageLeave(e.target.closest("a").href)); + }); + } + + registerPageLoadEvents() { let performance = window.performance.getEntriesByType('navigation')[0]; try { let statistics_enabled = document.querySelector("meta[name=statistics-enabled]").content; @@ -26,14 +38,6 @@ class Statistics { } } - #init() { - setTimeout(this.pageLoad.bind(this), 60000); - document.addEventListener("visibilitychange", this.pageLoad.bind(this)); - document.querySelectorAll("a").forEach(anchor => { - anchor.addEventListener("click", e => this.pageLeave(e.target.closest("a").href)); - }); - } - pageLeave(target) { let params = {}; @@ -96,5 +100,17 @@ class Statistics { navigator.sendBeacon("/stats/pl", new URLSearchParams(params)); } + + takeTilesClick(url) { + let params = {}; + if (this.#load_complete && !overwrite_params.hasOwnProperty("link")) return; + + params.e_c = "Take Tiles"; + params.e_a = "Click"; + params.e_n = "Take Tiles"; + params.e_v = url; + + navigator.sendBeacon("/stats/pl", new URLSearchParams(params)); + } } export const statistics = new Statistics(); \ No newline at end of file diff --git a/metager/resources/js/utility.js b/metager/resources/js/utility.js index 1ffad047fc834b7ae96c4dbf10c64ebbb28b3ed4..510896a17407a357d226cdd09bfaefd607729766 100644 --- a/metager/resources/js/utility.js +++ b/metager/resources/js/utility.js @@ -73,4 +73,8 @@ function backButtons() { history.back(); }); }); -} \ No newline at end of file +} + +(async () => { + statistics.registerPageLoadEvents(); +})(); \ No newline at end of file diff --git a/metager/resources/less/metager/pages/startpage/startpage.less b/metager/resources/less/metager/pages/startpage/startpage.less index 5dc355c4f2c068e56ac46e9ee21c440b40ee3687..807cee36cda98706fa9a21ea7decf35415235b3f 100644 --- a/metager/resources/less/metager/pages/startpage/startpage.less +++ b/metager/resources/less/metager/pages/startpage/startpage.less @@ -2,6 +2,7 @@ @scrollLinkHeight: 40px; @scrollLinkHeightMedium: 50px; @scrollLinkHeightMax: 70px; +@import "./tiles.less"; html { scroll-behavior: smooth; @@ -62,7 +63,6 @@ div.startpage { } >div#search-wrapper { - flex-grow: 1; display: grid; grid-template-rows: 20dvh max-content max-content max-content 1fr; diff --git a/metager/resources/less/metager/pages/startpage/tiles.less b/metager/resources/less/metager/pages/startpage/tiles.less new file mode 100644 index 0000000000000000000000000000000000000000..9ab89b38150099f92081e31c2b7dc778572b25fd --- /dev/null +++ b/metager/resources/less/metager/pages/startpage/tiles.less @@ -0,0 +1,127 @@ +div#tiles-container { + display: flex; + flex-grow: 1; + padding-inline: 1rem; + + + div#tiles { + flex-grow: 0; + width: 100%; + @tile_width: 115px; + --tile-width: @tile_width; // Variable is used by JS + @tile_gap: 16px; + + @nine_row_width: calc(calc(9 * @tile_width) + calc(8 * @tile_gap)); + @eight_row_width: calc(calc(8 * @tile_width) + calc(7 * @tile_gap)); + @seven_row_width: calc(calc(7 * @tile_width) + calc(6 * @tile_gap)); + @six_row_width: calc(calc(6 * @tile_width) + calc(5 * @tile_gap)); + @five_row_width: calc(calc(5 * @tile_width) + calc(4 * @tile_gap)); + @four_row_width: calc(calc(4 * @tile_width) + calc(3 * @tile_gap)); + @three_row_width: calc(calc(3 * @tile_width) + calc(2 * @tile_gap)); + @two_row_width: calc(calc(2 * @tile_width) + calc(1 * @tile_gap)); + + @media(max-width: @nine_row_width) { + width: @seven_row_width; + } + + @media(max-width: @eight_row_width) { + width: @five_row_width; + } + + @media(max-width: @six_row_width) { + width: @three_row_width; + } + + @media(max-width: @four_row_width) { + width: 100%; + } + + display: flex; + flex-wrap: wrap; + justify-content: center; + + padding-top: 8dvh; + gap: @tile_gap; + margin-inline: auto; + height: max-content; + + >a { + width: @tile_width; + + @media(max-width: @four_row_width) { + width: calc(50% - @tile_gap); + } + + display: flex; + row-gap: .5rem; + flex-direction: column; + align-items: center; + justify-content: flex-end; + color: @text-color; + + &.skeleton { + display: none; + } + + >div.image { + @image_size: 4rem; + width: @image_size; + height: @image_size; + padding: 0.5rem; + background-color: fade(@highlight-color, 60%); + border-radius: 5px; + transition: background-color 0.5s; + + >img { + width: 100%; + height: 100%; + + &.invert-dm { + filter: invert(@icon-color); + } + } + } + + &.orange>div.image { + background-color: fade(@metager-orange, 60%); + } + + >div.title { + font-size: .8rem; + color: fade(@text-color, 60%); + width: @tile_width; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + text-align: center; + line-height: 1; + + >div.main-title { + transition: color 0.5s; + } + + >div.sub-title { + height: 1em; + } + } + + &:hover { + >div.image { + background-color: @highlight-color; + } + + &.orange>div.image { + background-color: fade(@metager-orange, 80%); + } + + >div.title { + >div.main-title { + color: @text-color; + } + + + } + } + } + } +} \ No newline at end of file diff --git a/metager/resources/views/index.blade.php b/metager/resources/views/index.blade.php index 98a193f4144636c5dda0c5aa320db5d83a2b095c..7e36b9c93a2510224003821a6cf68d13371b732e 100644 --- a/metager/resources/views/index.blade.php +++ b/metager/resources/views/index.blade.php @@ -35,36 +35,24 @@ @if(Request::filled('key')) <input type="hidden" name="key" value="{{ Request::input('key', '') }}" form="searchForm"> @endif - @if(app(\App\SearchSettings::class)->self_advertisements) + @if(app(\App\Models\Authorization\Authorization::class)->availableTokens >= 0 && !app(\App\Models\Authorization\Authorization::class)->canDoAuthenticatedSearch(false)) <div id="startpage-quicklinks"> - @if(app(\App\Models\Authorization\Authorization::class)->availableTokens < 0) - <a class="metager-key no-key" href="{{ app(\App\Models\Authorization\Authorization::class)->getAdfreeLink() }}"> - <img src="/img/svg-icons/metager-lock.svg" alt="Key Icon" /> - <span> - @lang("index.adfree") - </span> - </a> - @elseif(!app(\App\Models\Authorization\Authorization::class)->canDoAuthenticatedSearch(false)) <a class="metager-key" href="{{ app(\App\Models\Authorization\Authorization::class)->getAdfreeLink() }}"> <img src="/img/svg-icons/key-empty.svg" alt="Key Icon" /> <span> - @lang("index.key.tooltip.empty") + @lang("index.key.tooltip.empty") </span> </a> - @endif - @if($agent->isMobile() && ($agent->browser() === "Chrome" || $agent->browser() === "Edge")) - <button type="submit" id="plugin-btn" form="searchForm" title="{{ trans('index.plugin-title') }}" - name="chrome-plugin" value="true"><img src="/img/svg-icons/svg-icons/plug-in.svg" alt="+"> - {{ trans('index.plugin') }}</a> - @else - <a id="plugin-btn" - href="{{ LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), '/plugin') }}" - title="{{ trans('index.plugin-title') }}"><img src="/img/svg-icons/plug-in.svg" alt="+"> - {{ trans('index.plugin') }}</a> - @endif </div> @endif </div> + <div id="tiles-container"> + <div id="tiles"> + @foreach($tiles as $tile) + @include("parts.tile", ["tile" => $tile]) + @endforeach + </div> + </div> <div id="language"> <a href="{{ route('lang-selector') }}">{{ LaravelLocalization::getCurrentLocaleNative() }}</a> </div> diff --git a/metager/resources/views/layouts/staticPages.blade.php b/metager/resources/views/layouts/staticPages.blade.php index ba139a47834a71ad854f4cd7bb0d607479f86496..b3521657479efb0dd5308079648f1e84fec8c28f 100644 --- a/metager/resources/views/layouts/staticPages.blade.php +++ b/metager/resources/views/layouts/staticPages.blade.php @@ -12,6 +12,9 @@ <meta name="audience" content="all" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <meta name="statistics-enabled" content="{{ config("metager.matomo.enabled") }}"> + @if(isset($tiles_update_url)) + <meta name="tiles-update-url" content="{{ $tiles_update_url }}"> + @endif <link href="/favicon.ico" rel="icon" type="image/x-icon" /> <link href="/favicon.ico" rel="shortcut icon" type="image/x-icon" /> @foreach(LaravelLocalization::getSupportedLocales() as $locale => $locale_data) diff --git a/metager/resources/views/parts/tile.blade.php b/metager/resources/views/parts/tile.blade.php new file mode 100644 index 0000000000000000000000000000000000000000..ebe3a635cc44c90f0f42722f23669cea51ce514d --- /dev/null +++ b/metager/resources/views/parts/tile.blade.php @@ -0,0 +1,13 @@ +<a href="{{$tile->url}}" class="{{ $tile->advertisement ? "advertisement " : "" }}{{ $tile->classes }}" {{ $tile->advertisement ? 'rel=nofollow target=_blank' : ''}}> + <div class="image"> + <img src="{{$tile->image}}" alt="{{$tile->image_alt}}" class="{{$tile->image_classes}}"> + </div> + <div class="title"> + <div class="main-title">{{$tile->title}}</div> + <div class="sub-title"> + @if($tile->advertisement) + @lang('tiles.sponsored') + @endif + </div> + </div> +</a> \ No newline at end of file diff --git a/metager/resources/views/settings/index.blade.php b/metager/resources/views/settings/index.blade.php index bc17de0b7a9c7744dd4659e551d01644b9362de4..c361b8bb39a0279a1b6a337cd16ffc2a4e9e7073 100644 --- a/metager/resources/views/settings/index.blade.php +++ b/metager/resources/views/settings/index.blade.php @@ -225,6 +225,16 @@ @lang('settings.suggestions.on')</option> </select> </div> + <div class="form-group"> + <label for="tiles_startpage">@lang('settings.tiles_startpage.label')</label> + <select name="tiles_startpage" id="tiles_startpage" class="form-control"> + <option value="off" + {{ app(App\SearchSettings::class)->tiles_startpage === false ? 'disabled selected' : '' }}> + @lang('settings.suggestions.off')</option> + <option value="on" {{ app(App\SearchSettings::class)->tiles_startpage === true ? 'disabled selected' : '' }}> + @lang('settings.suggestions.on')</option> + </select> + </div> <div class="form-group"> <label for="dm">@lang('settings.darkmode')</label> <select name="dm" id="dm" class="form-control"> diff --git a/metager/routes/web.php b/metager/routes/web.php index 6c6995dce0692e225a921a98cc6f97c9c9e9a256..4ceb2e57e538d5154bfb03849e1452fea9fe4945 100644 --- a/metager/routes/web.php +++ b/metager/routes/web.php @@ -15,6 +15,7 @@ use App\Http\Controllers\SitesearchController; use App\Http\Controllers\StartpageController; use App\Http\Controllers\StatisticsController; use App\Http\Controllers\SuggestionController; +use App\Http\Controllers\TilesController; use App\Http\Controllers\TTSController; use App\Http\Controllers\ZitatController; use App\Localization; @@ -341,6 +342,8 @@ Route::withoutMiddleware([\Illuminate\Foundation\Http\Middleware\ValidateCsrfTok ]); })->name("plugin"); + Route::get('tiles', [TilesController::class, 'loadTakeTiles'])->name("tiles"); + Route::get('settings', function () { return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), '/')); });