WebOrbiton
v1.0.0.9

Publisium

316 lines · 16.9 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/footer-links.php';
  6. require_once __DIR__ . '/social-icons.php';
  7. ​
  8. $footerColumns = Languages::footerColumns(FooterLinks::decodeColumns($settings['footer_columns'] ?? '[]'));
  9. $footerSocialLinks = FooterLinks::decodeSocialLinks($settings['footer_social_links'] ?? '[]');
  10. $consentCategories = SiteFront::consentCategories();
  11. ​
  12. ?>
  13. </main>
  14. ​
  15. </div>
  16. ​
  17. <?php foreach (SiteFront::activeAds('footer') as $footerAd): ?>
  18. <?= SiteFront::renderAd($footerAd) ?>
  19. <?php endforeach; ?>
  20. ​
  21. <footer class="site-footer">
  22. ​
  23. <?php if (!empty($footerColumns)): ?>
  24. <div class="site-footer-columns">
  25. <?php foreach ($footerColumns as $column): ?>
  26. <div>
  27. <h3 class="site-footer-column-title"><?= htmlspecialchars($column['title'] ?? '') ?></h3>
  28. <div class="site-footer-column-links">
  29. <?php foreach ($column['links'] ?? [] as $link): ?>
  30. <a href="<?= htmlspecialchars($link['url'] ?? '') ?>"><?= htmlspecialchars($link['label'] ?? '') ?></a>
  31. <?php endforeach; ?>
  32. </div>
  33. </div>
  34. <?php endforeach; ?>
  35. </div>
  36. <?php endif; ?>
  37. ​
  38. <div class="site-footer-bottom">
  39. <div class="site-footer-row">
  40. <span>&copy; <?= date('Y') ?> <?= htmlspecialchars($settings['site_name'] ?? '') ?>. <?= htmlspecialchars(Language::get('footer_rights', 'All rights reserved.')) ?></span>
  41. ​
  42. <div class="site-footer-links">
  43. <?php if ($settings['sitemap_rss_enabled'] === '1'): ?>
  44. <a href="sitemap.php"><?= htmlspecialchars(Language::get('footer_sitemap', 'Sitemap')) ?></a>
  45. <a href="rss.php"><?= htmlspecialchars(Language::get('footer_rss', 'RSS')) ?></a>
  46. <?php endif; ?>
  47. <?= SiteFront::renderSearchControl('footer') ?>
  48. <?php if ($consentCategories !== [] && ($settings['consent_footer_icon_enabled'] ?? '0') === '1'): ?>
  49. <button type="button" id="consent-reopen-btn" aria-label="<?= htmlspecialchars(Language::get('consent_manage', 'Manage preferences')) ?>" title="<?= htmlspecialchars(Language::get('consent_manage', 'Manage preferences')) ?>">
  50. <svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"/><path d="M8.5 8.5v.01"/><path d="M16 15.5v.01"/><path d="M12 12v.01"/><path d="M11 17v.01"/><path d="M7 14v.01"/></svg>
  51. </button>
  52. <?php endif; ?>
  53. <?= Languages::renderSwitcher() ?>
  54. <button type="button" id="theme-toggle-btn" aria-label="<?= htmlspecialchars(Language::get('theme_toggle', 'Switch between light and dark mode'), ENT_QUOTES) ?>">
  55. <?= Icons::icon('sun', 'icon icon-sm icon-sun') ?>
  56. <?= Icons::icon('moon', 'icon icon-sm icon-moon') ?>
  57. </button>
  58. </div>
  59. ​
  60. <?php if (!empty($footerSocialLinks)): ?>
  61. <div class="site-footer-social">
  62. <?php foreach ($footerSocialLinks as $social): ?>
  63. <a href="<?= htmlspecialchars($social['url'] ?? '') ?>" target="_blank" rel="noopener noreferrer" aria-label="<?= htmlspecialchars($social['label'] ?? '') ?>">
  64. <?php if (($social['key'] ?? '') === 'custom'): ?>
  65. <?= $social['svg'] ?? '' ?>
  66. <?php else: ?>
  67. <?= SocialIcons::builtinSvg((string) ($social['key'] ?? '')) ?>
  68. <?php endif; ?>
  69. </a>
  70. <?php endforeach; ?>
  71. </div>
  72. <?php endif; ?>
  73. </div>
  74. </div>
  75. ​
  76. </footer>
  77. ​
  78. <?php
  79. $appBannerAndroidUrl = trim((string) ($settings['app_android_url'] ?? ''));
  80. $appBannerIosUrl = trim((string) ($settings['app_ios_url'] ?? ''));
  81. $appBannerEnabled = ($settings['app_banner_enabled'] ?? '0') === '1' && ($appBannerAndroidUrl !== '' || $appBannerIosUrl !== '');
  82. ?>
  83. <?php if ($appBannerEnabled): ?>
  84. <?php $appBannerIcon = !empty($settings['pwa_icon_url']) ? $settings['pwa_icon_url'] : ($settings['site_logo_url'] ?? ''); ?>
  85. <div class="app-banner" id="app-banner" data-android-url="<?= htmlspecialchars($appBannerAndroidUrl, ENT_QUOTES) ?>" data-ios-url="<?= htmlspecialchars($appBannerIosUrl, ENT_QUOTES) ?>">
  86. <?php if (!empty($appBannerIcon)): ?>
  87. <img src="<?= htmlspecialchars($appBannerIcon) ?>" alt="" class="app-banner-icon">
  88. <?php endif; ?>
  89. <span class="app-banner-text"><?= htmlspecialchars(Language::get('app_available_text', 'Also available as an app')) ?></span>
  90. <a href="#" class="app-banner-link" id="app-banner-link" target="_blank" rel="noopener noreferrer"><?= htmlspecialchars(Language::get('app_get_button', 'Get the app')) ?></a>
  91. <button type="button" class="app-banner-close" id="app-banner-close" aria-label="<?= htmlspecialchars(Language::get('app_dismiss', 'Dismiss')) ?>">&times;</button>
  92. </div>
  93. <?php endif; ?>
  94. ​
  95. <?php if (($settings['pwa_enabled'] ?? '0') === '1'): ?>
  96. <script>
  97. if ('serviceWorker' in navigator) {
  98. window.addEventListener('load', function () {
  99. navigator.serviceWorker.register('<?= htmlspecialchars(rtrim(Config::get('APP_BASE_PATH', ''), '/')) ?>/sw.js');
  100. });
  101. }
  102. </script>
  103. <?php endif; ?>
  104. ​
  105. <?php $stickyAdsHtml = SiteFront::renderAds('sticky-bottom'); ?>
  106. <?php if ($stickyAdsHtml !== ''): ?>
  107. <div class="ad-sticky" id="ad-sticky">
  108. <?= $stickyAdsHtml ?>
  109. <button type="button" class="ad-sticky-close" id="ad-sticky-close" aria-label="<?= htmlspecialchars(Language::get('ad_close', 'Close ad'), ENT_QUOTES) ?>"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
  110. </div>
  111. <script>
  112. (function () {
  113. var bar = document.getElementById('ad-sticky');
  114. if (!bar) return;
  115. var body = document.body;
  116. var closed = false;
  117. // Ads that wait for cookie consent are still an empty <template>, so the bar stays hidden until one loads.
  118. function hasAd() {
  119. return Array.prototype.some.call(bar.querySelectorAll('.ad-slot'), function (slot) {
  120. return Array.prototype.some.call(slot.childNodes, function (node) {
  121. return (node.nodeType === 1 && node.tagName !== 'TEMPLATE') || (node.nodeType === 3 && node.textContent.trim() !== '');
  122. });
  123. });
  124. }
  125. function sync() {
  126. if (closed) {
  127. return;
  128. }
  129. var show = hasAd();
  130. bar.hidden = !show;
  131. body.classList.toggle('has-ad-sticky', show);
  132. if (show) {
  133. body.style.setProperty('--ad-sticky-h', bar.offsetHeight + 'px');
  134. } else {
  135. body.style.removeProperty('--ad-sticky-h');
  136. }
  137. }
  138. sync();
  139. if (window.ResizeObserver) {
  140. new ResizeObserver(sync).observe(bar);
  141. } else {
  142. window.addEventListener('resize', sync);
  143. }
  144. if (window.MutationObserver) {
  145. new MutationObserver(sync).observe(bar, { childList: true, subtree: true });
  146. }
  147. document.getElementById('ad-sticky-close').addEventListener('click', function () {
  148. closed = true;
  149. bar.remove();
  150. body.classList.remove('has-ad-sticky');
  151. body.style.removeProperty('--ad-sticky-h');
  152. });
  153. })();
  154. </script>
  155. <?php endif; ?>
  156. ​
  157. <?php if (($settings['adblock_notice_enabled'] ?? '0') === '1' && !SiteFront::visitorHasSubscription()): ?>
  158. <?php
  159. $adblockMode = SiteFront::adblockMode();
  160. $adblockLocked = in_array($adblockMode, ['lock', 'lock_max'], true);
  161. $adblockClasses = 'adblock-notice ' . ($adblockLocked ? 'adblock-notice-wall' : 'adblock-notice-gentle') . ($adblockMode === 'lock_max' ? ' adblock-notice-max' : '');
  162. ?>
  163. <div class="adsbygoogle ad-banner textads banner-ads ad-placement" id="adblock-bait" aria-hidden="true">&nbsp;</div>
  164. <div class="<?= $adblockClasses ?>" id="adblock-notice" data-mode="<?= htmlspecialchars($adblockMode, ENT_QUOTES) ?>" role="dialog" aria-modal="<?= $adblockLocked ? 'true' : 'false' ?>" aria-labelledby="adblock-notice-title" hidden>
  165. <div class="adblock-notice-box">
  166. <p class="adblock-notice-title" id="adblock-notice-title"><?= htmlspecialchars(Language::get('adblock_title', 'Looks like you’re using an ad blocker')) ?></p>
  167. <p class="adblock-notice-text"><?= htmlspecialchars(Language::get('adblock_text', 'Ads help us keep this site free for everyone. Please turn off your ad blocker for this site, or add it to your allow list.')) ?></p>
  168. <div class="adblock-notice-actions">
  169. <button type="button" id="adblock-notice-reload"><?= htmlspecialchars(Language::get('adblock_reload', 'I’ve turned it off, reload the page')) ?></button>
  170. <?php if (!$adblockLocked): ?>
  171. <button type="button" id="adblock-notice-dismiss" class="adblock-notice-secondary"><?= htmlspecialchars(Language::get('adblock_dismiss', 'Continue anyway')) ?></button>
  172. <?php endif; ?>
  173. </div>
  174. <?php if (($settings['subscription_checkout_url'] ?? '') !== ''): ?>
  175. <a href="<?= htmlspecialchars((string) $settings['subscription_checkout_url'], ENT_QUOTES) ?>" class="adblock-notice-subscribe"><?= htmlspecialchars(Language::get('adblock_subscribe', 'Or read without ads with a subscription')) ?></a>
  176. <?php endif; ?>
  177. </div>
  178. </div>
  179. <?= Asset::js('assets/ads.js') ?>
  180. <script>
  181. // Runs only in the browser: the result is never sent anywhere.
  182. (function () {
  183. var notice = document.getElementById('adblock-notice');
  184. var bait = document.getElementById('adblock-bait');
  185. var mode = notice.getAttribute('data-mode');
  186. var locked = mode === 'lock' || mode === 'lock_max';
  187. var SEEN_KEY = 'publisium_adblock_seen';
  188. ​
  189. // "once" remembers in localStorage, "session" in sessionStorage; both stay in this browser only.
  190. function storage() {
  191. try {
  192. return mode === 'once' ? window.localStorage : (mode === 'session' ? window.sessionStorage : null);
  193. } catch (e) {
  194. return null;
  195. }
  196. }
  197. ​
  198. function alreadySeen() {
  199. var store = storage();
  200. try { return store !== null && store.getItem(SEEN_KEY) === '1'; } catch (e) { return false; }
  201. }
  202. ​
  203. function rememberSeen() {
  204. var store = storage();
  205. try { if (store !== null) { store.setItem(SEEN_KEY, '1'); } } catch (e) {}
  206. }
  207. ​
  208. function baitHidden() {
  209. if (!bait || !bait.isConnected) {
  210. return true;
  211. }
  212. var style = window.getComputedStyle(bait);
  213. return bait.offsetHeight === 0 || style.display === 'none' || style.visibility === 'hidden';
  214. }
  215. ​
  216. function lockPage() {
  217. document.body.appendChild(notice);
  218. document.documentElement.classList.add('adblock-locked');
  219. if (mode === 'lock_max') {
  220. document.documentElement.classList.add('adblock-max');
  221. }
  222. Array.prototype.forEach.call(document.body.children, function (element) {
  223. if (element !== notice && element.tagName !== 'SCRIPT') {
  224. element.setAttribute('inert', '');
  225. element.setAttribute('aria-hidden', 'true');
  226. }
  227. });
  228. }
  229. ​
  230. function check() {
  231. var blocked = window.publisiumAdsLoaded !== true || baitHidden();
  232. if (bait && bait.isConnected) {
  233. bait.remove();
  234. }
  235. if (!blocked || alreadySeen()) {
  236. return;
  237. }
  238. notice.hidden = false;
  239. rememberSeen();
  240. if (locked) {
  241. lockPage();
  242. }
  243. document.getElementById('adblock-notice-reload').focus();
  244. }
  245. ​
  246. document.getElementById('adblock-notice-reload').addEventListener('click', function () {
  247. window.location.reload();
  248. });
  249. var dismiss = document.getElementById('adblock-notice-dismiss');
  250. if (dismiss) {
  251. dismiss.addEventListener('click', function () {
  252. notice.hidden = true;
  253. });
  254. }
  255. ​
  256. if (document.readyState === 'complete') {
  257. setTimeout(check, 200);
  258. } else {
  259. window.addEventListener('load', function () { setTimeout(check, 200); });
  260. }
  261. })();
  262. </script>
  263. <?php endif; ?>
  264. ​
  265. <?php if (($settings['back_to_top_enabled'] ?? '0') === '1'): ?>
  266. <button type="button" class="back-to-top" id="back-to-top" aria-label="<?= htmlspecialchars(Language::get('back_to_top', 'Back to top')) ?>" title="<?= htmlspecialchars(Language::get('back_to_top', 'Back to top')) ?>" tabindex="-1" data-progress="<?= ($settings['back_to_top_progress_enabled'] ?? '0') === '1' ? '1' : '0' ?>" data-left-text="<?= htmlspecialchars(Language::get('reading_progress_left', '{percent}% left'), ENT_QUOTES) ?>">
  267. <svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg>
  268. <span class="back-to-top-progress" hidden></span>
  269. </button>
  270. <?php endif; ?>
  271. ​
  272. <?php if ($consentCategories !== []): ?>
  273. <div class="consent-banner" id="consent-banner" role="dialog" aria-label="<?= htmlspecialchars(Language::get('consent_manage', 'Manage preferences'), ENT_QUOTES) ?>" data-categories="<?= htmlspecialchars(implode(' ', $consentCategories), ENT_QUOTES) ?>">
  274. <p><?= htmlspecialchars(Language::get('consent_text', 'We use cookies and similar technologies to improve your experience.')) ?></p>
  275. <div class="consent-options" id="consent-options" hidden>
  276. <label class="consent-option">
  277. <input type="checkbox" checked disabled>
  278. <span><strong><?= htmlspecialchars(Language::get('consent_necessary', 'Necessary')) ?></strong><?= htmlspecialchars(Language::get('consent_necessary_desc', 'Needed for the site to work, for example to remember these choices. Always on.')) ?></span>
  279. </label>
  280. <?php if (in_array('analytics', $consentCategories, true)): ?>
  281. <label class="consent-option">
  282. <input type="checkbox" id="consent-opt-analytics" data-category="analytics">
  283. <span><strong><?= htmlspecialchars(Language::get('consent_analytics', 'Analytics')) ?></strong><?= htmlspecialchars(Language::get('consent_analytics_desc', 'Helps us see which articles people read, so we can make the site better.')) ?></span>
  284. </label>
  285. <?php endif; ?>
  286. <?php if (in_array('marketing', $consentCategories, true)): ?>
  287. <label class="consent-option">
  288. <input type="checkbox" id="consent-opt-marketing" data-category="marketing">
  289. <span><strong><?= htmlspecialchars(Language::get('consent_marketing', 'Marketing')) ?></strong><?= htmlspecialchars(Language::get('consent_marketing_desc', 'Lets advertising partners show ads that fit your interests.')) ?></span>
  290. </label>
  291. <?php endif; ?>
  292. </div>
  293. <div class="consent-banner-actions">
  294. <button type="button" id="consent-decline"><?= htmlspecialchars(Language::get('consent_decline', 'Decline')) ?></button>
  295. <button type="button" id="consent-manage"><?= htmlspecialchars(Language::get('consent_manage_button', 'Manage')) ?></button>
  296. <button type="button" id="consent-save" hidden><?= htmlspecialchars(Language::get('consent_save', 'Save choices')) ?></button>
  297. <button type="button" id="consent-accept"><?= htmlspecialchars(Language::get('consent_accept', 'Accept')) ?></button>
  298. </div>
  299. </div>
  300. <?php endif; ?>
  301. ​
  302. <?= Asset::js('assets/site.js') ?>
  303. <?= Asset::js('assets/code-block.js') ?>
  304. ​
  305. <?php if (!empty($settings['custom_js'])): ?>
  306. <script>
  307. <?= $settings['custom_js'] ?>
  308. </script>
  309. <?php endif; ?>
  310. ​
  311. <?= SiteFront::renderCustomBodyHtml($pageType ?? null) ?>
  312. <?= SiteFront::renderAnalyticsScripts('body_end') ?>
  313. ​
  314. </body>
  315. ​
  316. </html>