If anyone here is good with JavaScript. I need a helping hand figuring out this user script to add star ratings to the thumbnails in Synology Photos.

Currently reading
If anyone here is good with JavaScript. I need a helping hand figuring out this user script to add star ratings to the thumbnails in Synology Photos.

19
6
NAS
DS918+
Router
  1. RT1900ac
Operating system
  1. Linux
  2. macOS
Mobile operating system
  1. iOS
Last edited:
I found an awesome Firefox add-on that when activated adds the 5 star rating buttons to every thumbnail instead of having to open each images info panel. It's been a fantastic change. Unfortunately I do not use Firefox much anymore, and even if I did I primarily use Photos through a Desktop web app called Fluid which is based on WebKit.

I tried finding a way to contact the add-on developer but I cant find any contact info, and any searches just return links to that add-on sprinkled with irrelevant things. Since it's Open Source and its fairly easy to get to the contents of add-ons I decided to pull out the 2 .js files that make up the core functions and I have been trying to figure out how it works, but am having no luck at all...which would stand to reason I don't know any JavaScript.

My goal here is to figure out what would need to be modified to do the following:
  1. Run as a "normal" user script that can run in any browser, instead of only being a Firefox add-on.
  2. Remove the reliance on clicking a toolbar icon to activate. Instead it should just run on page load with no need for user input.
    1. If this isn't completely possible then adding the toggle to the page itself instead of an external toolbar button would be the next best thing.
  3. Consolidate into one .js file. This would make it much easier to use in SSBs like WebCatalog.
Any help would be appreciated.

photos_rating.js
This is clearly the main part of the script.
JavaScript:
var observer;
var state = 'disabled';
var scan_counter = 0;
var current_rating = {};
var token = null;
var url = null;
var lightboxItemid = -1;

var updating_lightbox = false;

console.log('initializing add-on');

function time_it(output, last_time) {
    var now = Date.now();
    var diff = now - last_time;
    console.log('TIMING: ' + output + ' ' + diff);
    return now;
}

function createElementFromHTML(htmlString) {
  var div = document.createElement('div');
  div.innerHTML = htmlString.trim();
  return div.firstChild;
}

function rating_button_clicked(id, rating) {
    if(rating == current_rating[id]) {
        rating = 0;
    }
    set_rating_for_box(id, rating);
}

function add_click_listener_for_buttons_of_box(itemid, inlightbox) {
    if(!inlightbox) {
        var box = document.getElementById("ratingbox" + itemid);
    } else {
        var box = document.getElementById("lightratingbox" + itemid);
    }
  
    if(box != null) {
        for (let j = 0; j < box.children.length; j++) {
            let button = box.children[j];
            button.addEventListener("click", function(event) {
                event.stopPropagation();
                rating_button_clicked(itemid, j + 1);
            }, false);
        }
    } else {
        console.log('could not add click listeners to box as not found');
    }
}

function api_set_rating_for_id(itemid, rating) {
    const params = { 'id': '\[' + itemid + '\]', 'method': 'set', 'api': 'SYNO.FotoTeam.Browse.Item', 'rating': rating, 'version': 2, 'SynoToken': token };
    let u = Object.keys(params).map(function(k) {
        return encodeURIComponent(k) + '=' + encodeURIComponent(params[k])
    }).join('&');

    var full = url + '?' + u;
    console.log(full);
    fetch(full)
    .then(response => response.json())
    .then(data => {
        console.log('Success:', data);
    })
    .catch((error) => {
      console.error('Error:', error);
    });
}

function update_stars(boxid, rating) {
    var box = document.getElementById(boxid);  // ratingbox has only 5 button children
    if(box != null) {
        for (let j = 0; j < box.children.length; j++) {
            let starchild = box.children[j].children[0]; // button has only one star child
            starchild.classList.remove("star-off-btn-icon");
            starchild.classList.remove("star-on-btn-icon");
            if(j < rating) {
                starchild.classList.add("star-on-btn-icon"); // stardiv has two classes button-icon star-on-btn-icon
            } else {
                starchild.classList.add("star-off-btn-icon");
            }
        }
    }
}

function set_rating_for_box(itemid, rating) {
    api_set_rating_for_id(itemid, rating);
    current_rating[itemid] = rating;
  
    update_stars("ratingbox" + itemid, rating);
    update_stars("lightratingbox" + itemid, rating);
}

function set_rating_for_div(elem, rating, itemid, inlightbox) {
    // synofoto-icon-button-rating
    var style;
    var box_id;
    if(!inlightbox) {
        box_id = 'id="ratingbox' + itemid + '"';
        style = 'style="position: absolute; display: flex; align-items: end; top: 0px; bottom: 0px;" '
    } else {
        box_id = 'id="lightratingbox' + itemid + '"';
        style = '';
    }
    const rating_box_start = '<div class="synofoto-lightbox-info-rating" ' + style + box_id + '>'; //synofoto-selectable-overlay
    const rating_button_start = '<button class="synofoto-icon-button-rating" type="button" data-long-press-delay="500" data-tip="" currentitem="false">';
    const rating_div_on = '<div class="button-icon star-on-btn-icon">';
    const rating_div_off = '<div class="button-icon star-off-btn-icon">';
    const rating_button_end = '</button>';
    const rating_box_end = '</div>';

    var rating_cont = rating_box_start;
    for(var i = 0; i < 5; i++) {
        rating_cont += rating_button_start;
        if(i < rating) {
            rating_cont += rating_div_on;
        } else {
            rating_cont += rating_div_off;
        }
        rating_cont += rating_button_end;
    }
    rating_cont += rating_box_end;

    if(!inlightbox) {
        var node = createElementFromHTML(rating_cont);
        var grandparent = elem.parentElement.parentElement;

        if(grandparent.children.length == 2) {
            current_rating[itemid] = rating;
            let overlay_div = grandparent.children[1];
            overlay_div.style.bottom = '18px';  // make room for stars to be clickable
            grandparent.insertBefore(node, overlay_div);
            add_click_listener_for_buttons_of_box(itemid, inlightbox);
        }
    } else { // replace existing by new rating box
        for (let j = 0; j < elem.children.length; j++) {
            let child = elem.children[j];
            if(child.id.includes('lightratingbox')) {
                child.remove();
            }         
        }
        var insertelem = null;
        for (let j = 0; j < elem.children.length; j++) {
            let child = elem.children[j];
            if(child.classList.contains("synofoto-lightbox-info-rating")) {
                insertelem = child;
            }
          
        }
        var node = createElementFromHTML(rating_cont);
        current_rating[itemid] = rating;
        if(insertelem != null) {
            elem.insertBefore(node, insertelem);
            insertelem.style.display = "none";
            add_click_listener_for_buttons_of_box(itemid, inlightbox);
        }
      
        // var box = document.getElementById("lightratingbox" + imgid);
        // if(box != null) {
        //     console.log('LIGHTBOX succesfully added');
        // } else {
        //     console.log('LIGHTBOX adding failed');
        // }
    }
}


function get_and_set_rating_for_divs(update_dict, token, inlightbox) {
  
    if(Object.keys(update_dict).length == 0) {
        return;
    } else if(Object.keys(update_dict).length == 1) { // use existing rating, if there is only one rating (which is the case for inlightbox)
        let [id] = Object.keys(update_dict);
        if(current_rating[id] != undefined) {
            set_rating_for_div(update_dict[id], current_rating[id], id, inlightbox);
            if(inlightbox) {
                updating_lightbox = false;
            }
             return;
        }
    }

    ids = Object.keys(update_dict).join(',');
    const params = { 'id': '\[' + ids + '\]', 'method': 'get', 'api': 'SYNO.FotoTeam.Browse.Item',  'geocoding_accept_language': 'ger', 'additional': '\[\"rating\"\]', 'version': 2, 'SynoToken': token };

    let u = Object.keys(params).map(function(k) {
        return encodeURIComponent(k) + '=' + encodeURIComponent(params[k])
    }).join('&');

    var full = url + '?' + u;
    console.log(full);
    fetch(full)
    .then(response => response.json())
    .then(data => {
        // console.log('LIGHTBOX DATA RECEIVED', url, imgid, token);
        // console.log('Success:', data);
        var data_list = data['data']['list'];
        for(var key in data_list) {
            var entry = data_list[key];
            var rating = entry['additional']['rating'];
            var id = entry['id'];
            set_rating_for_div(update_dict[id], rating, id, inlightbox);
            if(inlightbox) {
                updating_lightbox = false;
            }
        }
    })
    .catch((error) => {
      console.error('Error:', error);
    });
}


function scan_and_update_all_divs() {
    cur_time = time_it('Start', Date.now());

    scan_counter += 1;
//    console.log('scan started ', scan_counter);
    var re = new RegExp("(http.*entry.cgi)?.*id=([0-9]+)&.*SynoToken=(.*)");

    var update_dict = {};

    // var divs = document.getElementsByTagName("div");
    var divs = document.getElementsByClassName("synofoto-item-image");
    for(var i = 0; i < divs.length; i++){
       if(!divs[i].classList.contains("rating-indicated")) {
            divs[i].classList.add("rating-indicated");
            for (let j = 0; j < divs[i].children.length; j++) {
                let child = divs[i].children[j];
                let imgsrc = child.src;
                var matches = re.exec(child.src);
                if(matches != null && matches.length > 3) {
                    url = matches[1];
                    imgid = matches[2];
                    token = matches[3];

                    update_dict[imgid] = divs[i];
                }
            }
        }
    }
    cur_time = time_it('Scanned timeline divs', cur_time);

    get_and_set_rating_for_divs(update_dict, token, false);
//    console.log('found ids ', Object.keys(update_dict).join(','));
//    console.log('scan finished ', scan_counter);

    // for lightbox rating
    imgid = -1;

    if(updating_lightbox) {
        return;
    }

    var found = false;

    cur_time = time_it('Start scan images', cur_time);
    var divs = document.getElementsByTagName("img");
    for(var i = 0; i < divs.length; i++){
        //console.log(divs[i].classList)
        if(divs[i].classList.contains("synofoto-lightbox-image") && divs[i].classList.contains("allowDefCtxMenu")) {
            var matches = re.exec(divs[i].src);
            if(matches != null && matches.length > 3) {
                found = true;
                url = matches[1];
                imgid = matches[2];
                token = matches[3];

                lightboxItemid = imgid;
              
                var box = document.getElementById("lightratingbox" + imgid);
                if(box != null) {
                    console.log('LIGHTBOX EXISTS', url, imgid, token);
                    imgid = -1;
                } else {
                    console.log('LIGHTBOX NEW', url, imgid, token);
                }
              
            }
        }
    }
    cur_time = time_it('Finish scan images', cur_time);
    if(!found) {  // there is no active lightbox
        lightboxItemid = -1;
    }

    var update_dict = {};
    var divs = document.getElementsByClassName("synofoto-lightbox-info-panel");
    if(divs.length == 1 && imgid != -1) {
        update_dict[imgid] = divs[0];
        updating_lightbox = true;
        get_and_set_rating_for_divs(update_dict, token, true);
    }
    cur_time = time_it('Finish scan lightbox info panel', cur_time);
    // console.log('LIGHTBOX UPDATE END', url, imgid, token);
}

function set_width_for_filter() {
    var divs = document.getElementsByTagName("div");
    for(var i = 0; i < divs.length; i++){
        if(divs[i].classList.contains("synofoto-filter-wrapper")) {
            divs[i].style.width = '400px';
        }
    }
}

var lastcallback = 0;

function init_mutation_observer() {
    nodes = document.getElementsByClassName('x-window-body');
    if(nodes.length == 1) {
        const targetNode = nodes[0];
        const config = { attributes: true, childList: true, subtree: true };

        const callback = function(mutationsList, observer) {
            // Use traditional 'for loops' for IE 11
            for(const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    var now = Date.now();
                    if(now - lastcallback > 200) {
                        scan_and_update_all_divs();
                        lastcallback = now;
                    } else {
                        // console.log('blocked incoming callback');
                    }
                }
            }
        };
        observer = new MutationObserver(callback);
        observer.observe(targetNode, config);
    }
}

function keyListener(event) {
    if(lightboxItemid >= 0) {
        for(var i = 1; i <= 5; i++){
            if (event.ctrlKey && event.key === i.toString()) {
                rating_button_clicked(lightboxItemid, i);
                // alert('Undo!');
            }
        }
    }
}

function enable_plugin() {
    console.log('enabling plugin');
    state = 'enabled';
    set_width_for_filter();
    scan_and_update_all_divs();
    init_mutation_observer();

    document.addEventListener('keydown', keyListener);
}

function disable_plugin() {
    console.log('disabling plugin');
    state = 'disabled';
    observer.disconnect();
    var divs = document.getElementsByTagName("div");
    for(var i = 0; i < divs.length; i++){
       if(divs[i].classList.contains("synofoto-item-image") && divs[i].classList.contains("rating-indicated")) {
            divs[i].classList.remove("rating-indicated");
            let grandparent = divs[i].parentElement.parentElement;
            if(grandparent.children.length == 3) {
                grandparent.removeChild(grandparent.children[1]);
            }
       }
    }
    document.removeEventListener('keydown', keyListener);

}

function get_plugin_state() {
    return state;
}

enable_plugin();

console.log('plugin loaded');

background.js
This appears to only watch for the toolbar button to be clicked, and then runs the main script above.
JavaScript:
var mode = -1;

function handleToolbarClick() {
    function onExecuted(result) {
      console.log('Executed');
    }

    function onError(error) {
      console.log('Error: ', error);
    }

    if(mode == -1) {
        mode = 1;
        updateIcon();
        const executing = browser.tabs.executeScript({
          file: "/photos_rating.js"
        });
        executing.then(onExecuted, onError);
    } else if(mode == 0) {
        mode = 1;
        updateIcon();
        const executing = browser.tabs.executeScript({
          code: "enable_plugin();"
        });
        executing.then(onExecuted, onError);
    }  else if(mode == 1) {
        mode = 0;
        updateIcon();
        const executing = browser.tabs.executeScript({
          code: "disable_plugin();"
        });
        executing.then(onExecuted, onError);
    }

}


function updateIcon(){
    if(mode == 1) {
        browser.browserAction.setIcon({
            path: {
                "128": "icons/star-on-128.png"
            }
        });
    } else {
        browser.browserAction.setIcon({
            path: {
                "128": "icons/star-off-128.png"
            }
        });
    }
}

function updateActiveTab() {

    function onExecuted(result) {
      console.log('Executed', result);
      if(result[0] === 'enabled') {
        mode = 1;
      } else {
        mode = 0;
      }
      updateIcon();
    }

    function onError(error) {
      console.log('Error: ', error);
      mode = -1;
      updateIcon();
    }

    const executing = browser.tabs.executeScript({
      code: "get_plugin_state();"
    });
    executing.then(onExecuted, onError);
}

browser.browserAction.onClicked.addListener(handleToolbarClick);
browser.tabs.onActivated.addListener(updateActiveTab);
 

Create an account or login to comment

You must be a member in order to leave a comment

Create account

Create an account on our community. It's easy!

Log in

Already have an account? Log in here.

Welcome to SynoForum.com!

SynoForum.com is an unofficial Synology forum for NAS owners and enthusiasts.

Registration is free, easy and fast!

Back
Top