您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
WPlace.liveで無制限のお気に入り保存、バックアップ・復元機能付き、Blue Marble完全対応、UI統合型
当前为
// ==UserScript== // @name WPlace Unlimited Favorites⭐ - 無制限保存 & エクスポート機能 // @description WPlace.liveで無制限のお気に入り保存、バックアップ・復元機能付き、Blue Marble完全対応、UI統合型 // @match *://wplace.live/* // @grant GM_setValue // @grant GM_getValue // @icon https://www.google.com/s2/favicons?sz=64&domain=wplace.live // @version 2025-08-21 // @author Defaulter // @license MIT // @namespace http://greasyfork.icu/users/1508363 // ==/UserScript== class WPlaceExtendedFavorites { constructor() { this.STORAGE_KEY = 'wplace_extended_favorites'; this.init(); } init() { this.observeAndInit(); } observeAndInit() { // ボタン設定 const buttonConfigs = [ { id: 'favorite-btn', selector: '[title="お気に入り"]', containerSelector: 'button[title="Toggle art opacity"]', create: this.createFavoriteButton.bind(this) }, { id: 'save-btn', selector: '[data-wplace-save="true"]', containerSelector: '.hide-scrollbar.flex.max-w-full.gap-1\\.5.overflow-x-auto', create: this.createSaveButton.bind(this) } ]; // 汎用ボタン監視 this.startButtonObserver(buttonConfigs); // モーダル作成 setTimeout(() => this.createModal(), 2000); } // 汎用ボタン監視システム startButtonObserver(configs) { const ensureButtons = () => { configs.forEach(config => { if (!document.querySelector(config.selector)) { const container = document.querySelector(config.containerSelector); if (container) { config.create(container); } } }); }; // DOM変更監視 const observer = new MutationObserver(() => { setTimeout(ensureButtons, 100); }); observer.observe(document.body, { childList: true, subtree: true }); // 初期配置 & 定期チェック setTimeout(ensureButtons, 1000); setInterval(ensureButtons, 5000); } // お気に入りボタン作成 createFavoriteButton(toggleButton) { const container = toggleButton.parentElement; if (!container) return; const button = document.createElement('button'); button.className = 'btn btn-lg sm:btn-xl btn-square shadow-md text-base-content/80 ml-2 z-30'; button.title = 'お気に入り'; button.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5"> <path d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"/> </svg> `; button.addEventListener('click', () => this.openModal()); container.appendChild(button); console.log('⭐ Favorite button added'); } // 保存ボタン作成 createSaveButton(container) { const button = document.createElement('button'); button.className = 'btn btn-primary btn-soft'; button.setAttribute('data-wplace-save', 'true'); button.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-4.5"> <path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"/> </svg> 保存 `; button.addEventListener('click', () => this.addFavorite()); container.appendChild(button); console.log('⭐ Save button added'); } // モーダルを作成 createModal() { const modal = document.createElement('dialog'); modal.id = 'favorite-modal'; modal.className = 'modal'; modal.innerHTML = ` <div class="modal-box max-w-4xl"> <form method="dialog"> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button> </form> <div class="flex items-center gap-1.5 mb-4"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5"> <path d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"/> </svg> <h3 class="text-lg font-bold">お気に入り</h3> </div> <!-- エクスポート・インポートボタン --> <div class="flex gap-2 mb-4"> <button id="export-btn" class="btn btn-outline btn-sm"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-4"> <path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z"/> </svg> エクスポート </button> <button id="import-btn" class="btn btn-outline btn-sm"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-4"> <path d="M260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H520q-33 0-56.5-23.5T440-240v-206l-64 62-56-56 160-160 160 160-56 56-64-62v206h220q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41h100v80H260Z"/> </svg> インポート </button> <input type="file" id="import-file" accept=".json" style="display: none;"> </div> <div id="favorites-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 max-h-96 overflow-y-auto"> <!-- ここにお気に入りが表示される --> </div> <div id="favorites-count" class="text-center text-sm text-base-content/80 mt-4"> <!-- 件数表示 --> </div> </div> <form method="dialog" class="modal-backdrop"> <button>close</button> </form> `; document.body.appendChild(modal); // イベントリスナー(既存のグリッドクリック) modal.querySelector('#favorites-grid').addEventListener('click', (e) => { const card = e.target.closest('.favorite-card'); const deleteBtn = e.target.closest('.delete-btn'); if (deleteBtn) { const id = parseInt(deleteBtn.dataset.id); this.deleteFavorite(id); } else if (card) { const lat = parseFloat(card.dataset.lat); const lng = parseFloat(card.dataset.lng); const zoom = parseFloat(card.dataset.zoom); this.goTo(lat, lng, zoom); modal.close(); } }); // エクスポート・インポートのイベントリスナー modal.querySelector('#export-btn').addEventListener('click', () => this.exportFavorites()); modal.querySelector('#import-btn').addEventListener('click', () => this.importFavorites()); } // エクスポート機能 async exportFavorites() { try { const favorites = await this.getFavorites(); if (favorites.length === 0) { this.showToast('エクスポートするお気に入りがありません'); return; } const exportData = { version: "1.0", exportDate: new Date().toISOString(), count: favorites.length, favorites: favorites }; const dataStr = JSON.stringify(exportData, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(dataBlob); link.download = `wplace-favorites-${new Date().toISOString().split('T')[0]}.json`; link.click(); this.showToast(`${favorites.length}件のお気に入りをエクスポートしました`); } catch (error) { console.error('エクスポートエラー:', error); this.showToast('エクスポートに失敗しました'); } } // インポート機能 importFavorites() { const fileInput = document.getElementById('import-file'); fileInput.click(); fileInput.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const importData = JSON.parse(text); // データ形式チェック if (!importData.favorites || !Array.isArray(importData.favorites)) { throw new Error('無効なファイル形式です'); } const currentFavorites = await this.getFavorites(); const importCount = importData.favorites.length; if (!confirm(`${importCount}件のお気に入りをインポートしますか?\n既存のデータは保持されます。`)) { return; } // 重複チェック(座標が同じものは除外) const newFavorites = importData.favorites.filter(importFav => { return !currentFavorites.some(existing => Math.abs(existing.lat - importFav.lat) < 0.001 && Math.abs(existing.lng - importFav.lng) < 0.001 ); }); // IDを新規採番(整数で) newFavorites.forEach((fav, index) => { fav.id = Date.now() + index; }); // マージして保存 const mergedFavorites = [...currentFavorites, ...newFavorites]; await GM.setValue(this.STORAGE_KEY, JSON.stringify(mergedFavorites)); this.renderFavorites(); this.showToast(`${newFavorites.length}件のお気に入りをインポートしました`); } catch (error) { console.error('インポートエラー:', error); this.showToast('インポートに失敗しました: ' + error.message); } // ファイル入力をクリア fileInput.value = ''; }; } // モーダルを開く openModal() { this.renderFavorites(); document.getElementById('favorite-modal').showModal(); } // 現在位置を取得 getCurrentPosition() { try { const locationStr = localStorage.getItem('location'); if (locationStr) { const location = JSON.parse(locationStr); return { lat: location.lat, lng: location.lng, zoom: location.zoom }; } } catch (error) { console.error('位置取得エラー:', error); } return null; } // お気に入りを追加 async addFavorite() { const position = this.getCurrentPosition(); if (!position) { alert('位置情報を取得できませんでした。マップをクリックしてから保存してください。'); return; } const name = prompt('お気に入り名を入力してください:', `地点 (${position.lat.toFixed(3)}, ${position.lng.toFixed(3)})`); if (!name) return; const favorite = { id: Date.now(), name: name, lat: position.lat, lng: position.lng, zoom: position.zoom || 14, date: new Date().toLocaleDateString('ja-JP') }; const favorites = await this.getFavorites(); favorites.push(favorite); await GM.setValue(this.STORAGE_KEY, JSON.stringify(favorites)); // 通知 this.showToast(`"${name}" を保存しました`); } // お気に入り一覧を取得 async getFavorites() { try { const stored = await GM.getValue(this.STORAGE_KEY, '[]'); return JSON.parse(stored); } catch (error) { console.error('お気に入り取得エラー:', error); return []; } } // お気に入り一覧を表示 async renderFavorites() { const favorites = await this.getFavorites(); const grid = document.getElementById('favorites-grid'); const count = document.getElementById('favorites-count'); if (!grid || !count) return; count.textContent = `保存済み: ${favorites.length} 件`; if (favorites.length === 0) { grid.innerHTML = ` <div class="col-span-full text-center py-12"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-12 mx-auto mb-4 text-base-content/50"> <path d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"/> </svg> <p class="text-base-content/80">お気に入りがありません</p> <p class="text-sm text-base-content/60">下の「保存」ボタンから追加してください</p> </div> `; return; } // 新しい順にソート favorites.sort((a, b) => b.id - a.id); grid.innerHTML = favorites.map(fav => ` <div class="favorite-card card bg-base-200 shadow-sm hover:shadow-md cursor-pointer transition-all relative" data-lat="${fav.lat}" data-lng="${fav.lng}" data-zoom="${fav.zoom}"> <button class="delete-btn btn btn-ghost btn-xs btn-circle absolute right-1 top-1 z-10" data-id="${fav.id}"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-3"> <path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/> </svg> </button> <div class="card-body p-3"> <h4 class="card-title text-sm line-clamp-2">${fav.name}</h4> <div class="text-xs text-base-content/70 space-y-1"> <div>📍 ${fav.lat.toFixed(3)}, ${fav.lng.toFixed(3)}</div> <div>📅 ${fav.date}</div> </div> </div> </div> `).join(''); } // 位置へ移動 goTo(lat, lng, zoom) { const url = new URL(window.location); url.searchParams.set('lat', lat); url.searchParams.set('lng', lng); url.searchParams.set('zoom', zoom); window.location.href = url.toString(); } // お気に入り削除 async deleteFavorite(id) { if (!confirm('このお気に入りを削除しますか?')) return; const favorites = await this.getFavorites(); const filtered = favorites.filter(fav => fav.id !== id); await GM.setValue(this.STORAGE_KEY, JSON.stringify(filtered)); this.renderFavorites(); this.showToast('削除しました'); } // トースト通知 showToast(message) { const toast = document.createElement('div'); toast.className = 'toast toast-top toast-end z-50'; toast.innerHTML = ` <div class="alert alert-success"> <span>${message}</span> </div> `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } } // 初期化 new WPlaceExtendedFavorites();