yad2-flags

Adds dedicated flag buttons for easier marking of real estate search results. Currently "pin", "done" and "hide" flags are supported. Adding new flags is super easy.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         yad2-flags
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  Adds dedicated flag buttons for easier marking of real estate search results. Currently "pin", "done" and "hide" flags are supported. Adding new flags is super easy.
// @author       Dmitry Gurovich
// @license      UNLICENSE
// @website      https://github.com/yrtimiD/yad2-flags-userscript
// @supportURL   https://github.com/yrtimiD/yad2-flags-userscript/issues
// @match        https://www.yad2.co.il/realestate/*
// @match        https://www.yad2.co.il/item/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=yad2.co.il
// @grant        none
// @run-at       document-end
// ==/UserScript==

/**
 * Changelog:
 * 0.9 Style fix
 * 0.8 Styling
 * 0.7 Save in the localStorage only flagged entries
 * 0.6 Flags on single item page
 * 0.5 Better icons, moved icons container to the search item start
 */
(function () {
	'use strict';

	const PREFIX = 'yad2flags';

	/**
	 * Defines all available flags.
	 * To add a new flag add a new field in FLAGS and (optionally), declare a new class in the below style block.
	 */
	const FLAGS = {
		save: { icon: 'icon-save', tooltip: 'Save item', class: `${PREFIX}-save` },
		done: { icon: 'icon-done', tooltip: 'Mark item as done', class: `${PREFIX}-done` },
		hidden: { icon: 'icon-hidden', tooltip: 'Hide item', class: `${PREFIX}-hidden` },
	};

	function setupCommonDependencies() {
		const frag = document.createRange().createContextualFragment(`
		<style type="text/css">
			input.${PREFIX}-icon-save {
				background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M18 2H6c-1.103 0-2 .897-2 2v18l8-4.572L20 22V4c0-1.103-.897-2-2-2zm0 16.553l-6-3.428l-6 3.428V4h12v14.553z"/></svg>');
			}

			input:checked.${PREFIX}-icon-save {
				background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M19 10.132v-6c0-1.103-.897-2-2-2H7c-1.103 0-2 .897-2 2V22l7-4.666L19 22V10.132z"/></svg>');
			}

			input.${PREFIX}-icon-done {
				background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M7 5c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2h10c1.103 0 2-.897 2-2V7c0-1.103-.897-2-2-2H7zm0 12V7h10l.002 10H7z"/></svg>');
			}

			input:checked.${PREFIX}-icon-done {
				background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M7 5c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2h10c1.103 0 2-.897 2-2V7c0-1.103-.897-2-2-2H7zm0 12V7h10l.002 10H7z"/><path fill="currentColor" d="M10.996 12.556L9.7 11.285l-1.4 1.43l2.704 2.647l4.699-4.651l-1.406-1.422z"/></svg>');
			}

			input.${PREFIX}-icon-hidden {
				background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M12 9a3.02 3.02 0 0 0-3 3c0 1.642 1.358 3 3 3c1.641 0 3-1.358 3-3c0-1.641-1.359-3-3-3z"/><path fill="currentColor" d="M12 5c-7.633 0-9.927 6.617-9.948 6.684L1.946 12l.105.316C2.073 12.383 4.367 19 12 19s9.927-6.617 9.948-6.684l.106-.316l-.105-.316C21.927 11.617 19.633 5 12 5zm0 12c-5.351 0-7.424-3.846-7.926-5C4.578 10.842 6.652 7 12 7c5.351 0 7.424 3.846 7.926 5c-.504 1.158-2.578 5-7.926 5z"/></svg>');
			}

			input:checked.${PREFIX}-icon-hidden {
				background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M12 19c.946 0 1.81-.103 2.598-.281l-1.757-1.757c-.273.021-.55.038-.841.038c-5.351 0-7.424-3.846-7.926-5a8.642 8.642 0 0 1 1.508-2.297L4.184 8.305c-1.538 1.667-2.121 3.346-2.132 3.379a.994.994 0 0 0 0 .633C2.073 12.383 4.367 19 12 19zm0-14c-1.837 0-3.346.396-4.604.981L3.707 2.293L2.293 3.707l18 18l1.414-1.414l-3.319-3.319c2.614-1.951 3.547-4.615 3.561-4.657a.994.994 0 0 0 0-.633C21.927 11.617 19.633 5 12 5zm4.972 10.558l-2.28-2.28c.19-.39.308-.819.308-1.278c0-1.641-1.359-3-3-3c-.459 0-.888.118-1.277.309L8.915 7.501A9.26 9.26 0 0 1 12 7c5.351 0 7.424 3.846 7.926 5c-.302.692-1.166 2.342-2.954 3.558z"/></svg>');
			}

			.${PREFIX}-button {
				width: 16px;
				height: 16px;
				appearance: none;
				cursor: pointer;
				border-radius: 9px;
				background-color: white;
			}

			.${FLAGS.save.class}{
				outline: red 2px dashed;
			}
			.${FLAGS.done.class}{
				opacity:50%;
				outline: green 2px solid;
			}
			.${FLAGS.hidden.class}{
				opacity:30%;
			}
		</style>`);
		document.querySelector('head').append(frag);
	}

	function setupSearchPageDependencies() {
		const frag = document.createRange().createContextualFragment(`
		<style type="text/css">
			.feeditem {
				display: flex; /* fixes layout for buttons container */
			}

			.${PREFIX}-buttons {
				display: flex;
				flex-direction: column;
				justify-content: space-evenly;
			}

			.${PREFIX}-button {
				width: 16px;
				height: 16px;
			}
		</style>
		`);
		document.querySelector('head').append(frag);
	}

	function setupItemPageDependencies() {
		const frag = document.createRange().createContextualFragment(`
		<style type="text/css">
			.${PREFIX}-buttons {
				display: inline-block;
				margin-right: 4px;
			}

			.${PREFIX}-button {
				width: 24px;
				height: 24px;
			}
		</style>
		`);
		document.querySelector('head').append(frag);
	}

	function setFlag(flag, id, value) {
		let data = JSON.parse(localStorage.getItem(PREFIX)) ?? {};
		(data[id] = data[id] ?? {})[flag] = value;
		if (Object.values(data[id]).reduce((p, c) => p || c, false) === false) delete data[id];
		localStorage.setItem(PREFIX, JSON.stringify(data));
	}

	function getFlag(flag, id, defaultValue) {
		let data = JSON.parse(localStorage.getItem(PREFIX)) ?? {};
		return data[id]?.[flag] ?? defaultValue;
	}

	function toggleFlag(flag, id, ele, value) {
		console.log(`${id} flagged with ${flag}:${value}`);
		setFlag(flag, id, value);

		if (value === true) {
			ele.classList.add(FLAGS[flag].class);
		} else {
			ele.classList.remove(FLAGS[flag].class);
		}
	}

	/**
	 * @param {string} flag Flag type
	 * @param {string} id Item ID
	 * @param {} container Buttons container element
	 * @param {*} ele Item element (for assigning flagged style)
	 */
	function addButton(flag, id, container, ele) {
		let state = getFlag(flag, id, false);
		let frag = document.createRange().createContextualFragment(`<input type="checkbox"${state ? " checked" : ""} title="${FLAGS[flag].tooltip}" class="${PREFIX}-button ${PREFIX}-icon-${flag}" />`);
		frag.children[0].addEventListener('change', (e) => { toggleFlag(flag, id, ele, e.target.checked); e.stopPropagation(); });
		container.append(frag);
		if (state === true) toggleFlag(flag, id, ele, state);
	}

	function initItemElement(itemElement) {
		if (itemElement.querySelector(`.${PREFIX}-buttons`)) return;
		let id = itemElement.querySelector('[item-id]')?.getAttribute('item-id');
		if (!id) return;

		itemElement.insertAdjacentHTML('afterbegin', `<div class="${PREFIX}-buttons"></div>`);

		let container = itemElement.querySelector(`.${PREFIX}-buttons`);
		Object.keys(FLAGS).forEach(flag => {
			addButton(flag, id, container, itemElement);
		});
	}

	let lastUpdate = 0;
	let pending = null;
	function update() {
		if (Date.now() - lastUpdate < 5000) {
			if (!pending) pending = setTimeout(update, lastUpdate + 5000 - Date.now());
			return;
		}
		lastUpdate = Date.now();
		pending = null;
		document.querySelectorAll('.feed_list .feeditem').forEach(ele => initItemElement(ele));
	}

	function initSearchPageMode() {
		const observer = new MutationObserver(() => update());
		observer.observe(document.body, { childList: true, subtree: true });

		update();
	}

	function initItemPageMode() {
		let id = window.location.pathname.match(/^\/item\/([A-Za-z0-9]+)/)?.[1];
		if (id) {
			document.querySelector('.like_icon_wrapper').insertAdjacentHTML('beforeend', `<div class="${PREFIX}-buttons"></div>`);
			let container = document.querySelector(`.${PREFIX}-buttons`);
			let itemElement = document.querySelector('.top_components');
			Object.keys(FLAGS).forEach(flag => {
				addButton(flag, id, container, itemElement);
				toggleFlag(flag, id, itemElement, getFlag(flag, id, false));
			});
		}
	}

	setupCommonDependencies();
	if (/^\/item\//.test(window.location.pathname)) {
		setupItemPageDependencies();
		initItemPageMode();
	} else {
		setupSearchPageDependencies();
		initSearchPageMode();
	}
})();