您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
WPlace Unlimited Favorites⭐ - Unlimited favorites storage on [WPlace.live], with backup & restore functionality, full Blue Marble support, integrated UI type
// ==UserScript== // @name wplace-infinite-bookmarks // @name:ja WPlace Unlimited Favorites⭐ - 無制限保存 & エクスポート機能 // @namespace https://greasyfork.org/users/1508363 // @version 1.0.1 // @description WPlace Unlimited Favorites⭐ - Unlimited favorites storage on [WPlace.live], with backup & restore functionality, full Blue Marble support, integrated UI type // @description:ja WPlace.liveで無制限のお気に入り保存、バックアップ・復元機能付き、Blue Marble完全対応、UI統合型 // @author Defaulter,gissehel // @homepage https://github.com/gissehel/userscripts // @supportURL https://github.com/gissehel/userscripts/issues // @match https://wplace.live/* // @icon https://www.google.com/s2/favicons?sz=64&domain=wplace.live // @grant GM_setValue // @grant GM_getValue // @grant GM.setValue // @grant GM.getValue // @license MIT // ==/UserScript== const script_name = GM_info?.script?.name || 'no-name' const script_version = GM_info?.script?.version || 'no-version' const script_id = `${script_name} ${script_version}` console.log(`Begin - ${script_id}`) // @main_begin{wplace-infinite-bookmarks} const i18n = { ja: { "save_button": "保存", "modal_title": "お気に入り", "export": "エクスポート", "import": "インポート", "no_favorites_to_export": "エクスポートするお気に入りがありません", "favorites_exported": "件のお気に入りをエクスポートしました", "export_error": "エクスポートエラー", "import_error": "インポートエラー", "export_failed": "エクスポートに失敗しました", "import_failed": "インポートに失敗しました", "imported": "件のお気に入りをインポートしました", "invalid_file_format": "無効なファイル形式です", "confirm_import_1": "", "confirm_import_2": "件のお気に入りをインポートしますか?\n既存のデータは保持されます。", "unable_to_retrieve_location": "位置情報を取得できませんでした。マップをクリックしてから保存してください。", "enter_favorite_name": "お気に入り名を入力してください", "location": "地点", "location_retrieval_error": "位置取得エラー", "favorite_saved": "を保存しました", "favorite_retrieval_error": "お気に入り取得エラー", "confirm_delete": "このお気に入りを削除しますか?", "favorite_deleted": "削除しました", "favorite_count": "保存済み", "items": "件", "no_favorites": "お気に入りがありません", "add_favorites": "下の「保存」ボタンから追加してください", }, en: { "save_button": "Save", "modal_title": "Favorites", "export": "Export", "import": "Import", "no_favorites_to_export": "No favorites to export", "favorites_exported": " favorites exported", "export_error": "Export error", "import_error": "Import error", "export_failed": "Export failed", "import_failed": "Import failed", "imported": " favorites imported", "invalid_file_format": "Invalid file format", "confirm_import_1": "Do you want to import", "confirm_import_2": "favorites?\nExisting data will be preserved.", "unable_to_retrieve_location": "Unable to retrieve location information. Please click on the map and then save.", "enter_favorite_name": "Please enter a name for the favorite", "location": "Location", "location_retrieval_error": "Location retrieval error", "favorite_saved": "Favorite saved", "favorite_retrieval_error": "Favorite retrieval error", "confirm_delete": "Are you sure you want to delete this favorite?", "favorite_deleted": "Deleted", "favorite_count": "Saved", "items": "items", "no_favorites": "No favorites", "add_favorites": "Please add from the 'Save' button below", }, fr: { "save_button": "Enregistrer", "modal_title": "Favoris", "export": "Exporter", "import": "Importer", "no_favorites_to_export": "Aucun favori à exporter", "favorites_exported": " favoris exportés", "export_error": "Erreur d'exportation", "import_error": "Erreur d'importation", "export_failed": "Échec de l'exportation", "import_failed": "Échec de l'importation", "imported": " favoris importés", "invalid_file_format": "Format de fichier invalide", "confirm_import_1": "Voulez-vous importer", "confirm_import_2": "favoris ?\nLes données existantes seront préservées.", "unable_to_retrieve_location": "Impossible de récupérer les informations de localisation. Veuillez cliquer sur la carte puis enregistrer.", "enter_favorite_name": "Veuillez entrer un nom pour le favori", "location": "Emplacement", "location_retrieval_error": "Erreur de récupération de la localisation", "favorite_saved": "Favori enregistré", "favorite_retrieval_error": "Erreur de récupération des favoris", "confirm_delete": "Êtes-vous sûr de vouloir supprimer ce favori ?", "favorite_deleted": "Supprimé", "favorite_count": "Enregistré", "items": "éléments", "no_favorites": "Aucun favori", "add_favorites": "Veuillez ajouter depuis le bouton 'Enregistrer' ci-dessous", }, es: { "save_button": "Guardar", "modal_title": "Favoritos", "export": "Exportar", "import": "Importar", "no_favorites_to_export": "No hay favoritos para exportar", "favorites_exported": " favoritos exportados", "export_error": "Error de exportación", "import_error": "Error de importación", "export_failed": "Error al exportar", "import_failed": "Error al importar", "imported": " favoritos importados", "invalid_file_format": "Formato de archivo inválido", "confirm_import_1": "¿Desea importar", "confirm_import_2": "favoritos?\nLos datos existentes se conservarán.", "unable_to_retrieve_location": "No se puede recuperar la información de ubicación. Haga clic en el mapa y luego guarde.", "enter_favorite_name": "Por favor, ingrese un nombre para el favorito", "location": "Ubicación", "location_retrieval_error": "Error al recuperar la ubicación", "favorite_saved": "Favorito guardado", "favorite_retrieval_error": "Error al recuperar los favoritos", "confirm_delete": "¿Está seguro de que desea eliminar este favorito?", "favorite_deleted": "Eliminado", "favorite_count": "Guardado", "items": "elementos", "no_favorites": "No hay favoritos", "add_favorites": "Por favor, agregue desde el botón 'Guardar' a continuación", } }; const getLang = (i18n) => { const userLang = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language; const langKey = userLang.split("-")[0]; return i18n[langKey] || i18n['en']; } const _ = getLang(i18n); class WPlaceExtendedFavorites { constructor() { this.STORAGE_KEY = 'wplace_extended_favorites'; this.init(); } init() { this.observeAndInit(); } observeAndInit() { // Button configurations const buttonConfigs = [ { id: 'favorite-btn', selector: `[title="${_.modal_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) } ]; // Start generic button observer this.startButtonObserver(buttonConfigs); // Create modal setTimeout(() => this.createModal(), 2000); } // Generic Button Observer System startButtonObserver(configs) { const ensureButtons = () => { configs.forEach(config => { if (!document.querySelector(config.selector)) { const container = document.querySelector(config.containerSelector); if (container) { config.create(container); } } }); }; // Observe DOM changes const observer = new MutationObserver(() => { setTimeout(ensureButtons, 100); }); observer.observe(document.body, { childList: true, subtree: true }); // Initial placement & periodic check setTimeout(ensureButtons, 1000); setInterval(ensureButtons, 5000); } // Create Favorite Button 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 = _.modal_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'); } // Create Save Button 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> ${_.save_button} `; button.addEventListener('click', () => this.addFavorite()); container.appendChild(button); console.log('⭐ Save button added'); } // Create Modal 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">${_.modal_title}</h3> </div> <!-- en: Export/Import Buttons --> <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> ${_.export} </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> ${_.import} </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"> <!-- Favorites will be displayed here --> </div> <div id="favorites-count" class="text-center text-sm text-base-content/80 mt-4"> <!-- Favorites count will be displayed here --> </div> </div> <form method="dialog" class="modal-backdrop"> <button>close</button> </form> `; document.body.appendChild(modal); // Event listener for existing grid clicks 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(); } }); // Export & Import event listeners modal.querySelector('#export-btn').addEventListener('click', () => this.exportFavorites()); modal.querySelector('#import-btn').addEventListener('click', () => this.importFavorites()); } // Export Function async exportFavorites() { try { const favorites = await this.getFavorites(); if (favorites.length === 0) { this.showToast(_.no_favorites_to_export); 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}${_.favorites_exported}`); } catch (error) { console.error(`${_.export_error}:`, error); this.showToast(_.export_failed); } } // Import Function 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); // Data format check if (!importData.favorites || !Array.isArray(importData.favorites)) { throw new Error(_.invalid_file_format); } const currentFavorites = await this.getFavorites(); const importCount = importData.favorites.length; if (!confirm(`${_.confirm_import_1} ${importCount} ${_.confirm_import_2}`)) { return; } // Duplicate check (exclude those with the same coordinates) 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 ); }); // Reassign new IDs (integers) newFavorites.forEach((fav, index) => { fav.id = Date.now() + index; }); // Merge and save const mergedFavorites = [...currentFavorites, ...newFavorites]; await GM.setValue(this.STORAGE_KEY, JSON.stringify(mergedFavorites)); this.renderFavorites(); this.showToast(`${newFavorites.length}${_.imported}`); } catch (error) { console.error(`${_.import_error}:`, error); this.showToast(`${_.import_failed}: ` + error.message); } // Clear file input fileInput.value = ''; }; } // Open Modal openModal() { this.renderFavorites(); document.getElementById('favorite-modal').showModal(); } // Get Current Position 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(`${_.location_retrieval_error}:`, error); } return null; } // Add Favorite async addFavorite() { const position = this.getCurrentPosition(); if (!position) { alert(_.unable_to_retrieve_location); return; } const name = prompt(`${_.enter_favorite_name}:`, `${_.location} (${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)); // Notification this.showToast(`"${name}" ${_.favorite_saved}`); } // Get Favorites List async getFavorites() { try { const stored = await GM.getValue(this.STORAGE_KEY, '[]'); return JSON.parse(stored); } catch (error) { console.error(`${_.favorite_retrieval_error}:`, error); return []; } } // Render Favorites List 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 = `${_.favorite_count}: ${favorites.length} ${_.items}`; 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">${_.no_favorites}</p> <p class="text-sm text-base-content/60">${_.add_favorites}</p> </div> `; return; } // Sort by newest first 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(''); } // Go To Location 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(); } // Delete Favorite async deleteFavorite(id) { if (!confirm(`${_.confirm_delete}`)) 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(`${_.favorite_deleted}`); } // Show Toast Notification 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); } } // Initialize new WPlaceExtendedFavorites(); // @main_end{wplace-infinite-bookmarks} console.log(`End - ${script_id}`)