// ==UserScript==
// @name SUUMO物件非表示マネージャ 🏠
// @name:ja SUUMO物件非表示マネージャ 🏠
// @name:en SUUMO Hidden Property Manager 🏠
// @version 3.7.1
// @description SUUMOの検索結果(建物ごとに表示)で「非表示」ボタンから不要な物件を隠せる!モーダルUIから復活も簡単。保存はローカル。
// @description:ja SUUMOの検索結果(建物ごとに表示)で「非表示」ボタンから不要な物件を隠せる!モーダルUIから復活も簡単。保存はローカル。
// @description:en Hide unwanted listings in SUUMO's grouped-by-building search results! Restore via modal UI. Data saved locally.
// @namespace https://github.com/koyasi777/suumo-hidden-property-manager
// @author koyasi777
// @match https://suumo.jp/jj/chintai/ichiran/FR*
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// @homepageURL https://github.com/koyasi777/suumo-hidden-property-manager
// @supportURL https://github.com/koyasi777/suumo-hidden-property-manager/issues
// @icon https://suumo.jp/front/img/favicon.ico
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'suumoHiddenProperties';
const PROPERTY_LI_PREFIX = 'property-li-';
console.log('SUUMO非表示スクリプト: 実行開始');
// --- データ管理ロジック (変更なし) ---
const storage = {
get: () => JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'),
save: (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data)),
add: (property) => {
const properties = storage.get();
if (!properties.some(p => p.id === property.id)) {
properties.push(property);
storage.save(properties);
}
},
remove: (propertyId) => {
let properties = storage.get();
properties = properties.filter(p => p.id !== propertyId);
storage.save(properties);
},
clear: () => localStorage.removeItem(STORAGE_KEY)
};
// --- UI/DOM操作 ---
const ui = {
injectCSS: () => {
GM_addStyle(`
/* ページスクロールロック用クラス */
body.hide-modal-open { overflow: hidden; }
.suumo-control-button {
padding: 8px 14px; color: white !important; border: none; cursor: pointer; border-radius: 4px; font-size: 14px; font-weight: bold;
font-family: "メイリオ", Meiryo, "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", "MS Pゴシック", sans-serif;
line-height: 1.2; text-decoration: none !important; display: inline-block; white-space: nowrap;
box-shadow: 0 2px 4px rgba(0,0,0,0.15); transition: all 0.15s ease-in-out;
}
.suumo-control-button:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); filter: brightness(1.1); }
.suumo-control-button:active { transform: translateY(1px); box-shadow: 0 1px 2px rgba(0,0,0,0.2); filter: brightness(0.95); }
.suumo-control-button--small { padding: 5px 10px; font-size: 12px; font-weight: normal; }
.hide-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9998; display: none; }
.hide-modal-content {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; z-index: 9999; padding: 20px 30px;
border-radius: 8px; width: 90%; max-width: 600px; max-height: 80vh; box-shadow: 0 5px 15px rgba(0,0,0,0.3);
display: none; flex-direction: column; overflow: hidden;
}
.hide-modal-header { display: flex; justify-content: flex-start; align-items: center; flex-shrink: 0; border-bottom: 1px solid #ccc; padding-bottom: 10px; }
.hide-modal-header h2 { margin: 0; font-family: "メイリオ", Meiryo, sans-serif; }
.hide-modal-header .suumo-control-button { margin-left: 15px; }
.hide-modal-close { position: absolute; top: 15px; right: 20px; font-size: 24px; font-weight: bold; cursor: pointer; color: #aaa; z-index: 10; }
.hide-modal-body { flex-grow: 1; overflow-y: auto; margin-top: 15px; min-height: 0; }
.hidden-property-list { list-style: none; padding: 0; }
.hidden-property-list li { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #eee; }
.hidden-property-info a { text-decoration: none; color: #0073e6; font-weight: bold; font-family: "メイリオ", Meiryo, sans-serif;}
.hidden-property-info span { display: block; font-size: 0.9em; color: #555; font-family: "メイリオ", Meiryo, sans-serif;}
.inquiry.inquiry--top { display: flex; justify-content: space-between; align-items: center; padding-right: 10px; }
`);
},
createControls: () => {
if (document.getElementById('suumo-hide-controls')) return;
const targetParent = document.querySelector('.inquiry.inquiry--top');
if (!targetParent) return;
const manageButton = document.createElement('button');
manageButton.type = 'button';
manageButton.id = 'suumo-hide-controls';
manageButton.textContent = '非表示リスト管理';
manageButton.className = 'suumo-control-button';
manageButton.style.backgroundColor = '#5bc0de';
manageButton.addEventListener('click', () => ui.toggleModal(true));
targetParent.appendChild(manageButton);
},
createModal: () => {
if (document.getElementById('hide-modal')) return;
document.body.insertAdjacentHTML('beforeend', `
<div id="hide-modal-overlay" class="hide-modal-overlay"></div>
<div id="hide-modal-content" class="hide-modal-content">
<span class="hide-modal-close">×</span>
<div class="hide-modal-header">
<h2>非表示物件リスト</h2>
<button id="restore-all-btn" class="suumo-control-button suumo-control-button--small" style="background-color: #f0ad4e;">全復活</button>
</div>
<div class="hide-modal-body">
<ul id="hidden-property-list" class="hidden-property-list"></ul>
</div>
</div>
`);
document.getElementById('hide-modal-overlay').addEventListener('click', () => ui.toggleModal(false));
document.querySelector('#hide-modal-content .hide-modal-close').addEventListener('click', () => ui.toggleModal(false));
const restoreAllBtn = document.getElementById('restore-all-btn');
restoreAllBtn.type = 'button';
restoreAllBtn.addEventListener('click', () => {
if (confirm('非表示中の全ての物件を復活させますか?')) {
const hiddenProperties = storage.get();
if(hiddenProperties.length === 0) {
alert('非表示の物件はありません。');
return;
}
hiddenProperties.forEach(p => ui.restoreProperty(p.id));
storage.clear();
ui.populateModal();
alert(`${hiddenProperties.length}件の物件を全て復活させました。`);
}
});
},
populateModal: () => {
const listElement = document.getElementById('hidden-property-list');
const hiddenProperties = storage.get();
listElement.innerHTML = '';
if (hiddenProperties.length === 0) {
listElement.innerHTML = '<li>非表示中の物件はありません。</li>';
return;
}
hiddenProperties.forEach(prop => {
const listItem = document.createElement('li');
listItem.id = `hidden-item-${prop.id}`;
listItem.innerHTML = `<div class="hidden-property-info"><a href="${prop.url}" target="_blank" rel="noopener noreferrer">${prop.name}</a><span>${prop.rent}</span></div>`;
const restoreButton = document.createElement('button');
restoreButton.type = 'button';
restoreButton.textContent = '復活';
restoreButton.className = 'suumo-control-button';
restoreButton.style.backgroundColor = '#5cb85c';
restoreButton.addEventListener('click', () => {
storage.remove(prop.id);
ui.restoreProperty(prop.id);
listItem.remove();
if (document.querySelectorAll('#hidden-property-list li').length === 0) {
listElement.innerHTML = '<li>非表示中の物件はありません。</li>';
}
});
listItem.appendChild(restoreButton);
listElement.appendChild(listItem);
});
},
/* ===== ★スクロールロック機能を実装 ===== */
toggleModal: (show) => {
const overlay = document.getElementById('hide-modal-overlay');
const content = document.getElementById('hide-modal-content');
if (show) {
document.body.classList.add('hide-modal-open'); // ページスクロールをロック
ui.populateModal();
overlay.style.display = 'block';
content.style.display = 'flex'; // display:flexに戻す
} else {
document.body.classList.remove('hide-modal-open'); // ページスクロールのロックを解除
overlay.style.display = 'none';
content.style.display = 'none';
}
},
restoreProperty: (propertyId) => {
const propertyLi = document.getElementById(PROPERTY_LI_PREFIX + propertyId);
if (propertyLi) {
propertyLi.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
propertyLi.style.display = 'block';
setTimeout(() => {
propertyLi.style.opacity = '1';
propertyLi.style.transform = 'scale(1)';
}, 10);
}
}
};
// --- メイン処理ロジック ---
function processPropertyItems() {
const hiddenIds = storage.get().map(p => p.id);
hiddenIds.forEach(hiddenId => {
const propLi = document.getElementById(PROPERTY_LI_PREFIX + hiddenId);
if (propLi && propLi.style.display !== 'none') {
propLi.style.display = 'none';
propLi.style.opacity = '0';
}
});
const propertyItems = document.querySelectorAll('.cassetteitem:not(.hide-processed)');
propertyItems.forEach(item => {
item.classList.add('hide-processed');
const checkbox = item.querySelector('input.js-single_checkbox[type="checkbox"]');
const bukkenId = checkbox ? checkbox.value : null;
if (!bukkenId) return;
const parentLi = item.closest('li');
if (!parentLi) return;
parentLi.id = PROPERTY_LI_PREFIX + bukkenId;
if (hiddenIds.includes(bukkenId)) {
if (parentLi.style.display !== 'none') {
parentLi.style.display = 'none';
parentLi.style.opacity = '0';
}
return;
}
const buttonContainer = item.querySelector('.cassetteitem_other-col09');
if (buttonContainer && !buttonContainer.querySelector('.hide-property-button')) {
const hideButton = document.createElement('button');
hideButton.type = 'button';
hideButton.textContent = '非表示';
hideButton.className = 'suumo-control-button hide-property-button';
hideButton.style.cssText = 'background-color: #d9534f; margin-top: 5px;';
hideButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const name = item.querySelector('.cassetteitem_content-title')?.textContent.trim() || '物件名不明';
const rent = item.querySelector('.cassetteitem_price--rent')?.textContent.trim() || '賃料不明';
const url = item.querySelector('.js-cassette_link_href')?.href || '#';
if (confirm(`【${name}】を非表示にしますか?`)) {
storage.add({ id: bukkenId, name, rent, url });
parentLi.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
parentLi.style.opacity = '0';
parentLi.style.transform = 'scale(0.95)';
setTimeout(() => { parentLi.style.display = 'none'; }, 400);
}
});
buttonContainer.appendChild(hideButton);
}
});
}
// --- 初期化と監視 ---
function init() {
ui.injectCSS();
ui.createModal();
const observer = new MutationObserver(() => {
if (!document.getElementById('suumo-hide-controls')) {
ui.createControls();
}
processPropertyItems();
});
observer.observe(document.body, { childList: true, subtree: true });
ui.createControls();
processPropertyItems();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();