(function($){
'use strict';
const SBTTHeicConverter={
processing: new Set(),
queue: [],
batchTimer: null,
maxBatchSize: 10,
maxRetries: 2,
blobUrls: new Set(),
observers: [],
heicSupport: {
detected: false,
isSupported: null,
detect: function(){
if(this.detected){
return Promise.resolve(this.isSupported);
}
const cached=sessionStorage.getItem('sbtt_heic_support');
if(cached!==null){
this.detected=true;
this.isSupported=cached==='true';
return Promise.resolve(this.isSupported);
}
return new Promise((resolve)=> {
const img=new Image();
const timeout=setTimeout(()=> {
this.detected=true;
this.isSupported=false;
sessionStorage.setItem('sbtt_heic_support', 'false');
resolve(false);
}, 1000);
img.onload=()=> {
clearTimeout(timeout);
this.detected=true;
this.isSupported=img.naturalHeight > 0;
sessionStorage.setItem('sbtt_heic_support', this.isSupported ? 'true':'false');
resolve(this.isSupported);
};
img.onerror=()=> {
clearTimeout(timeout);
this.detected=true;
this.isSupported=false;
sessionStorage.setItem('sbtt_heic_support', 'false');
resolve(false);
};
img.src='data:image/heic;base64,AAAAHGZ0eXBoZWljAAAAAG1pZjFoZWljbWlhZgAAABhtZXRhAAAAAAAAACFoZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAA';
});
}},
maxQueueSize: 20,
canSaveToServer: function(){
return sbttHeicData&&sbttHeicData.isUserLoggedIn&&sbttHeicData.optimizeImages;
},
storage: {
prefix: 'sbtt_heic_',
version: '1',
maxSize: 4 * 1024 * 1024,
maxAge: 7 * 24 * 60 * 60 * 1000,
isAvailable: function(){
if(sbttHeicData&&sbttHeicData.useLocalStorage===false){
return false;
}
try {
const test='__sbtt_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e){
return false;
}},
get: function(videoId){
if(!this.isAvailable()) return null;
const key=this.prefix + videoId + '_v' + this.version;
const data=localStorage.getItem(key);
if(!data) return null;
try {
const parsed=JSON.parse(data);
if(Date.now() - parsed.timestamp > this.maxAge){
this.remove(videoId);
return null;
}
parsed.lastAccessed=Date.now();
localStorage.setItem(key, JSON.stringify(parsed));
return parsed;
} catch (e){
console.error('Failed to parse stored HEIC data:', e);
this.remove(videoId);
return null;
}},
set: function(videoId, dataUrl){
if(!this.isAvailable()) return false;
const key=this.prefix + videoId + '_v' + this.version;
const data={
dataUrl: dataUrl,
timestamp: Date.now(),
lastAccessed: Date.now(),
size: dataUrl.length
};
try {
const estimatedSize=JSON.stringify(data).length;
if(this.getCurrentSize() + estimatedSize > this.maxSize){
this.evictLRU(estimatedSize);
}
localStorage.setItem(key, JSON.stringify(data));
return true;
} catch (e){
console.warn('Failed to store HEIC data:', e);
this.evictLRU(JSON.stringify(data).length);
try {
localStorage.setItem(key, JSON.stringify(data));
return true;
} catch (e2){
console.error('Failed after eviction:', e2);
return false;
}}
},
remove: function(videoId){
if(!this.isAvailable()) return;
const key=this.prefix + videoId + '_v' + this.version;
localStorage.removeItem(key);
},
getCurrentSize: function(){
let total=0;
for (let i=0; i < localStorage.length; i++){
const key=localStorage.key(i);
if(key.startsWith(this.prefix)){
total +=localStorage.getItem(key).length;
}}
return total;
},
evictLRU: function(spaceNeeded){
const items=[];
for (let i=0; i < localStorage.length; i++){
const key=localStorage.key(i);
if(key.startsWith(this.prefix)){
try {
const data=JSON.parse(localStorage.getItem(key));
items.push({
key: key,
lastAccessed: data.lastAccessed||data.timestamp,
size: localStorage.getItem(key).length
});
} catch (e){
localStorage.removeItem(key);
}}
}
items.sort((a, b)=> a.lastAccessed - b.lastAccessed);
let freedSpace=0;
for (const item of items){
if(freedSpace >=spaceNeeded) break;
localStorage.removeItem(item.key);
freedSpace +=item.size;
}}
},
init: function(){
this.loadLibrary().then(()=> {
this.scanImages();
this.watchForNewImages();
}).catch((error)=> {
console.error('Failed to load HEIC converter library:', error);
document.querySelectorAll('img[data-heic-url]').forEach(img=> {
if(!img.dataset.heicProcessed){
img.dataset.heicProcessed='failed';
}});
});
},
loadLibrary: function(){
return new Promise((resolve, reject)=> {
if(typeof heic2any!=='undefined'){
resolve();
return;
}
const script=document.createElement('script');
script.src=sbttHeicData.heic2anyUrl;
script.onload=resolve;
script.onerror=reject;
document.head.appendChild(script);
});
},
scanImages: function(){
const images=document.querySelectorAll('.sbtt-post-item img, .sbsw-tiktok-item .sbsw-item-media img'
);
images.forEach(img=> this.maybeConvert(img));
},
watchForNewImages: function(){
const self=this;
let rescanTimer=null;
const scheduleScan=function(){
clearTimeout(rescanTimer);
rescanTimer=setTimeout(function(){
self.scanImages();
}, 150);
};
const observer=new MutationObserver(scheduleScan);
const observerOptions={
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src']
};
const containers=document.querySelectorAll('[id^="sbtt-feed"], .sb-feed-posts, [id^="sb-wall"], .sb-wall-items-wrap'
);
if(containers.length){
containers.forEach(container=> observer.observe(container, observerOptions));
}else{
observer.observe(document.body, observerOptions);
}
this.observers.push(observer);
},
cleanup: function(){
this.observers.forEach(observer=> observer.disconnect());
this.observers=[];
this.blobUrls.forEach(url=> URL.revokeObjectURL(url));
this.blobUrls.clear();
clearTimeout(this.batchTimer);
this.queue=[];
this.processing.clear();
},
maybeConvert: function(img){
const postItem=img.closest('.sbtt-post-item')||img.closest('.sbsw-tiktok-item');
if(!postItem) return;
const videoId=postItem.dataset.postId;
if(!videoId||this.processing.has(videoId)) return;
const src=img.src;
if(!src.toLowerCase().includes('.heic')||src.includes(sbttHeicData.uploadUrl)) return;
const processingAttempted=postItem.dataset.processingAttempted==='1';
const heicProcessed=img.dataset.heicProcessed;
if(heicProcessed==='converting'||heicProcessed==='converted') return;
if(!this.canSaveToServer()&&this.storage.isAvailable()){
const cached=this.storage.get(videoId);
if(cached){
img.src=cached.dataUrl;
img.dataset.heicProcessed='cached';
return;
}}
if(heicProcessed==='temp'&&processingAttempted){
if(src.startsWith('blob:')){
this.processing.add(videoId);
this.saveBlob(img, src, videoId);
}
return;
}
if(heicProcessed==='native'&&processingAttempted){
this.processing.add(videoId);
this.convert(img, img.dataset.originalHeicUrl||src, videoId, this.canSaveToServer());
return;
}
if(heicProcessed) return;
img.dataset.originalHeicUrl=src;
this.heicSupport.detect().then(isSupported=> {
if(isSupported){
img.dataset.heicProcessed='native';
if(processingAttempted&&!this.processing.has(videoId)){
this.processing.add(videoId);
this.convert(img, src, videoId, this.canSaveToServer());
}}else{
if(!this.processing.has(videoId)){
this.processing.add(videoId);
this.convert(img, src, videoId, processingAttempted&&this.canSaveToServer());
}}
});
},
saveBlob: function(img, blobUrl, videoId){
fetch(blobUrl)
.then(res=> res.blob())
.then(blob=> this.blobToBase64(blob))
.then(base64=> {
if(this.queue.length >=this.maxQueueSize){
console.warn('Queue full after blob conversion, skipping save for ' + videoId);
this.processing.delete(videoId);
return;
}
this.queue.push({
videoId: videoId,
imageData: base64,
originalUrl: img.dataset.originalHeicUrl,
imgId: img.id||null,
blobUrl: blobUrl,
retryCount: 0
});
this.scheduleBatch();
img.dataset.heicProcessed='converting';
})
.catch(()=> {
this.processing.delete(videoId);
img.dataset.heicProcessed='failed';
});
},
isValidHeicUrl: function(url){
try {
const parsedUrl=new URL(url);
if(parsedUrl.protocol!=='https:'&&parsedUrl.protocol!=='http:'){
return false;
}
return true;
} catch (e){
return false;
}},
createBlobUrl: function(blob){
const url=URL.createObjectURL(blob);
this.blobUrls.add(url);
return url;
},
revokeBlobUrl: function(url){
if(url&&this.blobUrls.has(url)){
URL.revokeObjectURL(url);
this.blobUrls.delete(url);
}},
validateHeic: function(response, blob){
const contentType=response.headers.get('content-type');
if(contentType&&!contentType.includes('heic')&&!contentType.includes('heif')&&!contentType.includes('octet-stream')){
return Promise.reject('Invalid content type: ' + contentType);
}
return blob.slice(0, 12).arrayBuffer().then(buffer=> {
const bytes=new Uint8Array(buffer);
if(bytes[4]!==0x66||bytes[5]!==0x74||bytes[6]!==0x79||bytes[7]!==0x70){
return Promise.reject('Invalid HEIC file signature');
}
const brand=String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]);
const validBrands=['heic', 'heix', 'hevc', 'hevx', 'mif1'];
if(!validBrands.includes(brand)){
return Promise.reject('Invalid HEIC brand: ' + brand);
}
return blob;
});
},
convert: function(img, heicUrl, videoId, saveLocally){
if(!heicUrl.startsWith('blob:')&&!this.isValidHeicUrl(heicUrl)){
console.error('Invalid or unsafe HEIC URL:', heicUrl);
this.processing.delete(videoId);
img.dataset.heicProcessed='failed';
return;
}
img.dataset.heicProcessed='converting';
this.showLoading(img);
fetch(heicUrl)
.then(res=> {
if(!res.ok){
return Promise.reject('HTTP error: ' + res.status);
}
return res.blob().then(blob=> this.validateHeic(res, blob));
})
.then(blob=> heic2any({ blob: blob, toType: 'image/jpeg', quality: 0.9 }))
.then(jpeg=> {
const url=this.createBlobUrl(jpeg);
img.src=url;
this.hideLoading(img);
if(!saveLocally&&this.storage.isAvailable()){
return this.blobToBase64(jpeg).then(base64=> {
const saved=this.storage.set(videoId, base64);
if(saved){
img.dataset.heicProcessed='cached';
console.log('Saved to localStorage for ' + videoId);
}else{
img.dataset.heicProcessed='temp';
}
this.processing.delete(videoId);
});
}
if(saveLocally){
return this.blobToBase64(jpeg).then(base64=> {
if(this.queue.length >=this.maxQueueSize){
console.warn('Queue full after conversion, skipping upload for ' + videoId);
img.dataset.heicProcessed='temp';
this.processing.delete(videoId);
return;
}
this.queue.push({
videoId: videoId,
imageData: base64,
originalUrl: heicUrl,
imgId: img.id||null,
blobUrl: url,
retryCount: 0
});
this.scheduleBatch();
});
}else{
img.dataset.heicProcessed='temp';
this.processing.delete(videoId);
}})
.catch(error=> {
this.hideLoading(img);
this.processing.delete(videoId);
img.dataset.heicProcessed='failed';
console.error('HEIC conversion failed for ' + videoId + ':', error);
});
},
scheduleBatch: function(){
clearTimeout(this.batchTimer);
if(this.queue.length >=this.maxBatchSize){
this.uploadBatch();
}else{
this.batchTimer=setTimeout(()=> this.uploadBatch(), 500);
}},
uploadBatch: function(){
if(this.queue.length===0) return;
const batch=this.queue.splice(0, this.maxBatchSize);
const data=batch.map(item=> ({
video_id: item.videoId,
image_data: item.imageData,
original_url: item.originalUrl
}));
fetch(sbttHeicData.ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
action: 'sbtt_save_converted_heic_batch',
nonce: sbttHeicData.nonce,
images: JSON.stringify(data)
})
})
.then(res=> res.json())
.then(response=> {
const batchMap=new Map(batch.map(item=> [item.videoId, item]));
const processedIds=new Set();
if(response.success&&response.data&&response.data.results){
response.data.results.forEach(result=> {
const item=batchMap.get(result.video_id);
if(!item) return;
processedIds.add(result.video_id);
const img=this.getImageElement(item.videoId, item.imgId);
if(result.success){
this.revokeBlobUrl(item.blobUrl);
if(img){
img.src=result.local_url;
img.dataset.heicProcessed='converted';
this.hideLoading(img);
}
this.processing.delete(item.videoId);
}else{
this.handleUploadFailure(item, result.message||'Unknown error');
}});
}
batchMap.forEach((item, videoId)=> {
if(!processedIds.has(videoId)){
this.handleUploadFailure(item, 'Server did not process request');
}});
if(this.queue.length > 0){
this.scheduleBatch();
}})
.catch(error=> {
console.error('Batch upload failed:', error);
batch.forEach(item=> {
this.handleUploadFailure(item, 'Network error');
});
});
},
getImageElement: function(videoId, imgId){
if(imgId){
const img=document.getElementById(imgId);
if(img) return img;
}
var postItem=document.querySelector('.sbtt-post-item[data-post-id="' + videoId + '"]');
if(postItem) return postItem.querySelector('img');
postItem=document.querySelector('.sbsw-tiktok-item[data-post-id="' + videoId + '"]');
return postItem ? postItem.querySelector('.sbsw-item-media img'):null;
},
handleUploadFailure: function(item, errorMessage){
item.retryCount=(item.retryCount||0) + 1;
if(item.retryCount <=this.maxRetries){
if(this.queue.length < this.maxQueueSize){
console.log('Retrying upload for ' + item.videoId + ' (attempt ' + item.retryCount + ')');
this.queue.push(item);
this.scheduleBatch();
}else{
console.warn('Queue full, cannot retry ' + item.videoId);
this.cleanupFailedItem(item);
}}else{
console.error('Upload failed for ' + item.videoId + ' after ' + this.maxRetries + ' retries:', errorMessage);
this.cleanupFailedItem(item);
}},
cleanupFailedItem: function(item){
const img=this.getImageElement(item.videoId, item.imgId);
if(img){
this.hideLoading(img);
if(img.src!==item.blobUrl){
this.revokeBlobUrl(item.blobUrl);
}
img.dataset.heicProcessed='temp';
}else{
this.revokeBlobUrl(item.blobUrl);
}
this.processing.delete(item.videoId);
},
blobToBase64: function(blob){
return new Promise((resolve, reject)=> {
const reader=new FileReader();
reader.onloadend=()=> resolve(reader.result);
reader.onerror=reject;
reader.readAsDataURL(blob);
});
},
showLoading: function(img){
if(!img) return;
img.style.opacity='0.5';
const wrapper=img.closest('.sb-post-item-image')||img.closest('.sbsw-item-media');
if(wrapper&&!wrapper.querySelector('.sbtt-converting-badge')){
const badge=document.createElement('div');
badge.className='sbtt-converting-badge';
badge.innerHTML='<span class="sbtt-spinner"></span> Converting...';
wrapper.style.position='relative';
wrapper.appendChild(badge);
}},
hideLoading: function(img){
if(!img) return;
img.style.opacity='1';
const wrapper=img.closest('.sb-post-item-image')||img.closest('.sbsw-item-media');
if(wrapper){
const badge=wrapper.querySelector('.sbtt-converting-badge');
if(badge) badge.remove();
}}
};
$(document).ready(function(){
if(typeof sbttHeicData!=='undefined'){
SBTTHeicConverter.init();
}});
$(document).on('sbtt_feed_loaded', function(){
SBTTHeicConverter.scanImages();
});
$(window).on('beforeunload', function(){
SBTTHeicConverter.cleanup();
});
})(jQuery);