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