Stig's Flickr Fixr

Show photographer's albums on photostream-pages, Photographer's other photos by tag-links, Links to album-map and album-comments, Actually show a geotagged photo on the associated map, Top-pagers - And more...

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Stig's Flickr Fixr
// @namespace   dk.rockland.userscript.flickr.fixr
// @description Show photographer's albums on photostream-pages, Photographer's other photos by tag-links, Links to album-map and album-comments, Actually show a geotagged photo on the associated map, Top-pagers - And more...
// @author      Stig Nygaard, https://www.rockland.dk, https://www.flickr.com/photos/stignygaard/
// @homepageURL https://www.flickr.com/groups/flickrhacks/discuss/72157655601688753/
// @supportURL  https://www.flickr.com/groups/flickrhacks/discuss/72157655601688753/
// @icon        https://raw.githubusercontent.com/StigNygaard/Stigs_Flickr_Fixr/master/WebExtension/icons/fixr32.png
// @icon64      https://raw.githubusercontent.com/StigNygaard/Stigs_Flickr_Fixr/master/WebExtension/icons/fixr64.png
// @match       https://*.flickr.com/*
// @match       *://*.flickr.net/*
// @exclude     *://api.flickr.com/*
// @exclude     *://identify.flickr.com/*
// @exclude     *://*.flickr.com/signin/*
// @exclude     *://*.flickr.com/signup/*
// @exclude     *://*.flickr.com/account/*
// @version     2022.12.26.1
// @run-at      document-start
// @grant       none
// @noframes
// ==/UserScript==

// CHANGELOG - The most recent or important updates/versions:
const changelog = [
    {version: '2022.12.26.1', description: 'Last version of the userscript. You should make the switch to the "native" browser-extension versions instead, which is available for Chrome, Edge and Firefox compatible webbrowsers.'},
    {version: '2022.10.29.1', description: 'For webextension, optionally make sidebar in searchresults collapsible. Collapsible sidebar feature not available in userscript version.'},
    {version: '2022.06.19.0', description: 'Remove the photo upscale feature. Not so relevant/important anymore, and very was very unreliable (sensitive to site changes). Lots of old ugly code I got rid of there... '},
    {version: '2022.01.23.0', description: 'New feature: Control slideshow speed (Supported in webextension - not supported in userscript version of Flickr Fixr)'},
    {version: '2020.06.22.0', description: 'Removing 90% of map-fix for showing geolocation of a photo. Finally Flickr has mostly fixed issue themselves. Restoring insertion of Google Maps link which broke by the Flickr update.'},
    {version: '2020.06.21.0', description: 'A little bit of cleaning, a warning to userscript users - and "sub-options" for the tag-links feature (in webextension version)'},
    {version: '2019.10.19.0', description: 'Adjusting to Flickr 2019 updates.'},
    {version: '2018.11.29.0', description: 'New feature: Show available RSS/Atom newsfeeds on pages.'},
    {version: '2018.10.15.1', description: 'Add Options page to Firefox and Chrome browser extensions, to enable or disable individual features of Flickr Fixr (Userscript version is still all or nothing).'},
    {version: '2018.10.15.0', description: 'New feature: Added Collections and Map to topmenus.'},
    {version: '2018.08.19.0', description: 'New features: Added link leading to Tags page in topmenus. Added display of full Taken and Upload time, plus link for photographer\'s other photos from (approx.) same day.'},
    {version: '2018.05.20.0', description: 'New feature: Added a subtle warning if photostreams are shown in Date-taken order instead of Date-uploaded order.'},
    {version: '2017.07.31.0', description: 'New feature: Adding a Google Maps link on geotagged photos. Also: Removing unused code. Development code now in GitHub repository: https://github.com/StigNygaard/Stigs_Flickr_Fixr'},
    {version: '2016.03.11.1', description: 'New features: A link to "recent uploads page" added on the Explore page. Ctrl-click fix for opening tabs in background on search pages (Firefox-only problem?).'},
    {version: '2016.02.09.0', description: 'New feature: Link to Explore Calendar added to Explore page.'},
    {version: '2016.02.06.2', description: 'New feature: Top-pagers! Hover the mouse in the center just above photostreams to show a pagination-bar.'},
    {version: '2015.11.28.1', description: 'New feature: Album-headers are now updated with links to album-map and album-comments.'},
    {version: '2015.08.26.4', description: 'Initial userscript release version. Photo scale/replace, album column and tag-link feature.'}
];

const DEBUG = false;

function log(...s) {
    if (DEBUG && console) {
        console.log(...s);
    }
}

if (DEBUG) {
    if ('loading' === document.readyState) {
        log("This script is running at document-start time.");
    } else {
        log("This script is running with document.readyState: " + document.readyState);
    }
    window.addEventListener('DOMContentLoaded', function () {
        log('(onDOMContentLoaded)');
    }, false);
    window.addEventListener('focus', function () {
        log('(onfocus)');
    }, false);
    window.addEventListener('load', function () {
        log('(onload)');
    }, false);
    window.addEventListener('pageshow', function () {
        log('(onpageshow)');
    }, false);
    window.addEventListener('resize', function () {
        log('(onresize)');
    }, false);
    window.addEventListener('hashchange', function () {
        log('(onhashchange)');
    }, false);
    window.addEventListener('blur', function () {
        log('(onblur)');
    }, false);
}


// FIXR page-tracker
var fixr = fixr || {
    context: {
        pageType: '',
        pageSubType: '',
        userId: '',
        photographerId: '', // value might be delayed (If uninitialized, try call initPhotographerId())
        photographerIcon: '',
        photographerAlias: '', // (pathalias) bonus-info sometimes initialized (from url) when initializing photoId or albumId
        photographerName: '',
        photoId: '',
        albumId: '',
        groupId: '',
        galleryId: ''
    },
    content: null,
    pageactionsCount: 0,
    timerResizeActionDelayed: 0,
    onPageHandlers: [],
    onResizeHandlers: [],
    onFocusHandlers: [],
    onStandaloneHandlers: [],
    runningDirty: function () { // In-development and extra experiments enabled?
        return (DEBUG && (fixr.context.userId === '10259776@N00'));
    },
    timer: {
        _test: 0 // TODO
    },
    style: {
        _declarations: '',
        add: function (decl) {
            fixr.style._declarations += decl + ' ';
        },
        init() {
            if (!document.getElementById('fixrStyle')) {
                let styleElem = createRichElement('style', {
                    type: 'text/css',
                    id: 'fixrStyle'
                }, fixr.style._declarations);
                document.getElementsByTagName('head')[0].appendChild(styleElem);
                log('fixrStyle has been ADDED');
            } else {
                log('fixrStyle was already present');
            }
        }
    },
    clock: {
        _d: null,
        _pst: null, // Pacific Standard Time
        _explore: null,
        tick: function () {
            this._d = new Date();
            this._pst = new Date(this._d);
            this._pst.setHours(this._d.getHours() - 8); // PST = UTC-08
            this._explore = new Date(this._d);
            this._explore.setHours(this._d.getHours() - 28); // Explore beat, yesterday UTC-4
            // this._y.setDate(this._y.getDate() - 1);
            return this._pst;
        },
        pst: function () { // yyyy-mm-dd tt:mm PST
            return (this._pst || this.tick()).toISOString().substring(0, 16).replace('T', ' ') + ' PST';
        },
        explore: function () { // yyyy-mm-dd tt:mm Explore beat!
            if (this._explore === null) {
                this.tick();
            }
            return this._explore.toISOString().substring(0, 16).replace('T', ' ') + ' Explore beat!';
        }
    },
    isWebExtension: function () {
        return (typeof GM_info === 'undefined') && (typeof GM === 'undefined');
    },
    isUserscript: function () {
        return !fixr.isWebExtension();
    },
    initUserId: function () {
        if (window.auth?.user?.nsid) {
            fixr.context.userId = window.auth.user.nsid;
            return true;
        }
        return false;
    },
    initPhotographerName: function () {
        if (fixr.content.querySelector('a.owner-name')) {
            fixr.context.photographerName = fixr.content.querySelector('a.owner-name').innerText;
            return true;
        }
        return false;
    },
    initPhotographerId: function () { // photographer/attribution id

        // todo: This needs a rewrite some day...

        let elem;
        if (document.querySelector('div.photostream-page-view')) {
            // photostream
            elem = document.querySelector('div.photostream-page-view div.fluid-photostream-coverphoto-view .avatar.person');
        } else if (document.querySelector('div.photo-page-scrappy-view')) {
            // photopage
            elem = document.querySelector('div.photo-page-scrappy-view div.sub-photo-view .photo-attribution .avatar.person');
        } else if (document.querySelector('div.photo-page-lightbox-scrappy-view')) {
            // photopage lightbox
            elem = document.querySelector('div.photo-page-lightbox-scrappy-view div.photo-well-view .photo-attribution .avatar.person');
        } else if (document.querySelector('div.album-page-view')) {
            // album page
            elem = document.querySelector('div.album-page-view div.album-container div.album-header-view .album-attribution .avatar.person');
        } else if (document.querySelector('div.coverphoto-content .avatar.person')) {
            // fallback, modern design pages
            elem = document.querySelector('div.coverphoto-content .avatar.person');
        } else if (document.querySelector('div.subnav-middle div.sn-avatar > img')) {
            // fallback, old design pages
            elem = document.querySelector('div.subnav-middle div.sn-avatar > img');
        } else {
            log('fixr.initPhotographerId() - We do not look for photographerId on this page');
            return true;
        }
        if (!elem) {
            log('fixr.initPhotographerId() - Attribution elem NOT found - returning false');
            return false;
        } // re-run a little later???
        log('fixr.initPhotographerId() - Attribution elem found');
        let result;
        if (elem.tagName.toUpperCase() === 'IMG' && elem.src) {
            result = elem.src.match(/https:(\/\/[^#\?]+\.com\/[^#\?]+\/buddyicon[^\?\#]+)[^#]*#(\d+\@N\d{2})/i);
        } else if (elem.style.backgroundImage) {
            log('fixr.initPhotographerId() - elem has style.backgroundImage "' + elem.style.backgroundImage + '", now looking for the attribution id...');
            // var pattern = /\/buddyicons\/(\d+\@N\d{2})\D+/i;
            result = elem.style.backgroundImage.match(/url[^#\?]+(\/\/[^#\?]+\.com\/[^#\?]+\/buddyicon[^\?\#]+)[^#]*#(\d+\@N\d{2})/i);
        }
        if (result) {
            log('fixr.initPhotographerId() - Attribution pattern match found: ' + result[0]);
            log('fixr.initPhotographerId() - the attribution icon is ' + result[1]);
            log('fixr.initPhotographerId() - the attribution id is ' + result[2]);
            fixr.context.photographerIcon = result[1];
            fixr.context.photographerId = result[2];
            log('fixr.initPhotographerId() - returning true...');
            return true;
        } else {
            log('fixr.initPhotographerId() - attribution pattern match not found');
            return false;
        }
    },
    initPhotoId: function () { // Photo Id
        //  *flickr.com/photos/user/PId/*
        const pattern = /^\/photos\/([^\/]+)\/([\d]{2,})/i;
        const result = window.location.pathname.match(pattern);
        if (result) {
            log('url match med photoId=' + result[2]);
            log('url match med photographerAlias=' + result[1]);
            fixr.context.photoId = result[2];
            fixr.context.photographerAlias = result[1];
            return true;
        } else {
            log('*** initPhotoId() returnerer false! reg-pattern fandt ikke match i pathname=' + window.location.pathname);
        }
        return false;
    },
    initAlbumId: function () {
        //  *flickr.com/photos/user/albums/AId/*
        //  *flickr.com/photos/user/sets/AId/*
        let pattern = /^\/photos\/([^\/]+)\/albums\/([\d]{2,})/i;
        let result = window.location.pathname.match(pattern);
        if (!result) {
            pattern = /^\/photos\/([^\/]+)\/sets\/([\d]{2,})/i;
            result = window.location.pathname.match(pattern);
        }
        if (result) {
            log('url match med albumId=' + result[2]);
            log('url match med photographerAlias=' + result[1]);
            fixr.context.albumId = result[2];
            fixr.context.photographerAlias = result[1];
            return true;
        }
        return false;
    },
    pageActions: function () {
        fixr.clock.tick();
        if (fixr.content) {
            log('fixr.pageActions() has started with fixr.content defined');
        } else {
            log('fixr.pageActions() was called, but fixr.content NOT defined');
            return;
        }
        fixr.pageactionsCount++;
        for (let p in fixr.context) {  // reset context on new page
            if (fixr.context.hasOwnProperty(p)) {
                fixr.context[p] = '';
            }
        }
        if (fixr.content.querySelector('div.photostream-page-view')) {
            if (fixr.content.querySelector('div.slideshow-view')) {
                fixr.context.pageType = 'PHOTOSTREAM SLIDESHOW';
            } else {
                fixr.context.pageType = 'PHOTOSTREAM';
            }
        } else if (fixr.content.querySelector('div.photo-page-scrappy-view')) {
            fixr.context.pageType = 'PHOTOPAGE';
            if (fixr.content.querySelector('div.vr-overlay-view') && fixr.content.querySelector('div.vr-overlay-view').hasChildNodes()) {
                fixr.context.pageSubType = 'VR'; // maybe I can find a better way to detect, not sure how reliable this is?
            } else if (fixr.content.querySelector('div.videoplayer')) {
                fixr.context.pageSubType = 'VIDEO';
            } else {
                fixr.context.pageSubType = 'PHOTO';
            }
        } else if (fixr.content.querySelector('div.photo-page-lightbox-scrappy-view')) {
            fixr.context.pageType = 'PHOTOPAGE LIGHTBOX';
            if (fixr.content.querySelector('div.vr-overlay-view') && fixr.content.querySelector('div.vr-overlay-view').hasChildNodes()) {
                fixr.context.pageSubType = 'VR'; // VR-mode currently not supported in lightbox?
            } else if (fixr.content.querySelector('div.videoplayer')) {
                fixr.context.pageSubType = 'VIDEO';
            } else {
                fixr.context.pageSubType = 'PHOTO';
            }
        } else if (fixr.content.querySelector('div.albums-list-page-view')) {
            fixr.context.pageType = 'ALBUMSLIST';
        } else if (fixr.content.querySelector('div.album-page-view')) {
            if (fixr.content.querySelector('div.slideshow-view')) {
                fixr.context.pageType = 'ALBUM SLIDESHOW';
            } else {
                fixr.context.pageType = 'ALBUM';
            }
        } else if (fixr.content.querySelector('div.cameraroll-page-view')) {
            fixr.context.pageType = 'CAMERAROLL';
        } else if (fixr.content.querySelector('div.explore-page-view')) {
            fixr.context.pageType = 'EXPLORE';
        } else if (fixr.content.querySelector('div.favorites-page-view')) {
            if (fixr.content.querySelector('div.slideshow-view')) {
                fixr.context.pageType = 'FAVORITES SLIDESHOW';
            } else {
                fixr.context.pageType = 'FAVORITES';
            }
        } else if (fixr.content.querySelector('div.groups-list-view')) {
            fixr.context.pageType = 'GROUPSLIST'; // personal grouplist
        } else if (fixr.content.querySelector('div#activityFeed')) { // id=main i stedet for id=fixr.content
            fixr.context.pageType = 'ACTIVITYFEED'; // aka. front page -> UPDATES ?
        } else if (fixr.content.querySelector('div#allsizes-photo')) {
            fixr.context.pageType = 'SIZES'; // View all sizes - page
        } else if (fixr.content.querySelector('div.search-photos-unified-page-view')) {
            fixr.context.pageType = 'SEARCHRESULTPHOTOS';
        } else if (fixr.content.querySelector('div.search-people-page-view')) {
            fixr.context.pageType = 'SEARCHRESULTPEOPLE';
        } else if (fixr.content.querySelector('div.search-groups-page-view')) {
            fixr.context.pageType = 'SEARCHRESULTGROUPS';
        } else {
            // fixr.context.pageType = ''; // unknown
        }

        log('fixr.context.pageType = ' + fixr.context.pageType);
        log('fixr.context.pageSubType = ' + fixr.context.pageSubType);
        if (fixr.initUserId()) {
            log('fixr.initUserId() returned with succes: ' + fixr.context.userId);
        } else {
            log('fixr.initUserId() returned FALSE!');
        }
        if (fixr.initPhotographerId()) {
            log('fixr.initPhotographerId() returned true in first try...');
        } else {
            log('fixr.initPhotographerId() returned false - re-running delayed...');
            setTimeout(fixr.initPhotographerId, 1800);
        }
        if (fixr.initPhotoId()) {
            log('fixr.initPhotoId() returned true in first try...');
        } else {
            log('fixr.initPhotoId() returned false - re-running delayed...');
            setTimeout(fixr.initPhotoId, 1500);
        }
        if (fixr.initAlbumId()) {
            log('fixr.initAlbumId() returned true in first try...');
        }
        if (fixr.initPhotographerName()) {
            log('fixr.initPhotographerName() returned true in first try...');
        } else {
            setTimeout(fixr.initPhotographerName, 1500);
        }

        // Now run the page handlers....
        if (fixr.onPageHandlers && fixr.onPageHandlers.length) {
            log('We have ' + fixr.onPageHandlers.length + ' onPage handlers starting now...');
            for (let f = 0; f < fixr.onPageHandlers.length; f++) {
                fixr.onPageHandlers[f]();
            }
        }
    },
    setupContent: function () {
        if (document.getElementById('content')) {
            fixr.content = document.getElementById('content');
        } else if (document.getElementById('main')) {
            fixr.content = document.getElementById('main');    // frontpage
        }
        if (fixr.content?.id) {
            log('fixr.content.id = ' + fixr.content.id);
        } else {
            log('content or main element NOT found!');
        }
    },
    runPageActionsIfMissed: function () {
        if (fixr.pageactionsCount === 0) {
            log('Vi kører fixr.pageActions() på bagkant via onload...');
            fixr.setupContent();
            if (fixr.content === null) {
                log('Vi kan IKKE køre fixr.pageActions() på bagkant, da fixr.content ikke er defineret');
                return;
            }
            fixr.pageActions();
        } else {
            log('ej nødvendigt at køre fixr.pageActions() på bagkant i dette tilfælde...');
        }
    },
    runIfStandalonePage: function () {
        if (fixr.content === null && fixr.pageactionsCount === 0) { // if really looks like a "standalone page"...
            // Now run the standalone handlers
            if (fixr.onStandaloneHandlers && fixr.onStandaloneHandlers.length) {
                log('We have ' + fixr.onStandaloneHandlers.length + ' standalone handlers starting now...');
                for (let f = 0; f < fixr.onStandaloneHandlers.length; f++) {
                    fixr.onStandaloneHandlers[f]();
                }
            }
        }
    },
    runDelayedPageActionsIfMissed: function () {
        setTimeout(fixr.runPageActionsIfMissed, 2000);
        setTimeout(fixr.runIfStandalonePage, 500);
    },
    resizeActions: function () {
        if (fixr.onResizeHandlers  && fixr.onResizeHandlers.length) {
            for (let f = 0; f < fixr.onResizeHandlers.length; f++) {
                fixr.onResizeHandlers[f]();
            }
        }
    },
    resizeActionsDelayed: function () { // or "preburner"
        clearTimeout(fixr.timerResizeActionDelayed);
        fixr.timerResizeActionDelayed = setTimeout(fixr.resizeActions, 250);
    },
    focusActions: function () {
        if (fixr.onFocusHandlers && fixr.onFocusHandlers.length) {
            for (let f = 0; f < fixr.onFocusHandlers.length; f++) {
                fixr.onFocusHandlers[f]();
            }
        }
    },
    setupObserver: function () {
        log('fixr.setupObserve INITIALIZATION START');
        fixr.setupContent();
        if (fixr.content === null) {
            log('Init fails because content not defined');
            return;
        }
        // create an observer instance
        let observer = new MutationObserver(function (mutations) {
            log('NEW PAGE MUTATION!');
            //mutations.forEach(function(mutation) {
            //  log('MO: '+mutation.type); // might check for specific type of "mutations" (MutationRecord)
            //});
            fixr.pageActions();
        }); // MutationObserver end
        // configuration of the observer:
        let config = {attributes: false, childList: true, subtree: false, characterData: false};
        observer.observe(fixr.content, config);
        log('fixr.setupObserve INITIALIZATION DONE');
    },
    init: function (runNow, onPageHandlerArray, onResizeHandlerArray, onFocusHandlerArray, onStandaloneHandlerArray) {
        // General page-change observer setup:
        if (document.readyState === 'interactive') { // already late?
            fixr.setupObserver();
        }
        window.addEventListener('DOMContentLoaded', fixr.setupObserver, false); // Page on DOMContentLoaded
        window.addEventListener('load', fixr.runDelayedPageActionsIfMissed, false); // Page on load
        window.addEventListener('resize', fixr.resizeActionsDelayed, false); // også på resize
        window.addEventListener('focus', fixr.focusActions, false);
        if (onPageHandlerArray && onPageHandlerArray.length) {
            fixr.onPageHandlers = onPageHandlerArray; // Replace by adding with a one-by-one by "helper" for flexibility?
        }
        fixr.onPageHandlers.push(fixr.style.init); //  styles
        if (onResizeHandlerArray && onResizeHandlerArray.length) {
            fixr.onResizeHandlers = onResizeHandlerArray; // Replace by adding with a one-by-one by "helper" for flexibility?
        }
        if (onFocusHandlerArray && onFocusHandlerArray.length) {
            fixr.onFocusHandlers = onFocusHandlerArray;
        }
        if (onStandaloneHandlerArray && onStandaloneHandlerArray.length) { // on standalone pages, not part of "single page application"
            fixr.onStandaloneHandlers = onStandaloneHandlerArray;
            fixr.onStandaloneHandlers.push(fixr.style.init); //  styles
        }

        if (runNow && runNow.length) {
            log('We have ' + runNow.length + ' early running methods starting now at document.readyState = ' + document.readyState);
            for (let f = 0; f < runNow.length; f++) {
                runNow[f]();
            }
        }
    }
};
// FIXR page-tracker end


const fkey = "9b8140dc97b93a5c80751a9dad552bd4"; // This api key is for Flickr Fixr only. Get your own key for free at https://www.flickr.com/services/apps/create/

function escapeHTML(str) {
    return str.replace(/[&"'<>]/g, (m) => ({"&": "&amp;", '"': "&quot;", "'": "&#39;", "<": "&lt;", ">": "&gt;"})[m]);
}

function createRichElement(tagName, attributes, ...content) {
    let element = document.createElement(tagName);
    if (attributes) {
        for (const [attr, value] of Object.entries(attributes)) {
            element.setAttribute(attr, value);
        }
    }
    if (content?.length) {
        element.append(...content);
    }
    return element;
}

function insertGMapLink() {
    if (fixr.context.pageType !== 'PHOTOPAGE') {
        return; // exit if not photopage
    }
    log('insertGMapLink() running at readystate=' + document.readyState + ' and with photoId=' + fixr.context.photoId);
    if (fixr.context.photoId) {
        let maplink = fixr.content.querySelector('a.static-maps');
        if (maplink) {
            if (!document.getElementById('googlemapslink') && maplink.getAttribute('href') && (maplink.getAttribute('href').includes('map/?'))) {
                try {
                    let lat = maplink.getAttribute('href').match(/Lat=(\-?[\d\.]+)/i)[1];
                    let lon = maplink.getAttribute('href').match(/Lon=(\-?[\d\.]+)/i)[1];
                    let gmaplink = createRichElement('a', {
                        href: 'https://www.google.com/maps/search/?api=1&query=' + lat + ',' + lon,
                        id: 'googlemapslink'
                    }, 'Show location on Google Maps');
                    fixr.content.querySelector('li.c-charm-item-location').insertAdjacentElement('beforeend', createRichElement('div', {class: 'location-data-container'}, gmaplink));
                } catch (e) {
                    log('Failed creating Google Maps link: ' + e);
                }
            } else {
                log('link NOT inserted by insertGMapLink(). Invalid element or link already created. readystate=' + document.readyState);
            }
        } else {
            log('NO maplink found at readystate=' + document.readyState + '. Re-try later?');
        }
    } else {
        log('NO photoId found at readystate=' + document.readyState);
    }
}

function insertGMapLinkDelayed() {
    if (fixr.context.pageType === 'PHOTOPAGE') {
        log('insertGMapLinkDelayed() running... with pageType=' + fixr.context.pageType);
        setTimeout(insertGMapLink, 1500); // make maplink work better on photopage
        setTimeout(insertGMapLink, 3500); // Twice. Photopage is sometimes a bit slow building
        setTimeout(insertGMapLink, 8000); // Triple. Photopage is sometimes very slow building
    }
}

function mapInitializer() {
    if (window.location.href.includes('flickr.com/map/?')) {
        // https://developer.mozilla.org/en-US/docs/Web/API/URL
        const url = new URL(window.location.href);
        // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
        const imgId = url.searchParams.get('photo');
        if (imgId) {
            const focusImg = document.getElementById('f_img_thumb_' + imgId);
            if (focusImg) {
                focusImg.click(); // close and ...
                focusImg.click(); // reopen to highlight position on map
            }
        }
    }
}

const topMenuItems_style = '.fluid-subnav .extraitems a {padding: 12px 10px !important} .subnav-refresh ul.nav-links.extraitems li.sn-navitem a {padding: 13px 10px 12px 10px !important}';

function topMenuItems() {
    // User dropdown menu
    let m = document.querySelector('li[data-context=you] > ul.gn-submenu') || document.querySelector('li[data-context=you] div#you-panel ul');
    if (m) {
        let gid = null;
        if (m.querySelector('a[data-track=gnYouGroupsClick]')) {
            gid = m.querySelector('a[data-track=gnYouGroupsClick]').parentElement;
        }
        if (!gid && m.querySelector('a[data-track=You-groups]')) {
            gid = m.querySelector('a[data-track=You-groups]').parentElement;
        }
        let aad = m.querySelector('a[data-track=gnYouSetsClick]') || m.querySelector('a[data-track=You-sets]');
        if (aad && gid) {
            if (gid.hasAttribute('aria-label') && !m.querySelector('li[aria-label=Tags]')) {
                // latest design
                gid.insertAdjacentElement('afterend', createRichElement('li', {
                    class: 'menuitem',
                    role: 'menuitem',
                    'aria-label': 'Tags'
                }, createRichElement('a', {'data-track': 'gnYouTagsClick', href: '/photos/me/tags'}, 'Tags')));
                aad.parentElement.insertAdjacentElement('afterend', createRichElement('li', {
                    class: 'menuitem',
                    role: 'menuitem',
                    'aria-label': 'Map'
                }, createRichElement('a', {'data-track': 'gnYouMapClick', href: '/photos/me/map'}, 'Map')));
                aad.parentElement.insertAdjacentElement('afterend', createRichElement('li', {
                    class: 'menuitem',
                    role: 'menuitem',
                    'aria-label': 'Collections'
                }, createRichElement('a', {
                    'data-track': 'gnYouCollectionsClick',
                    href: '/photos/me/collections'
                }, 'Collections')));
            } else if (gid.classList.contains('gn-subnav-item') && !m.querySelector('a[data-track=You-tags]')) {
                // earlier design
                gid.insertAdjacentElement('afterend', createRichElement('li', {class: 'gn-subnav-item'}, createRichElement('a', {
                    'data-track': 'You-tags',
                    href: '/photos/me/tags'
                }, 'Tags')));
                aad.parentElement.insertAdjacentElement('afterend', createRichElement('li', {class: 'gn-subnav-item'}, createRichElement('a', {
                    'data-track': 'You-map',
                    href: '/photos/me/map'
                }, 'Map')));
                aad.parentElement.insertAdjacentElement('afterend', createRichElement('li', {class: 'gn-subnav-item'}, createRichElement('a', {
                    'data-track': 'You-collections',
                    href: '/photos/me/collections'
                }, 'Collections')));
            }
        }
    }
    // Photographer menu bar
    m = document.querySelector('ul.links[role=menubar]') || document.querySelector('ul.nav-links');
    if (m) {
        let gib = m.querySelector('li#groups') || m.querySelector('li.sn-groups');
        let aab = m.querySelector('li#albums a') || m.querySelector('li.sn-navitem-sets a');
        if (aab && gib) {
            m.classList.add('extraitems'); // mark extra items being added (so adjust spacing in style)
            if (gib.id === 'groups' && !m.querySelector('li#tags')) {
                // latest design
                gib.insertAdjacentElement('afterend', createRichElement('li', {
                    id: 'tags',
                    class: 'link',
                    role: 'menuitem'
                }, createRichElement('a', {href: '/photos/' + fixr.context.photographerId + '/tags'}, createRichElement('span', {}, 'Tags'))));
                aab.parentElement.insertAdjacentElement('afterend', createRichElement('li', {
                    id: 'map',
                    class: 'link',
                    role: 'menuitem'
                }, createRichElement('a', {href: '/photos/' + fixr.context.photographerId + '/map'}, createRichElement('span', {}, 'Map'))));
                aab.parentElement.insertAdjacentElement('afterend', createRichElement('li', {
                    id: 'collections',
                    class: 'link',
                    role: 'menuitem'
                }, createRichElement('a', {href: '/photos/' + fixr.context.photographerId + '/collections'}, createRichElement('span', {}, 'Collections'))));
            } else if (gib.classList.contains('sn-groups') && !m.querySelector('li.sn-tags')) {
                // earlier design
                gib.insertAdjacentElement('afterend', createRichElement('li', {class: 'sn-navitem sn-tags'}, createRichElement('a', {
                    'data-track': 'YouSubnav-tags',
                    href: '/photos/' + fixr.context.photographerId + '/tags'
                }, 'Tags')));
                aab.parentElement.insertAdjacentElement('afterend', createRichElement('li', {class: 'sn-navitem sn-map'}, createRichElement('a', {
                    'data-track': 'YouSubnav-map',
                    href: '/photos/' + fixr.context.photographerId + '/map'
                }, 'Map')));
                aab.parentElement.insertAdjacentElement('afterend', createRichElement('li', {class: 'sn-navitem sn-collections'}, createRichElement('a', {
                    'data-track': 'YouSubnav-collections',
                    href: '/photos/' + fixr.context.photographerId + '/collections'
                }, 'Collections')));
            }
        }
    }
}

let album = { // cache to avoid repeating requests
    albumId: '',
    commentCount: 0,
    comment: [],
    description: ''
};

let _wsGetPhotosetCommentsLock = 0;

function wsGetPhotosetComments() { // Call Flickr REST API to get album comments
    let diff = Date.now() - _wsGetPhotosetCommentsLock;
    if ((_wsGetPhotosetCommentsLock > 0) && (diff < 50)) {
        log('Skipping wsGetPhotosetComments() because already running?: ' + diff);
        // *** maybe add a check to see if we are still on same album?!
        return;
    }
    _wsGetPhotosetCommentsLock = Date.now();

    function handleResponse(response) {
        if (response.ok) {
            if (response.headers.get('content-type') && response.headers.get('content-type').includes('application/json')) {
                return response.json()
            }
            throw new Error('Response was not in expected json format.');
        }
        throw new Error('Network response was not ok.');
    }

    function handleResult(obj) {

        album.albumId = fixr.context.albumId;
        album.commentCount = -1;

        if (obj.stat === "ok") {
            log("flickr.photosets.comments.getList returned ok");
            if (obj.comments?.photoset_id) {
                album.albumId = obj.comments.photoset_id;
            }
            if (obj.comments?.comment) {
                album.commentCount = obj.comments.comment.length;
            } else {
                album.commentCount = 0;
            }
        } else {
            log('flickr.photosets.comments.getList returned an ERROR: obj.stat=' + obj.stat + ', obj.code=' + obj.code + ', obj.message=' + obj.message);
        }

        if (document.getElementById('albumCommentCount')) {
            if (album.commentCount === -1) {
                document.getElementById('albumCommentCount').innerText = '?';
            } else {
                document.getElementById('albumCommentCount').innerText = String(album.commentCount);
            }
        } else {
            log('albumCommentCount element not found');
        }

        _wsGetPhotosetCommentsLock = 0;
    }

    function handleError(error) {
        console.log('There has been a problem with your fetch operation: ', error.message);
        log('There has been a problem with your fetch operation: ' + error);
    }

    if (fixr.isWebExtension()) {
        // Call fetch() from background-script in WebExtensions, because changes in Chrome/Chromium https://www.chromium.org/Home/chromium-security/extension-content-script-fetches
        browser.runtime.sendMessage({
            msgtype: "flickrservice",
            method: "flickr.photosets.comments.getList",
            fkey: fkey,
            options: {photoset_id: fixr.context.albumId}
        }).then(handleResult).catch(handleError);
    } else { // Userscript (So far it still works, also on Chrome/Tampermonkey...)
        fetch('https://api.flickr.com/services/rest/?method=flickr.photosets.comments.getList&api_key=' + fkey + '&photoset_id=' + fixr.context.albumId + '&format=json&nojsoncallback=1').then(handleResponse).then(handleResult).catch(handleError);
    }
}


let albums = { // cache albums to avoid repeating requests
    ownerId: '',
    column: '',
    count: 0
};

function getAlbumlist() {
    let _reqAlbumlist = null;
    if (window.XMLHttpRequest) { // TODO: Surprised to see I'm still using XMLHttpRequest !
        _reqAlbumlist = new XMLHttpRequest();
        if (typeof _reqAlbumlist.overrideMimeType !== 'undefined') {
            _reqAlbumlist.overrideMimeType('text/html');
        }

        _reqAlbumlist.onreadystatechange = function () {
            if (_reqAlbumlist.readyState === 4 && _reqAlbumlist.status === 200) {
                log('_reqAlbumlist returned status=' + _reqAlbumlist.status); // + ', \ntext:\n' + _reqAlbumlist.responseText);
                let doc = document.implementation.createHTMLDocument("sizeDoc");
                doc.documentElement.innerHTML = _reqAlbumlist.responseText; // NOTICE, this is NOT inserted directly into HTML-document! In the following DOM-content of doc is being analyzed...
                albums.ownerId = fixr.context.photographerId;
                albums.column = new DocumentFragment();
                albums.count = 0;
                const alist = Array.from(doc.body.querySelectorAll('div.photo-list-album-view'));
                const imgPattern = /url\([\'\"]*([^\)\'\"]+)(\.[jpgtifn]{3,4})[\'\"]*\)/i;
                let columnhead = createRichElement('div', {style: 'margin:0 0 .8em 0'});
                if (document.getElementById('albumTeaser')) {
                    if (alist && alist.length > 0) {
                        columnhead.textContent = "Albums";
                        albums.column.appendChild(columnhead);
                        albums.count = alist.length;
                        for (let e of alist.slice(0, 10)) {
                            let imgUrl = '';
                            //log(e.outerHTML);
                            // var result = e.style.backgroundImage.match(imgPattern); // strangely not working in Chrome
                            let result = (e.outerHTML).match(imgPattern); // quick work-around for above (works for now)
                            if (result) {
                                // imgUrl = result[1].replace(/_[a-z]$/, '') + '_s' + result[2];
                                imgUrl = result[1].replace(/_[a-z]$/, '') + '_q' + result[2];
                                log('imgUrl=' + imgUrl);
                            } else {
                                log('No match on imgPattern');
                            }
                            var a = e.querySelector('a[href][title]'); // sub-element
                            if (a) {
                                log('Album title: ' + a.title);
                                log('Album url: ' + a.getAttribute('href'));
                                let album = document.createElement("div");
                                let thumbnail = createRichElement('img', {src: imgUrl, class: 'asquare', alt: ''});
                                let albumtitle = createRichElement('div', {style: 'margin:0 0 .8em 0'}, a.title);
                                let anchor = createRichElement('a', {href: a.getAttribute('href')}, thumbnail, albumtitle);
                                album.appendChild(anchor);
                                albums.column.appendChild(album);
                            } else {
                                log('a element not found?');
                            }
                        }
                    } else if (alist) {
                        if (doc.body.querySelector('h3')) {
                            columnhead.textContent = doc.body.querySelector('h3').innerText;
                            albums.column.appendChild(columnhead);
                        }
                    } else {
                        log('(e Undefined) Problem reading albums or no albums??? : ' + _reqAlbumlist.responseText);
                    }
                    let foot = document.createElement("div");
                    let cursive = document.createElement("i");
                    let moreanchor = document.createElement("a");
                    moreanchor.href = "/photos/" + (fixr.context.photographerAlias || fixr.context.photographerId) + "/albums/";
                    moreanchor.textContent = albums.count > 10 ? 'More albums...' : (albums.count === 0 ? 'No albums found...' : '');
                    cursive.appendChild(moreanchor);
                    foot.appendChild(cursive);
                    albums.column.appendChild(foot);
                    document.getElementById('albumTeaser').appendChild(albums.column);
                } else {
                    log('albumTeaser NOT FOUND!?!');
                }
            } else {
                // wait for the call to complete
            }
        };

        if (fixr.context.photographerId) {
            const url = 'https://' + window.location.hostname + '/photos/' + (fixr.context.photographerAlias || fixr.context.photographerId) + '/albums';
            _reqAlbumlist.open('GET', url, true);
            _reqAlbumlist.send(null);
        } else {
            log('Attribution user (photographer) not found');
        }
    } else {
        log('understøtter ikke XMLHttpRequest');
    }
}

const albumTeaser_style = 'div#albumTeaser {border:none;margin:0;padding:0;position:absolute;top:0;right:-120px;width:100px}';

function albumTeaser() {
    if (fixr.context.pageType !== 'PHOTOSTREAM') {
        return; // exit if not photostream
    }
    log('albumTeaser() running');
    let dpc = document.querySelector('div.photolist-container');
    if (!dpc) {
        return;
    }
    log('AlbumTeaser found div.photolist-container');
    if (!document.getElementById('albumTeaser')) {
        dpc.style.position = "relative";
        dpc.insertAdjacentElement('afterbegin', createRichElement('div', {id: 'albumTeaser'}));
    }
    if (document.getElementById('albumTeaser')) {
        getAlbumlist();  // også check på fixr.context.photographerId ?
    }
}

let _timerAlbumTeaserDelayed;

function albumTeaserDelayed() {
    if (fixr.context.pageType !== 'PHOTOSTREAM') {
        return; // exit if not photostream
    }
    log('albumTeaserDelayed() running...');
    clearTimeout(_timerAlbumTeaserDelayed);
    _timerAlbumTeaserDelayed = setTimeout(albumTeaser, 1500);
}

const exploreCalendar_style = '#exploreCalendar {border:none;margin:0;padding:0;position:absolute;top:38px;right:-120px;width:100px} #exploreCalendar div {margin:0 0 .8em 0} #exploreCalendar img.asquare {width:75px;height:59px}';

function exploreCalendar() {
    if (fixr.context.pageType !== 'EXPLORE') {
        return; // exit if not explore/interestingness
    }
    log('exploreCalendar() running');
    let dtr = document.querySelector('div.title-row');
    if (!dtr) {
        return;
    }
    log('exploreCalendar found div.photo-list-view');
    if (!document.getElementById('exploreCalendar')) {
        dtr.style.position = "relative";
        let exploreMonth = fixr.clock.explore().substring(0, 7).replace('-', '/');
        let explCal = createRichElement('a', {href: '/explore/interesting/' + exploreMonth + '/'}, createRichElement('img', {
            src: 'https://c2.staticflickr.com/2/1701/24895062996_78719dec15_o.jpg',
            class: 'asquare',
            alt: ''
        }), createRichElement('div', {}, 'Explore Calendar'));
        let freshUpl = createRichElement('a', {
            href: '/search/?text=&view_all=1&media=photos&content_type=1&dimension_search_mode=min&height=640&width=640&safe_search=2&sort=date-posted-desc&min_upload_date=' + (Math.floor(Date.now() / 1000) - 7200),
            title: 'If you are an adventurer and want to explore something different than everybody else...'
        }, createRichElement('img', {
            src: 'https://c2.staticflickr.com/2/1617/25534100345_b4a3fe78f1_o.jpg',
            class: 'asquare',
            alt: ''
        }), createRichElement('div', {}, 'Fresh uploads'));
        dtr.insertAdjacentElement('afterbegin', createRichElement('div', {id: 'exploreCalendar'}, explCal, freshUpl));
        log('San Francisco PST UTC-8: ' + fixr.clock.pst());
        log('Explore Beat (Yesterday, UTC-4): ' + fixr.clock.explore());
    }
}

// SIMPLE
// const collapsibleSidebar_style = "#search-unified-content {position: relative} #sidebartoggle {position: absolute; right: 0; top: 5em; width: 3em; height: 1.5em; background-color: #CFC; margin-right:-2em; z-index: 10} body.searchsidebar_closed .sidebar-column {display:none}";
// BETTER
const collapsibleSidebar_style = "body.searchsidebar_closed .search-container-w-sidebar .sidebar-column {width: 0px !important; margin-left: 0 !important} body.searchsidebar_closed .sidebar-content-container > div:not(#sidebartoggle) {overflow:hidden; display:none} .sidebar-content-container, #search-unified-content {position: relative} #sidebartoggle {position: absolute; right: -1.7em; top: 6em; display: flex; justify-content: center; align-items: center; width: 2em; height: 2em; color: #128fdc; border: 2px solid #128fdc; border-radius: 50%; background-color: #FFF; z-index: 10; cursor: pointer; transition: all 0.3s; }  body.searchsidebar_closed #sidebartoggle {transform: rotate(180deg);}";
function collapsibleSidebar() {
    if (['SEARCHRESULTPHOTOS', 'SEARCHRESULTPEOPLE', 'SEARCHRESULTGROUPS'].includes(fixr.context.pageType)) {
        // SIMPLE:
        // var cnt = document.querySelector('#search-unified-content');
        // if (cnt) {
        //     var toggle = createRichElement('div', {id: 'sidebartoggle'}, '❯');
        //     cnt.insertAdjacentElement('afterbegin', createRichElement('div', {id: 'sidebartoggle'}, '[<->]'));
        //     ...
        // BETTER:
        let cnt = document.querySelector('.sidebar-content-container');
        if (cnt) {
            if (!document.getElementById('sidebartoggle')) {
                let toggle = createRichElement('div', {id: 'sidebartoggle'}, '❯');
                cnt.insertAdjacentElement('afterbegin', toggle);
                setTimeout(function () {
                    window.dispatchEvent(new Event('resize', {'cancelable': true}))
                }, 100);
                toggle.addEventListener('click', function (e) {
                    document.body.classList.toggle('searchsidebar_closed');
                    setTimeout(function () {
                        window.dispatchEvent(new Event('resize', {'cancelable': true}))
                    }, 100);
                })
            }
        } else {
            setTimeout(collapsibleSidebar, 2000);
        }
    }
}

let _timerExploreCalendarDelayed;

function exploreCalendarDelayed() {
    if (fixr.context.pageType !== 'EXPLORE') {
        return; // exit if not explore/interestingness
    }
    log('albumTeaserDelayed() running...');
    clearTimeout(_timerExploreCalendarDelayed);
    _timerExploreCalendarDelayed = setTimeout(exploreCalendar, 1500);
}

function ctrlClick(e) {
    let elem, evt = e ? e : event;
    if (evt.srcElement) elem = evt.srcElement;
    else if (evt.target) elem = evt.target;
    if (evt.ctrlKey) {
        log('Ctrl clicked. Further scripted click-event handling canceled. Allow the default ctrl-click handling in my browser.');
        evt.stopPropagation();
    }
}

function ctrlClicking() {
    let plv = document.querySelectorAll('div.photo-list-view');
    for (let i = 0; i < plv.length; i++) {
        log('ctrlClicking(): plv[' + i + '] found!');
        // Allow me to open tabs in background by ctrl-click in Firefox:
        plv[i].parentNode.addEventListener('click', ctrlClick, true);
    }
}

let _timerCtrlClicking;

function ctrlClickingDelayed() {
    log('ctrlClickingDelayed() running...');
    clearTimeout(_timerCtrlClicking);
    _timerCtrlClicking = setTimeout(ctrlClicking, 1500);
}

const topPagination_style = '#topPaginationContainer{width:250px;height:40px;margin:0 auto;position:absolute;top:0;left:0;right:0;border:none} #topPagination{width:720px;margin:0;position:absolute;top:0;left:-235px;text-align:center;z-index:10;display:none;border:none;padding:10px 0 10px 0;overflow:hidden} .album-toolbar-content #topPagination{top:-16px} .group-pool-subheader-view #topPagination{top:-7px} .title-row #topPagination{width:830px;left:-290px;top:-12px} #topPaginationContainer:hover #topPagination{display:block}';

function topPagination() {
    log('topPagination()');
    let bottomPagination = document.querySelector('.pagination-view');
    if (!bottomPagination) {
        bottomPagination = document.querySelector('.explore-pagination');
    }
    if (bottomPagination && !document.getElementById('topPagination')) {
        if (bottomPagination.childElementCount > 0) {
            let topPagination = bottomPagination.cloneNode(true);
            topPagination.id = 'topPagination';
            let topPaginationContainer = document.createElement('div');
            topPaginationContainer.id = 'topPaginationContainer';
            topPaginationContainer.appendChild(topPagination);
            let topbar = document.querySelector('.fluid-magic-tools-view');
            if (!topbar) topbar = document.querySelector('.album-toolbar-content');
            if (!topbar) topbar = document.querySelector('.group-pool-subheader-view');
            if (!topbar) topbar = document.querySelector('.title-row');
            if (topbar) {
                log('topPagination: root found, inserting container');
                topbar.appendChild(topPaginationContainer);
            }
        }
    }
}

const albumExtras_style = '.album-map-icon{background:url("https://c2.staticflickr.com/6/5654/23426346485_334afa6e8f_o_d.png") no-repeat;height:21px;width:24px;top:6px;left:3px} .album-comments-icon{background:url("https://c1.staticflickr.com/5/4816/46041390622_f8a0cf0148_o.png") -32px -460px no-repeat;height:21px;width:24px;top:6px;left:3px}';

function albumExtras() { // links to album's map and comments
    if (fixr.context.pageType !== 'ALBUM') {
        return; // exit if not albumpage
    }
    if (fixr.context.albumId) {
        log('albumsExtra() med album=' + fixr.context.albumId);
    } else {
        log('Exit albumsExtra(). Mangler albumId');
        return;
    }
    let elist = document.querySelector('div.album-engagement-view');
    if (elist && !document.getElementById('albumCommentCount')) {
        // map-link:
        let mapdiv = document.createElement('div');
        mapdiv.className = 'create-book-container';
        mapdiv.title = 'Album on map';
        mapdiv.style.textAlign = 'center';
        let maplink = document.createElement('a');
        maplink.href = '/photos/' + fixr.context.photographerAlias + '/albums/' + fixr.context.albumId + '/map/';
        maplink.style.fontSize = '14px';
        maplink.style.color = '#FFF';
        let mapicon = document.createElement('span');
        mapicon.title = 'Album on map';
        mapicon.className = 'album-map-icon';
        maplink.appendChild(mapicon);
        mapdiv.appendChild(maplink);
        elist.appendChild(mapdiv);
        // comments-link:
        let comurl = '/photos/' + fixr.context.photographerAlias + '/sets/' + fixr.context.albumId + '/comments/';  // /sets/* urls works, /albums/* urls currently doesn't work (yet?)
        // var comurl = '#'; // NEW?!
        let cmdiv = document.createElement('div');
        cmdiv.className = 'create-book-container';
        cmdiv.title = 'Comments';
        cmdiv.style.textAlign = 'center';
        let cmlink = document.createElement('a');
        cmlink.href = comurl;
        cmlink.style.fontSize = '14px';
        cmlink.style.color = '#FFF';
        cmlink.id = 'albumCommentsLink';
        let cmicon = document.createElement('span');
        cmicon.title = 'Album comments';
        cmicon.className = 'album-comments-icon';
        cmicon.id = 'albumCommentCount';
        cmlink.appendChild(cmicon);
        cmdiv.appendChild(cmlink);
        elist.appendChild(cmdiv);

        wsGetPhotosetComments();
    }
}

const updateTags_style_avatar = 'a.fixrTag>img {width:1em;height:1em;margin:0;padding:0;position:relative;top:3px}';
const updateTags_style_hover = 'ul.tags-list>li.tag>a.fixrTag,ul.tags-list>li.autotag>a.fixrTag{display:none;} ul.tags-list>li.tag:hover>a.fixrTag,ul.tags-list>li.autotag:hover>a.fixrTag{display:inline;} ' + updateTags_style_avatar;
const updateTags_style_persist = 'ul.tags-list>li.tag>a.fixrTag,ul.tags-list>li.autotag>a.fixrTag{display:inline;} ' + updateTags_style_avatar;

function updateTags() {
    if (fixr.context.pageType !== 'PHOTOPAGE') {
        return; // exit if not photopage
    }
    if (fixr.context.photographerAlias === '') {
        fixr.initPhotoId();
    }
    if (fixr.context.photographerId === '') {
        fixr.initPhotographerId();
    }
    if (fixr.context.photographerName === '') {
        fixr.initPhotographerName();
    }
    log('updateTags() med photographerAlias=' + fixr.context.photographerAlias + ', photographerId=' + fixr.context.photographerId + ' og photographerName=' + fixr.context.photographerName);
    if (document.querySelector('ul.tags-list')) {
        let tags = document.querySelectorAll('ul.tags-list>li');
        if (tags && tags.length > 0) {
            log('updateTags() Looping ' + tags.length + ' tags...');
            let iconHref = fixr.context.photographerIcon.match(/^([^_]+)(_\w)?\.[jpgntif]{3,4}$/)[1] + String(fixr.context.photographerIcon.match(/^[^_]+(_\w)?(\.[jpgntif]{3,4})$/)[2]); // do we know for sure it is square?
            for (let tag of tags) {
                let atag = tag.querySelector('a[title][href*="/photos/tags/"],a[title][href*="?tags="],a[title][href*="?q="]');
                if (atag) {
                    let realtag = (atag.href.match(/((\/tags\/)|(\?tags\=)|(\?q\=))([\S]+)$/i))[5];
                    if (!(tag.querySelector('a.fixrTag'))) {
                        let avatar = document.createElement('img');
                        avatar.src = iconHref;
                        avatar.alt = '*';
                        let taglink = document.createElement('a');
                        taglink.className = 'fixrTag';
                        taglink.href = '/photos/' + (fixr.context.photographerAlias || fixr.context.photographerId) + '/tags/' + realtag + '/';
                        taglink.title = atag.title + ' by ' + fixr.context.photographerName;
                        taglink.appendChild(avatar);
                        tag.insertBefore(taglink, tag.firstChild);
                    }
                } else {
                    log('updateTags(): atag not matched.');
                }
            }
        } else {
            log('updateTags(): No tags defined (yet?)');
        }
    } else {
        log('updateTags(): taglist container not found');
    }
}

function updateTagsDelayed() {
    log('updateTagsDelayed() running... with pageType=' + fixr.context.pageType);
    if (fixr.context.pageType === 'PHOTOPAGE') {
        setTimeout(updateTags, 2500);
        setTimeout(updateTags, 4500); // Twice. Those tags are sometimes a bit slow emerging
        setTimeout(updateTags, 8500); // Triple. Those tags are sometimes very slow emerging
    }
}

const photoDates_style = '.has-date-info {position:relative} .date-info{z-index:10;padding:0 .5em 0 .5em;display:none;position:absolute;top:30px;left:-40px;width:400px;margin-right:-400px;background-color:rgba(255,250,150,0.9);color:#000;border:1px solid #d4b943;border-radius:4px;} .has-date-info:hover .date-info{display:block;} .date-info label {display:inline-block; min-width: 5em;}';

function photoDates() {
    let elem = document.querySelector('div.view.sub-photo-date-view');
    if (elem && !elem.classList.contains('has-date-info')) {
        elem.classList.add('has-date-info');
        elem.insertAdjacentElement('beforeend', createRichElement('div', {class: 'date-info'}, 'Date info!'));
        wsGetPhotoInfo(); // get dates
    }
}

function photoDatesDelayed() {
    log('photoDates() running... with pageType=' + fixr.context.pageType);
    if (fixr.context.pageType === 'PHOTOPAGE') {
        setTimeout(photoDates, 2000);
        setTimeout(photoDates, 4000); // Twice.
    }
}

function shootingSpaceballs() {
    // Enable image context-menu on "View sizes" page by removing overlaying div.
    // This is *not* meant as a tool for unauthorized copying and distribution of other peoples photos.
    // Please respect image ownership and copyrights!
    if (fixr.context.pageType === 'SIZES') {
        function trashing() {
            let trash = document.querySelector('div.spaceball');
            while (trash && trash.parentNode) {
                trash.parentNode.removeChild(trash);
                trash = document.querySelector('div.spaceball');
            }
        }

        let asp = document.querySelector('#allsizes-photo');
        if (asp) {
            document.body.addEventListener('click', trashing, false);
            asp.click();
        }
        // document.body.style.backgroundColor = "#cfc";
    }
}

const orderwarning_style = '.filter-sort.warning p {animation:wink 3s ease 1s 1;} @keyframes wink {0% {background-color:transparent;} 50% {background-color:rgba(255,250,150,0.9);} 100% {background-color:transparent;}} .filter-sort.warning:after{content:"You are looking at this photostream in Date-taken order. Change order to Date-uploaded, to be sure to see latest uploads in the top of photostreams.";z-index:10;padding:.5em;display:none;position:relative;top:-2px;right:-50px;width:400px;margin-right:-400px;background-color:rgba(255,250,150,0.9);color:#000;border:1px solid #d4b943;border-radius:4px;} .filter-sort.warning:hover:after{display:block;}';

function orderWarning() {
    if (fixr.context.pageType === 'PHOTOSTREAM') {
        let e = document.querySelector('.dropdown-link.filter-sort');
        if (e) {
            if (['Date taken', 'Fecha de captura', 'Aufnahmedatum', 'Date de prise de vue', 'Data dello scatto', 'Tirada na data', 'Ngày chụp', 'Tanggal pengambilan', '拍攝日期', '촬영 날짜'].includes(e.innerText.trim())) {
                e.classList.add('warning');
            } else {
                e.classList.remove('warning');
            }
        }
    }
}

const newsfeedLinks_style = 'div#feedlinks {border:none;margin:0;padding:0;position:absolute;top:0;right:10px;width:100px} ul.gn-tools>div#feedlinks {right:-120px} div#gn-wrap>div#feedlinks, div.header-wrap>div#feedlinks, header#branding>div#feedlinks {right:-120px} div#feedlinks>a {display:block;float:left;margin:10px 8px 0 0;width:16px} div#feedlinks>a>img {display:block;width:16px;height:16px} ul.gn-tools>div#feedlinks>a {margin-top:2px}';

function newsfeedLinks() {
    let elem = document.getElementById('feedlinks');
    if (elem) {
        elem.textContent = '';
    }
    setTimeout(updateNewsfeedLinks, 500); // give Flickr time to update link tags in head
}

function updateNewsfeedLinks() {
    let feedlinks = document.querySelectorAll('head > link[rel="alternate"][type="application/rss+xml"], head > link[rel="alternate"][type="application/atom+xml"], head > link[rel="alternate"][type="application/atom+xml"], head > link[rel="alternate"][type="application/json"]');
    log('Number of feed links found: ' + feedlinks.length);
    let dgnc = document.querySelector('div.global-nav-container ul.gn-tools') || document.querySelector('div#gn-wrap') || document.querySelector('div#global-nav') || document.querySelector('div.header-wrap') || document.querySelector('header#branding') || document.querySelector('div.custom-header-container>div>div');
    if (dgnc) {
        if (!document.getElementById('feedlinks')) {
            dgnc.style.position = "relative";
            dgnc.insertAdjacentElement('afterbegin', createRichElement('div', {id: 'feedlinks'}));
        }
        let elem = document.getElementById('feedlinks');
        if (elem) {
            let feedicons = new DocumentFragment();
            for (const link of feedlinks) {
                let feedlink = document.createElement('a');
                feedlink.href = link.href;
                let feedsymbol = document.createElement('img');
                feedsymbol.src = 'https://c1.staticflickr.com/5/4869/32220441998_601de47e20_o.png';
                feedsymbol.alt = 'Feedlink';
                feedsymbol.style.width = '16px';
                feedsymbol.style.height = '16px';
                feedsymbol.title = link.title;
                feedlink.appendChild(feedsymbol);
                feedicons.appendChild(feedlink);
            }
            elem.textContent = ''; // remove existing content
            elem.appendChild(feedicons);
        }
    }
}

function initSlideshowSpeedHook() {
    let timeoutScript = document.createElement('script');
    timeoutScript.src = browser.runtime.getURL('inject/timeout.js');
    timeoutScript.onload = function () {
        this.remove();
    };
    (document.head || document.documentElement).appendChild(timeoutScript);
}

let slideshowSpeed = '5'; // Flickr default (seconds)
function initSlideshowSpeed() {
    if (document.body) {
        document.body.setAttribute('data-slideshowspeed', slideshowSpeed);
    }
}

let _wsGetPhotoInfoLock = 0;

function wsGetPhotoInfo() { // Call Flickr REST API to get photo info
    let diff = Date.now() - _wsGetPhotoInfoLock;
    if ((_wsGetPhotoInfoLock > 0) && (diff < 50)) {
        log('Skipping wsGetPhotoInfo() because already running?: ' + diff);
        // *** maybe add a check to see if we are still on same photo?!
        return;
    }
    _wsGetPhotoInfoLock = Date.now();

    function handleResponse(response) {
        if (response.ok) {
            if (response.headers.get('content-type') && response.headers.get('content-type').includes('application/json')) {
                return response.json()
            }
            throw new Error('Response was not in expected json format.');
        }
        throw new Error('Network response was not ok.');
    }

    function handleResult(obj) {
        let elem = document.querySelector('.date-info');
        if (obj.stat === "ok") {
            log("flickr.photos.getInfo returned ok");
            if (obj.photo?.id) {
                let uploadDate = new Date(0);
                let debugstr = '';
                if (obj.photo.dateuploaded) {
                    uploadDate = new Date(obj.photo.dateuploaded * 1000);
                    debugstr = 'UploadDate: ' + uploadDate.toString();
                }
                let dateDetail = createRichElement('p', {'x-ms-format-detection': 'none'});
                let takenLabel = createRichElement('label', {}, 'Taken:');
                let uploadedLabel = createRichElement('label', {}, 'Uploaded:');
                let brElem = document.createElement('br');
                if (obj.photo.dates) {
                    if (obj.photo.dateuploaded !== obj.photo.dates.posted) {
                        log('Unexpected Date difference!');
                    }
                    if (obj.photo.dates.posted) {
                        if (obj.photo.dates.posted < obj.photo.dateuploaded) {
                            uploadDate = new Date(obj.photo.dates.posted * 1000); // GMT/UTC
                        }
                        debugstr += '<br />PostDate: ' + uploadDate.toString();
                    }
                    if (obj.photo.dates.taken && obj.photo.dates.takenunknown.toString() === '0') {
                        debugstr += '<br />TakenDate: ' + obj.photo.dates.taken + ' (granularity=' + obj.photo.dates.takengranularity + ')';
                        let takenDateTmp = obj.photo.dates.taken.replace(/[^\d-:\s]/g, ''); // "2018-03-30 00:35:44" (Remove any unexpected characters)
                        let takenDate = new Date(Date.parse(takenDateTmp.replace(' ', 'T')));
                        let dayStart = new Date(Date.parse(takenDateTmp.substring(0, 10) + 'T00:00:00'));
                        let dayEnd = new Date(Date.parse(takenDateTmp.substring(0, 10) + 'T23:59:59'));
                        let takenTimeIndex = takenDate.toString().search(/\d{2}[:\.]\d{2}[:\.]\d{2}/);
                        if (obj.photo.dates.takengranularity.toString() === '0') { // 0	Y-m-d H:i:s - full datetime
                            let linkElem = createRichElement('a', {href: '/search/?user_id=' + fixr.context.photographerId + '&view_all=1&min_taken_date=' + (Math.floor(dayStart.getTime() / 1000) - 43200) + '&max_taken_date=' + (Math.floor(dayEnd.getTime() / 1000) + 43200)}, takenDate.toString().substring(0, takenTimeIndex - 1));
                            dateDetail.append(takenLabel, ' ', linkElem, takenDate.toString().substring(takenTimeIndex - 1, takenTimeIndex + 8) + ' "Camera Time"', brElem);
                        } else if (obj.photo.dates.takengranularity.toString() === '4') { // 4	Y-m
                            dateDetail.append(takenLabel, ' ' + takenDateTmp.substring(0, 7), brElem);
                        } else if (obj.photo.dates.takengranularity.toString() === '6') { // 6	Y
                            dateDetail.append(takenLabel, ' ' + takenDateTmp.substring(0, 4), brElem);
                        } else if (obj.photo.dates.takengranularity.toString() === '8') { // 8	Circa...
                            dateDetail.append(takenLabel, ' Circa ' + takenDateTmp.substring(0, 4), brElem);
                        } else {
                            log('Unexpected value for photo.dates.takengranularity: ' + obj.photo.dates.takengranularity);
                        }
                    }
                    if (obj.photo.dates.lastupdate) { // photo has been updated/replaced
                        debugstr += '<br />UpdateDate: ' + (new Date(obj.photo.dates.lastupdate * 1000)).toString();
                    }
                }
                if (elem) {
                    let uploadDateStr = uploadDate.toString();
                    let n = uploadDateStr.indexOf('(');
                    if (n > 0) {
                        dateDetail.append(uploadedLabel, ' ' + uploadDateStr.substring(0, n));
                    } else {
                        dateDetail.append(uploadedLabel, ' ' + uploadDateStr);
                    }
                    elem.textContent = '';
                    elem.append(dateDetail);
                    let withTitle = elem.parentElement.querySelector('span[title]');
                    if (withTitle) {
                        withTitle.removeAttribute('title');
                    }
                }
            }
        } else {
            if (elem) {
                elem.textContent = 'Cannot fetch detailed date details on private photos';
            }
            log('flickr.photos.getInfo returned an ERROR: obj.stat=' + obj.stat + ', obj.code=' + obj.code + ', obj.message=' + obj.message);
        }
        _wsGetPhotoInfoLock = 0;
    }

    function handleError(error) {
        console.log('There has been a problem with your fetch operation: ', error.message);
        log('There has been a problem with your fetch operation: ' + error);
        let elem = document.querySelector('.date-info');
        if (elem) {
            elem.textContent = 'There was an error fetching detailed date details...';
        }
    }

    if (fixr.isWebExtension()) {
        // Call fetch() from background-script in WebExtensions, because changes in Chrome/Chromium https://www.chromium.org/Home/chromium-security/extension-content-script-fetches
        browser.runtime.sendMessage({
            msgtype: "flickrservice",
            method: "flickr.photos.getInfo",
            fkey: fkey,
            options: {photo_id: fixr.context.photoId}
        }).then(handleResult).catch(handleError);
    } else { // Userscript (So far it still works, also on Chrome/Tampermonkey...)
        fetch('https://api.flickr.com/services/rest/?method=flickr.photos.getInfo&api_key=' + fkey + '&photo_id=' + fixr.context.photoId + '&format=json&nojsoncallback=1').then(handleResponse).then(handleResult).catch(handleError);
    }
}

function stereotest() {
    let self = "flickrfixruserscript";
    let other = "flickrfixrwebextension";
    if (fixr.isWebExtension()) {
        self = "flickrfixrwebextension";
        other = "flickrfixruserscript";
    }
    document.body.classList.add(self);
    if (document.body.classList.contains(other)) {
        alert("It looks like you are running both Stigs Flickr Fixr userscript and Flickr Fixr browser extension at once. Please uninstall or disable one of them to avoid errors and unpredictable behaviors!");
    }
}

function runEarly() {
    //localStorage.setItem('filterFeedEvents', 'people'); // Try to make People feed default.
}

function weekNo(dt) {
    let date = dt || new Date();
    date.setHours(0, 0, 0, 0);
    // Thursday in current week decides the year.
    date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
    // January 4 is always in week 1.
    let week1 = new Date(date.getFullYear(), 0, 4);
    // Adjust to Thursday in week 1 and count number of weeks from date to week1.
    return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
        - 3 + (week1.getDay() + 6) % 7) / 7);
}
function userscriptWarning() {
    if (!document.body.classList.contains("flickrfixrwebextension")) { // Skip warning if both versions are installed (There will be another warning from stereotest()).
        let info = (GM_info ? GM_info : (typeof GM === 'object' && GM !== null && typeof GM.info === 'object' ? GM.info : null));
        if (info) {
            let prevWeekNo = localStorage.getItem("fixr_userscript_warning_weekno");
            let prevVersion = localStorage.getItem("fixr_userscript_warning_version");
            if (!prevVersion || prevVersion !== info.script.version || !prevWeekNo || prevWeekNo != weekNo()) {
                localStorage.setItem("fixr_userscript_warning_weekno", weekNo());
                localStorage.setItem("fixr_userscript_warning_version", info.script.version);
                alert('\nYou are running the userscript-version of Flickr Fixr via ' + info.scriptHandler + '. \n\nUserscript version of Flickr Fixr is not supported anymore, and probably wont be upgraded even if a Flickr update breaks this userscript. \n\nFor continued support and updates, make the switch to the Flickr Fixr browser-extension. You can find Flickr Fixr browser extension in the Add-on webstores for Chrome, Firefox and Edge.\n');
            }
        }
    }
}

const shared_style = 'img.asquare {width:75px;height:75px;border:none;margin:0;padding:0;transition:all 0.3s ease} a:hover>img.asquare{transform:scale(1.3)}'; // used by multiple features

function handlerInitFixr(options) { // Webextension init
    let runNow = [];
    let onPageHandlers = [];
    let onResizeHandlers = [];
    let onFocusHandlers = [];
    let onStandaloneHandlers = [];

    fixr.style.add(shared_style);
    onPageHandlers.push(stereotest);
    if (options.topMenuItems) {
        fixr.style.add(topMenuItems_style);
        onPageHandlers.push(topMenuItems);
        onStandaloneHandlers.push(topMenuItems);
    }
    if (options.ctrlClicking) {
        onPageHandlers.push(ctrlClicking);
    }
    if (options.albumExtras) {
        fixr.style.add(albumExtras_style);
        onPageHandlers.push(albumExtras);
    }
    if (options.topPagination) {
        fixr.style.add(topPagination_style);
        onPageHandlers.push(topPagination);
    }
    if (options.shootingSpaceballs) {
        onPageHandlers.push(shootingSpaceballs);
    }
    if (options.orderWarning) {
        fixr.style.add(orderwarning_style);
        onPageHandlers.push(orderWarning);
    }
    if (options.newsfeedLinks) {
        fixr.style.add(newsfeedLinks_style);
        onPageHandlers.push(newsfeedLinks);
        onStandaloneHandlers.push(newsfeedLinks);
    }
    if (options.photoDates) {
        fixr.style.add(photoDates_style);
        onPageHandlers.push(photoDatesDelayed);
    }
    if (options.ctrlClicking) {
        onPageHandlers.push(ctrlClickingDelayed);
    }
    if (options.exploreCalendar) {
        fixr.style.add(exploreCalendar_style);
        onPageHandlers.push(exploreCalendarDelayed);
    }
    if (options.albumTeaser) {
        fixr.style.add(albumTeaser_style);
        onPageHandlers.push(albumTeaserDelayed);
    }
    if (options.insertGMapLink) {
        onPageHandlers.push(insertGMapLinkDelayed);
        onStandaloneHandlers.push(mapInitializer);
    }
    if (options.updateTags) {
        if (options.updateTags_tagmode === 'updateTags_persist') {
            fixr.style.add(updateTags_style_persist);
        } else {
            fixr.style.add(updateTags_style_hover);
        }
        onPageHandlers.push(updateTagsDelayed);
    }
    if (options.searchresultsCollapsibleSidebar) {
        fixr.style.add(collapsibleSidebar_style);
        onPageHandlers.push(collapsibleSidebar);
    }
    if (options.slideshowSpeedControl) {
        slideshowSpeed = options.slideshowSpeedControl_value;
        runNow.push(initSlideshowSpeedHook);
        onPageHandlers.push(initSlideshowSpeed);
    }
    fixr.init(runNow, onPageHandlers, onResizeHandlers, onFocusHandlers, onStandaloneHandlers); // WebExtension
}

if (window.location.href.includes('flickr.com\/services\/api\/explore\/')) {
    // We are on Flickr API Explorer (WAS used for note handling before Flickr returned native note-support) and outside "normal" flickr page flow. fixr wont do here...
} else {
    if (fixr.isWebExtension()) {

        // WEBEXTENSION SETUP with options

        log('WebExtension - init with options...');
        withOptionsDo(handlerInitFixr); // Load selected features and run fixr.init with them...

    } else {

        // USERSCRIPT SETUP

        log('Userscript - fixr.init...');
        fixr.style.add(shared_style);
        fixr.style.add(albumExtras_style);
        fixr.style.add(topPagination_style);
        fixr.style.add(orderwarning_style);
        fixr.style.add(topMenuItems_style);
        fixr.style.add(photoDates_style);
        fixr.style.add(newsfeedLinks_style);
        fixr.style.add(exploreCalendar_style);
        fixr.style.add(albumTeaser_style);
        fixr.style.add(updateTags_style_hover);
        // FIXR fixr.init([runNow], [onPageHandlers], [onResizeHandlers], [onFocusHandlers], [onStandaloneHandlers])
        fixr.init([/* runEarly */], [stereotest, topMenuItems, ctrlClicking, albumExtras, topPagination, shootingSpaceballs, orderWarning, newsfeedLinks, photoDatesDelayed, ctrlClickingDelayed, exploreCalendarDelayed, albumTeaserDelayed, insertGMapLinkDelayed, updateTagsDelayed, userscriptWarning], [], [], [topMenuItems, newsfeedLinks, mapInitializer]); // Userscript

    }
}