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:
photos_rating.js
This is clearly the main part of the script.
background.js
This appears to only watch for the toolbar button to be clicked, and then runs the main script above.
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:
- Run as a "normal" user script that can run in any browser, instead of only being a Firefox add-on.
- Remove the reliance on clicking a toolbar icon to activate. Instead it should just run on page load with no need for user input.
- 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.
- Consolidate into one .js file. This would make it much easier to use in SSBs like WebCatalog.
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);