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