const _IMG = '/luci-static/ipip/'; const translate = {}; let cachedIP = null; let translationEnabled = localStorage.getItem('translationEnabled') !== 'false'; let currentLanguage = 'zh-CN'; const translationsIP = { zh: { unknown: '未知', clickRefresh: '点击刷新 IP 地址', failedUpdate: '更新 IP 信息失败', checking: '获取中...', failed: '获取 IP 信息失败', translate: '翻译' }, en: { unknown: 'Unknown', clickRefresh: 'Click to refresh IP address', failedUpdate: 'Failed to update IP information', checking: 'Checking...', failed: 'Failed to get IP information', translate: 'Translate' }, hk: { unknown: '未知', clickRefresh: '點擊刷新 IP 地址', failedUpdate: '更新 IP 資訊失敗', checking: '獲取中...', failed: '獲取 IP 資訊失敗', translate: '翻譯' }, ko: { unknown: '알 수 없음', clickRefresh: 'IP 주소 새로고침', failedUpdate: 'IP 정보 업데이트 실패', checking: '가져오는 중...', failed: 'IP 정보를 가져오지 못했습니다', translate: '번역' }, ja: { unknown: '不明', clickRefresh: 'IP アドレスを更新', failedUpdate: 'IP 情報の更新に失敗しました', checking: '取得中...', failed: 'IP 情報の取得に失敗しました', translate: '翻訳' }, vi: { unknown: 'Không xác định', clickRefresh: 'Nhấn để làm mới địa chỉ IP', failedUpdate: 'Cập nhật thông tin IP thất bại', checking: 'Đang lấy...', failed: 'Lấy thông tin IP thất bại', translate: 'Dịch' }, th: { unknown: 'ไม่ทราบ', clickRefresh: 'คลิกเพื่อรีเฟรชที่อยู่ IP', failedUpdate: 'อัปเดตข้อมูล IP ล้มเหลว', checking: 'กำลังดึงข้อมูล...', failed: 'ไม่สามารถดึงข้อมูล IP', translate: 'แปล' }, ru: { unknown: 'Неизвестно', clickRefresh: 'Нажмите, чтобы обновить IP-адрес', failedUpdate: 'Не удалось обновить информацию об IP', checking: 'Получение...', failed: 'Не удалось получить информацию об IP', translate: 'Перевести' }, ar: { unknown: 'غير معروف', clickRefresh: 'اضغط لتحديث عنوان الـ IP', failedUpdate: 'فشل تحديث معلومات الـ IP', checking: 'جارٍ الجلب...', failed: 'فشل في جلب معلومات الـ IP', translate: 'ترجمة' }, es: { unknown: 'Desconocido', clickRefresh: 'Haga clic para actualizar la dirección IP', failedUpdate: 'Error al actualizar la información de IP', checking: 'Obteniendo...', failed: 'Error al obtener la información de IP', translate: 'Traducir' }, de: { unknown: 'Unbekannt', clickRefresh: 'Klicken, um die IP-Adresse zu aktualisieren', failedUpdate: 'Aktualisieren der IP-Informationen fehlgeschlagen', checking: 'Wird abgerufen...', failed: 'Abrufen der IP-Informationen fehlgeschlagen', translate: 'Übersetzen' }, fr: { unknown: 'Inconnu', clickRefresh: 'Cliquez pour actualiser l’adresse IP', failedUpdate: 'Échec de la mise à jour des informations IP', checking: 'Récupération...', failed: 'Échec de la récupération des informations IP', translate: 'Traduire' }, bn: { unknown: 'অজানা', clickRefresh: 'IP ঠিকানা রিফ্রেশ করতে ক্লিক করুন', failedUpdate: 'IP তথ্য আপডেট ব্যর্থ হয়েছে', checking: 'আনছে...', failed: 'IP তথ্য আনতে ব্যর্থ হয়েছে', translate: 'অনুবাদ' } }; async function initIPDisplay() { const lang = await getRealTimeLanguage(); const text = translationsIP[lang] || translationsIP['zh']; const modalText = modalTranslations[lang] || modalTranslations['zh']; const flag = document.getElementById('flag'); if (flag) flag.title = text.clickRefresh || 'Click to Refresh'; } document.addEventListener('DOMContentLoaded', () => { initIPDisplay(); }); async function getCurrentLanguage() { try { const response = await fetch('/spectra/lib/language.txt', {cache: 'no-store'}); if (response.ok) { const lang = await response.text(); return lang.trim() || 'zh-CN'; } } catch (error) { console.error('Failed to read language file:', error); } return 'zh-CN'; } async function getRealTimeLanguage() { return await getCurrentLanguage(); } async function onLanguageChange() { //console.log('Language changed, refreshing display'); const keys = Object.keys(localStorage); keys.forEach(key => { if (key.startsWith('trans_')) { localStorage.removeItem(key); } }); if (cachedIP) { await myAppIP.Ipip(cachedIP, 'ipip'); } } function setupLanguageChangeListener() { let lastLanguage = 'zh-CN'; setInterval(async () => { const currentLang = await getRealTimeLanguage(); if (currentLang !== lastLanguage) { //console.log('Language changed detected:', currentLang); lastLanguage = currentLang; onLanguageChange(); } }, 1000); } document.addEventListener('DOMContentLoaded', function() { const savedState = localStorage.getItem('translationEnabled'); if (savedState !== null) { translationEnabled = savedState === 'true'; } updateTranslationToggleColor(); }); function updateTranslationToggleColor() { const existingToggles = document.querySelectorAll('.translation-toggle'); existingToggles.forEach(toggle => { const icon = toggle.querySelector('i'); if (translationEnabled) { icon.style.color = '#28a745'; } else { icon.style.color = '#6c757d'; } }); } function toggleTranslation() { translationEnabled = !translationEnabled; localStorage.setItem('translationEnabled', translationEnabled); updateTranslationToggleColor(); const translations = languageTranslations[currentLang] || languageTranslations['zh']; const translationMsg = translationEnabled ? translations['translation_enabled'] : translations['translation_disabled']; if (typeof showLogMessage === 'function') { showLogMessage(encodeURIComponent(translationMsg), 'info'); } if (typeof speakMessage === 'function') { speakMessage(translationMsg); } if (cachedIP) { myAppIP.Ipip(cachedIP, 'ipip'); } } function setupHoverEffect() { const ipContainer = document.getElementById('d-ip'); const toggle = document.querySelector('.translation-toggle'); if (ipContainer && toggle) { ipContainer.addEventListener('mouseenter', () => toggle.style.opacity = '1'); ipContainer.addEventListener('mouseleave', () => toggle.style.opacity = '0'); } } async function myAppTranslate(text) { if (!translationEnabled) return text; if (!text || typeof text !== 'string' || text.trim() === '') return text; const targetLanguage = await getRealTimeLanguage(); const langMap = { 'zh':'zh-CN','zh-tw':'zh-TW','zh-CN':'zh-CN','hk':'zh-TW', 'zh-mo': 'zh-TW', 'en':'en','ja':'ja','ko':'ko','fr':'fr','de':'de','es':'es', 'it':'it','pt':'pt','ru':'ru','ar':'ar','hi':'hi','bn':'bn', 'ms':'ms','id':'id','vi':'vi','th':'th','nl':'nl','pl':'pl', 'tr':'tr','sv':'sv','no':'no','fi':'fi','da':'da','cs':'cs', 'he':'he','el':'el','hu':'hu','ro':'ro','sk':'sk','bg':'bg','uk':'uk' }; const targetLang = langMap[targetLanguage] || targetLanguage.split('-')[0]; if (targetLang.startsWith('en')) return text; const cacheKey = `trans_${text}_${targetLang}`; const cachedTranslation = localStorage.getItem(cacheKey); if (cachedTranslation) return cachedTranslation; const apis = [ { url: `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=en|${targetLang}`, method: 'GET', parseResponse: d => d && d.responseData && d.responseData.translatedText ? d.responseData.translatedText : text }, { url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`, method: 'GET', parseResponse: d => Array.isArray(d) && d[0]?.[0]?.[0] ? d[0][0][0] : text } ]; for(const api of apis){ try{ const response = await fetch(api.url, { method: api.method, headers: api.headers || {}, body: api.body || null, signal: AbortSignal.timeout(3000) }); if(response.ok){ const data = await response.json(); const translatedText = api.parseResponse(data); if (translatedText && translatedText !== text) { localStorage.setItem(cacheKey, translatedText); return translatedText; } } } catch(e){ console.warn(`Translation API ${api.url} failed:`, e); continue; } } return text; } async function saveIPCache(data) { if (!data.ip) return; data.language = await getRealTimeLanguage(); const saveData = { ip: data.ip, country: data.country || '', region: data.region || '', city: data.city || '', isp: data.isp || '', asn: data.asn || '', asn_organization: data.asn_organization || '', country_code: data.country_code || '', timezone: data.timezone || data.time_zone || '', latitude: data.latitude || data.lat || '', longitude: data.longitude || data.lon || data.lng || '', language: data.language, translatedCountry: data.translatedCountry || '', translatedRegion: data.translatedRegion || '', translatedCity: data.translatedCity || '', translatedISP: data.translatedISP || '', translatedASNOrg: data.translatedASNOrg || '' }; console.log('Saving cache with language:', saveData.language); try { const response = await fetch('/spectra/save_ip_cache.php', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(saveData) }); const result = await response.json(); console.log('Save result:', result); } catch (err) { console.error("Failed to save IP cache:", err); } } async function getCachedIPInfo(ip) { try { const resp = await fetch('/spectra/lib/ip_cache.json', {cache:'no-store'}); const cache = await resp.json(); const item = cache.find(item=>item.ip===ip); if (!item) return null; const currentLang = await getRealTimeLanguage(); //console.log('Cache check - Current language:', currentLang); //console.log('Available translations:', item.translations ? Object.keys(item.translations) : 'none'); if (item.translations && item.translations[currentLang]) { //console.log('Using translation for language:', currentLang); return { ...item, ...item.translations[currentLang], language: currentLang }; } console.log('No translation for current language, returning base data'); return { ...item, language: currentLang }; } catch (err) { console.log('Cache read error:', err); return null; } } let myAppIP = { isRefreshing:false, lastGeoData:null, ipApis:[ {url:'https://api.ipify.org?format=json',type:'json',key:'ip'}, {url:'https://api-ipv4.ip.sb/geoip',type:'json',key:'ip'}, {url:'https://myip.ipip.net',type:'text'}, {url:'http://pv.sohu.com/cityjson',type:'text'}, {url:'https://ipinfo.io/json',type:'json',key:'ip'}, {url:'https://ipapi.co/json/',type:'json'}, {url:'https://freegeoip.app/json/',type:'json'}, {url: ip => `https://ipapi.com/${ip}/json`, type: 'json', concurrent: true}, {url: ip => `https://api.ip.sb/geoip/${ip}`, type: 'json', concurrent: true}, {url: ip => `https://ipwho.is/${ip}`, type: 'json', concurrent: true}, {url: ip => `https://api.ipapi.is/?q=${ip}`, type: 'json', concurrent: true} ], get:(url,type)=>fetch(url,{method:'GET',cache:'no-store'}) .then(resp=>type==='text'?Promise.all([resp.ok,resp.status,resp.text(),resp.headers]):Promise.all([resp.ok,resp.status,resp.json(),resp.headers])) .then(([ok,status,data,headers])=>ok?{ok,status,data,headers}:Promise.reject(data.error)) .catch(e=>{console.error("Error fetching data:",e);throw e;}), concurrentGeoQuery: async function(ip) { const concurrentApis = this.ipApis.filter(api => api.concurrent); if (!concurrentApis.length) throw new Error("No concurrent APIs configured"); return new Promise((resolve, reject) => { let resolved = false; let completed = 0; concurrentApis.forEach(api => { const apiUrl = typeof api.url === 'function' ? api.url(ip) : api.url; this.get(apiUrl, api.type) .then(resp => { if (!resolved && resp.data) { resolved = true; resolve(resp.data); } }) .catch(err => console.warn("Concurrent API failed:", apiUrl, err)) .finally(() => { completed++; if (completed === concurrentApis.length && !resolved) { reject(new Error("All concurrent APIs failed")); } }); }); }); }, fetchIP: async ()=>{ let error; for(let api of myAppIP.ipApis){ if (api.concurrent) continue; try{ const response = await myAppIP.get(api.url,api.type); let ipData = api.type==='json' ? (api.key?response.data[api.key]:response.data) : response.data.match(/\d+\.\d+\.\d+\.\d+/)?.[0]; if(ipData){ cachedIP=ipData; document.getElementById('d-ip').innerHTML=ipData; return ipData; } }catch(e){ error=e; continue; } } throw error||new Error("All IP APIs failed"); }, Ipip: async(ip,elID)=>{ //console.log('Ipip called with IP:', ip); let cachedData = await getCachedIPInfo(ip); const currentLang = await getRealTimeLanguage(); //console.log('Current language:', currentLang); if(cachedData && cachedData.translatedCountry) { //console.log('Using cached data with translation'); myAppIP.lastGeoData = cachedData; await myAppIP.updateUI(cachedData,elID); return; } if(cachedData) { console.log('Cached data found but no translation for current language, re-translating'); const baseData = { country: cachedData.country || '', region: cachedData.region || '', city: cachedData.city || '', isp: cachedData.isp || '', asn_organization: cachedData.asn_organization || '', asn: cachedData.asn || '', country_code: cachedData.country_code || '', timezone: cachedData.timezone || '', latitude: cachedData.latitude || '', longitude: cachedData.longitude || '' }; myAppIP.lastGeoData = {...baseData}; if(translationEnabled){ console.log('Starting translation for language:', currentLang); myAppIP.lastGeoData.translatedCountry = await myAppTranslate(baseData.country); myAppIP.lastGeoData.translatedRegion = await myAppTranslate(baseData.region); myAppIP.lastGeoData.translatedCity = await myAppTranslate(baseData.city); myAppIP.lastGeoData.translatedISP = await myAppTranslate(baseData.isp); myAppIP.lastGeoData.translatedASNOrg = await myAppTranslate(baseData.asn_organization); myAppIP.lastGeoData.language = currentLang; console.log('Translation completed'); } myAppIP.lastGeoData.ip = ip; myAppIP.lastGeoData.asn = baseData.asn; myAppIP.lastGeoData.country_code = baseData.country_code; await myAppIP.updateUI(myAppIP.lastGeoData,elID); saveIPCache(myAppIP.lastGeoData); return; } try { const geoData = await this.concurrentGeoQuery(ip); cachedIP=ip; myAppIP.lastGeoData=geoData; const standardizedData = { ip: geoData.ip || ip, country: geoData.country || geoData.country_name || '', region: geoData.region || geoData.region_name || geoData.state || '', city: geoData.city || '', isp: geoData.isp || geoData.connection?.isp || geoData.org || '', asn: geoData.asn || geoData.connection?.asn || '', asn_organization: geoData.org || geoData.connection?.org || geoData.connection?.isp || '', country_code: geoData.country_code || geoData.countryCode || geoData.country_code || '', timezone: geoData.timezone || geoData.time_zone || '', latitude: geoData.latitude || geoData.lat || '', longitude: geoData.longitude || geoData.lon || geoData.lng || '' }; if(translationEnabled){ standardizedData.translatedCountry = await myAppTranslate(standardizedData.country||''); standardizedData.translatedRegion = await myAppTranslate(standardizedData.region||''); standardizedData.translatedCity = await myAppTranslate(standardizedData.city||''); standardizedData.translatedISP = await myAppTranslate(standardizedData.isp||''); standardizedData.translatedASNOrg = await myAppTranslate(standardizedData.asn_organization||''); standardizedData.language = currentLang; } await myAppIP.updateUI(standardizedData,elID); saveIPCache(standardizedData); } catch(concurrentError) { console.log("Concurrent query failed, falling back to sequential:", concurrentError); const geoApis = [ {url:`https://api.ip.sb/geoip/${ip}`,type:'json'}, {url:'https://myip.ipip.net',type:'text'}, {url:`http://ip-api.com/json/${ip}`,type:'json'}, {url:`https://ipinfo.io/${ip}/json`,type:'json'}, {url:`https://ipapi.co/${ip}/json`,type:'json'}, {url:`https://freegeoip.app/json/${ip}`,type:'json'} ]; let geoData=null,error; for(let api of geoApis){ try{ const response = await myAppIP.get(api.url,api.type); geoData=response.data; break; } catch(e){ error=e; continue; } } if(!geoData) throw error||new Error("All Geo APIs failed"); cachedIP=ip; myAppIP.lastGeoData=geoData; geoData.timezone = geoData.timezone || geoData.time_zone || ''; geoData.latitude = geoData.latitude || geoData.lat || ''; geoData.longitude = geoData.longitude || geoData.lon || geoData.lng || ''; if(translationEnabled){ geoData.translatedCountry = await myAppTranslate(geoData.country||''); geoData.translatedRegion = await myAppTranslate(geoData.region||''); geoData.translatedCity = await myAppTranslate(geoData.city||''); geoData.translatedISP = await myAppTranslate(geoData.isp||''); geoData.translatedASNOrg = await myAppTranslate(geoData.asn_organization||''); geoData.language = currentLang; } await myAppIP.updateUI(geoData,elID); saveIPCache(geoData); } }, updateUI: async(data,elID)=>{ try{ const lang = await getRealTimeLanguage(); const text = translationsIP[lang] || translationsIP['zh']; const country = translationEnabled && data.translatedCountry ? data.translatedCountry : data.country || text.unknown; const region = translationEnabled && data.translatedRegion ? data.translatedRegion : data.region || ""; const city = translationEnabled && data.translatedCity ? data.translatedCity : data.city || ""; const isp = translationEnabled && data.translatedISP ? data.translatedISP : data.isp || ""; const asnOrg = translationEnabled && data.translatedASNOrg ? data.translatedASNOrg : data.asn_organization || ""; let location = region && city && region!==city ? `${region} ${city}` : region||city||''; let displayISP = isp; let displayASN = asnOrg; if(isp && asnOrg && asnOrg.includes(isp)) displayISP=''; else if(isp && asnOrg && isp.includes(asnOrg)) displayASN=''; document.getElementById('d-ip').innerHTML = `