From ee92c212a0140c6697f9106cd68a41d794e340b0 Mon Sep 17 00:00:00 2001 From: Dominik Hebeler <dominik@suma-ev.de> Date: Thu, 18 Jul 2024 16:32:56 +0200 Subject: [PATCH] include advertisements --- .../Http/Controllers/StartpageController.php | 28 +--- .../app/Http/Controllers/TilesController.php | 130 ++++++++++++++++++ metager/app/Models/Tile.php | 41 ++++++ metager/config/metager/taketiles.php | 8 ++ metager/resources/js/startpage/app.js | 6 +- metager/resources/js/startpage/tiles.js | 73 ++++++++++ .../less/metager/pages/startpage/tiles.less | 53 +++++-- metager/resources/views/index.blade.php | 7 +- .../views/layouts/staticPages.blade.php | 3 + metager/resources/views/parts/tile.blade.php | 6 +- metager/routes/web.php | 3 + 11 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 metager/app/Http/Controllers/TilesController.php create mode 100644 metager/app/Models/Tile.php create mode 100644 metager/config/metager/taketiles.php create mode 100644 metager/resources/js/startpage/tiles.js diff --git a/metager/app/Http/Controllers/StartpageController.php b/metager/app/Http/Controllers/StartpageController.php index b29496598..8a5b1bdd3 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/TilesController.php b/metager/app/Http/Controllers/TilesController.php new file mode 100644 index 000000000..16473dd8c --- /dev/null +++ b/metager/app/Http/Controllers/TilesController.php @@ -0,0 +1,130 @@ +<?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); + 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 + { + $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: "SUMA-EV", 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"); + 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, classes: "advertisement"); + } + } 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 000000000..73e1de114 --- /dev/null +++ b/metager/app/Models/Tile.php @@ -0,0 +1,41 @@ +<?php + +namespace App\Models; + +/** + * 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 = ""; + /** + * + * @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 = "") + { + $this->title = $title; + $this->image = $image; + $this->image_alt = $image_alt; + $this->url = $url; + $this->classes = $classes; + $this->image_classes = $image_classes; + } + + 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/config/metager/taketiles.php b/metager/config/metager/taketiles.php new file mode 100644 index 000000000..827872660 --- /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/resources/js/startpage/app.js b/metager/resources/js/startpage/app.js index 9241a27bb..a8845d071 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 000000000..71475b33f --- /dev/null +++ b/metager/resources/js/startpage/tiles.js @@ -0,0 +1,73 @@ +(async () => { + let tile_container = document.querySelector("#tiles"); + let tile_count = tile_container.querySelectorAll("a").length; + + let advertisements = []; + fetchAdvertisements().then(() => udpateInterface()); + + async function fetchAdvertisements() { + let desired_tile_count = calculateDesiredTileCount(); + let regular_tile_count = getRegularTileCount(); + 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 fetch(update_url).then(response => response.json()).then(response => { + advertisements = response; + }); + } + 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; + + 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 > a")).width.replace("px", "")); + let tile_gap = parseFloat(window.getComputedStyle(document.querySelector("#tiles"))["column-gap"].replace("px", "")); + let client_width = document.querySelector("html").clientWidth; + + 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; + } + + if (native_tile_count + min_advertisements > desired_tile_count) { + desired_tile_count *= 2; + } + + return desired_tile_count; + } + window.addEventListener("resize", e => { + fetchAdvertisements().then(() => udpateInterface()); + }) +})(); + diff --git a/metager/resources/less/metager/pages/startpage/tiles.less b/metager/resources/less/metager/pages/startpage/tiles.less index f9473ad27..92245173d 100644 --- a/metager/resources/less/metager/pages/startpage/tiles.less +++ b/metager/resources/less/metager/pages/startpage/tiles.less @@ -3,18 +3,48 @@ div#tiles-container { flex-grow: 1; padding-inline: 1rem; + div#tiles { flex-grow: 0; + width: 100%; + @tile_width: 13ch; + + @nine_row_width: calc(calc(9 * @tile_width) + calc(8 * 1rem)); + @eight_row_width: calc(calc(8 * @tile_width) + calc(7 * 1rem)); + @seven_row_width: calc(calc(7 * @tile_width) + calc(6 * 1rem)); + @six_row_width: calc(calc(6 * @tile_width) + calc(5 * 1rem)); + @five_row_width: calc(calc(5 * @tile_width) + calc(4 * 1rem)); + @four_row_width: calc(calc(4 * @tile_width) + calc(3 * 1rem)); + @three_row_width: calc(calc(3 * @tile_width) + calc(2 * 1rem)); + @two_row_width: calc(calc(2 * @tile_width) + calc(1 * 1rem)); + + @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: @two_row_width; + } + display: flex; - align-items: flex-start; - padding-top: 5dvh; flex-wrap: wrap; + justify-content: center; + + padding-top: 8dvh; gap: 1rem; margin-inline: auto; height: max-content; - justify-content: space-between; >a { + width: @tile_width; display: flex; row-gap: .5rem; flex-direction: column; @@ -22,14 +52,18 @@ div#tiles-container { justify-content: flex-end; color: @text-color; + &.skeleton { + display: none; + } + >div.image { - @image_size: 4.5rem; + @image_size: 5rem; width: @image_size; height: @image_size; padding: 1rem; background-color: @highlight-color; border-radius: 5px; - transition: width 0.5s, height 0.5s; + transition: background-color 0.5s; >img { width: 100%; @@ -44,8 +78,8 @@ div#tiles-container { >div.title { font-size: .8rem; color: fade(@text-color, 40%); - transition: color 0.5s, width 0.5s, overflow 0.5s; - width: 11ch; + transition: color 0.5s; + width: @tile_width; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -54,14 +88,11 @@ div#tiles-container { &:hover { >div.image { - width: 6rem; - height: 6rem; + background-color: lighten(@highlight-color, 10%); } >div.title { color: @text-color; - width: unset; - overflow: unset; } } } diff --git a/metager/resources/views/index.blade.php b/metager/resources/views/index.blade.php index 2a2294551..1f48b113a 100644 --- a/metager/resources/views/index.blade.php +++ b/metager/resources/views/index.blade.php @@ -39,10 +39,9 @@ </div> <div id="tiles-container"> <div id="tiles"> - @include("parts.tile", ["url" => "https://suma-ev.de", "image" => "/img/tiles/sumaev.png", "image_alt" => "SUMA-EV Logo", "title" => "SUMA-EV"]) - @include("parts.tile", ["url" => "https://maps.metager.de", "image" => "/img/tiles/maps.png", "image_alt" => "MetaGer Maps Logo", "title" => "Maps"]) - @include("parts.tile", ["url" => route("settings", ["focus" => $focus, "url" => url()->full()]), "image" => "/img/icon-settings.svg", "image_alt" => "SUMA-EV Logo", "title" => __('sidebar.nav28'), "options" => ["img_class" => "invert-dm"]]) - @include("parts.tile", ["url" => route("plugin"), "image" => "/img/svg-icons/plug-in.svg", "image_alt" => "MetaGer Plugin Logo", "title" => __("index.plugin")]) + @foreach($tiles as $tile) + @include("parts.tile", ["tile" => $tile]) + @endforeach </div> </div> <div id="language"> diff --git a/metager/resources/views/layouts/staticPages.blade.php b/metager/resources/views/layouts/staticPages.blade.php index ba139a478..b35216574 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 index ce94a9838..9d8bd6d47 100644 --- a/metager/resources/views/parts/tile.blade.php +++ b/metager/resources/views/parts/tile.blade.php @@ -1,6 +1,6 @@ -<a href="{{$url}}"> +<a href="{{$tile->url}}" class="{{ $tile->classes }}"> <div class="image"> - <img src="{{$image}}" alt="{{$image_alt}}" @if(isset($options) && array_key_exists("img_class",$options))class="{{$options["img_class"]}}"@endif> + <img src="{{$tile->image}}" alt="{{$tile->image_alt}}" class="{{$tile->image_classes}}"> </div> - <div class="title">{{$title}}</div> + <div class="title">{{$tile->title}}</div> </a> \ No newline at end of file diff --git a/metager/routes/web.php b/metager/routes/web.php index 6c6995dce..4ceb2e57e 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(), '/')); }); -- GitLab