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