mirror of
https://github.com/tiennm99/WPlace-AutoBOT.git
synced 2026-10-13 12:14:09 +00:00
3919 lines
139 KiB
JavaScript
3919 lines
139 KiB
JavaScript
;(async () => {
|
|
// CONFIGURATION CONSTANTS
|
|
const CONFIG = {
|
|
COOLDOWN_DEFAULT: 31000,
|
|
TRANSPARENCY_THRESHOLD: 100,
|
|
WHITE_THRESHOLD: 250,
|
|
LOG_INTERVAL: 10,
|
|
PAINTING_SPEED: {
|
|
MIN: 1, // Minimum 1 pixel per second
|
|
MAX: 1000, // Maximum 1000 pixels per second
|
|
DEFAULT: 5, // Default 5 pixels per second
|
|
},
|
|
PAINTING_SPEED_ENABLED: false,
|
|
// --- START: Color data from colour-converter.js ---
|
|
COLOR_PALETTE: [
|
|
[0,0,0],[60,60,60],[120,120,120],[170,170,170],[210,210,210],[255,255,255],
|
|
[96,0,24],[165,14,30],[237,28,36],[250,128,114],[228,92,26],[255,127,39],[246,170,9],
|
|
[249,221,59],[255,250,188],[156,132,49],[197,173,49],[232,212,95],[74,107,58],[90,148,74],[132,197,115],
|
|
[14,185,104],[19,230,123],[135,255,94],[12,129,110],[16,174,166],[19,225,190],[15,121,159],[96,247,242],
|
|
[187,250,242],[40,80,158],[64,147,228],[125,199,255],[77,49,184],[107,80,246],[153,177,251],
|
|
[74,66,132],[122,113,196],[181,174,241],[170,56,185],[224,159,249],
|
|
[203,0,122],[236,31,128],[243,141,169],[155,82,73],[209,128,120],[250,182,164],
|
|
[104,70,52],[149,104,42],[219,164,99],[123,99,82],[156,132,107],[214,181,148],
|
|
[209,128,81],[248,178,119],[255,197,165],[109,100,63],[148,140,107],[205,197,158],
|
|
[51,57,65],[109,117,141],[179,185,209]
|
|
],
|
|
COLOR_NAMES: {
|
|
"0,0,0": "Black", "60,60,60": "Dark Gray", "120,120,120": "Gray", "210,210,210": "Light Gray", "255,255,255": "White",
|
|
"96,0,24": "Deep Red", "237,28,36": "Red", "255,127,39": "Orange", "246,170,9": "Gold", "249,221,59": "Yellow",
|
|
"255,250,188": "Light Yellow", "14,185,104": "Dark Green", "19,230,123": "Green", "135,255,94": "Light Green",
|
|
"12,129,110": "Dark Teal", "16,174,166": "Teal", "19,225,190": "Light Teal", "96,247,242": "Cyan", "40,80,158": "Dark Blue",
|
|
"64,147,228": "Blue", "107,80,246": "Indigo", "153,177,251": "Light Indigo", "120,12,153": "Dark Purple",
|
|
"170,56,185": "Purple", "224,159,249": "Light Purple", "203,0,122": "Dark Pink", "236,31,128": "Pink",
|
|
"243,141,169": "Light Pink", "104,70,52": "Dark Brown", "149,104,42": "Brown", "248,178,119": "Beige",
|
|
"170,170,170": "Medium Gray", "165,14,30": "Dark Red", "250,128,114": "Light Red", "228,92,26": "Dark Orange",
|
|
"156,132,49": "Dark Goldenrod", "197,173,49": "Goldenrod", "232,212,95": "Light Goldenrod", "74,107,58": "Dark Olive",
|
|
"90,148,74": "Olive", "132,197,115": "Light Olive", "15,121,159": "Dark Cyan", "187,250,242": "Light Cyan",
|
|
"125,199,255": "Light Blue", "77,49,184": "Dark Indigo", "74,66,132": "Dark Slate Blue", "122,113,196": "Slate Blue",
|
|
"181,174,241": "Light Slate Blue", "155,82,73": "Dark Peach", "209,128,120": "Peach", "250,182,164": "Light Peach",
|
|
"219,164,99": "Light Brown", "123,99,82": "Dark Tan", "156,132,107": "Tan", "214,181,148": "Light Tan",
|
|
"209,128,81": "Dark Beige", "255,197,165": "Light Beige", "109,100,63": "Dark Stone", "148,140,107": "Stone",
|
|
"205,197,158": "Light Stone", "51,57,65": "Dark Slate", "109,117,141": "Slate", "179,185,209": "Light Slate",
|
|
},
|
|
PAID_COLORS: new Set([
|
|
"170,170,170", "165,14,30", "250,128,114", "228,92,26", "156,132,49", "197,173,49", "232,212,95", "74,107,58",
|
|
"90,148,74", "132,197,115", "15,121,159", "187,250,242", "125,199,255", "77,49,184", "74,66,132", "122,113,196",
|
|
"181,174,241", "155,82,73", "209,128,120", "250,182,164", "219,164,99", "123,99,82", "156,132,107", "214,181,148",
|
|
"209,128,81", "255,197,165", "109,100,63", "148,140,107", "205,197,158", "51,57,65", "109,117,141", "179,185,209",
|
|
]),
|
|
// --- END: Color data ---
|
|
// Optimized CSS Classes for reuse
|
|
CSS_CLASSES: {
|
|
BUTTON_PRIMARY: `
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
color: white; border: none; border-radius: 8px; padding: 10px 16px;
|
|
cursor: pointer; font-weight: 500; transition: all 0.3s ease;
|
|
display: flex; align-items: center; gap: 8px;
|
|
`,
|
|
BUTTON_SECONDARY: `
|
|
background: rgba(255,255,255,0.1); color: white;
|
|
border: 1px solid rgba(255,255,255,0.2); border-radius: 8px;
|
|
padding: 8px 12px; cursor: pointer; transition: all 0.3s ease;
|
|
`,
|
|
MODERN_CARD: `
|
|
background: rgba(255,255,255,0.1); border-radius: 12px;
|
|
padding: 18px; border: 1px solid rgba(255,255,255,0.1);
|
|
backdrop-filter: blur(5px);
|
|
`,
|
|
GRADIENT_TEXT: `
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text; font-weight: bold;
|
|
`
|
|
},
|
|
THEMES: {
|
|
"Classic Autobot": {
|
|
primary: "#000000",
|
|
secondary: "#111111",
|
|
accent: "#222222",
|
|
text: "#ffffff",
|
|
highlight: "#775ce3",
|
|
success: "#00ff00",
|
|
error: "#ff0000",
|
|
warning: "#ffaa00",
|
|
fontFamily: "'Segoe UI', Roboto, sans-serif",
|
|
borderRadius: "12px",
|
|
borderStyle: "solid",
|
|
borderWidth: "1px",
|
|
boxShadow: "0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)",
|
|
backdropFilter: "blur(10px)",
|
|
animations: {
|
|
glow: false,
|
|
scanline: false,
|
|
pixelBlink: false,
|
|
},
|
|
},
|
|
"Neon Retro": {
|
|
primary: "#1a1a2e",
|
|
secondary: "#16213e",
|
|
accent: "#0f3460",
|
|
text: "#00ff41",
|
|
highlight: "#ff6b35",
|
|
success: "#39ff14",
|
|
error: "#ff073a",
|
|
warning: "#ffff00",
|
|
neon: "#00ffff",
|
|
purple: "#bf00ff",
|
|
pink: "#ff1493",
|
|
fontFamily: "'Press Start 2P', monospace",
|
|
borderRadius: "0",
|
|
borderStyle: "solid",
|
|
borderWidth: "3px",
|
|
boxShadow: "0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)",
|
|
backdropFilter: "none",
|
|
animations: {
|
|
glow: true,
|
|
scanline: true,
|
|
pixelBlink: true,
|
|
},
|
|
},
|
|
},
|
|
currentTheme: "Classic Autobot",
|
|
}
|
|
|
|
const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]
|
|
|
|
const switchTheme = (themeName) => {
|
|
if (CONFIG.THEMES[themeName]) {
|
|
CONFIG.currentTheme = themeName
|
|
saveThemePreference()
|
|
|
|
// Remove existing theme styles
|
|
const existingStyle = document.querySelector('style[data-wplace-theme="true"]')
|
|
if (existingStyle) {
|
|
existingStyle.remove()
|
|
}
|
|
|
|
// Recreate UI with new theme (cleanup is handled in createUI)
|
|
createUI()
|
|
}
|
|
}
|
|
|
|
const saveThemePreference = () => {
|
|
try {
|
|
localStorage.setItem("wplace-theme", CONFIG.currentTheme)
|
|
} catch (e) {
|
|
console.warn("Could not save theme preference:", e)
|
|
}
|
|
}
|
|
|
|
const loadThemePreference = () => {
|
|
try {
|
|
const saved = localStorage.getItem("wplace-theme")
|
|
if (saved && CONFIG.THEMES[saved]) {
|
|
CONFIG.currentTheme = saved
|
|
}
|
|
} catch (e) {
|
|
console.warn("Could not load theme preference:", e)
|
|
}
|
|
}
|
|
|
|
const loadLanguagePreference = () => {
|
|
try {
|
|
const saved = localStorage.getItem("wplace_language")
|
|
if (saved && TEXT[saved]) {
|
|
state.language = saved
|
|
}
|
|
} catch (e) {
|
|
console.warn("Could not load language preference:", e)
|
|
}
|
|
}
|
|
|
|
// BILINGUAL TEXT STRINGS
|
|
const TEXT = {
|
|
en: {
|
|
title: "WPlace Auto-Image",
|
|
initBot: "Start Auto-BOT",
|
|
uploadImage: "Upload Image",
|
|
resizeImage: "Resize Image",
|
|
selectPosition: "Select Position",
|
|
startPainting: "Start Painting",
|
|
stopPainting: "Stop Painting",
|
|
checkingColors: "🔍 Checking available colors...",
|
|
noColorsFound: "❌ Open the color palette on the site and try again!",
|
|
colorsFound: "✅ {count} available colors found",
|
|
loadingImage: "🖼️ Loading image...",
|
|
imageLoaded: "✅ Image loaded with {count} valid pixels",
|
|
imageError: "❌ Error loading image",
|
|
selectPositionAlert: "Paint the first pixel at the location where you want the art to start!",
|
|
waitingPosition: "👆 Waiting for you to paint the reference pixel...",
|
|
positionSet: "✅ Position set successfully!",
|
|
positionTimeout: "❌ Timeout for position selection",
|
|
startPaintingMsg: "🎨 Starting painting...",
|
|
paintingProgress: "🧱 Progress: {painted}/{total} pixels...",
|
|
noCharges: "⌛ No charges. Waiting {time}...",
|
|
paintingStopped: "⏹️ Painting stopped by user",
|
|
paintingComplete: "✅ Painting complete! {count} pixels painted.",
|
|
paintingError: "❌ Error during painting",
|
|
missingRequirements: "❌ Load an image and select a position first",
|
|
progress: "Progress",
|
|
pixels: "Pixels",
|
|
charges: "Charges",
|
|
estimatedTime: "Estimated time",
|
|
initMessage: "Click 'Upload Image' to begin",
|
|
waitingInit: "Waiting for initialization...",
|
|
resizeSuccess: "✅ Image resized to {width}x{height}",
|
|
paintingPaused: "⏸️ Painting paused at position X: {x}, Y: {y}",
|
|
captchaNeeded: "❗ CAPTCHA token needed. Paint one pixel manually to continue.",
|
|
saveData: "Save Progress",
|
|
loadData: "Load Progress",
|
|
saveToFile: "Save to File",
|
|
loadFromFile: "Load from File",
|
|
dataManager: "Data Manager",
|
|
autoSaved: "✅ Progress saved automatically",
|
|
dataLoaded: "✅ Progress loaded successfully",
|
|
fileSaved: "✅ Progress saved to file successfully",
|
|
fileLoaded: "✅ Progress loaded from file successfully",
|
|
noSavedData: "❌ No saved progress found",
|
|
savedDataFound: "✅ Saved progress found! Load to continue?",
|
|
savedDate: "Saved on: {date}",
|
|
clickLoadToContinue: "Click 'Load Progress' to continue.",
|
|
fileError: "❌ Error processing file",
|
|
invalidFileFormat: "❌ Invalid file format",
|
|
paintingSpeed: "Painting Speed",
|
|
pixelsPerSecond: "pixels/second",
|
|
speedSetting: "Speed: {speed} pixels/sec",
|
|
settings: "Settings",
|
|
botSettings: "Bot Settings",
|
|
close: "Close",
|
|
language: "Language",
|
|
themeSettings: "Theme Settings",
|
|
themeSettingsDesc: "Choose your preferred color theme for the interface.",
|
|
languageSelectDesc: "Select your preferred language. Changes will take effect immediately.",
|
|
},
|
|
ru: {
|
|
title: "WPlace Авто-Изображение",
|
|
initBot: "Запустить Авто-БОТ",
|
|
uploadImage: "Загрузить изображение",
|
|
resizeImage: "Изменить размер изображения",
|
|
selectPosition: "Выбрать позицию",
|
|
startPainting: "Начать рисование",
|
|
stopPainting: "Остановить рисование",
|
|
checkingColors: "🔍 Проверка доступных цветов...",
|
|
noColorsFound: "❌ Откройте палитру цветов на сайте и попробуйте снова!",
|
|
colorsFound: "✅ Найдено доступных цветов: {count}",
|
|
loadingImage: "🖼️ Загрузка изображения...",
|
|
imageLoaded: "✅ Изображение загружено, валидных пикселей: {count}",
|
|
imageError: "❌ Ошибка при загрузке изображения",
|
|
selectPositionAlert: "Нарисуйте первый пиксель в месте, откуда начнётся рисунок!",
|
|
waitingPosition: "👆 Ожидание, пока вы нарисуете опорный пиксель...",
|
|
positionSet: "✅ Позиция успешно установлена!",
|
|
positionTimeout: "❌ Время ожидания выбора позиции истекло",
|
|
startPaintingMsg: "🎨 Начинаем рисование...",
|
|
paintingProgress: "🧱 Прогресс: {painted}/{total} пикселей...",
|
|
noCharges: "⌛ Нет зарядов. Ожидание {time}...",
|
|
paintingStopped: "⏹️ Рисование остановлено пользователем",
|
|
paintingComplete: "✅ Рисование завершено! Нарисовано пикселей: {count}.",
|
|
paintingError: "❌ Ошибка во время рисования",
|
|
missingRequirements: "❌ Сначала загрузите изображение и выберите позицию",
|
|
progress: "Прогресс",
|
|
pixels: "Пиксели",
|
|
charges: "Заряды",
|
|
estimatedTime: "Примерное время",
|
|
initMessage: "Нажмите 'Загрузить изображение', чтобы начать",
|
|
waitingInit: "Ожидание инициализации...",
|
|
resizeSuccess: "✅ Изображение изменено до {width}x{height}",
|
|
paintingPaused: "⏸️ Рисование приостановлено на позиции X: {x}, Y: {y}",
|
|
captchaNeeded: "❗ Требуется токен CAPTCHA. Нарисуйте один пиксель вручную, чтобы продолжить.",
|
|
saveData: "Сохранить прогресс",
|
|
loadData: "Загрузить прогресс",
|
|
saveToFile: "Сохранить в файл",
|
|
loadFromFile: "Загрузить из файла",
|
|
dataManager: "Менеджер данных",
|
|
autoSaved: "✅ Прогресс сохранён автоматически",
|
|
dataLoaded: "✅ Прогресс успешно загружен",
|
|
fileSaved: "✅ Прогресс успешно сохранён в файл",
|
|
fileLoaded: "✅ Прогресс успешно загружен из файла",
|
|
noSavedData: "❌ Сохранённый прогресс не найден",
|
|
savedDataFound: "✅ Найден сохранённый прогресс! Загрузить, чтобы продолжить?",
|
|
savedDate: "Сохранено: {date}",
|
|
clickLoadToContinue: "Нажмите 'Загрузить прогресс', чтобы продолжить.",
|
|
fileError: "❌ Ошибка при обработке файла",
|
|
invalidFileFormat: "❌ Неверный формат файла",
|
|
paintingSpeed: "Скорость рисования",
|
|
pixelsPerSecond: "пикселей/сек",
|
|
speedSetting: "Скорость: {speed} пикс./сек",
|
|
settings: "Настройки",
|
|
botSettings: "Настройки бота",
|
|
close: "Закрыть",
|
|
language: "Язык",
|
|
themeSettings: "Настройки темы",
|
|
themeSettingsDesc: "Выберите предпочтительную цветовую тему интерфейса.",
|
|
languageSelectDesc: "Выберите предпочтительный язык. Изменения вступят в силу немедленно."
|
|
},
|
|
pt: {
|
|
title: "WPlace Auto-Image",
|
|
initBot: "Iniciar Auto-BOT",
|
|
uploadImage: "Upload da Imagem",
|
|
resizeImage: "Redimensionar Imagem",
|
|
selectPosition: "Selecionar Posição",
|
|
startPainting: "Iniciar Pintura",
|
|
stopPainting: "Parar Pintura",
|
|
checkingColors: "🔍 Verificando cores disponíveis...",
|
|
noColorsFound: "❌ Abra a paleta de cores no site e tente novamente!",
|
|
colorsFound: "✅ {count} cores disponíveis encontradas",
|
|
loadingImage: "🖼️ Carregando imagem...",
|
|
imageLoaded: "✅ Imagem carregada com {count} pixels válidos",
|
|
imageError: "❌ Erro ao carregar imagem",
|
|
selectPositionAlert: "Pinte o primeiro pixel на localização onde deseja que a arte comece!",
|
|
waitingPosition: "👆 Aguardando você pintar o pixel de referência...",
|
|
positionSet: "✅ Posição definida com sucesso!",
|
|
positionTimeout: "❌ Tempo esgotado para selecionar posição",
|
|
startPaintingMsg: "🎨 Iniciando pintura...",
|
|
paintingProgress: "🧱 Progresso: {painted}/{total} pixels...",
|
|
noCharges: "⌛ Sem cargas. Aguardando {time}...",
|
|
paintingStopped: "⏹️ Pintura interromпида pelo usuário",
|
|
paintingComplete: "✅ Pintura concluída! {count} pixels pintados.",
|
|
paintingError: "❌ Erro durante a pintura",
|
|
missingRequirements: "❌ Carregue uma imagem e selecione uma posição primeiro",
|
|
progress: "Progresso",
|
|
pixels: "Pixels",
|
|
charges: "Cargas",
|
|
estimatedTime: "Tempo estimado",
|
|
initMessage: "Clique em 'Upload da Imagem' para começar",
|
|
waitingInit: "Aguardando inicialização...",
|
|
resizeSuccess: "✅ Imagem redimensionada para {width}x{height}",
|
|
paintingPaused: "⏸️ Pintura pausada na posição X: {x}, Y: {y}",
|
|
captchaNeeded: "❗ Token CAPTCHA necessário. Pinte um pixel manualmente para continuar.",
|
|
saveData: "Salvar Progresso",
|
|
loadData: "Carregar Progresso",
|
|
saveToFile: "Salvar em Arquivo",
|
|
loadFromFile: "Carregar de Arquivo",
|
|
dataManager: "Dados",
|
|
autoSaved: "✅ Progresso salvo automaticamente",
|
|
dataLoaded: "✅ Progresso carregado com sucesso",
|
|
fileSaved: "✅ Salvo em arquivo com sucesso",
|
|
fileLoaded: "✅ Carregado de arquivo com sucesso",
|
|
noSavedData: "❌ Nenhum progresso salvo encontrado",
|
|
savedDataFound: "✅ Progresso salvo encontrado! Carregar para continuar?",
|
|
savedDate: "Salvo em: {date}",
|
|
clickLoadToContinue: "Clique em 'Carregar Progresso' para continuar.",
|
|
fileError: "❌ Erro ao processar arquivo",
|
|
invalidFileFormat: "❌ Formato de arquivo inválido",
|
|
paintingSpeed: "Velocidade de Pintura",
|
|
pixelsPerSecond: "pixels/segundo",
|
|
speedSetting: "Velocidade: {speed} pixels/seg",
|
|
settings: "Configurações",
|
|
botSettings: "Configurações do Bot",
|
|
close: "Fechar",
|
|
language: "Idioma",
|
|
themeSettings: "Configurações de Tema",
|
|
themeSettingsDesc: "Escolha seu tema de cores preferido para a interface.",
|
|
languageSelectDesc: "Selecione seu idioma preferido. As alterações terão efeito imediatamente.",
|
|
},
|
|
vi: {
|
|
title: "WPlace Auto-Image",
|
|
initBot: "Khởi động Auto-BOT",
|
|
uploadImage: "Tải lên hình ảnh",
|
|
resizeImage: "Thay đổi kích thước",
|
|
selectPosition: "Chọn vị trí",
|
|
startPainting: "Bắt đầu vẽ",
|
|
stopPainting: "Dừng vẽ",
|
|
checkingColors: "🔍 Đang kiểm tra màu sắc có sẵn...",
|
|
noColorsFound: "❌ Hãy mở bảng màu trên trang web và thử lại!",
|
|
colorsFound: "✅ Tìm thấy {count} màu sắc có sẵn",
|
|
loadingImage: "🖼️ Đang tải hình ảnh...",
|
|
imageLoaded: "✅ Đã tải hình ảnh với {count} pixel hợp lệ",
|
|
imageError: "❌ Lỗi khi tải hình ảnh",
|
|
selectPositionAlert: "Vẽ pixel đầu tiên tại vị trí bạn muốn tác phẩm nghệ thuật bắt đầu!",
|
|
waitingPosition: "👆 Đang chờ bạn vẽ pixel tham chiếu...",
|
|
positionSet: "✅ Đã đặt vị trí thành công!",
|
|
positionTimeout: "❌ Hết thời gian chọn vị trí",
|
|
startPaintingMsg: "🎨 Bắt đầu vẽ...",
|
|
paintingProgress: "🧱 Tiến trình: {painted}/{total} pixel...",
|
|
noCharges: "⌛ Không có điện tích. Đang chờ {time}...",
|
|
paintingStopped: "⏹️ Người dùng đã dừng vẽ",
|
|
paintingComplete: "✅ Hoàn thành vẽ! Đã vẽ {count} pixel.",
|
|
paintingError: "❌ Lỗi trong quá trình vẽ",
|
|
missingRequirements: "❌ Hãy tải lên hình ảnh và chọn vị trí trước",
|
|
progress: "Tiến trình",
|
|
pixels: "Pixel",
|
|
charges: "Điện tích",
|
|
estimatedTime: "Thời gian ước tính",
|
|
initMessage: "Nhấp 'Tải lên hình ảnh' để bắt đầu",
|
|
waitingInit: "Đang chờ khởi tạo...",
|
|
resizeSuccess: "✅ Đã thay đổi kích thước hình ảnh thành {width}x{height}",
|
|
paintingPaused: "⏸️ Tạm dừng vẽ tại vị trí X: {x}, Y: {y}",
|
|
captchaNeeded: "❗ Cần token CAPTCHA. Vẽ một pixel thủ công để tiếp tục.",
|
|
saveData: "Lưu tiến trình",
|
|
loadData: "Tải tiến trình",
|
|
saveToFile: "Lưu vào tệp",
|
|
loadFromFile: "Tải từ tệp",
|
|
dataManager: "Dữ liệu",
|
|
autoSaved: "✅ Đã tự động lưu tiến trình",
|
|
dataLoaded: "✅ Đã tải tiến trình thành công",
|
|
fileSaved: "✅ Đã lưu vào tệp thành công",
|
|
fileLoaded: "✅ Đã tải từ tệp thành công",
|
|
noSavedData: "❌ Không tìm thấy tiến trình đã lưu",
|
|
savedDataFound: "✅ Tìm thấy tiến trình đã lưu! Tải để tiếp tục?",
|
|
savedDate: "Đã lưu vào: {date}",
|
|
clickLoadToContinue: "Nhấp 'Tải tiến trình' để tiếp tục.",
|
|
fileError: "❌ Lỗi khi xử lý tệp",
|
|
invalidFileFormat: "❌ Định dạng tệp không hợp lệ",
|
|
paintingSpeed: "Tốc độ vẽ",
|
|
pixelsPerSecond: "pixel/giây",
|
|
speedSetting: "Tốc độ: {speed} pixel/giây",
|
|
settings: "Cài đặt",
|
|
botSettings: "Cài đặt Bot",
|
|
close: "Đóng",
|
|
language: "Ngôn ngữ",
|
|
themeSettings: "Cài đặt Giao diện",
|
|
themeSettingsDesc: "Chọn chủ đề màu sắc yêu thích cho giao diện.",
|
|
languageSelectDesc: "Chọn ngôn ngữ ưa thích. Thay đổi sẽ có hiệu lực ngay lập tức.",
|
|
},
|
|
fr: {
|
|
title: "WPlace Auto-Image",
|
|
initBot: "Démarrer Auto-BOT",
|
|
uploadImage: "Télécharger l'image",
|
|
resizeImage: "Redimensionner l'image",
|
|
selectPosition: "Sélectionner la position",
|
|
startPainting: "Commencer à peindre",
|
|
stopPainting: "Arrêter de peindre",
|
|
checkingColors: "🔍 Vérification des couleurs disponibles...",
|
|
noColorsFound: "❌ Ouvrez la palette de couleurs sur le site et réessayez!",
|
|
colorsFound: "✅ {count} couleurs disponibles trouvées",
|
|
loadingImage: "🖼️ Chargement de l'image...",
|
|
imageLoaded: "✅ Image chargée avec {count} pixels valides",
|
|
imageError: "❌ Erreur lors du chargement de l'image",
|
|
selectPositionAlert: "Peignez le premier pixel à l'endroit où vous voulez que l'art commence!",
|
|
waitingPosition: "👆 En attente que vous peigniez le pixel de référence...",
|
|
positionSet: "✅ Position définie avec succès!",
|
|
positionTimeout: "❌ Délai d'attente pour la sélection de position",
|
|
startPaintingMsg: "🎨 Début de la peinture...",
|
|
paintingProgress: "🧱 Progrès: {painted}/{total} pixels...",
|
|
noCharges: "⌛ Aucune charge. En attente {time}...",
|
|
paintingStopped: "⏹️ Peinture arrêtée par l'utilisateur",
|
|
paintingComplete: "✅ Peinture terminée! {count} pixels peints.",
|
|
paintingError: "❌ Erreur pendant la peinture",
|
|
missingRequirements: "❌ Veuillez charger une image et sélectionner une position d'abord",
|
|
progress: "Progrès",
|
|
pixels: "Pixels",
|
|
charges: "Charges",
|
|
estimatedTime: "Temps estimé",
|
|
initMessage: "Cliquez sur 'Télécharger l'image' pour commencer",
|
|
waitingInit: "En attente d'initialisation...",
|
|
resizeSuccess: "✅ Image redimensionnée en {width}x{height}",
|
|
paintingPaused: "⏸️ Peinture en pause à la position X: {x}, Y: {y}",
|
|
captchaNeeded: "❗ Token CAPTCHA nécessaire. Peignez un pixel manuellement pour continuer.",
|
|
saveData: "Sauvegarder le progrès",
|
|
loadData: "Charger le progrès",
|
|
saveToFile: "Sauvegarder dans un fichier",
|
|
loadFromFile: "Charger depuis un fichier",
|
|
dataManager: "Données",
|
|
autoSaved: "✅ Progrès sauvegardé automatiquement",
|
|
dataLoaded: "✅ Progrès chargé avec succès",
|
|
fileSaved: "✅ Sauvegardé dans un fichier avec succès",
|
|
fileLoaded: "✅ Chargé depuis un fichier avec succès",
|
|
noSavedData: "❌ Aucun progrès sauvegardé trouvé",
|
|
savedDataFound: "✅ Progrès sauvegardé trouvé! Charger pour continuer?",
|
|
savedDate: "Sauvegardé le: {date}",
|
|
clickLoadToContinue: "Cliquez sur 'Charger le progrès' pour continuer.",
|
|
fileError: "❌ Erreur lors du traitement du fichier",
|
|
invalidFileFormat: "❌ Format de fichier invalide",
|
|
paintingSpeed: "Vitesse de peinture",
|
|
pixelsPerSecond: "pixels/seconde",
|
|
speedSetting: "Vitesse: {speed} pixels/sec",
|
|
settings: "Paramètres",
|
|
botSettings: "Paramètres du Bot",
|
|
close: "Fermer",
|
|
language: "Langue",
|
|
themeSettings: "Paramètres de Thème",
|
|
themeSettingsDesc: "Choisissez votre thème de couleurs préféré pour l'interface.",
|
|
languageSelectDesc: "Sélectionnez votre langue préférée. Les changements prendront effet immédiatement.",
|
|
},
|
|
}
|
|
|
|
// GLOBAL STATE
|
|
const state = {
|
|
running: false,
|
|
imageLoaded: false,
|
|
processing: false,
|
|
totalPixels: 0,
|
|
paintedPixels: 0,
|
|
availableColors: [],
|
|
activeColorPalette: [], // User-selected colors for conversion
|
|
paintWhitePixels: true, // Default to ON
|
|
currentCharges: 0,
|
|
cooldown: CONFIG.COOLDOWN_DEFAULT,
|
|
imageData: null,
|
|
stopFlag: false,
|
|
colorsChecked: false,
|
|
startPosition: null,
|
|
selectingPosition: false,
|
|
region: null,
|
|
minimized: false,
|
|
lastPosition: { x: 0, y: 0 },
|
|
estimatedTime: 0,
|
|
language: "en",
|
|
paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels per second
|
|
}
|
|
|
|
// Placeholder for the resize preview update function
|
|
let _updateResizePreview = () => {};
|
|
|
|
// Turnstile token handling (promise-based) inspired by external logic
|
|
let turnstileToken = null
|
|
let _resolveToken = null
|
|
let tokenPromise = new Promise((resolve) => { _resolveToken = resolve })
|
|
|
|
function setTurnstileToken(t) {
|
|
if (_resolveToken) {
|
|
_resolveToken(t)
|
|
_resolveToken = null
|
|
}
|
|
turnstileToken = t
|
|
}
|
|
|
|
async function ensureToken() {
|
|
if (!turnstileToken) {
|
|
updateUI("captchaNeeded", "error")
|
|
Utils.showAlert(Utils.t("captchaNeeded"), "error")
|
|
try { await tokenPromise } catch (_) {}
|
|
}
|
|
return turnstileToken
|
|
}
|
|
|
|
// Intercept fetch to capture Turnstile token from pixel placement requests
|
|
const originalFetch = window.fetch
|
|
window.fetch = async (url, options) => {
|
|
if (typeof url === "string" && url.includes("https://backend.wplace.live/s0/pixel/")) {
|
|
try {
|
|
const payload = JSON.parse(options.body)
|
|
if (payload.t) {
|
|
console.log("✅ Turnstile Token Captured:", payload.t)
|
|
setTurnstileToken(payload.t)
|
|
if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) {
|
|
Utils.showAlert("Token captured successfully! You can start the bot now.", "success")
|
|
updateUI("colorsFound", "success", { count: state.availableColors.length })
|
|
}
|
|
}
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
return originalFetch(url, options)
|
|
}
|
|
|
|
// LANGUAGE DETECTION
|
|
async function detectLanguage() {
|
|
try {
|
|
const response = await fetch("https://backend.wplace.live/me", {
|
|
credentials: "include",
|
|
})
|
|
const data = await response.json()
|
|
state.language = data.language === "pt" ? "pt" : "en"
|
|
} catch {
|
|
state.language = navigator.language.startsWith("pt") ? "pt" : "en"
|
|
}
|
|
}
|
|
|
|
// UTILITY FUNCTIONS
|
|
const Utils = {
|
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
|
|
// Optimized DOM creation helpers
|
|
createElement: (tag, props = {}, children = []) => {
|
|
const element = document.createElement(tag)
|
|
|
|
// Set properties efficiently
|
|
Object.entries(props).forEach(([key, value]) => {
|
|
if (key === 'style' && typeof value === 'object') {
|
|
Object.assign(element.style, value)
|
|
} else if (key === 'className') {
|
|
element.className = value
|
|
} else if (key === 'innerHTML') {
|
|
element.innerHTML = value
|
|
} else {
|
|
element.setAttribute(key, value)
|
|
}
|
|
})
|
|
|
|
// Append children efficiently
|
|
if (typeof children === 'string') {
|
|
element.textContent = children
|
|
} else if (Array.isArray(children)) {
|
|
children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
element.appendChild(document.createTextNode(child))
|
|
} else {
|
|
element.appendChild(child)
|
|
}
|
|
})
|
|
}
|
|
|
|
return element
|
|
},
|
|
|
|
// Create button with common styling
|
|
createButton: (id, text, icon, onClick, style = CONFIG.CSS_CLASSES.BUTTON_PRIMARY) => {
|
|
const button = Utils.createElement('button', {
|
|
id: id,
|
|
style: style,
|
|
innerHTML: `${icon ? `<i class="${icon}"></i>` : ''}<span>${text}</span>`
|
|
})
|
|
if (onClick) button.addEventListener('click', onClick)
|
|
return button
|
|
},
|
|
|
|
t: (key, params = {}) => {
|
|
let text = TEXT[state.language]?.[key] || TEXT.en[key] || key
|
|
Object.keys(params).forEach((param) => {
|
|
text = text.replace(`{${param}}`, params[param])
|
|
})
|
|
return text
|
|
},
|
|
|
|
showAlert: (message, type = "info") => {
|
|
const alertDiv = document.createElement("div")
|
|
alertDiv.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
color: white;
|
|
font-weight: 600;
|
|
z-index: 10001;
|
|
max-width: 400px;
|
|
text-align: center;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
animation: slideDown 0.3s ease-out;
|
|
font-family: 'Segoe UI', sans-serif;
|
|
`
|
|
|
|
const colors = {
|
|
info: "background: linear-gradient(135deg, #3498db, #2980b9);",
|
|
success: "background: linear-gradient(135deg, #27ae60, #229954);",
|
|
warning: "background: linear-gradient(135deg, #f39c12, #e67e22);",
|
|
error: "background: linear-gradient(135deg, #e74c3c, #c0392b);",
|
|
}
|
|
|
|
alertDiv.style.cssText += colors[type] || colors.info
|
|
|
|
const style = document.createElement("style")
|
|
style.textContent = `
|
|
@keyframes slideDown {
|
|
from { transform: translateX(-50%) translateY(-20px); opacity: 0; }
|
|
to { transform: translateX(-50%) translateY(0); opacity: 1; }
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|
|
|
|
alertDiv.textContent = message
|
|
document.body.appendChild(alertDiv)
|
|
|
|
setTimeout(() => {
|
|
alertDiv.style.animation = "slideDown 0.3s ease-out reverse"
|
|
setTimeout(() => {
|
|
document.body.removeChild(alertDiv)
|
|
document.head.removeChild(style)
|
|
}, 300)
|
|
}, 4000)
|
|
},
|
|
|
|
colorDistance: (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)),
|
|
|
|
// The color metric from colour-converter.js for higher accuracy
|
|
findClosestPaletteColor: (r, g, b, palette) => {
|
|
let menorDist = Infinity;
|
|
let cor = [0, 0, 0];
|
|
if (!palette || palette.length === 0) return cor;
|
|
|
|
for (let i = 0; i < palette.length; i++) {
|
|
const [pr, pg, pb] = palette[i];
|
|
const rmean = (pr + r) / 2;
|
|
const rdiff = pr - r;
|
|
const gdiff = pg - g;
|
|
const bdiff = pb - b;
|
|
const dist = Math.sqrt(((512 + rmean) * rdiff * rdiff >> 8) + 4 * gdiff * gdiff + ((767 - rmean) * bdiff * bdiff >> 8));
|
|
if (dist < menorDist) {
|
|
menorDist = dist;
|
|
cor = [pr, pg, pb];
|
|
}
|
|
}
|
|
return cor;
|
|
},
|
|
|
|
isWhitePixel: (r, g, b) =>
|
|
r >= CONFIG.WHITE_THRESHOLD && g >= CONFIG.WHITE_THRESHOLD && b >= CONFIG.WHITE_THRESHOLD,
|
|
|
|
createImageUploader: () =>
|
|
new Promise((resolve) => {
|
|
const input = document.createElement("input")
|
|
input.type = "file"
|
|
input.accept = "image/png,image/jpeg"
|
|
input.onchange = () => {
|
|
const fr = new FileReader()
|
|
fr.onload = () => resolve(fr.result)
|
|
fr.readAsDataURL(input.files[0])
|
|
}
|
|
input.click()
|
|
}),
|
|
|
|
createFileDownloader: (data, filename) => {
|
|
const blob = new Blob([data], { type: "application/json" })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement("a")
|
|
a.href = url
|
|
a.download = filename
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
URL.revokeObjectURL(url)
|
|
},
|
|
|
|
createFileUploader: () =>
|
|
new Promise((resolve, reject) => {
|
|
const input = document.createElement("input")
|
|
input.type = "file"
|
|
input.accept = ".json"
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0]
|
|
if (file) {
|
|
const reader = new FileReader()
|
|
reader.onload = () => {
|
|
try {
|
|
const data = JSON.parse(reader.result)
|
|
resolve(data)
|
|
} catch (error) {
|
|
reject(new Error("Invalid JSON file"))
|
|
}
|
|
}
|
|
reader.onerror = () => reject(new Error("File reading error"))
|
|
reader.readAsText(file)
|
|
} else {
|
|
reject(new Error("No file selected"))
|
|
}
|
|
}
|
|
input.click()
|
|
}),
|
|
|
|
extractAvailableColors: () => {
|
|
const colorElements = document.querySelectorAll('[id^="color-"]')
|
|
return Array.from(colorElements)
|
|
.filter((el) => !el.querySelector("svg"))
|
|
.filter((el) => {
|
|
const id = Number.parseInt(el.id.replace("color-", ""))
|
|
return id !== 0 && id !== 5
|
|
})
|
|
.map((el) => {
|
|
const id = Number.parseInt(el.id.replace("color-", ""))
|
|
const rgbStr = el.style.backgroundColor.match(/\d+/g)
|
|
const rgb = rgbStr ? rgbStr.map(Number) : [0, 0, 0]
|
|
return { id, rgb }
|
|
})
|
|
},
|
|
|
|
formatTime: (ms) => {
|
|
const seconds = Math.floor((ms / 1000) % 60)
|
|
const minutes = Math.floor((ms / (1000 * 60)) % 60)
|
|
const hours = Math.floor((ms / (1000 * 60 * 60)) % 24)
|
|
const days = Math.floor(ms / (1000 * 60 * 60 * 24))
|
|
|
|
let result = ""
|
|
if (days > 0) result += `${days}d `
|
|
if (hours > 0 || days > 0) result += `${hours}h `
|
|
if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}m `
|
|
result += `${seconds}s`
|
|
|
|
return result
|
|
},
|
|
|
|
calculateEstimatedTime: (remainingPixels, charges, cooldown) => {
|
|
if (remainingPixels <= 0) return 0
|
|
|
|
// Calculate time based on painting speed (pixels per second)
|
|
const paintingSpeedDelay = state.paintingSpeed > 0 ? (1000 / state.paintingSpeed) : 1000
|
|
const timeFromSpeed = remainingPixels * paintingSpeedDelay // ms
|
|
|
|
// Calculate time based on charges and cooldown
|
|
const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1))
|
|
const timeFromCharges = cyclesNeeded * cooldown // ms
|
|
|
|
// Return the maximum of both calculations (the limiting factor)
|
|
return Math.max(timeFromSpeed, timeFromCharges)
|
|
},
|
|
|
|
// Save/Load Progress Functions
|
|
saveProgress: () => {
|
|
try {
|
|
const progressData = {
|
|
timestamp: Date.now(),
|
|
state: {
|
|
totalPixels: state.totalPixels,
|
|
paintedPixels: state.paintedPixels,
|
|
lastPosition: state.lastPosition,
|
|
startPosition: state.startPosition,
|
|
region: state.region,
|
|
imageLoaded: state.imageLoaded,
|
|
colorsChecked: state.colorsChecked,
|
|
availableColors: state.availableColors,
|
|
},
|
|
imageData: state.imageData
|
|
? {
|
|
width: state.imageData.width,
|
|
height: state.imageData.height,
|
|
pixels: Array.from(state.imageData.pixels),
|
|
totalPixels: state.imageData.totalPixels,
|
|
}
|
|
: null,
|
|
paintedMap: state.paintedMap ? state.paintedMap.map((row) => Array.from(row)) : null,
|
|
}
|
|
|
|
localStorage.setItem("wplace-bot-progress", JSON.stringify(progressData))
|
|
return true
|
|
} catch (error) {
|
|
console.error("Error saving progress:", error)
|
|
return false
|
|
}
|
|
},
|
|
|
|
loadProgress: () => {
|
|
try {
|
|
const saved = localStorage.getItem("wplace-bot-progress")
|
|
return saved ? JSON.parse(saved) : null
|
|
} catch (error) {
|
|
console.error("Error loading progress:", error)
|
|
return null
|
|
}
|
|
},
|
|
|
|
clearProgress: () => {
|
|
try {
|
|
localStorage.removeItem("wplace-bot-progress")
|
|
return true
|
|
} catch (error) {
|
|
console.error("Error clearing progress:", error)
|
|
return false
|
|
}
|
|
},
|
|
|
|
restoreProgress: (savedData) => {
|
|
try {
|
|
// Restore state
|
|
Object.assign(state, savedData.state)
|
|
|
|
// Restore image data
|
|
if (savedData.imageData) {
|
|
state.imageData = {
|
|
...savedData.imageData,
|
|
pixels: new Uint8ClampedArray(savedData.imageData.pixels),
|
|
}
|
|
}
|
|
|
|
// Restore painted map
|
|
if (savedData.paintedMap) {
|
|
state.paintedMap = savedData.paintedMap.map((row) => Array.from(row))
|
|
}
|
|
|
|
return true
|
|
} catch (error) {
|
|
console.error("Error restoring progress:", error)
|
|
return false
|
|
}
|
|
},
|
|
|
|
saveProgressToFile: () => {
|
|
try {
|
|
const progressData = {
|
|
timestamp: Date.now(),
|
|
version: "1.0",
|
|
state: {
|
|
totalPixels: state.totalPixels,
|
|
paintedPixels: state.paintedPixels,
|
|
lastPosition: state.lastPosition,
|
|
startPosition: state.startPosition,
|
|
region: state.region,
|
|
imageLoaded: state.imageLoaded,
|
|
colorsChecked: state.colorsChecked,
|
|
availableColors: state.availableColors,
|
|
},
|
|
imageData: state.imageData
|
|
? {
|
|
width: state.imageData.width,
|
|
height: state.imageData.height,
|
|
pixels: Array.from(state.imageData.pixels),
|
|
totalPixels: state.imageData.totalPixels,
|
|
}
|
|
: null,
|
|
paintedMap: state.paintedMap ? state.paintedMap.map((row) => Array.from(row)) : null,
|
|
}
|
|
|
|
const filename = `wplace-bot-progress-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`
|
|
Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename)
|
|
return true
|
|
} catch (error) {
|
|
console.error("Error saving to file:", error)
|
|
return false
|
|
}
|
|
},
|
|
|
|
loadProgressFromFile: async () => {
|
|
try {
|
|
const data = await Utils.createFileUploader()
|
|
|
|
if (!data.version || !data.state) {
|
|
throw new Error("Invalid file format")
|
|
}
|
|
|
|
const success = Utils.restoreProgress(data)
|
|
return success
|
|
} catch (error) {
|
|
console.error("Error loading from file:", error)
|
|
throw error
|
|
}
|
|
},
|
|
}
|
|
|
|
// IMAGE PROCESSOR CLASS
|
|
class ImageProcessor {
|
|
constructor(imageSrc) {
|
|
this.imageSrc = imageSrc
|
|
this.img = null
|
|
this.canvas = null
|
|
this.ctx = null
|
|
}
|
|
|
|
async load() {
|
|
return new Promise((resolve, reject) => {
|
|
this.img = new Image()
|
|
this.img.crossOrigin = "anonymous"
|
|
this.img.onload = () => {
|
|
this.canvas = document.createElement("canvas")
|
|
this.ctx = this.canvas.getContext("2d")
|
|
this.canvas.width = this.img.width
|
|
this.canvas.height = this.img.height
|
|
this.ctx.drawImage(this.img, 0, 0)
|
|
resolve()
|
|
}
|
|
this.img.onerror = reject
|
|
this.img.src = this.imageSrc
|
|
})
|
|
}
|
|
|
|
getDimensions() {
|
|
return {
|
|
width: this.canvas.width,
|
|
height: this.canvas.height,
|
|
}
|
|
}
|
|
|
|
getPixelData() {
|
|
return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data
|
|
}
|
|
|
|
resize(newWidth, newHeight) {
|
|
const tempCanvas = document.createElement("canvas")
|
|
const tempCtx = tempCanvas.getContext("2d")
|
|
|
|
tempCanvas.width = newWidth
|
|
tempCanvas.height = newHeight
|
|
|
|
tempCtx.imageSmoothingEnabled = false
|
|
tempCtx.drawImage(this.canvas, 0, 0, newWidth, newHeight)
|
|
|
|
this.canvas.width = newWidth
|
|
this.canvas.height = newHeight
|
|
this.ctx.imageSmoothingEnabled = false
|
|
this.ctx.drawImage(tempCanvas, 0, 0)
|
|
|
|
return this.ctx.getImageData(0, 0, newWidth, newHeight).data
|
|
}
|
|
|
|
generatePreview(width, height) {
|
|
const previewCanvas = document.createElement("canvas")
|
|
const previewCtx = previewCanvas.getContext("2d")
|
|
|
|
previewCanvas.width = width
|
|
previewCanvas.height = height
|
|
|
|
previewCtx.imageSmoothingEnabled = false
|
|
previewCtx.drawImage(this.img, 0, 0, width, height)
|
|
|
|
return previewCanvas.toDataURL()
|
|
}
|
|
}
|
|
|
|
// WPLACE API SERVICE
|
|
const WPlaceService = {
|
|
async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) {
|
|
try {
|
|
await ensureToken()
|
|
if (!turnstileToken) return "token_error"
|
|
const payload = { coords: [pixelX, pixelY], colors: [color], t: turnstileToken }
|
|
const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "text/plain;charset=UTF-8" },
|
|
credentials: "include",
|
|
body: JSON.stringify(payload),
|
|
})
|
|
if (res.status === 403) {
|
|
console.error("❌ 403 Forbidden. Turnstile token might be invalid or expired.")
|
|
turnstileToken = null
|
|
tokenPromise = new Promise((resolve) => { _resolveToken = resolve })
|
|
return "token_error"
|
|
}
|
|
const data = await res.json()
|
|
return data?.painted === 1
|
|
} catch (e) {
|
|
console.error("Paint request failed:", e)
|
|
return false
|
|
}
|
|
},
|
|
|
|
async getCharges() {
|
|
try {
|
|
const res = await fetch("https://backend.wplace.live/me", {
|
|
credentials: "include",
|
|
})
|
|
const data = await res.json()
|
|
return {
|
|
charges: data.charges?.count || 0,
|
|
cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT,
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to get charges:", e)
|
|
return {
|
|
charges: 0,
|
|
cooldown: CONFIG.COOLDOWN_DEFAULT,
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
// COLOR MATCHING FUNCTION - Optimized with caching
|
|
const colorCache = new Map()
|
|
|
|
function findClosestColor(targetRgb, availableColors) {
|
|
// Create cache key from RGB values
|
|
const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}`
|
|
|
|
// Check cache first
|
|
if (colorCache.has(cacheKey)) {
|
|
return colorCache.get(cacheKey)
|
|
}
|
|
|
|
let minDistance = Number.POSITIVE_INFINITY
|
|
let closestColorId = availableColors[0]?.id || 1
|
|
|
|
// Use optimized loop for better performance
|
|
for (let i = 0; i < availableColors.length; i++) {
|
|
const color = availableColors[i]
|
|
const distance = Utils.colorDistance(targetRgb, color.rgb)
|
|
if (distance < minDistance) {
|
|
minDistance = distance
|
|
closestColorId = color.id
|
|
|
|
// If perfect match, break early
|
|
if (distance === 0) break
|
|
}
|
|
}
|
|
|
|
// Cache the result for future use
|
|
colorCache.set(cacheKey, closestColorId)
|
|
|
|
// Limit cache size to prevent memory leaks
|
|
if (colorCache.size > 10000) {
|
|
const firstKey = colorCache.keys().next().value
|
|
colorCache.delete(firstKey)
|
|
}
|
|
|
|
return closestColorId
|
|
}
|
|
|
|
// UI UPDATE FUNCTIONS (declared early to avoid reference errors)
|
|
let updateUI = () => {}
|
|
let updateStats = () => {}
|
|
let updateDataButtons = () => {}
|
|
|
|
// --- START: Color Palette Functions ---
|
|
function updateActiveColorPalette() {
|
|
state.activeColorPalette = [];
|
|
const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active');
|
|
if (activeSwatches) {
|
|
activeSwatches.forEach(swatch => {
|
|
const rgb = swatch.getAttribute('data-rgb').split(',').map(Number);
|
|
state.activeColorPalette.push(rgb);
|
|
});
|
|
}
|
|
// If the resize dialog is open, update its preview
|
|
if (document.querySelector('.resize-container')?.style.display === 'block') {
|
|
_updateResizePreview();
|
|
}
|
|
}
|
|
|
|
function toggleAllColors(select, isPaid) {
|
|
const selector = isPaid ? '.wplace-color-swatch.paid' : '.wplace-color-swatch:not(.paid)';
|
|
const swatches = document.querySelectorAll(selector);
|
|
if (swatches) {
|
|
swatches.forEach(swatch => {
|
|
swatch.classList.toggle('active', select);
|
|
});
|
|
}
|
|
updateActiveColorPalette();
|
|
}
|
|
|
|
function initializeColorPalette(container) {
|
|
const freeContainer = container.querySelector('#colors-free');
|
|
const paidContainer = container.querySelector('#colors-paid');
|
|
if (!freeContainer || !paidContainer) return;
|
|
|
|
freeContainer.innerHTML = '';
|
|
paidContainer.innerHTML = '';
|
|
|
|
// Create a unique set of colors to avoid duplicates
|
|
const uniqueColors = [...new Set(CONFIG.COLOR_PALETTE.map(JSON.stringify))].map(JSON.parse);
|
|
|
|
uniqueColors.forEach(rgb => {
|
|
const key = rgb.join(',');
|
|
const name = CONFIG.COLOR_NAMES[key] || `rgb(${key})`;
|
|
const isPaid = CONFIG.PAID_COLORS.has(key);
|
|
|
|
const colorItem = Utils.createElement('div', { className: 'wplace-color-item' });
|
|
const swatch = Utils.createElement('button', {
|
|
className: `wplace-color-swatch ${isPaid ? 'paid' : ''}`,
|
|
title: name,
|
|
'data-rgb': key,
|
|
});
|
|
swatch.style.backgroundColor = `rgb(${key})`;
|
|
|
|
const nameLabel = Utils.createElement('span', { className: 'wplace-color-item-name' }, name);
|
|
|
|
// Default state: free are active, paid are not
|
|
if (!isPaid) {
|
|
swatch.classList.add('active');
|
|
}
|
|
|
|
swatch.addEventListener('click', () => {
|
|
swatch.classList.toggle('active');
|
|
updateActiveColorPalette();
|
|
});
|
|
|
|
colorItem.appendChild(swatch);
|
|
colorItem.appendChild(nameLabel);
|
|
|
|
if (isPaid) {
|
|
paidContainer.appendChild(colorItem);
|
|
} else {
|
|
freeContainer.appendChild(colorItem);
|
|
}
|
|
});
|
|
|
|
// Add event listeners for master buttons
|
|
container.querySelector('#selectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(true, false));
|
|
container.querySelector('#unselectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(false, false));
|
|
container.querySelector('#selectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(true, true));
|
|
container.querySelector('#unselectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(false, true));
|
|
|
|
// Set the initial state
|
|
updateActiveColorPalette();
|
|
}
|
|
// --- END: Color Palette Functions ---
|
|
|
|
// Refactored function to handle color checking
|
|
async function checkAndSetColors() {
|
|
// If colors are already checked, no need to do it again.
|
|
if (state.colorsChecked) return true;
|
|
|
|
try {
|
|
updateUI("checkingColors", "default");
|
|
state.availableColors = Utils.extractAvailableColors();
|
|
|
|
if (state.availableColors.length === 0) {
|
|
Utils.showAlert(Utils.t("noColorsFound"), "error");
|
|
updateUI("noColorsFound", "error");
|
|
return false; // Indicate failure
|
|
}
|
|
|
|
state.colorsChecked = true;
|
|
|
|
// Enable other buttons now that we're initialized
|
|
const selectPosBtn = document.getElementById('selectPosBtn');
|
|
if(selectPosBtn) selectPosBtn.disabled = false;
|
|
|
|
updateUI("colorsFound", "success", {
|
|
count: state.availableColors.length,
|
|
});
|
|
updateStats();
|
|
return true; // Indicate success
|
|
} catch (e) {
|
|
console.error("Error checking colors:", e);
|
|
updateUI("imageError", "error");
|
|
return false; // Indicate failure
|
|
}
|
|
}
|
|
|
|
|
|
async function createUI() {
|
|
await detectLanguage()
|
|
|
|
// Clean up existing UI elements to prevent duplicates
|
|
const existingContainer = document.getElementById("wplace-image-bot-container")
|
|
const existingStats = document.getElementById("wplace-stats-container")
|
|
const existingSettings = document.getElementById("wplace-settings-container")
|
|
const existingResizeContainer = document.querySelector(".resize-container")
|
|
const existingResizeOverlay = document.querySelector(".resize-overlay")
|
|
|
|
if (existingContainer) existingContainer.remove()
|
|
if (existingStats) existingStats.remove()
|
|
if (existingSettings) existingSettings.remove()
|
|
if (existingResizeContainer) existingResizeContainer.remove()
|
|
if (existingResizeOverlay) existingResizeOverlay.remove()
|
|
|
|
loadThemePreference()
|
|
loadLanguagePreference()
|
|
|
|
const theme = getCurrentTheme()
|
|
|
|
const fontAwesome = document.createElement("link")
|
|
fontAwesome.rel = "stylesheet"
|
|
fontAwesome.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
|
document.head.appendChild(fontAwesome)
|
|
|
|
if (theme.fontFamily.includes("Press Start 2P")) {
|
|
const googleFonts = document.createElement("link")
|
|
googleFonts.rel = "stylesheet"
|
|
googleFonts.href = "https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
|
|
document.head.appendChild(googleFonts)
|
|
}
|
|
|
|
const style = document.createElement("style")
|
|
style.setAttribute("data-wplace-theme", "true")
|
|
|
|
style.textContent = `
|
|
${
|
|
theme.animations.glow
|
|
? `
|
|
@keyframes neonGlow {
|
|
0%, 100% {
|
|
text-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor;
|
|
}
|
|
50% {
|
|
text-shadow: 0 0 2px currentColor, 0 0 5px currentColor, 0 0 8px currentColor;
|
|
}
|
|
}`
|
|
: ""
|
|
}
|
|
|
|
${
|
|
theme.animations.pixelBlink
|
|
? `
|
|
@keyframes pixelBlink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0.7; }
|
|
}`
|
|
: ""
|
|
}
|
|
|
|
${
|
|
theme.animations.scanline
|
|
? `
|
|
@keyframes scanline {
|
|
0% { transform: translateY(-100%); }
|
|
100% { transform: translateY(400px); }
|
|
}`
|
|
: ""
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0.7); }
|
|
70% { box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0); }
|
|
}
|
|
@keyframes slideIn {
|
|
from { transform: translateY(-10px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
@keyframes shimmer {
|
|
0% { transform: translateX(-100%); }
|
|
100% { transform: translateX(100%); }
|
|
}
|
|
|
|
#wplace-image-bot-container {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
width: ${CONFIG.currentTheme === "Neon Retro" ? "280px" : "280px"};
|
|
max-height: calc(100vh - 40px);
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.primary} 0%, #1a1a1a 100%)`
|
|
: theme.primary
|
|
};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text};
|
|
border-radius: ${theme.borderRadius};
|
|
padding: 0;
|
|
box-shadow: ${theme.boxShadow};
|
|
z-index: 9998;
|
|
font-family: ${theme.fontFamily};
|
|
color: ${theme.text};
|
|
animation: slideIn 0.4s ease-out;
|
|
overflow-y: auto; /* Allow scrolling for main panel */
|
|
overflow-x: hidden;
|
|
${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""}
|
|
transition: all 0.3s ease;
|
|
user-select: none;
|
|
${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""}
|
|
}
|
|
|
|
${
|
|
theme.animations.scanline
|
|
? `
|
|
#wplace-image-bot-container::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, transparent, ${theme.neon}, transparent);
|
|
animation: scanline 3s linear infinite;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
}`
|
|
: ""
|
|
}
|
|
|
|
${
|
|
CONFIG.currentTheme === "Neon Retro"
|
|
? `
|
|
#wplace-image-bot-container::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background:
|
|
repeating-linear-gradient(
|
|
0deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0, 255, 65, 0.03) 2px,
|
|
rgba(0, 255, 65, 0.03) 4px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}`
|
|
: ""
|
|
}
|
|
|
|
#wplace-image-bot-container.wplace-dragging {
|
|
transition: none;
|
|
box-shadow: 0 12px 40px rgba(0,0,0,0.8), 0 0 0 2px rgba(255,255,255,0.2);
|
|
transform: scale(1.02);
|
|
z-index: 9999;
|
|
}
|
|
#wplace-image-bot-container.wplace-minimized {
|
|
width: 200px;
|
|
height: auto;
|
|
overflow: hidden;
|
|
}
|
|
#wplace-image-bot-container.wplace-compact {
|
|
width: 240px;
|
|
}
|
|
|
|
/* Stats Container */
|
|
#wplace-stats-container {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 20px;
|
|
width: ${CONFIG.currentTheme === "Neon Retro" ? "280px" : "280px"};
|
|
max-height: calc(100vh - 40px);
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.primary} 0%, #1a1a1a 100%)`
|
|
: theme.primary
|
|
};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text};
|
|
border-radius: ${theme.borderRadius};
|
|
padding: 0;
|
|
box-shadow: ${theme.boxShadow};
|
|
z-index: 9997;
|
|
font-family: ${theme.fontFamily};
|
|
color: ${theme.text};
|
|
animation: slideIn 0.4s ease-out;
|
|
overflow: hidden;
|
|
${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""}
|
|
transition: all 0.3s ease;
|
|
user-select: none;
|
|
${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""}
|
|
}
|
|
|
|
/* FIX: Disable transition during drag to prevent lag */
|
|
#wplace-stats-container.wplace-dragging {
|
|
transition: none;
|
|
}
|
|
|
|
.wplace-header {
|
|
padding: ${CONFIG.currentTheme === "Neon Retro" ? "8px 12px" : "8px 12px"};
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.secondary} 0%, #2a2a2a 100%)`
|
|
: theme.secondary
|
|
};
|
|
color: ${theme.highlight};
|
|
font-size: ${CONFIG.currentTheme === "Neon Retro" ? "11px" : "13px"};
|
|
font-weight: ${CONFIG.currentTheme === "Neon Retro" ? "normal" : "700"};
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
cursor: move;
|
|
user-select: none;
|
|
border-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "2px" : "1px"} solid ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.text};
|
|
${CONFIG.currentTheme === "Classic Autobot" ? "text-shadow: 0 1px 2px rgba(0,0,0,0.5);" : "text-transform: uppercase; letter-spacing: 1px;"}
|
|
transition: background 0.2s ease;
|
|
position: relative;
|
|
z-index: 2;
|
|
${theme.animations.glow ? "animation: neonGlow 2s ease-in-out infinite alternate;" : ""}
|
|
}
|
|
|
|
.wplace-header-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: ${CONFIG.currentTheme === "Neon Retro" ? "6px" : "6px"};
|
|
}
|
|
|
|
.wplace-header-controls {
|
|
display: flex;
|
|
gap: ${CONFIG.currentTheme === "Neon Retro" ? "6px" : "6px"};
|
|
}
|
|
|
|
.wplace-header-btn {
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.accent};
|
|
border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "none"};
|
|
color: ${theme.text};
|
|
cursor: pointer;
|
|
border-radius: ${CONFIG.currentTheme === "Classic Autobot" ? "4px" : "0"};
|
|
width: ${CONFIG.currentTheme === "Classic Autobot" ? "18px" : "auto"};
|
|
height: ${CONFIG.currentTheme === "Classic Autobot" ? "18px" : "auto"};
|
|
padding: ${CONFIG.currentTheme === "Neon Retro" ? "4px 6px" : "0"};
|
|
font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "10px"};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s;
|
|
font-family: ${theme.fontFamily};
|
|
${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""}
|
|
}
|
|
.wplace-header-btn:hover {
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.text : theme.primary};
|
|
transform: ${CONFIG.currentTheme === "Classic Autobot" ? "scale(1.1)" : "none"};
|
|
${CONFIG.currentTheme === "Neon Retro" ? `box-shadow: 0 0 10px ${theme.text};` : ""}
|
|
}
|
|
|
|
.wplace-content {
|
|
padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "12px"};
|
|
display: block;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
.wplace-content.wplace-hidden {
|
|
display: none;
|
|
}
|
|
|
|
.wplace-status-section {
|
|
margin-bottom: 12px;
|
|
padding: 8px;
|
|
background: rgba(255,255,255,0.03);
|
|
border-radius: ${theme.borderRadius};
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
|
|
.wplace-section {
|
|
margin-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "12px"};
|
|
padding: 12px;
|
|
background: rgba(255,255,255,0.03);
|
|
border-radius: ${theme.borderRadius};
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
|
|
.wplace-section-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
color: ${theme.highlight};
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.wplace-controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.wplace-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
.wplace-row.single {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.wplace-btn {
|
|
padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px 8px" : "8px 12px"};
|
|
border: ${CONFIG.currentTheme === "Neon Retro" ? "2px solid" : "none"};
|
|
border-radius: ${theme.borderRadius};
|
|
font-weight: ${CONFIG.currentTheme === "Neon Retro" ? "normal" : "500"};
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "6px"};
|
|
font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"};
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
font-family: ${theme.fontFamily};
|
|
${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px; image-rendering: pixelated;" : ""}
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.accent} 0%, #4a4a4a 100%)`
|
|
: theme.accent
|
|
};
|
|
${CONFIG.currentTheme === "Classic Autobot" ? "border: 1px solid rgba(255,255,255,0.1);" : ""}
|
|
}
|
|
|
|
${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `
|
|
.wplace-btn::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
|
transition: left 0.5s ease;
|
|
}
|
|
.wplace-btn:hover:not(:disabled)::before {
|
|
left: 100%;
|
|
}`
|
|
: `
|
|
.wplace-btn::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
transition: left 0.5s;
|
|
}
|
|
.wplace-btn:hover::before {
|
|
left: 100%;
|
|
}`
|
|
}
|
|
|
|
.wplace-btn:hover:not(:disabled) {
|
|
transform: ${CONFIG.currentTheme === "Classic Autobot" ? "translateY(-1px)" : "none"};
|
|
box-shadow: ${
|
|
CONFIG.currentTheme === "Classic Autobot" ? "0 4px 12px rgba(0,0,0,0.4)" : "0 0 15px currentColor"
|
|
};
|
|
${theme.animations.pixelBlink ? "animation: pixelBlink 0.5s infinite;" : ""}
|
|
}
|
|
.wplace-btn:active:not(:disabled) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.wplace-btn-primary {
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.accent} 0%, #6a5acd 100%)`
|
|
: theme.accent
|
|
};
|
|
color: ${theme.text};
|
|
${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.text};` : ""}
|
|
}
|
|
.wplace-btn-upload {
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.secondary} 0%, #4a4a4a 100%)`
|
|
: theme.purple
|
|
};
|
|
color: ${theme.text};
|
|
${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `border: 1px dashed ${theme.highlight};`
|
|
: `border-color: ${theme.text}; border-style: dashed;`
|
|
}
|
|
}
|
|
.wplace-btn-start {
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.success} 0%, #228b22 100%)`
|
|
: theme.success
|
|
};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary};
|
|
${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.success};` : ""}
|
|
}
|
|
.wplace-btn-stop {
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.error} 0%, #dc143c 100%)`
|
|
: theme.error
|
|
};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.text};
|
|
${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.error};` : ""}
|
|
}
|
|
.wplace-btn-select {
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.highlight} 0%, #9370db 100%)`
|
|
: theme.highlight
|
|
};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary};
|
|
${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.highlight};` : ""}
|
|
}
|
|
.wplace-btn-file {
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? "linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%)"
|
|
: theme.warning
|
|
};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary};
|
|
${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.warning};` : ""}
|
|
}
|
|
.wplace-btn:disabled {
|
|
opacity: ${CONFIG.currentTheme === "Classic Autobot" ? "0.5" : "0.3"};
|
|
cursor: not-allowed;
|
|
transform: none !important;
|
|
${theme.animations.pixelBlink ? "animation: none !important;" : ""}
|
|
box-shadow: none !important;
|
|
}
|
|
.wplace-btn:disabled::before {
|
|
display: none;
|
|
}
|
|
|
|
.wplace-stats {
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.03)" : theme.secondary};
|
|
padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "8px"};
|
|
border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "1px solid rgba(255,255,255,0.1)"};
|
|
border-radius: ${theme.borderRadius};
|
|
margin-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "15px" : "8px"};
|
|
${CONFIG.currentTheme === "Neon Retro" ? "box-shadow: inset 0 0 10px rgba(0, 255, 65, 0.1);" : ""}
|
|
}
|
|
|
|
.wplace-stat-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: ${CONFIG.currentTheme === "Neon Retro" ? "6px 0" : "4px 0"};
|
|
font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"};
|
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""}
|
|
}
|
|
.wplace-stat-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.wplace-stat-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
opacity: 0.9;
|
|
font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "10px"};
|
|
}
|
|
.wplace-stat-value {
|
|
font-weight: 600;
|
|
color: ${theme.highlight};
|
|
}
|
|
|
|
.wplace-progress {
|
|
width: 100%;
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(0,0,0,0.3)" : theme.secondary};
|
|
border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "1px solid rgba(255,255,255,0.1)"};
|
|
border-radius: ${theme.borderRadius};
|
|
margin: ${CONFIG.currentTheme === "Neon Retro" ? "10px 0" : "8px 0"};
|
|
overflow: hidden;
|
|
height: ${CONFIG.currentTheme === "Neon Retro" ? "16px" : "6px"};
|
|
position: relative;
|
|
}
|
|
|
|
${
|
|
CONFIG.currentTheme === "Neon Retro"
|
|
? `
|
|
.wplace-progress::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background:
|
|
repeating-linear-gradient(
|
|
45deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0, 255, 65, 0.1) 2px,
|
|
rgba(0, 255, 65, 0.1) 4px
|
|
);
|
|
pointer-events: none;
|
|
}`
|
|
: ""
|
|
}
|
|
|
|
.wplace-progress-bar {
|
|
height: ${CONFIG.currentTheme === "Neon Retro" ? "100%" : "6px"};
|
|
background: ${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `linear-gradient(135deg, ${theme.highlight} 0%, #9370db 100%)`
|
|
: `linear-gradient(90deg, ${theme.success}, ${theme.neon})`
|
|
};
|
|
transition: width ${CONFIG.currentTheme === "Neon Retro" ? "0.3s" : "0.5s"} ease;
|
|
position: relative;
|
|
${CONFIG.currentTheme === "Neon Retro" ? `box-shadow: 0 0 10px ${theme.success};` : ""}
|
|
}
|
|
|
|
${
|
|
CONFIG.currentTheme === "Classic Autobot"
|
|
? `
|
|
.wplace-progress-bar::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
|
animation: shimmer 2s infinite;
|
|
}`
|
|
: `
|
|
.wplace-progress-bar::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 4px;
|
|
height: 100%;
|
|
background: ${theme.text};
|
|
animation: pixelBlink 1s infinite;
|
|
}`
|
|
}
|
|
|
|
.wplace-status {
|
|
padding: ${CONFIG.currentTheme === "Neon Retro" ? "10px" : "6px"};
|
|
border: ${CONFIG.currentTheme === "Neon Retro" ? "2px solid" : "1px solid"};
|
|
border-radius: ${theme.borderRadius};
|
|
text-align: center;
|
|
font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"};
|
|
${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""}
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.status-default {
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.accent};
|
|
border-color: ${theme.text};
|
|
color: ${theme.text};
|
|
}
|
|
.status-success {
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(0, 255, 0, 0.1)" : theme.success};
|
|
border-color: ${theme.success};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.success : theme.primary};
|
|
box-shadow: 0 0 15px ${theme.success};
|
|
}
|
|
.status-error {
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255, 0, 0, 0.1)" : theme.error};
|
|
border-color: ${theme.error};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.error : theme.text};
|
|
box-shadow: 0 0 15px ${theme.error};
|
|
${theme.animations.pixelBlink ? "animation: pixelBlink 0.5s infinite;" : ""}
|
|
}
|
|
.status-warning {
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255, 165, 0, 0.1)" : theme.warning};
|
|
border-color: ${theme.warning};
|
|
color: ${CONFIG.currentTheme === "Classic Autobot" ? "orange" : theme.primary};
|
|
box-shadow: 0 0 15px ${theme.warning};
|
|
}
|
|
|
|
.resize-container {
|
|
display: none;
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: ${theme.primary};
|
|
padding: 20px;
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.text};
|
|
border-radius: ${theme.borderRadius};
|
|
z-index: 10000;
|
|
box-shadow: ${
|
|
CONFIG.currentTheme === "Classic Autobot" ? "0 0 20px rgba(0,0,0,0.5)" : "0 0 30px rgba(0, 255, 65, 0.5)"
|
|
};
|
|
width: 90%;
|
|
max-width: 700px; /* Increased width */
|
|
max-height: 90%;
|
|
overflow: auto;
|
|
font-family: ${theme.fontFamily};
|
|
}
|
|
|
|
.resize-preview-wrapper {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border: 1px solid ${theme.accent};
|
|
background: rgba(0,0,0,0.2);
|
|
margin: 15px 0;
|
|
height: 300px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.resize-preview {
|
|
max-width: none; /* Allow image to exceed wrapper for zoom */
|
|
transition: transform 0.1s ease;
|
|
image-rendering: pixelated;
|
|
image-rendering: -moz-crisp-edges;
|
|
image-rendering: crisp-edges;
|
|
}
|
|
|
|
.resize-controls {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 15px;
|
|
align-items: center;
|
|
}
|
|
|
|
.resize-controls label {
|
|
font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "12px"};
|
|
${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""}
|
|
color: ${theme.text};
|
|
}
|
|
|
|
.resize-slider {
|
|
width: 100%;
|
|
height: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "4px"};
|
|
background: ${CONFIG.currentTheme === "Classic Autobot" ? "#ccc" : theme.secondary};
|
|
border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "none"};
|
|
border-radius: ${theme.borderRadius};
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
${
|
|
CONFIG.currentTheme === "Neon Retro"
|
|
? `
|
|
.resize-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: ${theme.highlight};
|
|
border: 2px solid ${theme.text};
|
|
border-radius: 0;
|
|
cursor: pointer;
|
|
box-shadow: 0 0 5px ${theme.highlight};
|
|
}
|
|
|
|
.resize-slider::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: ${theme.highlight};
|
|
border: 2px solid ${theme.text};
|
|
border-radius: 0;
|
|
cursor: pointer;
|
|
box-shadow: 0 0 5px ${theme.highlight};
|
|
}`
|
|
: ""
|
|
}
|
|
|
|
.resize-zoom-controls {
|
|
grid-column: 1 / -1; /* Span full width */
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.resize-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.resize-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
z-index: 9999;
|
|
display: none;
|
|
}
|
|
/* --- START: Color Palette Styles --- */
|
|
.wplace-color-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); /* Wider columns for name */
|
|
gap: 10px;
|
|
padding-top: 8px;
|
|
}
|
|
.wplace-color-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.wplace-color-item-name {
|
|
font-size: 9px;
|
|
color: #ccc;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
width: 100%;
|
|
}
|
|
.wplace-color-swatch {
|
|
width: 22px;
|
|
height: 22px;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: transform 0.1s ease, box-shadow 0.2s ease;
|
|
position: relative;
|
|
margin: 0 auto;
|
|
}
|
|
.wplace-color-swatch.paid {
|
|
border-color: gold;
|
|
}
|
|
.wplace-color-swatch:hover {
|
|
transform: scale(1.1);
|
|
z-index: 1;
|
|
}
|
|
.wplace-color-swatch:not(.active) {
|
|
opacity: 0.3;
|
|
filter: grayscale(80%);
|
|
}
|
|
.wplace-color-swatch.active::after {
|
|
content: '✔';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
text-shadow: 0 0 3px black;
|
|
}
|
|
.wplace-color-divider {
|
|
border: none;
|
|
height: 1px;
|
|
background: rgba(255,255,255,0.1);
|
|
margin: 8px 0;
|
|
}
|
|
/* --- END: Color Palette Styles --- */
|
|
|
|
${
|
|
CONFIG.currentTheme === "Neon Retro"
|
|
? `
|
|
/* Retro checkbox styling */
|
|
input[type="checkbox"] {
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid ${theme.text};
|
|
background: ${theme.secondary};
|
|
margin-right: 8px;
|
|
position: relative;
|
|
cursor: pointer;
|
|
}
|
|
|
|
input[type="checkbox"]:checked {
|
|
background: ${theme.success};
|
|
}
|
|
|
|
input[type="checkbox"]:checked::after {
|
|
content: '✓';
|
|
position: absolute;
|
|
top: -2px;
|
|
left: 1px;
|
|
color: ${theme.primary};
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Icon styling for retro feel */
|
|
.fas, .fa {
|
|
filter: drop-shadow(0 0 3px currentColor);
|
|
}
|
|
|
|
/* Speed Control Styles */
|
|
.wplace-speed-control {
|
|
margin-top: 12px;
|
|
padding: 12px;
|
|
background: ${theme.secondary};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
|
|
border-radius: ${theme.borderRadius};
|
|
backdrop-filter: ${theme.backdropFilter};
|
|
}
|
|
|
|
.wplace-speed-label {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
color: ${theme.text};
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wplace-speed-label i {
|
|
margin-right: 6px;
|
|
color: ${theme.highlight};
|
|
}
|
|
|
|
.wplace-speed-slider-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.wplace-speed-slider {
|
|
flex: 1;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background: ${theme.primary};
|
|
outline: none;
|
|
cursor: pointer;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
}
|
|
|
|
.wplace-speed-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: ${theme.highlight};
|
|
cursor: pointer;
|
|
border: 2px solid ${theme.text};
|
|
box-shadow: ${theme.boxShadow};
|
|
}
|
|
|
|
.wplace-speed-slider::-moz-range-thumb {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: ${theme.highlight};
|
|
cursor: pointer;
|
|
border: 2px solid ${theme.text};
|
|
box-shadow: ${theme.boxShadow};
|
|
}
|
|
|
|
.wplace-speed-display {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
min-width: 90px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
#speedValue {
|
|
color: ${theme.highlight};
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.wplace-speed-unit {
|
|
color: ${theme.text};
|
|
font-size: 11px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Settings Window Styles */
|
|
#wplace-settings-container {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 10001;
|
|
min-width: 400px;
|
|
max-width: 500px;
|
|
background: ${theme.primary};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
|
|
border-radius: ${theme.borderRadius};
|
|
box-shadow: ${theme.boxShadow};
|
|
backdrop-filter: ${theme.backdropFilter};
|
|
}
|
|
|
|
.wplace-settings {
|
|
padding: 16px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.wplace-setting-section {
|
|
margin-bottom: 20px;
|
|
padding: 12px;
|
|
background: ${theme.secondary};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
|
|
border-radius: ${theme.borderRadius};
|
|
}
|
|
|
|
.wplace-setting-title {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
color: ${theme.text};
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wplace-setting-title i {
|
|
margin-right: 8px;
|
|
color: ${theme.highlight};
|
|
}
|
|
|
|
.wplace-setting-content {
|
|
color: ${theme.text};
|
|
}
|
|
|
|
.wplace-section {
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background: ${theme.secondary};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
|
|
border-radius: ${theme.borderRadius};
|
|
}
|
|
|
|
.wplace-section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
color: ${theme.text};
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wplace-section-title i {
|
|
margin-right: 8px;
|
|
color: ${theme.highlight};
|
|
}
|
|
|
|
.wplace-speed-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.wplace-slider {
|
|
flex: 1;
|
|
height: 6px;
|
|
background: ${theme.accent};
|
|
border-radius: 3px;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
.wplace-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
background: ${theme.highlight};
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: 2px solid ${theme.primary};
|
|
}
|
|
|
|
.wplace-speed-display {
|
|
background: ${theme.accent};
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
color: ${theme.text};
|
|
font-weight: 600;
|
|
min-width: 80px;
|
|
text-align: center;
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.highlight};
|
|
}
|
|
|
|
.wplace-select {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
background: ${theme.secondary};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
|
|
border-radius: ${theme.borderRadius};
|
|
color: ${theme.text};
|
|
font-size: 14px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.wplace-select:focus {
|
|
outline: none;
|
|
border-color: ${theme.highlight};
|
|
}
|
|
|
|
.wplace-description {
|
|
color: ${theme.text};
|
|
font-size: 12px;
|
|
opacity: 0.8;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.wplace-theme-custom {
|
|
margin-top: 15px;
|
|
padding: 15px;
|
|
background: ${theme.accent};
|
|
border-radius: ${theme.borderRadius};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.highlight};
|
|
}
|
|
|
|
.wplace-custom-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.wplace-custom-label {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
color: ${theme.text};
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wplace-custom-label i {
|
|
margin-right: 8px;
|
|
color: ${theme.highlight};
|
|
width: 16px;
|
|
}
|
|
|
|
.wplace-color-input-group {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.wplace-color-input {
|
|
width: 50px;
|
|
height: 30px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
}
|
|
|
|
.wplace-color-text {
|
|
flex: 1;
|
|
padding: 6px 10px;
|
|
background: ${theme.secondary};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
|
|
border-radius: 4px;
|
|
color: ${theme.text};
|
|
font-size: 12px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.wplace-animation-controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.wplace-checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: ${theme.text};
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wplace-checkbox-label input[type="checkbox"] {
|
|
accent-color: ${theme.highlight};
|
|
}
|
|
|
|
.wplace-slider-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.wplace-slider-container .wplace-slider {
|
|
flex: 1;
|
|
}
|
|
|
|
.wplace-slider-container span {
|
|
color: ${theme.text};
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
min-width: 40px;
|
|
}
|
|
|
|
.wplace-custom-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
border-top: 1px solid ${theme.accent};
|
|
padding-top: 15px;
|
|
}
|
|
|
|
.wplace-btn-secondary {
|
|
background: ${theme.accent};
|
|
color: ${theme.text};
|
|
border: ${theme.borderWidth} ${theme.borderStyle} ${theme.highlight};
|
|
}
|
|
|
|
.wplace-btn-secondary:hover {
|
|
background: ${theme.secondary};
|
|
}`
|
|
: ""
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|
|
|
|
const container = document.createElement("div")
|
|
container.id = "wplace-image-bot-container"
|
|
container.innerHTML = `
|
|
<div class="wplace-header">
|
|
<div class="wplace-header-title">
|
|
<i class="fas fa-image"></i>
|
|
<span>${Utils.t("title")}</span>
|
|
</div>
|
|
<div class="wplace-header-controls">
|
|
<button id="settingsBtn" class="wplace-header-btn" title="${Utils.t("settings")}">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
<button id="statsBtn" class="wplace-header-btn" title="Show Stats">
|
|
<i class="fas fa-chart-bar"></i>
|
|
</button>
|
|
<button id="compactBtn" class="wplace-header-btn" title="Compact Mode">
|
|
<i class="fas fa-compress"></i>
|
|
</button>
|
|
<button id="minimizeBtn" class="wplace-header-btn" title="${Utils.t("minimize")}">
|
|
<i class="fas fa-minus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="wplace-content">
|
|
<!-- Status Section - Always visible -->
|
|
<div class="wplace-status-section">
|
|
<div id="statusText" class="wplace-status status-default">
|
|
${Utils.t("waitingInit")}
|
|
</div>
|
|
<div class="wplace-progress">
|
|
<div id="progressBar" class="wplace-progress-bar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Section -->
|
|
<div class="wplace-section">
|
|
<div class="wplace-section-title">🖼️ Image Management</div>
|
|
<div class="wplace-controls">
|
|
<div class="wplace-row">
|
|
<button id="uploadBtn" class="wplace-btn wplace-btn-upload">
|
|
<i class="fas fa-upload"></i>
|
|
<span>${Utils.t("uploadImage")}</span>
|
|
</button>
|
|
<button id="resizeBtn" class="wplace-btn wplace-btn-primary" disabled>
|
|
<i class="fas fa-expand"></i>
|
|
<span>${Utils.t("resizeImage")}</span>
|
|
</button>
|
|
</div>
|
|
<div class="wplace-row single">
|
|
<button id="selectPosBtn" class="wplace-btn wplace-btn-select" disabled>
|
|
<i class="fas fa-crosshairs"></i>
|
|
<span>${Utils.t("selectPosition")}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Control Section -->
|
|
<div class="wplace-section">
|
|
<div class="wplace-section-title">🎮 Painting Control</div>
|
|
<div class="wplace-controls">
|
|
<div class="wplace-row">
|
|
<button id="startBtn" class="wplace-btn wplace-btn-start" disabled>
|
|
<i class="fas fa-play"></i>
|
|
<span>${Utils.t("startPainting")}</span>
|
|
</button>
|
|
<button id="stopBtn" class="wplace-btn wplace-btn-stop" disabled>
|
|
<i class="fas fa-stop"></i>
|
|
<span>${Utils.t("stopPainting")}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Section -->
|
|
<div class="wplace-section">
|
|
<div class="wplace-section-title">💾 Data Management</div>
|
|
<div class="wplace-controls">
|
|
<div class="wplace-row">
|
|
<button id="saveBtn" class="wplace-btn wplace-btn-primary" disabled>
|
|
<i class="fas fa-save"></i>
|
|
<span>${Utils.t("saveData")}</span>
|
|
</button>
|
|
<button id="loadBtn" class="wplace-btn wplace-btn-primary">
|
|
<i class="fas fa-folder-open"></i>
|
|
<span>${Utils.t("loadData")}</span>
|
|
</button>
|
|
</div>
|
|
<div class="wplace-row">
|
|
<button id="saveToFileBtn" class="wplace-btn wplace-btn-file" disabled>
|
|
<i class="fas fa-download"></i>
|
|
<span>${Utils.t("saveToFile")}</span>
|
|
</button>
|
|
<button id="loadFromFileBtn" class="wplace-btn wplace-btn-file">
|
|
<i class="fas fa-upload"></i>
|
|
<span>${Utils.t("loadFromFile")}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
// Stats Window - Separate UI
|
|
const statsContainer = document.createElement("div")
|
|
statsContainer.id = "wplace-stats-container"
|
|
statsContainer.style.display = "none"
|
|
statsContainer.innerHTML = `
|
|
<div class="wplace-header">
|
|
<div class="wplace-header-title">
|
|
<i class="fas fa-chart-bar"></i>
|
|
<span>Painting Stats</span>
|
|
</div>
|
|
<div class="wplace-header-controls">
|
|
<button id="refreshChargesBtn" class="wplace-header-btn" title="Refresh Charges">
|
|
<i class="fas fa-sync"></i>
|
|
</button>
|
|
<button id="closeStatsBtn" class="wplace-header-btn" title="Close Stats">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="wplace-content">
|
|
<div class="wplace-stats">
|
|
<div id="statsArea">
|
|
<div class="wplace-stat-item">
|
|
<div class="wplace-stat-label"><i class="fas fa-info-circle"></i> ${Utils.t("initMessage")}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
// Modern Settings Container
|
|
const settingsContainer = document.createElement("div")
|
|
settingsContainer.id = "wplace-settings-container"
|
|
settingsContainer.style.cssText = `
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border: none;
|
|
border-radius: 16px;
|
|
padding: 0;
|
|
z-index: 10002;
|
|
display: none;
|
|
min-width: 380px;
|
|
max-width: 420px;
|
|
color: white;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1);
|
|
backdrop-filter: blur(10px);
|
|
overflow: hidden;
|
|
animation: settingsSlideIn 0.4s ease-out;
|
|
`
|
|
|
|
settingsContainer.innerHTML = `
|
|
<div class="wplace-settings-header" style="background: rgba(255,255,255,0.1); padding: 20px; border-bottom: 1px solid rgba(255,255,255,0.1); cursor: move;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<h3 style="margin: 0; color: white; font-size: 20px; font-weight: 300; display: flex; align-items: center; gap: 10px;">
|
|
<i class="fas fa-cog" style="font-size: 18px; animation: spin 2s linear infinite;"></i>
|
|
${Utils.t("settings")}
|
|
</h3>
|
|
<button id="closeSettingsBtn" style="
|
|
background: rgba(255,255,255,0.1);
|
|
color: white;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 50%;
|
|
width: 32px;
|
|
height: 32px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s ease;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
" onmouseover="this.style.background='rgba(255,255,255,0.2)'; this.style.transform='scale(1.1)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'; this.style.transform='scale(1)'">✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="padding: 25px;">
|
|
<!-- Speed Control Section -->
|
|
<div style="margin-bottom: 25px;">
|
|
<label style="display: block; margin-bottom: 12px; color: white; font-weight: 500; font-size: 16px; display: flex; align-items: center; gap: 8px;">
|
|
<i class="fas fa-tachometer-alt" style="color: #4facfe; font-size: 16px;"></i>
|
|
${Utils.t("paintingSpeed")}
|
|
</label>
|
|
<div style="background: rgba(255,255,255,0.1); border-radius: 12px; padding: 18px; border: 1px solid rgba(255,255,255,0.1);">
|
|
<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 10px;">
|
|
<input type="range" id="speedSlider" min="${CONFIG.PAINTING_SPEED.MIN}" max="${CONFIG.PAINTING_SPEED.MAX}" value="${CONFIG.PAINTING_SPEED.DEFAULT}"
|
|
style="
|
|
flex: 1;
|
|
height: 8px;
|
|
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
|
|
border-radius: 4px;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
cursor: pointer;
|
|
">
|
|
<div id="speedValue" style="
|
|
min-width: 70px;
|
|
text-align: center;
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
padding: 8px 12px;
|
|
border-radius: 8px;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
box-shadow: 0 3px 10px rgba(79, 172, 254, 0.3);
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
">${CONFIG.PAINTING_SPEED.DEFAULT} px/s</div>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; color: rgba(255,255,255,0.7); font-size: 11px; margin-top: 8px;">
|
|
<span><i class="fas fa-turtle"></i> ${CONFIG.PAINTING_SPEED.MIN}</span>
|
|
<span><i class="fas fa-rabbit"></i> ${CONFIG.PAINTING_SPEED.MAX}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Painting Speed Enable Toggle -->
|
|
<div style="margin-bottom: 25px;">
|
|
<label style="display: flex; align-items: center; gap: 8px; color: white;">
|
|
<input type="checkbox" id="enableSpeedToggle" ${CONFIG.PAINTING_SPEED_ENABLED ? 'checked' : ''} style="cursor: pointer;"/>
|
|
<span>Enable painting speed</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Theme Selection Section -->
|
|
<div style="margin-bottom: 25px;">
|
|
<label style="display: block; margin-bottom: 12px; color: white; font-weight: 500; font-size: 16px; display: flex; align-items: center; gap: 8px;">
|
|
<i class="fas fa-palette" style="color: #f093fb; font-size: 16px;"></i>
|
|
${Utils.t("themeSettings")}
|
|
</label>
|
|
<div style="background: rgba(255,255,255,0.1); border-radius: 12px; padding: 18px; border: 1px solid rgba(255,255,255,0.1);">
|
|
<select id="themeSelect" style="
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
background: rgba(255,255,255,0.15);
|
|
color: white;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
outline: none;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
font-family: inherit;
|
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
|
">
|
|
${Object.keys(CONFIG.THEMES).map(themeName =>
|
|
`<option value="${themeName}" ${CONFIG.currentTheme === themeName ? 'selected' : ''} style="background: #2d3748; color: white; padding: 10px;">${themeName}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Language Selection Section -->
|
|
<div style="margin-bottom: 15px;">
|
|
<label style="display: block; margin-bottom: 12px; color: white; font-weight: 500; font-size: 16px; display: flex; align-items: center; gap: 8px;">
|
|
<i class="fas fa-globe" style="color: #ffeaa7; font-size: 16px;"></i>
|
|
${Utils.t("language")}
|
|
</label>
|
|
<div style="background: rgba(255,255,255,0.1); border-radius: 12px; padding: 18px; border: 1px solid rgba(255,255,255,0.1);">
|
|
<select id="languageSelect" style="
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
background: rgba(255,255,255,0.15);
|
|
color: white;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
outline: none;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
font-family: inherit;
|
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
|
">
|
|
<option value="vi" ${state.language === 'vi' ? 'selected' : ''} style="background: #2d3748; color: white;">🇻🇳 Tiếng Việt</option>
|
|
<option value="ru" ${state.language === 'ru' ? 'selected' : ''} style="background: #2d3748; color: white;">🇷🇺 Русский</option>
|
|
<option value="en" ${state.language === 'en' ? 'selected' : ''} style="background: #2d3748; color: white;">🇺🇸 English</option>
|
|
<option value="pt" ${state.language === 'pt' ? 'selected' : ''} style="background: #2d3748; color: white;">🇧🇷 Português</option>
|
|
<option value="fr" ${state.language === 'fr' ? 'selected' : ''} style="background: #2d3748; color: white;">🇫🇷 Français</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@keyframes settingsSlideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translate(-50%, -50%) scale(0.9);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translate(-50%, -50%) scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes settingsFadeOut {
|
|
from {
|
|
opacity: 1;
|
|
transform: translate(-50%, -50%) scale(1);
|
|
}
|
|
to {
|
|
opacity: 0;
|
|
transform: translate(-50%, -50%) scale(0.9);
|
|
}
|
|
}
|
|
|
|
#speedSlider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
box-shadow: 0 3px 6px rgba(0,0,0,0.3), 0 0 0 2px #4facfe;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
#speedSlider::-webkit-slider-thumb:hover {
|
|
transform: scale(1.2);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.4), 0 0 0 3px #4facfe;
|
|
}
|
|
|
|
#speedSlider::-moz-range-thumb {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
box-shadow: 0 3px 6px rgba(0,0,0,0.3), 0 0 0 2px #4facfe;
|
|
cursor: pointer;
|
|
border: none;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
#themeSelect:hover, #languageSelect:hover {
|
|
border-color: rgba(255,255,255,0.4);
|
|
background: rgba(255,255,255,0.2);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
#themeSelect:focus, #languageSelect:focus {
|
|
border-color: #4facfe;
|
|
box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3);
|
|
}
|
|
|
|
#themeSelect option, #languageSelect option {
|
|
background: #2d3748;
|
|
color: white;
|
|
padding: 10px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
#themeSelect option:hover, #languageSelect option:hover {
|
|
background: #4a5568;
|
|
}
|
|
|
|
/* Dragging state styles */
|
|
.wplace-dragging {
|
|
opacity: 0.9;
|
|
box-shadow: 0 30px 60px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.2);
|
|
transition: none;
|
|
}
|
|
|
|
.wplace-settings-header:hover {
|
|
background: rgba(255,255,255,0.15) !important;
|
|
}
|
|
|
|
.wplace-settings-header:active {
|
|
background: rgba(255,255,255,0.2) !important;
|
|
}
|
|
</style>
|
|
`
|
|
|
|
const resizeContainer = document.createElement("div")
|
|
resizeContainer.className = "resize-container"
|
|
resizeContainer.innerHTML = `
|
|
<h3 style="margin-top: 0; color: ${theme.text}">${Utils.t("resizeImage")}</h3>
|
|
<div class="resize-controls">
|
|
<label>
|
|
${Utils.t("width")}: <span id="widthValue">0</span>px
|
|
<input type="range" id="widthSlider" class="resize-slider" min="10" max="500" value="100">
|
|
</label>
|
|
<label>
|
|
${Utils.t("height")}: <span id="heightValue">0</span>px
|
|
<input type="range" id="heightSlider" class="resize-slider" min="10" max="500" value="100">
|
|
</label>
|
|
<label style="display: flex; align-items: center;">
|
|
<input type="checkbox" id="keepAspect" checked>
|
|
Keep Aspect Ratio
|
|
</label>
|
|
<label style="display: flex; align-items: center;">
|
|
<input type="checkbox" id="paintWhiteToggle" checked>
|
|
Paint White Pixels
|
|
</label>
|
|
<div class="resize-zoom-controls">
|
|
<i class="fas fa-search-minus"></i>
|
|
<input type="range" id="zoomSlider" class="resize-slider" min="1" max="10" value="1" step="0.1">
|
|
<i class="fas fa-search-plus"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="resize-preview-wrapper">
|
|
<img id="resizePreview" class="resize-preview" src="" alt="Resized image preview will appear here.">
|
|
</div>
|
|
|
|
<!-- START: Moved Color Palette -->
|
|
<div class="wplace-section" id="color-palette-section" style="margin-top: 15px;">
|
|
<div class="wplace-section-title">
|
|
<i class="fas fa-palette"></i> Color Palette
|
|
</div>
|
|
<div class="wplace-controls">
|
|
<div class="wplace-row">
|
|
<button id="selectAllFreeBtn" class="wplace-btn">All Free</button>
|
|
<button id="unselectAllFreeBtn" class="wplace-btn">None Free</button>
|
|
</div>
|
|
<div id="colors-free" class="wplace-color-grid"></div>
|
|
<hr class="wplace-color-divider">
|
|
<div class="wplace-row">
|
|
<button id="selectAllPaidBtn" class="wplace-btn">All Paid</button>
|
|
<button id="unselectAllPaidBtn" class="wplace-btn">None Paid</button>
|
|
</div>
|
|
<div id="colors-paid" class="wplace-color-grid"></div>
|
|
</div>
|
|
</div>
|
|
<!-- END: Moved Color Palette -->
|
|
|
|
<div class="resize-buttons">
|
|
<button id="downloadPreviewBtn" class="wplace-btn wplace-btn-primary">
|
|
<i class="fas fa-download"></i>
|
|
<span>Download Preview</span>
|
|
</button>
|
|
<button id="confirmResize" class="wplace-btn wplace-btn-start">
|
|
<i class="fas fa-check"></i>
|
|
<span>Apply</span>
|
|
</button>
|
|
<button id="cancelResize" class="wplace-btn wplace-btn-stop">
|
|
<i class="fas fa-times"></i>
|
|
<span>Cancel</span>
|
|
</button>
|
|
</div>
|
|
`
|
|
|
|
const resizeOverlay = document.createElement("div")
|
|
resizeOverlay.className = "resize-overlay"
|
|
|
|
document.body.appendChild(container)
|
|
document.body.appendChild(resizeOverlay)
|
|
document.body.appendChild(resizeContainer)
|
|
document.body.appendChild(statsContainer)
|
|
document.body.appendChild(settingsContainer)
|
|
|
|
// Query all UI elements after appending to DOM
|
|
const uploadBtn = container.querySelector("#uploadBtn")
|
|
const resizeBtn = container.querySelector("#resizeBtn")
|
|
const selectPosBtn = container.querySelector("#selectPosBtn")
|
|
const startBtn = container.querySelector("#startBtn")
|
|
const stopBtn = container.querySelector("#stopBtn")
|
|
const saveBtn = container.querySelector("#saveBtn")
|
|
const loadBtn = container.querySelector("#loadBtn")
|
|
const saveToFileBtn = container.querySelector("#saveToFileBtn")
|
|
const loadFromFileBtn = container.querySelector("#loadFromFileBtn")
|
|
const minimizeBtn = container.querySelector("#minimizeBtn")
|
|
const compactBtn = container.querySelector("#compactBtn")
|
|
const statsBtn = container.querySelector("#statsBtn")
|
|
const statusText = container.querySelector("#statusText")
|
|
const progressBar = container.querySelector("#progressBar")
|
|
const statsArea = statsContainer.querySelector("#statsArea")
|
|
const content = container.querySelector(".wplace-content")
|
|
const closeStatsBtn = statsContainer.querySelector("#closeStatsBtn")
|
|
const refreshChargesBtn = statsContainer.querySelector("#refreshChargesBtn")
|
|
|
|
// Check if all elements are found
|
|
if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) {
|
|
console.error("Some UI elements not found:", {
|
|
uploadBtn: !!uploadBtn,
|
|
selectPosBtn: !!selectPosBtn,
|
|
startBtn: !!startBtn,
|
|
stopBtn: !!stopBtn,
|
|
})
|
|
}
|
|
|
|
if (!statsContainer || !statsArea || !closeStatsBtn) {
|
|
console.error("Stats UI elements not found:", {
|
|
statsContainer: !!statsContainer,
|
|
statsArea: !!statsArea,
|
|
closeStatsBtn: !!closeStatsBtn,
|
|
})
|
|
}
|
|
|
|
const header = container.querySelector(".wplace-header")
|
|
|
|
// Use the shared makeDraggable function for consistency
|
|
makeDraggable(container)
|
|
|
|
function makeDraggable(element) {
|
|
let pos1 = 0,
|
|
pos2 = 0,
|
|
pos3 = 0,
|
|
pos4 = 0
|
|
let isDragging = false
|
|
const header = element.querySelector(".wplace-header") || element.querySelector(".wplace-settings-header")
|
|
|
|
// Check if header exists to prevent null error
|
|
if (!header) {
|
|
console.warn("No draggable header found for element:", element)
|
|
return
|
|
}
|
|
|
|
header.onmousedown = dragMouseDown
|
|
|
|
function dragMouseDown(e) {
|
|
if (e.target.closest(".wplace-header-btn") || e.target.closest("button")) return
|
|
|
|
e.preventDefault()
|
|
isDragging = true
|
|
|
|
// Get current position
|
|
const rect = element.getBoundingClientRect()
|
|
|
|
// Remove transform and set absolute position
|
|
element.style.transform = "none"
|
|
element.style.top = rect.top + "px"
|
|
element.style.left = rect.left + "px"
|
|
|
|
pos3 = e.clientX
|
|
pos4 = e.clientY
|
|
element.classList.add("wplace-dragging")
|
|
document.onmouseup = closeDragElement
|
|
document.onmousemove = elementDrag
|
|
|
|
// Prevent text selection during drag
|
|
document.body.style.userSelect = "none"
|
|
}
|
|
|
|
function elementDrag(e) {
|
|
if (!isDragging) return
|
|
|
|
e.preventDefault()
|
|
pos1 = pos3 - e.clientX
|
|
pos2 = pos4 - e.clientY
|
|
pos3 = e.clientX
|
|
pos4 = e.clientY
|
|
|
|
let newTop = element.offsetTop - pos2
|
|
let newLeft = element.offsetLeft - pos1
|
|
|
|
// Boundary checking to keep UI within viewport
|
|
const rect = element.getBoundingClientRect()
|
|
const maxTop = window.innerHeight - rect.height
|
|
const maxLeft = window.innerWidth - rect.width
|
|
|
|
newTop = Math.max(0, Math.min(newTop, maxTop))
|
|
newLeft = Math.max(0, Math.min(newLeft, maxLeft))
|
|
|
|
element.style.top = newTop + "px"
|
|
element.style.left = newLeft + "px"
|
|
}
|
|
|
|
function closeDragElement() {
|
|
isDragging = false
|
|
element.classList.remove("wplace-dragging")
|
|
document.onmouseup = null
|
|
document.onmousemove = null
|
|
document.body.style.userSelect = ""
|
|
}
|
|
}
|
|
|
|
// Make stats container draggable
|
|
makeDraggable(statsContainer)
|
|
|
|
// Make main container draggable
|
|
makeDraggable(container)
|
|
|
|
// Stats window functionality
|
|
if (statsBtn && closeStatsBtn) {
|
|
statsBtn.addEventListener("click", () => {
|
|
const isVisible = statsContainer.style.display !== "none"
|
|
if (isVisible) {
|
|
statsContainer.style.display = "none"
|
|
statsBtn.innerHTML = '<i class="fas fa-chart-bar"></i>'
|
|
statsBtn.title = "Show Stats"
|
|
} else {
|
|
statsContainer.style.display = "block"
|
|
statsBtn.innerHTML = '<i class="fas fa-chart-line"></i>'
|
|
statsBtn.title = "Hide Stats"
|
|
}
|
|
})
|
|
|
|
closeStatsBtn.addEventListener("click", () => {
|
|
statsContainer.style.display = "none"
|
|
statsBtn.innerHTML = '<i class="fas fa-chart-bar"></i>'
|
|
statsBtn.title = "Show Stats"
|
|
})
|
|
|
|
// Refresh charges button
|
|
if (refreshChargesBtn) {
|
|
refreshChargesBtn.addEventListener("click", async () => {
|
|
refreshChargesBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'
|
|
refreshChargesBtn.disabled = true
|
|
|
|
try {
|
|
await updateStats()
|
|
} catch (error) {
|
|
console.error("Error refreshing charges:", error)
|
|
} finally {
|
|
refreshChargesBtn.innerHTML = '<i class="fas fa-sync"></i>'
|
|
refreshChargesBtn.disabled = false
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Settings window functionality
|
|
const settingsBtn = container.querySelector("#settingsBtn")
|
|
const closeSettingsBtn = settingsContainer.querySelector("#closeSettingsBtn")
|
|
|
|
if (settingsBtn && closeSettingsBtn) {
|
|
settingsBtn.addEventListener("click", () => {
|
|
const isVisible = settingsContainer.style.display !== "none"
|
|
if (isVisible) {
|
|
// Add fade out animation
|
|
settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards"
|
|
setTimeout(() => {
|
|
settingsContainer.style.display = "none"
|
|
settingsContainer.style.animation = ""
|
|
}, 300)
|
|
} else {
|
|
// Reset position to center before showing
|
|
settingsContainer.style.top = "50%"
|
|
settingsContainer.style.left = "50%"
|
|
settingsContainer.style.transform = "translate(-50%, -50%)"
|
|
settingsContainer.style.display = "block"
|
|
settingsContainer.style.animation = "settingsSlideIn 0.4s ease-out"
|
|
}
|
|
})
|
|
|
|
closeSettingsBtn.addEventListener("click", () => {
|
|
// Add fade out animation
|
|
settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards"
|
|
setTimeout(() => {
|
|
settingsContainer.style.display = "none"
|
|
settingsContainer.style.animation = ""
|
|
// Reset position for next time
|
|
settingsContainer.style.top = "50%"
|
|
settingsContainer.style.left = "50%"
|
|
settingsContainer.style.transform = "translate(-50%, -50%)"
|
|
}, 300)
|
|
})
|
|
|
|
// Make settings window draggable
|
|
makeDraggable(settingsContainer)
|
|
|
|
// Language selector event listener
|
|
const languageSelect = settingsContainer.querySelector("#languageSelect")
|
|
if (languageSelect) {
|
|
languageSelect.addEventListener("change", (e) => {
|
|
const newLanguage = e.target.value
|
|
state.language = newLanguage
|
|
localStorage.setItem('wplace_language', newLanguage)
|
|
|
|
// Refresh the UI to apply new language
|
|
setTimeout(() => {
|
|
// Hide settings first
|
|
settingsContainer.style.display = "none"
|
|
|
|
// Recreate UI with new language (cleanup is handled in createUI)
|
|
createUI()
|
|
}, 100)
|
|
})
|
|
}
|
|
|
|
// Theme selector event listener
|
|
const themeSelect = settingsContainer.querySelector("#themeSelect")
|
|
if (themeSelect) {
|
|
themeSelect.addEventListener("change", (e) => {
|
|
const newTheme = e.target.value
|
|
switchTheme(newTheme)
|
|
})
|
|
}
|
|
|
|
// Theme customization event listeners
|
|
const primaryColor = settingsContainer.querySelector("#primaryColor")
|
|
const primaryColorText = settingsContainer.querySelector("#primaryColorText")
|
|
const secondaryColor = settingsContainer.querySelector("#secondaryColor")
|
|
const secondaryColorText = settingsContainer.querySelector("#secondaryColorText")
|
|
const highlightColor = settingsContainer.querySelector("#highlightColor")
|
|
const highlightColorText = settingsContainer.querySelector("#highlightColorText")
|
|
const borderRadiusSlider = settingsContainer.querySelector("#borderRadiusSlider")
|
|
const borderRadiusValue = settingsContainer.querySelector("#borderRadiusValue")
|
|
const applyChangesBtn = settingsContainer.querySelector("#applyThemeChanges")
|
|
const resetDefaultsBtn = settingsContainer.querySelector("#resetThemeDefaults")
|
|
|
|
// Color input synchronization
|
|
if (primaryColor && primaryColorText) {
|
|
primaryColor.addEventListener("input", (e) => {
|
|
primaryColorText.value = e.target.value
|
|
})
|
|
primaryColorText.addEventListener("input", (e) => {
|
|
if (/^#[0-9A-F]{6}$/i.test(e.target.value)) {
|
|
primaryColor.value = e.target.value
|
|
}
|
|
})
|
|
}
|
|
|
|
if (secondaryColor && secondaryColorText) {
|
|
secondaryColor.addEventListener("input", (e) => {
|
|
secondaryColorText.value = e.target.value
|
|
})
|
|
secondaryColorText.addEventListener("input", (e) => {
|
|
if (/^#[0-9A-F]{6}$/i.test(e.target.value)) {
|
|
secondaryColor.value = e.target.value
|
|
}
|
|
})
|
|
}
|
|
|
|
if (highlightColor && highlightColorText) {
|
|
highlightColor.addEventListener("input", (e) => {
|
|
highlightColorText.value = e.target.value
|
|
})
|
|
highlightColorText.addEventListener("input", (e) => {
|
|
if (/^#[0-9A-F]{6}$/i.test(e.target.value)) {
|
|
highlightColor.value = e.target.value
|
|
}
|
|
})
|
|
}
|
|
|
|
// Border radius slider
|
|
if (borderRadiusSlider && borderRadiusValue) {
|
|
borderRadiusSlider.addEventListener("input", (e) => {
|
|
borderRadiusValue.textContent = e.target.value + "px"
|
|
})
|
|
}
|
|
|
|
// Apply theme changes
|
|
if (applyChangesBtn) {
|
|
applyChangesBtn.addEventListener("click", () => {
|
|
const currentTheme = getCurrentTheme()
|
|
const currentThemeName = CONFIG.currentTheme
|
|
|
|
// Get all values
|
|
const newValues = {
|
|
primary: primaryColorText?.value || currentTheme.primary,
|
|
secondary: secondaryColorText?.value || currentTheme.secondary,
|
|
highlight: highlightColorText?.value || currentTheme.highlight,
|
|
borderRadius: (borderRadiusSlider?.value || 0) + "px",
|
|
animations: {
|
|
glow: settingsContainer.querySelector("#glowAnimation")?.checked || false,
|
|
scanline: settingsContainer.querySelector("#scanlineAnimation")?.checked || false,
|
|
pixelBlink: settingsContainer.querySelector("#pixelBlinkAnimation")?.checked || false
|
|
}
|
|
}
|
|
|
|
// Update theme
|
|
CONFIG.THEMES[currentThemeName] = {
|
|
...currentTheme,
|
|
...newValues
|
|
}
|
|
|
|
// Save and apply
|
|
saveThemePreference()
|
|
setTimeout(() => {
|
|
settingsContainer.style.display = "none"
|
|
createUI()
|
|
}, 100)
|
|
})
|
|
}
|
|
|
|
// Reset to defaults
|
|
if (resetDefaultsBtn) {
|
|
resetDefaultsBtn.addEventListener("click", () => {
|
|
// Reset to original theme values (you'll need to store defaults)
|
|
const confirmReset = confirm("Reset theme to default settings?")
|
|
if (confirmReset) {
|
|
// Reload original theme (this is a simplified approach)
|
|
location.reload()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const widthSlider = resizeContainer.querySelector("#widthSlider")
|
|
const heightSlider = resizeContainer.querySelector("#heightSlider")
|
|
const widthValue = resizeContainer.querySelector("#widthValue")
|
|
const heightValue = resizeContainer.querySelector("#heightValue")
|
|
const keepAspect = resizeContainer.querySelector("#keepAspect")
|
|
const paintWhiteToggle = resizeContainer.querySelector("#paintWhiteToggle");
|
|
const zoomSlider = resizeContainer.querySelector("#zoomSlider");
|
|
const resizePreview = resizeContainer.querySelector("#resizePreview")
|
|
const confirmResize = resizeContainer.querySelector("#confirmResize")
|
|
const cancelResize = resizeContainer.querySelector("#cancelResize")
|
|
const downloadPreviewBtn = resizeContainer.querySelector("#downloadPreviewBtn");
|
|
|
|
// Compact mode functionality
|
|
if (compactBtn) {
|
|
compactBtn.addEventListener("click", () => {
|
|
container.classList.toggle("wplace-compact")
|
|
const isCompact = container.classList.contains("wplace-compact")
|
|
|
|
if (isCompact) {
|
|
compactBtn.innerHTML = '<i class="fas fa-expand"></i>'
|
|
compactBtn.title = "Expand Mode"
|
|
} else {
|
|
compactBtn.innerHTML = '<i class="fas fa-compress"></i>'
|
|
compactBtn.title = "Compact Mode"
|
|
}
|
|
})
|
|
}
|
|
|
|
// Minimize functionality
|
|
if (minimizeBtn) {
|
|
minimizeBtn.addEventListener("click", () => {
|
|
state.minimized = !state.minimized
|
|
if (state.minimized) {
|
|
container.classList.add("wplace-minimized")
|
|
content.classList.add("wplace-hidden")
|
|
minimizeBtn.innerHTML = '<i class="fas fa-expand"></i>'
|
|
minimizeBtn.title = "Restore"
|
|
} else {
|
|
container.classList.remove("wplace-minimized")
|
|
content.classList.remove("wplace-hidden")
|
|
minimizeBtn.innerHTML = '<i class="fas fa-minus"></i>'
|
|
minimizeBtn.title = "Minimize"
|
|
}
|
|
})
|
|
}
|
|
|
|
// Save progress functionality
|
|
if (saveBtn) {
|
|
saveBtn.addEventListener("click", () => {
|
|
if (!state.imageLoaded) {
|
|
Utils.showAlert(Utils.t("missingRequirements"), "error")
|
|
return
|
|
}
|
|
|
|
const success = Utils.saveProgress()
|
|
if (success) {
|
|
updateUI("autoSaved", "success")
|
|
Utils.showAlert(Utils.t("autoSaved"), "success")
|
|
} else {
|
|
Utils.showAlert("❌ Erro ao salvar progresso", "error")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Load progress functionality
|
|
if (loadBtn) {
|
|
loadBtn.addEventListener("click", () => {
|
|
const savedData = Utils.loadProgress()
|
|
if (!savedData) {
|
|
updateUI("noSavedData", "warning")
|
|
Utils.showAlert(Utils.t("noSavedData"), "warning")
|
|
return
|
|
}
|
|
|
|
// Show confirmation dialog
|
|
const confirmLoad = confirm(
|
|
`${Utils.t("savedDataFound")}\n\n` +
|
|
`Saved: ${new Date(savedData.timestamp).toLocaleString()}\n` +
|
|
`Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels`,
|
|
)
|
|
|
|
if (confirmLoad) {
|
|
const success = Utils.restoreProgress(savedData)
|
|
if (success) {
|
|
updateUI("dataLoaded", "success")
|
|
Utils.showAlert(Utils.t("dataLoaded"), "success")
|
|
updateDataButtons()
|
|
|
|
// Check charges immediately after loading auto-save
|
|
updateStats()
|
|
|
|
if (!state.colorsChecked) {
|
|
// Re-run color check automatically if loaded data is missing it
|
|
checkAndSetColors();
|
|
}
|
|
|
|
if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) {
|
|
startBtn.disabled = false
|
|
}
|
|
} else {
|
|
Utils.showAlert("❌ Erro ao carregar progresso", "error")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Save to file functionality
|
|
if (saveToFileBtn) {
|
|
saveToFileBtn.addEventListener("click", () => {
|
|
const success = Utils.saveProgressToFile()
|
|
if (success) {
|
|
updateUI("fileSaved", "success")
|
|
Utils.showAlert(Utils.t("fileSaved"), "success")
|
|
} else {
|
|
Utils.showAlert(Utils.t("fileError"), "error")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Load from file functionality
|
|
if (loadFromFileBtn) {
|
|
loadFromFileBtn.addEventListener("click", async () => {
|
|
try {
|
|
const success = await Utils.loadProgressFromFile()
|
|
if (success) {
|
|
updateUI("fileLoaded", "success")
|
|
Utils.showAlert(Utils.t("fileLoaded"), "success")
|
|
updateDataButtons()
|
|
|
|
// Check charges immediately after loading file
|
|
await updateStats()
|
|
|
|
// Auto-enable buttons after loading from file
|
|
if (state.colorsChecked) {
|
|
uploadBtn.disabled = false
|
|
selectPosBtn.disabled = false
|
|
resizeBtn.disabled = false
|
|
} else {
|
|
// If loaded file is missing color data, check for it
|
|
await checkAndSetColors();
|
|
}
|
|
|
|
if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) {
|
|
startBtn.disabled = false
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error.message === "Invalid JSON file") {
|
|
Utils.showAlert(Utils.t("invalidFileFormat"), "error")
|
|
} else {
|
|
Utils.showAlert(Utils.t("fileError"), "error")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
updateUI = (messageKey, type = "default", params = {}) => {
|
|
const message = Utils.t(messageKey, params)
|
|
statusText.textContent = message
|
|
statusText.className = `wplace-status status-${type}`
|
|
statusText.style.animation = "none"
|
|
void statusText.offsetWidth
|
|
statusText.style.animation = "slideIn 0.3s ease-out"
|
|
}
|
|
|
|
updateStats = async () => {
|
|
if (!state.imageLoaded) return; // Only show stats if image is loaded
|
|
|
|
const { charges, cooldown } = await WPlaceService.getCharges()
|
|
state.currentCharges = Math.floor(charges)
|
|
state.cooldown = cooldown
|
|
|
|
const progress = state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0
|
|
const remainingPixels = state.totalPixels - state.paintedPixels
|
|
|
|
state.estimatedTime = Utils.calculateEstimatedTime(remainingPixels, state.currentCharges, state.cooldown)
|
|
|
|
progressBar.style.width = `${progress}%`
|
|
|
|
statsArea.innerHTML = `
|
|
<div class="wplace-stat-item">
|
|
<div class="wplace-stat-label"><i class="fas fa-image"></i> ${Utils.t("progress")}</div>
|
|
<div class="wplace-stat-value">${progress}%</div>
|
|
</div>
|
|
<div class="wplace-stat-item">
|
|
<div class="wplace-stat-label"><i class="fas fa-paint-brush"></i> ${Utils.t("pixels")}</div>
|
|
<div class="wplace-stat-value">${state.paintedPixels}/${state.totalPixels}</div>
|
|
</div>
|
|
<div class="wplace-stat-item">
|
|
<div class="wplace-stat-label"><i class="fas fa-bolt"></i> ${Utils.t("charges")}</div>
|
|
<div class="wplace-stat-value">${Math.floor(state.currentCharges)}</div>
|
|
</div>
|
|
${
|
|
state.imageLoaded
|
|
? `
|
|
<div class="wplace-stat-item">
|
|
<div class="wplace-stat-label"><i class="fas fa-clock"></i> ${Utils.t("estimatedTime")}</div>
|
|
<div class="wplace-stat-value">${Utils.formatTime(state.estimatedTime)}</div>
|
|
</div>
|
|
`
|
|
: ""
|
|
}
|
|
`
|
|
}
|
|
|
|
// Helper function to update data management buttons
|
|
updateDataButtons = () => {
|
|
const hasImageData = state.imageLoaded && state.imageData
|
|
saveBtn.disabled = !hasImageData
|
|
saveToFileBtn.disabled = !hasImageData
|
|
}
|
|
|
|
// Initialize data buttons state
|
|
updateDataButtons()
|
|
|
|
function showResizeDialog(processor) {
|
|
const { width, height } = processor.getDimensions();
|
|
const aspectRatio = width / height;
|
|
|
|
widthSlider.value = width;
|
|
heightSlider.value = height;
|
|
widthSlider.max = width * 2; // Set a reasonable max
|
|
heightSlider.max = height * 2; // Set a reasonable max
|
|
widthValue.textContent = width;
|
|
heightValue.textContent = height;
|
|
zoomSlider.value = 1;
|
|
paintWhiteToggle.checked = state.paintWhitePixels;
|
|
|
|
_updateResizePreview = () => {
|
|
const newWidth = parseInt(widthSlider.value, 10);
|
|
const newHeight = parseInt(heightSlider.value, 10);
|
|
const zoomLevel = parseFloat(zoomSlider.value);
|
|
|
|
widthValue.textContent = newWidth;
|
|
heightValue.textContent = newHeight;
|
|
|
|
const tempCanvas = document.createElement('canvas');
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
tempCanvas.width = newWidth;
|
|
tempCanvas.height = newHeight;
|
|
tempCtx.imageSmoothingEnabled = false;
|
|
tempCtx.drawImage(processor.img, 0, 0, newWidth, newHeight);
|
|
|
|
const imgData = tempCtx.getImageData(0, 0, newWidth, newHeight);
|
|
const data = imgData.data;
|
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3];
|
|
|
|
if (a < CONFIG.TRANSPARENCY_THRESHOLD || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) {
|
|
data[i + 3] = 0; // Make transparent
|
|
continue;
|
|
}
|
|
|
|
const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
|
|
data[i] = nr;
|
|
data[i + 1] = ng;
|
|
data[i + 2] = nb;
|
|
data[i + 3] = 255;
|
|
}
|
|
tempCtx.putImageData(imgData, 0, 0);
|
|
resizePreview.src = tempCanvas.toDataURL();
|
|
resizePreview.style.transform = `scale(${zoomLevel})`;
|
|
};
|
|
|
|
const onWidthInput = () => {
|
|
if (keepAspect.checked) {
|
|
heightSlider.value = Math.round(parseInt(widthSlider.value, 10) / aspectRatio);
|
|
}
|
|
_updateResizePreview();
|
|
};
|
|
|
|
const onHeightInput = () => {
|
|
if (keepAspect.checked) {
|
|
widthSlider.value = Math.round(parseInt(heightSlider.value, 10) * aspectRatio);
|
|
}
|
|
_updateResizePreview();
|
|
};
|
|
|
|
paintWhiteToggle.onchange = (e) => {
|
|
state.paintWhitePixels = e.target.checked;
|
|
_updateResizePreview();
|
|
};
|
|
|
|
zoomSlider.addEventListener('input', _updateResizePreview);
|
|
widthSlider.addEventListener("input", onWidthInput);
|
|
heightSlider.addEventListener("input", onHeightInput);
|
|
|
|
confirmResize.onclick = () => {
|
|
const newWidth = parseInt(widthSlider.value, 10);
|
|
const newHeight = parseInt(heightSlider.value, 10);
|
|
const newPixels = processor.resize(newWidth, newHeight);
|
|
|
|
let totalValidPixels = 0;
|
|
for (let i = 0; i < newPixels.length; i += 4) {
|
|
const isTransparent = newPixels[i + 3] < CONFIG.TRANSPARENCY_THRESHOLD;
|
|
const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(newPixels[i], newPixels[i+1], newPixels[i+2]);
|
|
if (!isTransparent && !isWhiteAndSkipped) {
|
|
totalValidPixels++;
|
|
}
|
|
}
|
|
|
|
state.imageData.pixels = newPixels;
|
|
state.imageData.width = newWidth;
|
|
state.imageData.height = newHeight;
|
|
state.imageData.totalPixels = totalValidPixels;
|
|
state.totalPixels = totalValidPixels;
|
|
state.paintedPixels = 0;
|
|
|
|
updateStats();
|
|
updateUI("resizeSuccess", "success", { width: newWidth, height: newHeight });
|
|
closeResizeDialog();
|
|
};
|
|
|
|
downloadPreviewBtn.onclick = () => {
|
|
const link = document.createElement('a');
|
|
link.download = 'wplace-preview.png';
|
|
link.href = resizePreview.src;
|
|
link.click();
|
|
};
|
|
|
|
cancelResize.onclick = closeResizeDialog;
|
|
|
|
resizeOverlay.style.display = "block";
|
|
resizeContainer.style.display = "block";
|
|
_updateResizePreview(); // Initial preview
|
|
}
|
|
|
|
function closeResizeDialog() {
|
|
resizeOverlay.style.display = "none";
|
|
resizeContainer.style.display = "none";
|
|
_updateResizePreview = () => {}; // Clear the function to prevent memory leaks
|
|
}
|
|
|
|
if (uploadBtn) {
|
|
uploadBtn.addEventListener("click", async () => {
|
|
// First, automatically run the color check if it hasn't been done.
|
|
const colorsReady = await checkAndSetColors();
|
|
if (!colorsReady) {
|
|
// Stop if colors were not found. The error message is already shown.
|
|
return;
|
|
}
|
|
|
|
// If colors are ready, proceed with the original upload logic.
|
|
try {
|
|
updateUI("loadingImage", "default")
|
|
const imageSrc = await Utils.createImageUploader()
|
|
if (!imageSrc) return; // User cancelled the file dialog
|
|
|
|
const processor = new ImageProcessor(imageSrc)
|
|
await processor.load()
|
|
|
|
const { width, height } = processor.getDimensions()
|
|
const pixels = processor.getPixelData()
|
|
|
|
let totalValidPixels = 0;
|
|
for (let i = 0; i < pixels.length; i += 4) {
|
|
const isTransparent = pixels[i + 3] < CONFIG.TRANSPARENCY_THRESHOLD;
|
|
const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(pixels[i], pixels[i+1], pixels[i+2]);
|
|
if (!isTransparent && !isWhiteAndSkipped) {
|
|
totalValidPixels++;
|
|
}
|
|
}
|
|
|
|
state.imageData = {
|
|
width,
|
|
height,
|
|
pixels,
|
|
totalPixels: totalValidPixels,
|
|
processor,
|
|
}
|
|
|
|
state.totalPixels = totalValidPixels
|
|
state.paintedPixels = 0
|
|
state.imageLoaded = true
|
|
state.lastPosition = { x: 0, y: 0 }
|
|
|
|
resizeBtn.disabled = false
|
|
saveBtn.disabled = false
|
|
|
|
if (state.startPosition) {
|
|
startBtn.disabled = false
|
|
}
|
|
|
|
updateStats()
|
|
updateDataButtons()
|
|
updateUI("imageLoaded", "success", { count: totalValidPixels })
|
|
} catch {
|
|
updateUI("imageError", "error")
|
|
}
|
|
})
|
|
}
|
|
|
|
if (resizeBtn) {
|
|
resizeBtn.addEventListener("click", () => {
|
|
if (state.imageLoaded && state.imageData.processor) {
|
|
showResizeDialog(state.imageData.processor)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (selectPosBtn) {
|
|
selectPosBtn.addEventListener("click", async () => {
|
|
if (state.selectingPosition) return
|
|
|
|
state.selectingPosition = true
|
|
state.startPosition = null
|
|
state.region = null
|
|
startBtn.disabled = true
|
|
|
|
Utils.showAlert(Utils.t("selectPositionAlert"), "info")
|
|
updateUI("waitingPosition", "default")
|
|
|
|
const originalFetch = window.fetch
|
|
|
|
window.fetch = async (url, options) => {
|
|
if (
|
|
typeof url === "string" &&
|
|
url.includes("https://backend.wplace.live/s0/pixel/") &&
|
|
options?.method?.toUpperCase() === "POST"
|
|
) {
|
|
try {
|
|
const response = await originalFetch(url, options)
|
|
const clonedResponse = response.clone()
|
|
const data = await clonedResponse.json()
|
|
|
|
if (data?.painted === 1) {
|
|
const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/)
|
|
if (regionMatch && regionMatch.length >= 3) {
|
|
state.region = {
|
|
x: Number.parseInt(regionMatch[1]),
|
|
y: Number.parseInt(regionMatch[2]),
|
|
}
|
|
}
|
|
|
|
const payload = JSON.parse(options.body)
|
|
if (payload?.coords && Array.isArray(payload.coords)) {
|
|
state.startPosition = {
|
|
x: payload.coords[0],
|
|
y: payload.coords[1],
|
|
}
|
|
state.lastPosition = { x: 0, y: 0 }
|
|
|
|
if (state.imageLoaded) {
|
|
startBtn.disabled = false
|
|
}
|
|
|
|
window.fetch = originalFetch
|
|
state.selectingPosition = false
|
|
updateUI("positionSet", "success")
|
|
}
|
|
}
|
|
|
|
return response
|
|
} catch {
|
|
return originalFetch(url, options)
|
|
}
|
|
}
|
|
return originalFetch(url, options)
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (state.selectingPosition) {
|
|
window.fetch = originalFetch
|
|
state.selectingPosition = false
|
|
updateUI("positionTimeout", "error")
|
|
Utils.showAlert(Utils.t("positionTimeout"), "error")
|
|
}
|
|
}, 120000)
|
|
})
|
|
}
|
|
|
|
// Function to start painting (can be called programmatically)
|
|
async function startPainting() {
|
|
if (!state.imageLoaded || !state.startPosition || !state.region) {
|
|
updateUI("missingRequirements", "error")
|
|
return false
|
|
}
|
|
// Ensure we have a valid token before starting
|
|
await ensureToken()
|
|
if (!turnstileToken) return false
|
|
|
|
state.running = true
|
|
state.stopFlag = false
|
|
startBtn.disabled = true
|
|
stopBtn.disabled = false
|
|
uploadBtn.disabled = true
|
|
selectPosBtn.disabled = true
|
|
resizeBtn.disabled = true
|
|
saveBtn.disabled = true
|
|
|
|
updateUI("startPaintingMsg", "success")
|
|
|
|
try {
|
|
await processImage()
|
|
return true
|
|
} catch {
|
|
updateUI("paintingError", "error")
|
|
return false
|
|
} finally {
|
|
state.running = false
|
|
stopBtn.disabled = true
|
|
saveBtn.disabled = false
|
|
|
|
if (!state.stopFlag) {
|
|
startBtn.disabled = true
|
|
uploadBtn.disabled = false
|
|
selectPosBtn.disabled = false
|
|
resizeBtn.disabled = false
|
|
} else {
|
|
startBtn.disabled = false
|
|
}
|
|
}
|
|
}
|
|
|
|
if (startBtn) {
|
|
startBtn.addEventListener("click", startPainting)
|
|
}
|
|
|
|
if (stopBtn) {
|
|
stopBtn.addEventListener("click", () => {
|
|
state.stopFlag = true
|
|
state.running = false
|
|
stopBtn.disabled = true
|
|
updateUI("paintingStopped", "warning")
|
|
|
|
// Auto save when stopping
|
|
if (state.imageLoaded && state.paintedPixels > 0) {
|
|
Utils.saveProgress()
|
|
Utils.showAlert(Utils.t("autoSaved"), "success")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Check for saved progress on startup
|
|
const checkSavedProgress = () => {
|
|
const savedData = Utils.loadProgress()
|
|
if (savedData && savedData.state.paintedPixels > 0) {
|
|
const savedDate = new Date(savedData.timestamp).toLocaleString()
|
|
const progress = Math.round((savedData.state.paintedPixels / savedData.state.totalPixels) * 100)
|
|
|
|
Utils.showAlert(
|
|
`${Utils.t("savedDataFound")}\n\n` +
|
|
`Saved: ${savedDate}\n` +
|
|
`Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels (${progress}%)\n` +
|
|
`${Utils.t("clickLoadToContinue")}`,
|
|
"info",
|
|
)
|
|
}
|
|
}
|
|
|
|
// Check for saved progress after a short delay to let UI settle
|
|
setTimeout(checkSavedProgress, 1000)
|
|
|
|
// Speed Slider Event Listener (in settings window)
|
|
const speedSlider = settingsContainer.querySelector("#speedSlider")
|
|
const speedValue = settingsContainer.querySelector("#speedValue")
|
|
if (speedSlider && speedValue) {
|
|
speedSlider.addEventListener("input", (e) => {
|
|
const speed = parseInt(e.target.value) // Use integer instead of float
|
|
state.paintingSpeed = speed
|
|
speedValue.textContent = speed + " px/s" // Add px/s unit
|
|
|
|
// Save speed preference to localStorage
|
|
try {
|
|
localStorage.setItem("wplace-painting-speed", speed.toString())
|
|
} catch (error) {
|
|
console.warn("Could not save painting speed preference:", error)
|
|
}
|
|
})
|
|
|
|
// Load saved speed preference
|
|
try {
|
|
const savedSpeed = localStorage.getItem("wplace-painting-speed")
|
|
if (savedSpeed) {
|
|
const speed = parseInt(savedSpeed)
|
|
if (speed >= CONFIG.PAINTING_SPEED.MIN && speed <= CONFIG.PAINTING_SPEED.MAX) {
|
|
state.paintingSpeed = speed
|
|
speedSlider.value = speed
|
|
speedValue.textContent = speed.toString()
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn("Could not load painting speed preference:", error)
|
|
}
|
|
}
|
|
|
|
// Painting speed toggle
|
|
const enableSpeedToggle = settingsContainer.querySelector("#enableSpeedToggle")
|
|
if (enableSpeedToggle) {
|
|
// Initialize speed slider disabled state
|
|
const speedSliderToggle = settingsContainer.querySelector("#speedSlider")
|
|
if (speedSliderToggle) speedSliderToggle.disabled = !CONFIG.PAINTING_SPEED_ENABLED
|
|
enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED
|
|
enableSpeedToggle.addEventListener("change", (e) => {
|
|
CONFIG.PAINTING_SPEED_ENABLED = e.target.checked
|
|
// Toggle speed slider
|
|
if (speedSliderToggle) speedSliderToggle.disabled = !CONFIG.PAINTING_SPEED_ENABLED
|
|
// Save preference to localStorage
|
|
try {
|
|
localStorage.setItem("wplace-painting-speed-enabled", CONFIG.PAINTING_SPEED_ENABLED.toString())
|
|
} catch (error) {
|
|
console.warn("Could not save painting speed enabled preference:", error)
|
|
}
|
|
})
|
|
// Load saved preference
|
|
try {
|
|
const savedEnabled = localStorage.getItem("wplace-painting-speed-enabled")
|
|
if (savedEnabled !== null) {
|
|
CONFIG.PAINTING_SPEED_ENABLED = savedEnabled === "true"
|
|
enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED
|
|
if (speedSliderToggle) speedSliderToggle.disabled = !CONFIG.PAINTING_SPEED_ENABLED
|
|
}
|
|
} catch (error) {
|
|
console.warn("Could not load painting speed enabled preference:", error)
|
|
}
|
|
}
|
|
// --- Initialize Color Palette UI ---
|
|
initializeColorPalette(resizeContainer);
|
|
}
|
|
|
|
async function processImage() {
|
|
const { width, height, pixels } = state.imageData
|
|
const { x: startX, y: startY } = state.startPosition
|
|
const { x: regionX, y: regionY } = state.region
|
|
|
|
const startRow = state.lastPosition.y || 0
|
|
const startCol = state.lastPosition.x || 0
|
|
|
|
if (!state.paintedMap) {
|
|
state.paintedMap = Array(height)
|
|
.fill()
|
|
.map(() => Array(width).fill(false))
|
|
}
|
|
|
|
let pixelBatch = []
|
|
|
|
try {
|
|
outerLoop: for (let y = startRow; y < height; y++) {
|
|
for (let x = y === startRow ? startCol : 0; x < width; x++) {
|
|
if (state.stopFlag) {
|
|
if (pixelBatch.length > 0) {
|
|
await sendPixelBatch(pixelBatch, regionX, regionY)
|
|
}
|
|
state.lastPosition = { x, y }
|
|
updateUI("paintingPaused", "warning", { x, y })
|
|
break outerLoop
|
|
}
|
|
|
|
if (state.paintedMap[y][x]) continue
|
|
|
|
const idx = (y * width + x) * 4
|
|
const r = pixels[idx]
|
|
const g = pixels[idx + 1]
|
|
const b = pixels[idx + 2]
|
|
const alpha = pixels[idx + 3]
|
|
|
|
if (alpha < CONFIG.TRANSPARENCY_THRESHOLD || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) {
|
|
continue;
|
|
}
|
|
|
|
// Step 1: Quantize source pixel to the user's selected palette
|
|
const targetRgb = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
|
|
|
|
// Step 2: Find the closest available in-game color to the quantized color
|
|
const colorId = findClosestColor(targetRgb, state.availableColors);
|
|
|
|
const pixelX = startX + x
|
|
const pixelY = startY + y
|
|
|
|
pixelBatch.push({
|
|
x: pixelX,
|
|
y: pixelY,
|
|
color: colorId,
|
|
localX: x,
|
|
localY: y,
|
|
})
|
|
|
|
if (pixelBatch.length >= Math.floor(state.currentCharges)) {
|
|
const success = await sendPixelBatch(pixelBatch, regionX, regionY)
|
|
|
|
if (success === "token_error") {
|
|
state.stopFlag = true
|
|
updateUI("captchaNeeded", "error")
|
|
Utils.showAlert(Utils.t("captchaNeeded"), "error")
|
|
break outerLoop
|
|
}
|
|
|
|
if (success) {
|
|
pixelBatch.forEach((pixel) => {
|
|
state.paintedMap[pixel.localY][pixel.localX] = true
|
|
state.paintedPixels++
|
|
})
|
|
|
|
state.currentCharges -= pixelBatch.length
|
|
updateStats()
|
|
updateUI("paintingProgress", "default", {
|
|
painted: state.paintedPixels,
|
|
total: state.totalPixels,
|
|
})
|
|
|
|
// Auto-save progress every 50 pixels
|
|
if (state.paintedPixels % 50 === 0) {
|
|
Utils.saveProgress()
|
|
}
|
|
|
|
// Apply painting speed delay if enabled
|
|
if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.length > 0) {
|
|
const delayPerPixel = 1000 / state.paintingSpeed // ms per pixel
|
|
const totalDelay = Math.max(100, delayPerPixel * pixelBatch.length) // minimum 100ms
|
|
await Utils.sleep(totalDelay)
|
|
}
|
|
}
|
|
|
|
pixelBatch = []
|
|
|
|
if (state.currentCharges < 1) {
|
|
updateUI("noCharges", "warning", {
|
|
time: Utils.formatTime(state.cooldown),
|
|
})
|
|
await Utils.sleep(state.cooldown)
|
|
|
|
const chargeUpdate = await WPlaceService.getCharges()
|
|
state.currentCharges = chargeUpdate.charges
|
|
state.cooldown = chargeUpdate.cooldown
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pixelBatch.length > 0 && !state.stopFlag) {
|
|
const success = await sendPixelBatch(pixelBatch, regionX, regionY)
|
|
if (success) {
|
|
pixelBatch.forEach((pixel) => {
|
|
state.paintedMap[pixel.localY][pixel.localX] = true
|
|
state.paintedPixels++
|
|
})
|
|
state.currentCharges -= pixelBatch.length
|
|
// Apply painting speed delay for remaining pixels if enabled
|
|
if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.length > 0) {
|
|
const delayPerPixel = 1000 / state.paintingSpeed // ms per pixel
|
|
const totalDelay = Math.max(100, delayPerPixel * pixelBatch.length) // minimum 100ms
|
|
await Utils.sleep(totalDelay)
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
if (window._chargesInterval) clearInterval(window._chargesInterval)
|
|
window._chargesInterval = null
|
|
}
|
|
|
|
if (state.stopFlag) {
|
|
updateUI("paintingStopped", "warning")
|
|
// Save progress when stopped
|
|
Utils.saveProgress()
|
|
} else {
|
|
updateUI("paintingComplete", "success", { count: state.paintedPixels })
|
|
state.lastPosition = { x: 0, y: 0 }
|
|
state.paintedMap = null
|
|
// Clear saved data when completed
|
|
Utils.clearProgress()
|
|
}
|
|
|
|
updateStats()
|
|
}
|
|
|
|
async function sendPixelBatch(pixelBatch, regionX, regionY) {
|
|
if (!turnstileToken) return "token_error"
|
|
|
|
// Pre-allocate arrays for performance
|
|
const coords = new Array(pixelBatch.length * 2)
|
|
const colors = new Array(pixelBatch.length)
|
|
for (let i = 0; i < pixelBatch.length; i++) {
|
|
const pixel = pixelBatch[i]
|
|
coords[i * 2] = pixel.x
|
|
coords[i * 2 + 1] = pixel.y
|
|
colors[i] = pixel.color
|
|
}
|
|
|
|
try {
|
|
const payload = { coords, colors, t: turnstileToken }
|
|
|
|
const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "text/plain;charset=UTF-8" },
|
|
credentials: "include",
|
|
body: JSON.stringify(payload),
|
|
})
|
|
|
|
if (res.status === 403) {
|
|
let data = null
|
|
try { data = await res.json() } catch (_) {}
|
|
console.error("❌ 403 Forbidden. Turnstile token might be invalid or expired.")
|
|
// Reset token & create new promise to await fresh token
|
|
turnstileToken = null
|
|
tokenPromise = new Promise((resolve) => { _resolveToken = resolve })
|
|
Utils.showAlert(Utils.t("captchaNeeded"), "error")
|
|
return "token_error"
|
|
}
|
|
const data = await res.json()
|
|
return data?.painted === pixelBatch.length
|
|
} catch (e) {
|
|
console.error("Batch paint request failed:", e)
|
|
return false
|
|
}
|
|
}
|
|
|
|
createUI()
|
|
})()
|