1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17declare(strict_types=1); 18 19namespace Fisharebest\Webtrees\Module; 20 21use Fisharebest\Webtrees\Auth; 22use Fisharebest\Webtrees\Fact; 23use Fisharebest\Webtrees\Gedcom; 24use Fisharebest\Webtrees\GedcomTag; 25use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; 26use Fisharebest\Webtrees\Http\RequestHandlers\LoginPage; 27use Fisharebest\Webtrees\Http\RequestHandlers\Logout; 28use Fisharebest\Webtrees\Http\RequestHandlers\SelectLanguage; 29use Fisharebest\Webtrees\Http\RequestHandlers\SelectTheme; 30use Fisharebest\Webtrees\I18N; 31use Fisharebest\Webtrees\Individual; 32use Fisharebest\Webtrees\Menu; 33use Fisharebest\Webtrees\Services\ModuleService; 34use Fisharebest\Webtrees\Tree; 35use Fisharebest\Webtrees\Webtrees; 36use Psr\Http\Message\ServerRequestInterface; 37 38use function app; 39use function route; 40 41/** 42 * Trait ModuleThemeTrait - default implementation of ModuleThemeInterface 43 */ 44trait ModuleThemeTrait 45{ 46 /** 47 * A sentence describing what this module does. 48 * 49 * @return string 50 */ 51 public function description(): string 52 { 53 return I18N::translate('Theme') . ' — ' . $this->title(); 54 } 55 56 /** 57 * Display an icon for this fact. 58 * 59 * @TODO use CSS for this 60 * 61 * @param Fact $fact 62 * 63 * @return string 64 */ 65 public function icon(Fact $fact): string 66 { 67 $asset = 'public/css/' . $this->name() . '/images/facts/' . $fact->getTag() . '.png'; 68 if (file_exists(Webtrees::ROOT_DIR . 'public' . $asset)) { 69 return '<img src="' . e(asset($asset)) . '" title="' . GedcomTag::getLabel($fact->getTag()) . '">'; 70 } 71 72 // Spacer image - for alignment - until we move to a sprite. 73 $asset = 'public/css/' . $this->name() . '/images/facts/NULL.png'; 74 if (file_exists(Webtrees::ROOT_DIR . 'public' . $asset)) { 75 return '<img src="' . e(asset($asset)) . '">'; 76 } 77 78 return ''; 79 } 80 81 /** 82 * Generate the facts, for display in charts. 83 * 84 * @param Individual $individual 85 * 86 * @return string 87 */ 88 public function individualBoxFacts(Individual $individual): string 89 { 90 $html = ''; 91 92 $opt_tags = preg_split('/\W/', $individual->tree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY); 93 // Show BIRT or equivalent event 94 foreach (Gedcom::BIRTH_EVENTS as $birttag) { 95 if (!in_array($birttag, $opt_tags, true)) { 96 $event = $individual->facts([$birttag])->first(); 97 if ($event instanceof Fact) { 98 $html .= $event->summary(); 99 break; 100 } 101 } 102 } 103 // Show optional events (before death) 104 foreach ($opt_tags as $key => $tag) { 105 if (!in_array($tag, Gedcom::DEATH_EVENTS, true)) { 106 $event = $individual->facts([$tag])->first(); 107 if ($event instanceof Fact) { 108 $html .= $event->summary(); 109 unset($opt_tags[$key]); 110 } 111 } 112 } 113 // Show DEAT or equivalent event 114 foreach (Gedcom::DEATH_EVENTS as $deattag) { 115 $event = $individual->facts([$deattag])->first(); 116 if ($event instanceof Fact) { 117 $html .= $event->summary(); 118 if (in_array($deattag, $opt_tags, true)) { 119 unset($opt_tags[array_search($deattag, $opt_tags, true)]); 120 } 121 break; 122 } 123 } 124 // Show remaining optional events (after death) 125 foreach ($opt_tags as $tag) { 126 $event = $individual->facts([$tag])->first(); 127 if ($event instanceof Fact) { 128 $html .= $event->summary(); 129 } 130 } 131 132 return $html; 133 } 134 135 /** 136 * Links, to show in chart boxes; 137 * 138 * @param Individual $individual 139 * 140 * @return Menu[] 141 */ 142 public function individualBoxMenu(Individual $individual): array 143 { 144 $menus = array_merge( 145 $this->individualBoxMenuCharts($individual), 146 $this->individualBoxMenuFamilyLinks($individual) 147 ); 148 149 return $menus; 150 } 151 152 /** 153 * Chart links, to show in chart boxes; 154 * 155 * @param Individual $individual 156 * 157 * @return Menu[] 158 */ 159 public function individualBoxMenuCharts(Individual $individual): array 160 { 161 $menus = []; 162 foreach (app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $individual->tree(), Auth::user()) as $chart) { 163 $menu = $chart->chartBoxMenu($individual); 164 if ($menu) { 165 $menus[] = $menu; 166 } 167 } 168 169 usort($menus, static function (Menu $x, Menu $y): int { 170 return I18N::strcasecmp($x->getLabel(), $y->getLabel()); 171 }); 172 173 return $menus; 174 } 175 176 /** 177 * Family links, to show in chart boxes. 178 * 179 * @param Individual $individual 180 * 181 * @return Menu[] 182 */ 183 public function individualBoxMenuFamilyLinks(Individual $individual): array 184 { 185 $menus = []; 186 187 foreach ($individual->spouseFamilies() as $family) { 188 $menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', $family->url()); 189 $spouse = $family->spouse($individual); 190 if ($spouse && $spouse->canShowName()) { 191 $menus[] = new Menu($spouse->fullName(), $spouse->url()); 192 } 193 foreach ($family->children() as $child) { 194 if ($child->canShowName()) { 195 $menus[] = new Menu($child->fullName(), $child->url()); 196 } 197 } 198 } 199 200 return $menus; 201 } 202 203 /** 204 * Generate a menu item to change the blocks on the current tree/user page. 205 * 206 * @param Tree $tree 207 * 208 * @return Menu|null 209 */ 210 public function menuChangeBlocks(Tree $tree): ?Menu 211 { 212 /** @var ServerRequestInterface $request */ 213 $request = app(ServerRequestInterface::class); 214 215 $route = $request->getAttribute('route'); 216 217 if (Auth::check() && $route === 'user-page') { 218 return new Menu(I18N::translate('Customize this page'), route('user-page-edit', ['tree' => $tree->name()]), 'menu-change-blocks'); 219 } 220 221 if (Auth::isManager($tree) && $route === 'tree-page') { 222 return new Menu(I18N::translate('Customize this page'), route('tree-page-edit', ['tree' => $tree->name()]), 'menu-change-blocks'); 223 } 224 225 return null; 226 } 227 228 /** 229 * Generate a menu item for the control panel. 230 * 231 * @param Tree $tree 232 * 233 * @return Menu|null 234 */ 235 public function menuControlPanel(Tree $tree): ?Menu 236 { 237 if (Auth::isAdmin()) { 238 return new Menu(I18N::translate('Control panel'), route(ControlPanel::class), 'menu-admin'); 239 } 240 241 if (Auth::isManager($tree)) { 242 return new Menu(I18N::translate('Control panel'), route('manage-trees'), 'menu-admin'); 243 } 244 245 return null; 246 } 247 248 /** 249 * A menu to show a list of available languages. 250 * 251 * @return Menu|null 252 */ 253 public function menuLanguages(): ?Menu 254 { 255 $menu = new Menu(I18N::translate('Language'), '#', 'menu-language'); 256 257 foreach (I18N::activeLocales() as $locale) { 258 $language_tag = $locale->languageTag(); 259 $class = 'menu-language-' . $language_tag . (WT_LOCALE === $language_tag ? ' active' : ''); 260 $menu->addSubmenu(new Menu($locale->endonym(), '#', $class, [ 261 'data-post-url' => route(SelectLanguage::class, ['language' => $language_tag]), 262 ])); 263 } 264 265 if (count($menu->getSubmenus()) > 1) { 266 return $menu; 267 } 268 269 return null; 270 } 271 272 /** 273 * A login menu option (or null if we are already logged in). 274 * 275 * @return Menu|null 276 */ 277 public function menuLogin(): ?Menu 278 { 279 if (Auth::check()) { 280 return null; 281 } 282 283 // Return to this page after login... 284 $url = app(ServerRequestInterface::class)->getUri(); 285 286 // ...but switch from the tree-page to the user-page 287 $url = str_replace('route=tree-page', 'route=user-page', $url); 288 289 return new Menu(I18N::translate('Sign in'), route(LoginPage::class, ['url' => $url]), 'menu-login', ['rel' => 'nofollow']); 290 } 291 292 /** 293 * A logout menu option (or null if we are already logged out). 294 * 295 * @return Menu|null 296 */ 297 public function menuLogout(): ?Menu 298 { 299 if (Auth::check()) { 300 return new Menu(I18N::translate('Sign out'), '#', 'menu-logout', ['data-post-url' => route(Logout::class)]); 301 } 302 303 return null; 304 } 305 306 /** 307 * A link to allow users to edit their account settings. 308 * 309 * @return Menu|null 310 */ 311 public function menuMyAccount(): ?Menu 312 { 313 if (Auth::check()) { 314 return new Menu(I18N::translate('My account'), route('my-account')); 315 } 316 317 return null; 318 } 319 320 /** 321 * A link to the user's individual record (individual.php). 322 * 323 * @param Tree $tree 324 * 325 * @return Menu|null 326 */ 327 public function menuMyIndividualRecord(Tree $tree): ?Menu 328 { 329 $record = Individual::getInstance($tree->getUserPreference(Auth::user(), 'gedcomid'), $tree); 330 331 if ($record) { 332 return new Menu(I18N::translate('My individual record'), $record->url(), 'menu-myrecord'); 333 } 334 335 return null; 336 } 337 338 /** 339 * A link to the user's personal home page. 340 * 341 * @param Tree $tree 342 * 343 * @return Menu 344 */ 345 public function menuMyPage(Tree $tree): Menu 346 { 347 return new Menu(I18N::translate('My page'), route('user-page', ['tree' => $tree->name()]), 'menu-mypage'); 348 } 349 350 /** 351 * A menu for the user's personal pages. 352 * 353 * @param Tree|null $tree 354 * 355 * @return Menu|null 356 */ 357 public function menuMyPages(?Tree $tree): ?Menu 358 { 359 if ($tree instanceof Tree && Auth::id()) { 360 return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([ 361 $this->menuMyPage($tree), 362 $this->menuMyIndividualRecord($tree), 363 $this->menuMyPedigree($tree), 364 $this->menuMyAccount(), 365 $this->menuControlPanel($tree), 366 $this->menuChangeBlocks($tree), 367 ])); 368 } 369 370 return null; 371 } 372 373 /** 374 * A link to the user's individual record. 375 * 376 * @param Tree $tree 377 * 378 * @return Menu|null 379 */ 380 public function menuMyPedigree(Tree $tree): ?Menu 381 { 382 $gedcomid = $tree->getUserPreference(Auth::user(), 'gedcomid'); 383 384 $pedigree_chart = app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $tree, Auth::user()) 385 ->filter(static function (ModuleInterface $module): bool { 386 return $module instanceof PedigreeChartModule; 387 }); 388 389 if ($gedcomid !== '' && $pedigree_chart instanceof PedigreeChartModule) { 390 return new Menu( 391 I18N::translate('My pedigree'), 392 route('pedigree', [ 393 'xref' => $gedcomid, 394 'ged' => $tree->name(), 395 ]), 396 'menu-mypedigree' 397 ); 398 } 399 400 return null; 401 } 402 403 /** 404 * Create a pending changes menu. 405 * 406 * @param Tree|null $tree 407 * 408 * @return Menu|null 409 */ 410 public function menuPendingChanges(?Tree $tree): ?Menu 411 { 412 if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) { 413 $url = route('show-pending', [ 414 'ged' => $tree->name(), 415 'url' => (string) app(ServerRequestInterface::class)->getUri(), 416 ]); 417 418 return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending'); 419 } 420 421 return null; 422 } 423 424 /** 425 * Themes menu. 426 * 427 * @return Menu|null 428 */ 429 public function menuThemes(): ?Menu 430 { 431 $themes = app(ModuleService::class)->findByInterface(ModuleThemeInterface::class, false, true); 432 433 $current_theme = app(ModuleThemeInterface::class); 434 435 if ($themes->count() > 1) { 436 $submenus = $themes->map(static function (ModuleThemeInterface $theme) use ($current_theme): Menu { 437 $active = $theme->name() === $current_theme->name(); 438 $class = 'menu-theme-' . $theme->name() . ($active ? ' active' : ''); 439 440 return new Menu($theme->title(), '#', $class, [ 441 'data-post-url' => route(SelectTheme::class, ['theme' => $theme->name()]), 442 ]); 443 }); 444 445 return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all()); 446 } 447 448 return null; 449 } 450 451 /** 452 * Misecellaneous dimensions, fonts, styles, etc. 453 * 454 * @param string $parameter_name 455 * 456 * @return string|int|float 457 */ 458 public function parameter($parameter_name) 459 { 460 return ''; 461 } 462 463 /** 464 * Generate a list of items for the main menu. 465 * 466 * @param Tree|null $tree 467 * 468 * @return Menu[] 469 */ 470 public function genealogyMenu(?Tree $tree): array 471 { 472 if ($tree === null) { 473 return []; 474 } 475 476 return app(ModuleService::class)->findByComponent(ModuleMenuInterface::class, $tree, Auth::user()) 477 ->map(static function (ModuleMenuInterface $menu) use ($tree): ?Menu { 478 return $menu->getMenu($tree); 479 }) 480 ->filter() 481 ->all(); 482 } 483 484 /** 485 * Create the genealogy menu. 486 * 487 * @param Menu[] $menus 488 * 489 * @return string 490 */ 491 public function genealogyMenuContent(array $menus): string 492 { 493 return implode('', array_map(static function (Menu $menu): string { 494 return $menu->bootstrap4(); 495 }, $menus)); 496 } 497 498 /** 499 * Generate a list of items for the user menu. 500 * 501 * @param Tree|null $tree 502 * 503 * @return Menu[] 504 */ 505 public function userMenu(?Tree $tree): array 506 { 507 return array_filter([ 508 $this->menuPendingChanges($tree), 509 $this->menuMyPages($tree), 510 $this->menuThemes(), 511 $this->menuLanguages(), 512 $this->menuLogin(), 513 $this->menuLogout(), 514 ]); 515 } 516 517 /** 518 * A list of CSS files to include for this page. 519 * 520 * @return string[] 521 */ 522 public function stylesheets(): array 523 { 524 return []; 525 } 526} 527