document.getElementById(id);
const defaultAvatar = (seed='flikzee') => `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(seed)}`;
const getUsername = (user) => (user?.displayName || 'creator').trim().toLowerCase().replace(/[^a-z0-9_]+/g,'_').replace(/^_+|_+$/g,'').slice(0,24) || 'creator';
const formatCount = (n=0) => {
const x = Number(n) || 0;
if (x >= 1000000) return (x/1000000).toFixed(x >= 10000000 ? 0 : 1) + 'M';
if (x >= 1000) return (x/1000).toFixed(x >= 10000 ? 0 : 1) + 'K';
return String(x);
};
const escapeHtml = (s='') => String(s).replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
const timestampMillis = (value) => value?.toMillis ? value.toMillis() : (value?.seconds ? value.seconds * 1000 : 0);
const safeUrl = (url, fallback='') => typeof url === 'string' && /^https?:\/\//i.test(url) ? url : fallback;
function normalizeHashtags(raw=''){
const parts=String(raw||'').split(/[\s,]+/).map(x=>x.trim()).filter(Boolean);
const out=[]; const seen=new Set();
parts.forEach(x=>{let tag=x.startsWith('#')?x:'#'+x; tag=tag.replace(/[^#\p{L}\p{N}_]+/gu,''); if(tag.length>1){const key=tag.toLowerCase();if(!seen.has(key)){seen.add(key);out.push(tag);}}});
return out;
}
function renderHashtagHtml(text='', hashtags=[]){
let html=escapeHtml(text);
html=html.replace(/#[\p{L}\p{N}_]+/gu,m=>`${m} `);
if(hashtags?.length){
const present=new Set((String(text||'').match(/#[\p{L}\p{N}_]+/gu)||[]).map(x=>x.toLowerCase()));
const extra=hashtags.filter(h=>h && !present.has(String(h).toLowerCase())).map(h=>`${escapeHtml(h.startsWith('#')?h:'#'+h)} `).join(' ');
if(extra) html += ' '+extra;
}
return html;
}
function renderHashtagChips(raw,containerId){
const c=$(containerId); if(!c)return; c.innerHTML=normalizeHashtags(raw).map(h=>`${escapeHtml(h)} `).join('');
}
window.handleHashtagInput=(value)=>renderHashtagChips(value,'hashtagPreview');
window.renderEditHashtagPreview=(value)=>renderHashtagChips(value,'editHashtagPreview');
function authErrorMessage(err) {
const map = {
'auth/email-already-in-use':'This email is already registered.',
'auth/invalid-email':'Please enter a valid email address.',
'auth/weak-password':'Password is too weak. Use at least 6 characters.',
'auth/invalid-credential':'Email/password is incorrect.',
'auth/user-not-found':'No account was found for this email.',
'auth/popup-closed-by-user':'Google sign-in was cancelled.',
'auth/popup-blocked':'Your browser blocked the Google sign-in popup.',
'auth/operation-not-allowed':'Enable Google sign-in in Firebase Authentication first.'
};
return map[err?.code] || err?.message || 'Something went wrong. Please try again.';
}
function showAuthPanel(mode='login') {
$('authModal').style.display = 'flex';
document.body.classList.add('modal-open');
$('loginFormWrap').style.display = mode === 'login' ? 'block' : 'none';
$('signupFormWrap').style.display = mode === 'signup' ? 'block' : 'none';
$('verificationWrap').style.display = mode === 'verify' ? 'block' : 'none';
$('authTitle').innerText = mode === 'signup' ? 'Create FlikZee Account' : mode === 'verify' ? 'Email Verification' : 'Sign in to FlikZee';
}
window.toggleAuth = showAuthPanel;
window.closeAuthModal = () => {
$('authModal').style.display = 'none';
document.body.classList.remove('modal-open');
};
async function ensureUserDoc(user, extra={}) {
if (!user) return null;
const userRef = doc(db, 'users', user.uid);
const existing = await getDoc(userRef);
const existingData = existing.exists() ? existing.data() : {};
const base = {
uid: user.uid,
displayName: existingData.displayName || user.displayName || extra.fullName || 'Creator',
username: existingData.username || extra.username || getUsername(user),
avatar: existingData.avatar || user.photoURL || defaultAvatar(user.uid),
bio: existingData.bio || extra.bio || 'FlikZee Creator 🎬',
email: user.email || existingData.email || '',
dob: existingData.dob || extra.dob || '',
followers: Array.isArray(existingData.followers) ? existingData.followers : [],
following: Array.isArray(existingData.following) ? existingData.following : [],
totalViews: Number(existingData.totalViews) || 0,
coins: Number(existingData.coins) || 50,
rewardPoints: Number(existingData.rewardPoints) || 0
};
if (!existing.exists()) base.createdAt = serverTimestamp();
await setDoc(userRef, base, { merge:true });
window.profileCache[user.uid] = { ...existingData, ...base };
return userRef;
}
async function activateVerifiedUser(user) {
window.currentUser = user;
window.userLoggedIn = true;
$('authModal').style.display = 'none';
document.body.classList.remove('modal-open');
try { await ensureUserDoc(user); } catch(e) { console.error('User profile sync:', e); }
startNotificationListener(user.uid);
startWalletListeners(user.uid);
$('nav-profile-btn').querySelector('.nav-text').innerText = 'Profile';
renderLiveHub();
updateAdminVisibility();
}
onAuthStateChanged(auth, async (user) => {
if (!user) {
window.currentUser = null;
window.userLoggedIn = false;
if (notificationUnsubscribe) { notificationUnsubscribe(); notificationUnsubscribe = null; }
notificationCache = [];
if(walletUnsubscribe){walletUnsubscribe();walletUnsubscribe=null;}
if(walletTxUnsubscribe){walletTxUnsubscribe();walletTxUnsubscribe=null;}
$('inboxBadge').style.display = 'none';
if($('adminCenterSetting')) $('adminCenterSetting').style.display='none';
return;
}
window.currentUser = user;
if (user.emailVerified) {
await activateVerifiedUser(user);
authUserBootstrapped = true;
return;
}
window.userLoggedIn = false;
$('verificationEmail').innerText = user.email || '';
showAuthPanel('verify');
});
window.handleGoogleLogin = async () => {
try {
const result = await signInWithPopup(auth, googleProvider);
const user = result.user;
await ensureUserDoc(user, { fullName:user.displayName || 'Creator' });
showToastMsg(`Welcome ${user.displayName || 'to FlikZee'}!`);
} catch(err) { alert(authErrorMessage(err)); }
};
window.handleSignup = async (e) => {
e.preventDefault();
const fullName = $('regName').value.trim();
const dob = $('regDob').value;
const email = $('regEmail').value.trim();
const password = $('regPassword').value;
const password2 = $('regPassword2').value;
if (password !== password2) return alert('Passwords do not match.');
if (!fullName || !dob || !email || !password) return alert('Please complete all fields.');
const btn = $('signupBtn');
btn.disabled = true; btn.innerText = 'Creating...';
try {
const cred = await createUserWithEmailAndPassword(auth, email, password);
await updateProfile(cred.user, { displayName: fullName });
await ensureUserDoc(cred.user, { fullName, dob, username:getUsername({displayName:fullName}), bio:'FlikZee Creator 🎬' });
await sendEmailVerification(cred.user);
$('verificationEmail').innerText = email;
showAuthPanel('verify');
showToastMsg('Verification email sent.');
} catch(err) {
alert(authErrorMessage(err));
} finally {
btn.disabled = false; btn.innerText = 'Create Account';
}
};
window.resendVerification = async () => {
const user = auth.currentUser;
if (!user) return showAuthPanel('login');
try { await sendEmailVerification(user); showToastMsg('Verification email sent again.'); }
catch(err) { alert(authErrorMessage(err)); }
};
window.checkVerificationStatus = async () => {
const user = auth.currentUser;
if (!user) return showAuthPanel('login');
try {
await reload(user);
if (auth.currentUser.emailVerified) {
await activateVerifiedUser(auth.currentUser);
showToastMsg('Email verified successfully!');
} else {
showToastMsg('Email is not verified yet. Please open the verification link first.');
}
} catch(err) { alert(authErrorMessage(err)); }
};
window.signOutForAuth = async () => {
try { await signOut(auth); showAuthPanel('login'); }
catch(err) { console.error(err); }
};
window.handleLogin = async (e) => {
e.preventDefault();
const email = $('logEmail').value.trim();
const password = $('logPassword').value;
const btn = $('loginBtn');
btn.disabled = true; btn.innerText = 'Signing in...';
try {
const cred = await signInWithEmailAndPassword(auth, email, password);
await reload(cred.user);
if (!cred.user.emailVerified) {
window.currentUser = cred.user;
window.userLoggedIn = false;
$('verificationEmail').innerText = email;
showAuthPanel('verify');
showToastMsg('Please verify your email before continuing.');
}
} catch(err) { alert(authErrorMessage(err)); }
finally { btn.disabled = false; btn.innerText = 'Sign In'; }
};
window.logoutApp = async () => {
try {
if (notificationUnsubscribe) { notificationUnsubscribe(); notificationUnsubscribe = null; }
if (walletUnsubscribe) { walletUnsubscribe(); walletUnsubscribe = null; }
if (walletTxUnsubscribe) { walletTxUnsubscribe(); walletTxUnsubscribe = null; }
await signOut(auth);
$('settingsMenu').classList.remove('open');
switchTab('home');
showToastMsg('Logged out.');
} catch(err) { alert(authErrorMessage(err)); }
};
function startNotificationListener(uid) {
if (notificationUnsubscribe) notificationUnsubscribe();
const notifRef = collection(db, 'users', uid, 'notifications');
const q = query(notifRef, orderBy('createdAt','desc'), limit(100));
notificationUnsubscribe = onSnapshot(q, snapshot => {
notificationCache = snapshot.docs.map(d => ({id:d.id, ...d.data()}));
renderNotifications(notificationCache);
const unread = notificationCache.filter(n => !n.read).length;
const b = $('inboxBadge');
b.innerText = unread > 99 ? '99+' : String(unread);
b.style.display = unread ? 'flex' : 'none';
}, err => console.error('Notification listener:', err));
}
function formatNotificationTime(value) {
const ms = timestampMillis(value);
if (!ms) return 'now';
const diff = Math.max(0, Date.now()-ms);
const mins = Math.floor(diff/60000);
if (mins < 1) return 'now';
if (mins < 60) return `${mins}m`;
const hrs = Math.floor(mins/60);
if (hrs < 24) return `${hrs}h`;
const days = Math.floor(hrs/24);
return `${days}d`;
}
function renderNotifications(notifs) {
const container = $('notificationsContainer');
if (!notifs.length) {
container.innerHTML = 'No notifications yet. Likes, comments, tips and follows will appear here in real-time.
';
return;
}
const frag = document.createDocumentFragment();
notifs.forEach(n => {
const row = document.createElement('div');
row.className = 'inbox-item' + (n.read ? '' : ' notification-unread');
const avatar = document.createElement('img');
avatar.className = 'inbox-avatar'; avatar.src = safeUrl(n.fromUserAvatar, defaultAvatar(n.fromUserId || 'user')); avatar.alt='';
avatar.onclick = () => { if(n.fromUserId) openCreatorProfile(n.fromUserId); };
const wrap = document.createElement('div'); wrap.className='inbox-item-text';
const name = document.createElement('h4'); name.innerText='@' + (n.fromUsername || 'flikzee_user');
name.onclick = () => { if(n.fromUserId) openCreatorProfile(n.fromUserId); };
const msg = document.createElement('p'); msg.innerText=n.message || 'You have a new notification.';
const time = document.createElement('div'); time.className='notification-time'; time.innerText=formatNotificationTime(n.createdAt);
wrap.append(name,msg,time); row.append(avatar,wrap); frag.append(row);
});
container.innerHTML=''; container.appendChild(frag);
}
async function markNotificationsRead() {
if (!window.userLoggedIn || !notificationCache.some(n => !n.read)) return;
try {
await Promise.all(notificationCache.filter(n=>!n.read).slice(0,100).map(n => updateDoc(doc(db,'users',window.currentUser.uid,'notifications',n.id), {read:true})));
} catch(err) { console.error('Mark notifications:', err); }
}
function startCategoryListener() {
if (categoryUnsubscribe) return;
const q = query(collection(db,'categories'), orderBy('order','asc'), limit(30));
categoryUnsubscribe = onSnapshot(q, snapshot => {
if (snapshot.empty) {
window.categoryData = DEFAULT_CATEGORIES.slice();
} else {
window.categoryData = snapshot.docs.map(d => ({id:d.id, ...d.data()}));
}
renderCategories();
renderDiscoverSearch($('searchInput').value);
}, err => {
console.warn('Categories listener unavailable, using defaults:', err);
window.categoryData = DEFAULT_CATEGORIES.slice();
renderCategories();
});
}
window.loadFeed = function() {
if (feedUnsubscribe) return;
const q = query(collection(db,'videos'), orderBy('timestamp','desc'), limit(FEED_LIMIT));
feedUnsubscribe = onSnapshot(q, snapshot => {
const docs = snapshot.docs.map(d => ({id:d.id, ...d.data()}));
if (snapshot.empty) {
window.allVideos = [];
feedInitialized = true;
usingFallbackFeed = false;
renderEmptyFeed();
return;
}
if (usingFallbackFeed || !feedInitialized) {
$('feed').innerHTML='';
usingFallbackFeed=false;
feedInitialized=true;
window.allVideos=docs;
docs.forEach(v=>appendVideoToFeed(v));
} else {
const byId = new Map(window.allVideos.map(v=>[v.id,v]));
snapshot.docChanges().forEach(change => {
const v = {id:change.doc.id, ...change.doc.data()};
if (change.type === 'removed') {
byId.delete(change.doc.id);
const old = $('feed').querySelector(`[data-id="${CSS.escape(change.doc.id)}"]`);
old?.remove();
return;
}
byId.set(change.doc.id, v);
if (change.type === 'added') insertVideoCardAt(v, change.newIndex);
else if (change.type === 'modified') {
const w = $('feed').querySelector(`[data-id="${CSS.escape(change.doc.id)}"]`);
if (!w) insertVideoCardAt(v, change.newIndex); else updateVideoCardUI(v,w);
if (w && change.newIndex !== change.oldIndex) moveVideoCard(w,change.newIndex);
}
});
window.allVideos = snapshot.docs.map(d=>({id:d.id,...d.data()}));
}
setupObserver();
renderDiscoverSearch($('searchInput').value);
}, err => console.error('Feed listener:', err));
};
function renderEmptyFeed() {
const feed = $('feed');
feed.innerHTML = `🎬
Welcome to FlikZee Be the first creator to publish a short video on FlikZee and earn creator rewards.
Create First Video `;
}
function appendVideoToFeed(video) { insertVideoCardAt(video, $('feed').children.length); }
function insertVideoCardAt(video, index) {
const feed = $('feed');
const existing = feed.querySelector(`[data-id="${CSS.escape(video.id)}"]`);
if (existing) { updateVideoCardUI(video,existing); return existing; }
const wrapper = document.createElement('div');
wrapper.className='video-wrapper'; wrapper.dataset.id=video.id;
const mediaUrl=safeUrl(video.mediaUrl || video.videoUrl);
let mediaEl;
if((video.mediaType||'video')==='text'){
mediaEl=document.createElement('div');
mediaEl.style.cssText='width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:32px;text-align:center;font-size:1.35rem;font-weight:800;line-height:1.5;background:radial-gradient(circle at center,#1f1024,#080808);color:#fff;';
mediaEl.innerText=video.caption||'FlikZee Post';
} else if((video.mediaType||'video')==='image'){
mediaEl=document.createElement('img');
mediaEl.src=mediaUrl; mediaEl.alt=video.title||'FlikZee post'; mediaEl.loading='lazy'; mediaEl.decoding='async';
mediaEl.style.cssText='width:100%;height:100%;object-fit:cover;';
mediaEl.style.filter=FILTERS[video.videoFilter] || FILTERS.Original;
mediaEl.addEventListener('click',()=>openSingleMedia(mediaUrl,video.id));
} else {
mediaEl=document.createElement('video');
mediaEl.src=mediaUrl; mediaEl.playsInline=true; mediaEl.loop=true; mediaEl.preload='none'; mediaEl.muted=window.isGlobalMuted;
mediaEl.style.filter=FILTERS[video.videoFilter] || FILTERS.Original;
mediaEl.addEventListener('click', () => {
if (mediaEl.paused) {
pauseAllFeedVideosExcept(mediaEl);
mediaEl.play().catch(()=>{});
if(wrapper._soundAudio && !window.isGlobalMuted) wrapper._soundAudio.play().catch(()=>{});
wrapper.querySelector('.play-pause-icon').classList.remove('show');
} else {
mediaEl.pause();
if(wrapper._soundAudio){wrapper._soundAudio.pause(); wrapper._soundAudio.currentTime=0;}
wrapper.querySelector('.play-pause-icon').classList.add('show');
}
});
mediaEl.addEventListener('dblclick', (e) => { e.preventDefault(); const likeBtn=wrapper.querySelector('[data-action="like"]'); toggleLike(video.id,likeBtn); });
}
let soundAudio=null;
const resolvedSoundUrl=video.soundUrl||((video.soundId||video.soundCategory)?getSoundUrl(video.soundId,video.soundCategory):'');
if(resolvedSoundUrl && video.soundCategory !== 'Original'){
soundAudio=document.createElement('audio');
soundAudio.src=safeUrl(resolvedSoundUrl);
soundAudio.loop=true;
soundAudio.preload='none';
soundAudio.volume=.78;
soundAudio.muted=window.isGlobalMuted;
soundAudio.style.display='none';
wrapper._soundAudio=soundAudio;
}
const openCreatorHandler = (e) => {
e.preventDefault();
e.stopPropagation();
if (video.userId) openCreatorProfile(video.userId);
};
const playIcon=document.createElement('div'); playIcon.className='play-pause-icon'; playIcon.innerText='▶';
const overlay=document.createElement('div'); overlay.className='overlay-ui';
const info=document.createElement('div'); info.className='text-info';
const user=document.createElement('span'); user.className='username';
user.innerHTML = '@' + escapeHtml(video.username||'creator') + ((video.isBoosted || video.sponsoredUrl) ? ' ' : '');
user.onclick = openCreatorHandler;
const title=document.createElement('div'); title.className='media-title'; title.innerText=video.title||''; title.style.display=video.title?'block':'none';
const desc=document.createElement('p'); desc.className='description'; desc.innerHTML=renderHashtagHtml(video.caption||'', video.hashtags||[]);
const music=document.createElement('div'); music.className='music-bar'; music.innerText='🎵 '+(video.soundName || ('Original Sound - @'+(video.username||'creator')));
info.append(user,title,desc,music);
if (video.sponsoredUrl) {
const cta = document.createElement('a');
cta.className = 'sponsored-cta-btn';
cta.href = safeUrl(video.sponsoredUrl, '#');
cta.target = '_blank';
cta.rel = 'noopener noreferrer';
cta.innerText = 'Visit Partner Link ↗';
cta.onclick = (e) => e.stopPropagation();
info.appendChild(cta);
}
const actions=document.createElement('div'); actions.className='action-buttons';
const profileWrap=document.createElement('div'); profileWrap.className='profile-avatar-wrap'; profileWrap.title='Open creator profile';
profileWrap.onclick = openCreatorHandler;
const profileImg=document.createElement('img'); profileImg.className='profile-img-btn'; profileImg.src=safeUrl(video.userAvatar,defaultAvatar(video.username||video.userId)); profileImg.alt=''; profileImg.loading='lazy';
profileImg.onclick = openCreatorHandler;
const followBadge=document.createElement('div'); followBadge.className='profile-follow-badge'; followBadge.innerText='+';
profileWrap.append(profileImg,followBadge);
const commentTotal = Number(video.commentsCount) || 0;
const likeBtn=createActionButton('like','♥',video.likes?.length||0,()=>toggleLike(video.id,likeBtn));
const commentBtn=createActionButton('comment','💬',commentTotal,()=>openCommentsModal(video.id));
const shareBtn=createActionButton('share','↗',video.shares||0,()=>shareVideo(video.id), 'share-btn');
actions.append(profileWrap,likeBtn,commentBtn,shareBtn);
overlay.append(info,actions);
const viewPill=document.createElement('div'); viewPill.className='video-view-pill'; viewPill.innerText=`${formatCount(video.views||0)} views`; overlay.appendChild(viewPill);
if(window.currentUser?.uid===video.userId){
const more=document.createElement('button'); more.type='button'; more.className='media-more-btn'; more.innerText='⋯'; more.title='More options';
const menu=document.createElement('div'); menu.className='video-menu';
menu.innerHTML='✏️ Edit video ↗ Share 🗑️ Delete ';
more.onclick=(e)=>{e.stopPropagation();document.querySelectorAll('.video-menu.open').forEach(x=>x.classList.remove('open'));menu.classList.toggle('open');};
const [editBtn,shareMenuBtn,deleteBtn]=menu.children;
editBtn.onclick=(e)=>{e.stopPropagation();openEditVideo(video.id);};
shareMenuBtn.onclick=(e)=>{e.stopPropagation();menu.classList.remove('open');shareVideo(video.id);};
deleteBtn.onclick=(e)=>{e.stopPropagation();menu.classList.remove('open');deleteVideo(video.id);};
wrapper.append(more,menu);
}
wrapper.append(mediaEl,playIcon,overlay);
if(soundAudio) wrapper.appendChild(soundAudio);
updateVideoCardUI(video,wrapper);
const ref=feed.children[index] || null;
if (ref) feed.insertBefore(wrapper,ref); else feed.appendChild(wrapper);
return wrapper;
}
function createActionButton(type, symbol, count, handler, extraClass='') {
const btn=document.createElement('button'); btn.className='action-btn '+extraClass; btn.type='button'; btn.dataset.action=type;
btn.onclick=(e)=>{ e.stopPropagation(); handler(); };
const circle=document.createElement('div'); circle.className='action-icon-circle';
if(type==='like') circle.innerHTML=' ';
else circle.innerText=symbol;
const span=document.createElement('span'); span.style='font-size:12px;font-weight:700;'; span.innerText=formatCount(count);
btn.append(circle,span); return btn;
}
function updateVideoCardUI(video, wrapper) {
const likeBtn=wrapper.querySelector('[data-action="like"]');
const commentBtn=wrapper.querySelector('[data-action="comment"]');
const shareBtn=wrapper.querySelector('[data-action="share"]');
const currentUid=window.currentUser?.uid;
likeBtn?.classList.toggle('liked', !!currentUid && (video.likes||[]).includes(currentUid));
if(likeBtn) likeBtn.querySelector('span').innerText=formatCount((video.likes||[]).length);
if(commentBtn) commentBtn.querySelector('span').innerText=formatCount(Number(video.commentsCount)||0);
if(shareBtn) shareBtn.querySelector('span').innerText=formatCount(video.shares||0);
const views=wrapper.querySelector('.video-view-pill'); if(views) views.innerText=`${formatCount(video.views||0)} views`;
const media=wrapper.querySelector('video, img');
if(media){
const nextUrl = safeUrl(video.mediaUrl || video.videoUrl, media.src);
if (media.src !== nextUrl && nextUrl) media.src = nextUrl;
media.style.filter=FILTERS[video.videoFilter] || FILTERS.Original;
if(media.tagName==='VIDEO') media.muted=window.isGlobalMuted;
}
const openCreatorHandler = (e) => {
e.preventDefault();
e.stopPropagation();
if (video.userId) openCreatorProfile(video.userId);
};
const profileWrap=wrapper.querySelector('.profile-avatar-wrap');
if(profileWrap) profileWrap.onclick=openCreatorHandler;
const img=wrapper.querySelector('.profile-img-btn');
if(img){ img.src=safeUrl(video.userAvatar,defaultAvatar(video.username||video.userId)); img.onclick=openCreatorHandler; }
const user=wrapper.querySelector('.username');
if(user){
user.innerHTML = '@' + escapeHtml(video.username||'creator') + ((video.isBoosted || video.sponsoredUrl) ? ' ' : '');
user.onclick = openCreatorHandler;
}
const title=wrapper.querySelector('.media-title'); if(title){title.innerText=video.title||'';title.style.display=video.title?'block':'none';}
const desc=wrapper.querySelector('.description'); if(desc) desc.innerHTML=renderHashtagHtml(video.caption||'', video.hashtags||[]);
const music=wrapper.querySelector('.music-bar'); if(music) music.innerText='🎵 '+(video.soundName || ('Original Sound - @'+(video.username||'creator')));
}
function moveVideoCard(wrapper,index) {
const feed=$('feed'); const nodes=Array.from(feed.children).filter(n=>n!==wrapper); const ref=nodes[index] || null;
if(ref) feed.insertBefore(wrapper,ref); else feed.appendChild(wrapper);
}
async function recordVideoView(videoId, video){
if(!videoId || !window.userLoggedIn || !window.currentUser) return;
if(!video || video.userId===window.currentUser.uid) return;
if(window.sessionViewedVideos.has(videoId)) return;
const day=new Date().toISOString().slice(0,10);
const storageKey=`flikzee_view_${window.currentUser.uid}_${videoId}_${day}`;
if(localStorage.getItem(storageKey)) { window.sessionViewedVideos.add(videoId); return; }
window.sessionViewedVideos.add(videoId);
localStorage.setItem(storageKey,'1');
try {
await updateDoc(doc(db,'videos',videoId),{views:increment(1)});
if(video.userId) {
await setDoc(doc(db,'users',video.userId),{totalViews:increment(1), rewardPoints:increment(1)},{merge:true});
}
} catch(err){
console.warn('View count update:',err);
window.sessionViewedVideos.delete(videoId);
localStorage.removeItem(storageKey);
}
}
function pauseAllFeedVideosExcept(activeVideoEl = null) {
document.querySelectorAll('#feed .video-wrapper').forEach(w => {
const v = w.querySelector('video');
if (v && v !== activeVideoEl) {
if (!v.paused) v.pause();
v.preload = 'none';
}
if (w._soundAudio && (!activeVideoEl || w.querySelector('video') !== activeVideoEl)) {
w._soundAudio.pause();
w._soundAudio.currentTime = 0;
}
});
}
let feedObserver;
function setupObserver() {
if (feedObserver) feedObserver.disconnect();
feedObserver=new IntersectionObserver(entries=>entries.forEach(entry=>{
const media=entry.target.querySelector('video, img');
const audio=entry.target._soundAudio;
if(entry.isIntersecting){
if(media && media.tagName==='VIDEO'){
pauseAllFeedVideosExcept(media);
media.preload = 'metadata';
media.muted = window.isGlobalMuted;
media.play().catch(()=>{});
entry.target.querySelector('.play-pause-icon')?.classList.remove('show');
} else {
pauseAllFeedVideosExcept(null);
}
if(audio && !window.isGlobalMuted) audio.play().catch(()=>{});
const id=entry.target.dataset.id;
if(id){
const currentVideo=window.allVideos.find(x=>x.id===id);
recordVideoView(id,currentVideo);
}
} else {
if(media && media.tagName==='VIDEO'){
media.pause();
media.currentTime=0;
media.preload='none';
}
if(audio){audio.pause();audio.currentTime=0;}
}
}),{threshold:0.60});
document.querySelectorAll('#feed .video-wrapper').forEach(w=>feedObserver.observe(w));
}
function spawnHeartEffect(btn){
if(!btn)return;
const r=btn.getBoundingClientRect(); const burst=document.createElement('div'); burst.className='heart-burst'; burst.textContent='❤'; burst.style.left=(r.left+r.width/2)+'px'; burst.style.top=(r.top+r.height/2)+'px'; document.body.appendChild(burst); setTimeout(()=>burst.remove(),500);
for(let i=0;i<4;i++){const h=document.createElement('div');h.className='heart-fly';h.textContent=i%2?'💗':'❤';h.style.left=(r.left+r.width/2)+'px';h.style.top=(r.top+4)+'px';h.style.setProperty('--dx',`${(Math.random()*90-45).toFixed(0)}px`);h.style.setProperty('--rot',`${(Math.random()*50-25).toFixed(0)}deg`);document.body.appendChild(h);setTimeout(()=>h.remove(),950);}
}
window.toggleLike=async (videoId,btn)=>{
if(!window.userLoggedIn){showAuthPanel('login');return;}
const uid=window.currentUser.uid;
const video=window.allVideos.find(v=>v.id===videoId);
if(!video) return;
const liked=(video.likes||[]).includes(uid);
spawnHeartEffect(btn);
const nextLikes=liked?(video.likes||[]).filter(x=>x!==uid):[...(video.likes||[]),uid];
video.likes=nextLikes;
const wrapper=$('feed').querySelector(`[data-id="${CSS.escape(videoId)}"]`); if(wrapper) updateVideoCardUI(video,wrapper);
try{
await updateDoc(doc(db,'videos',videoId),{likes:liked?arrayRemove(uid):arrayUnion(uid)});
if(!liked && video.userId && video.userId!==uid){
await setDoc(doc(db,'users',video.userId),{rewardPoints:increment(2)},{merge:true});
await addNotification(video.userId,{fromUserId:uid,fromUsername:getUsername(window.currentUser),fromUserAvatar:window.currentUser.photoURL||'',message:`liked your video: "${(video.caption||'').slice(0,40)}"`});
}
}catch(err){
video.likes=liked?nextLikes.concat(uid):nextLikes.filter(x=>x!==uid); if(wrapper) updateVideoCardUI(video,wrapper);
console.error('Like error:',err); showToastMsg('Like failed. Try again.');
}
};
async function addNotification(targetUid,payload){
if(!targetUid || !window.currentUser || targetUid===window.currentUser.uid) return;
await addDoc(collection(db,'users',targetUid,'notifications'),{...payload,read:false,createdAt:serverTimestamp()});
}
function syncCommentCountOnFeedUI(videoId, newCount) {
const countNum = Math.max(0, Number(newCount) || 0);
const video = window.allVideos.find(v => v.id === videoId);
if (video) video.commentsCount = countNum;
const wrapper = $('feed').querySelector(`[data-id="${CSS.escape(videoId)}"]`);
if (wrapper) {
const commentSpan = wrapper.querySelector('[data-action="comment"] span');
if (commentSpan) commentSpan.innerText = formatCount(countNum);
}
if (window.activeVideoIdForComment === videoId) {
$('commentHeaderTitle').innerText = `Comments (${countNum})`;
}
}
window.openCommentsModal=async(videoId)=>{
window.activeVideoIdForComment=videoId;
if(commentsUnsubscribe){commentsUnsubscribe();commentsUnsubscribe=null;}
$('commentsModal').classList.add('open'); document.body.classList.add('modal-open');
const video=window.allVideos.find(v=>v.id===videoId);
$('commentHeaderTitle').innerText=`Comments (${Number(video?.commentsCount)||0})`;
const list=$('commentsList'); list.innerHTML='Loading comments...
';
const commRef=collection(db,'videos',videoId,'comments');
const q=query(commRef,orderBy('createdAt','desc'),limit(100));
commentsUnsubscribe=onSnapshot(q,snapshot=>{
const liveCount = snapshot.size;
syncCommentCountOnFeedUI(videoId, liveCount);
if(snapshot.empty){list.innerHTML='No comments yet. Be first!
';return;}
const frag=document.createDocumentFragment();
snapshot.docs.forEach(ds=>{
const c=ds.data(); const item=document.createElement('div'); item.className='comment-item';
const img=document.createElement('img'); img.className='comment-avatar'; img.src=safeUrl(c.userAvatar,defaultAvatar(c.userId)); img.alt='';
img.onclick=()=>{ if(c.userId){ closeModal('commentsModal'); openCreatorProfile(c.userId); } };
const content=document.createElement('div'); content.className='comment-content';
const h=document.createElement('h4'); h.innerText='@'+(c.username||'user');
h.onclick=()=>{ if(c.userId){ closeModal('commentsModal'); openCreatorProfile(c.userId); } };
const p=document.createElement('p'); p.innerText=c.text||'';
content.append(h,p); item.append(img,content); frag.append(item);
});
list.innerHTML='';list.appendChild(frag);
},err=>{list.innerHTML='Could not load comments.
';console.error(err);});
};
window.postLiveComment=async()=>{
if(!window.userLoggedIn){showAuthPanel('login');return;}
const text=$('newCommentText').value.trim(); const videoId=window.activeVideoIdForComment;
if(!text||!videoId)return;
const uid=window.currentUser.uid;
const video=window.allVideos.find(v=>v.id===videoId);
const previousCount = Number(video?.commentsCount) || 0;
syncCommentCountOnFeedUI(videoId, previousCount + 1);
$('newCommentText').disabled=true;
try{
const userDoc = window.profileCache[uid] || {};
const uname = userDoc.username || getUsername(window.currentUser);
const uavatar = userDoc.avatar || window.currentUser.photoURL || defaultAvatar(uid);
const batch=writeBatch(db);
const commentRef=doc(collection(db,'videos',videoId,'comments'));
batch.set(commentRef,{userId:uid,username:uname,userAvatar:uavatar,text,createdAt:serverTimestamp()});
batch.update(doc(db,'videos',videoId),{commentsCount:increment(1)});
if(video?.userId && video.userId!==uid){
const notifRef=doc(collection(db,'users',video.userId,'notifications'));
batch.set(notifRef,{fromUserId:uid,fromUsername:uname,fromUserAvatar:uavatar,message:`commented on your video: "${text.slice(0,40)}"`,read:false,createdAt:serverTimestamp()});
}
await batch.commit();
$('newCommentText').value='';
}catch(err){
syncCommentCountOnFeedUI(videoId, previousCount);
console.error('Comment error:',err);
showToastMsg('Comment failed. Try again.');
}
finally{$('newCommentText').disabled=false;}
};
window.shareVideo=async(videoId)=>{
const video=window.allVideos.find(v=>v.id===videoId);
const url=`${location.origin}${location.pathname}?video=${encodeURIComponent(videoId)}`;
try{
if(navigator.share) await navigator.share({title:'FlikZee Video',text:video?.caption||'Watch this on FlikZee (Flikzee.com)',url});
else { await navigator.clipboard.writeText(url); showToastMsg('FlikZee video link copied!'); }
}catch(err){ if(err?.name!=='AbortError'){ try{await navigator.clipboard.writeText(url);showToastMsg('FlikZee video link copied!');}catch(_){} } }
try{ if(videoId) await updateDoc(doc(db,'videos',videoId),{shares:increment(1)}); }catch(err){console.warn('Share counter:',err);}
};
window.closeSingleVideo=()=>{
document.querySelectorAll('#singleVideoWrapper video,#singleVideoWrapper audio').forEach(m=>{try{m.pause();m.src='';}catch(_){}});
$('singleVideoWrapper').innerHTML='';
$('singleVideoView').style.display='none';
};
function startProfileListeners(uid){
if(profileUnsubscribe)profileUnsubscribe(); if(profileVideosUnsubscribe)profileVideosUnsubscribe();
profileUnsubscribe=null; profileVideosUnsubscribe=null;
const userRef=doc(db,'users',uid);
profileUnsubscribe=onSnapshot(userRef,snap=>{
const p=snap.exists()?snap.data():{uid,displayName:'Creator',username:uid.slice(0,10),avatar:defaultAvatar(uid),bio:'FlikZee Creator 🎬',followers:[],following:[],dob:''};
window.profileCache[uid]=p; renderProfileHeader(p);
},err=>console.error('Profile listener:',err));
const vq=query(collection(db,'videos'),where('userId','==',uid),limit(PROFILE_VIDEO_LIMIT));
profileVideosUnsubscribe=onSnapshot(vq,snap=>{
currentProfileVideos=snap.docs.map(d=>({id:d.id,...d.data()})).sort((a,b)=>timestampMillis(b.timestamp)-timestampMillis(a.timestamp));
renderProfileVideos(currentProfileVideos);
const likes=currentProfileVideos.reduce((sum,v)=>sum+(v.likes?.length||0),0);
const views=currentProfileVideos.reduce((sum,v)=>sum+(v.views||0),0);
$('statLikes').innerText=formatCount(likes); $('profPostCount').innerText=`${currentProfileVideos.length} posts`; $('profViewCount').innerText=`${formatCount(views)} views`;
renderMonetization(currentProfileVideos, window.profileCache[uid] || {});
},err=>console.error('Profile videos listener:',err));
}
function renderProfileHeader(p){
$('profTopName').innerText=p.displayName||p.username||'Profile';
$('profDisplayName').innerText=p.displayName||'Creator';
$('profUsername').innerText='@'+(p.username||p.uid||'user');
$('profImage').src=safeUrl(p.avatar,defaultAvatar(p.uid));
$('profBio').innerText=p.bio||'';
$('statFollowers').innerText=formatCount(p.followers?.length||0);
$('statFollowing').innerText=formatCount(p.following?.length||0);
profileIsMine=!!window.currentUser && window.currentUser.uid===p.uid;
$('myProfileActions').style.display=profileIsMine?'flex':'none';
$('creatorProfileActions').style.display=profileIsMine?'none':'flex';
$('avatarChangeBtn').style.display=profileIsMine?'flex':'none';
$('profileWalletRow').style.display=profileIsMine?'flex':'none';
if(!profileIsMine){
const following=(p.followers||[]).includes(window.currentUser?.uid);
$('followBtn').innerText=following?'Following':'Follow';
$('followBtn').style.background=following?'#171717':'';
$('followBtn').style.border=following?'1px solid #333':'';
}
const dob=p.dob||'Not provided';
const email=profileIsMine ? (window.currentUser?.email || p.email || 'Not provided') : 'Hidden';
$('profileAboutCard').innerHTML=`Full Name ${escapeHtml(p.displayName||'Creator')}
Username @${escapeHtml(p.username||'user')}
Email ${escapeHtml(email)}
Date of Birth ${escapeHtml(profileIsMine ? dob : 'Hidden')}
Bio ${escapeHtml(p.bio||'')}
`;
renderMonetization(currentProfileVideos,p);
}
function renderProfileVideos(videos){
const grid=$('profileVideoGrid'); grid.innerHTML='';
if(!videos.length){grid.innerHTML='No videos uploaded yet.
';return;}
const frag=document.createDocumentFragment();
videos.forEach(v=>{
const item=document.createElement('div'); item.className='grid-item';
item.onclick=()=>openSingleMedia(v.mediaUrl||v.videoUrl,v.id);
let media;
if((v.mediaType||'video')==='image'){ media=document.createElement('img'); media.src=safeUrl(v.mediaUrl||v.videoUrl); media.alt=v.title||'FlikZee post'; media.loading='lazy'; media.style.cssText='width:100%;height:100%;object-fit:cover;'; }
else if((v.mediaType||'video')==='text'){ media=document.createElement('div'); media.style.cssText='width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:8px;text-align:center;font-size:.75rem;font-weight:700;background:#181020;color:#fff;'; media.innerText=(v.caption||'Text').slice(0,60); }
else { media=document.createElement('video'); media.src=safeUrl(v.mediaUrl||v.videoUrl); media.muted=true; media.playsInline=true; media.preload='none'; }
media.style.filter=FILTERS[v.videoFilter]||FILTERS.Original;
const stats=document.createElement('div'); stats.className='grid-item-stats'; stats.innerText=`❤️ ${formatCount(v.likes?.length||0)}`;
const views=document.createElement('div'); views.className='video-view-pill'; views.innerText=`${formatCount(v.views||0)} views`;
item.append(media,stats,views);
if(profileIsMine){
const menuBtn=document.createElement('button'); menuBtn.className='profile-grid-menu'; menuBtn.type='button'; menuBtn.innerText='⋯';
const menu=document.createElement('div'); menu.className='grid-video-menu';
menu.innerHTML='✏️ Edit ↗ Share 🗑️ Delete ';
menuBtn.onclick=(e)=>{e.stopPropagation();document.querySelectorAll('.grid-video-menu.open').forEach(x=>x.classList.remove('open'));menu.classList.toggle('open');};
const [eb,sb,dbtn]=menu.children;
eb.onclick=e=>{e.stopPropagation();openEditVideo(v.id);};
sb.onclick=e=>{e.stopPropagation();shareVideo(v.id);menu.classList.remove('open');};
dbtn.onclick=e=>{e.stopPropagation();deleteVideo(v.id);menu.classList.remove('open');};
item.append(menuBtn,menu);
}
frag.append(item);
});
grid.appendChild(frag);
}
window.openCreatorProfile=async(uid)=>{
if(!uid)return;
window.currentViewingCreatorUid=uid;
closeModal('commentsModal');
if($('singleVideoView').style.display==='block') closeSingleVideo();
switchTab('profile',true);
startProfileListeners(uid);
};
window.openMyProfile=()=>{
if(!window.userLoggedIn){showAuthPanel('login');return;}
openCreatorProfile(window.currentUser.uid);
};
window.goBackFromProfile=()=>{
switchTab(profileIsMine?'home':lastNonProfileTab);
};
window.toggleFollowCreator=async()=>{
if(!window.userLoggedIn){showAuthPanel('login');return;}
const targetUid=window.currentViewingCreatorUid; if(!targetUid||targetUid===window.currentUser.uid)return;
const target=window.profileCache[targetUid]||{}; const following=(target.followers||[]).includes(window.currentUser.uid);
$('followBtn').disabled=true;
try{
await setDoc(doc(db,'users',targetUid),{followers:following?arrayRemove(window.currentUser.uid):arrayUnion(window.currentUser.uid)},{merge:true});
await setDoc(doc(db,'users',window.currentUser.uid),{following:following?arrayRemove(targetUid):arrayUnion(targetUid)},{merge:true});
if(!following) await addNotification(targetUid,{fromUserId:window.currentUser.uid,fromUsername:getUsername(window.currentUser),fromUserAvatar:window.currentUser.photoURL||'',message:'started following you'});
showToastMsg(following?'Unfollowed':'Following creator!');
}catch(err){console.error('Follow error:',err);showToastMsg('Follow action failed.');}
finally{$('followBtn').disabled=false;}
};
window.shareProfile=async()=>{
const uid=window.currentViewingCreatorUid||window.currentUser?.uid; if(!uid)return;
const p=window.profileCache[uid]||{}; const url=`${location.origin}${location.pathname}?profile=${encodeURIComponent(uid)}`;
try{
if(navigator.share) await navigator.share({title:'FlikZee Profile',text:`Check out @${p.username||'creator'} on FlikZee`,url});
else {await navigator.clipboard.writeText(url);showToastMsg('Profile link copied!');}
}catch(err){if(err?.name!=='AbortError')showToastMsg('Could not share profile.');}
};
window.switchProfilePanel=(panel)=>{
$('profileTabVideos').classList.toggle('active',panel==='videos'); $('profileTabAbout').classList.toggle('active',panel==='about');
$('profilePanelVideos').classList.toggle('active',panel==='videos'); $('profilePanelAbout').classList.toggle('active',panel==='about');
};
window.openEditProfile=()=>{
if(!window.currentUser)return showAuthPanel('login');
const p=window.profileCache[window.currentUser.uid]||{};
$('editProfileName').value=p.displayName||window.currentUser.displayName||'';
$('editProfileUsername').value=p.username||getUsername(window.currentUser);
$('editProfileBio').value=p.bio||'';
$('editProfileDob').value=p.dob||'';
$('editProfileModal').classList.add('open'); document.body.classList.add('modal-open');
$('settingsMenu').classList.remove('open');
};
window.saveProfileEdit=async()=>{
if(!window.currentUser)return;
const name=$('editProfileName').value.trim();
const username=$('editProfileUsername').value.trim().toLowerCase().replace(/[^a-z0-9_]+/g,'_').replace(/^_+|_+$/g,'').slice(0,24);
const bio=$('editProfileBio').value.trim();
if(!name||!username)return alert('Name and username are required.');
const btn=$('saveProfileBtn'); btn.disabled=true; btn.innerText='Saving...';
try{
await updateProfile(window.currentUser,{displayName:name});
await setDoc(doc(db,'users',window.currentUser.uid),{displayName:name,username,bio,avatar:window.currentUser.photoURL||defaultAvatar(window.currentUser.uid),email:window.currentUser.email||''},{merge:true});
$('editProfileModal').classList.remove('open'); document.body.classList.remove('modal-open');
showToastMsg('Profile updated!');
}catch(err){console.error(err);alert(err.message||'Profile update failed.');}
finally{btn.disabled=false;btn.innerText='Save Changes';}
};
window.handleAvatarUpload=async(e)=>{
const file=e.target.files?.[0]; if(!file||!window.currentUser)return;
if(!file.type.startsWith('image/'))return alert('Please choose an image.');
showToastMsg('Uploading profile picture...');
try{
const formData=new FormData(); formData.append('file',file); formData.append('upload_preset',CLOUDINARY_PRESET);
const res=await fetch(CLOUDINARY_IMAGE_URL,{method:'POST',body:formData}); const data=await res.json();
if(!data.secure_url)throw new Error(data.error?.message||'Image upload failed');
await updateProfile(window.currentUser,{photoURL:data.secure_url});
await setDoc(doc(db,'users',window.currentUser.uid),{avatar:data.secure_url},{merge:true});
$('profImage').src=data.secure_url; showToastMsg('Profile picture updated!');
}catch(err){console.error(err);alert(err.message||'Profile picture upload failed.');}
finally{e.target.value='';}
};
function renderMonetization(videos,p){
const totalViews=(videos||[]).reduce((s,v)=>s+(v.views||0),0);
const followers=(p?.followers||[]).length;
const rewardPts = Number(p?.rewardPoints) || Math.floor(totalViews / 5);
const estEarnings = ((rewardPts * 0.005) + (totalViews * 0.0005)).toFixed(2);
const vp=Math.min(100,totalViews/MONETIZATION_REQUIRED_VIEWS*100);
const fp=Math.min(100,followers/MONETIZATION_REQUIRED_FOLLOWERS*100);
const ready=totalViews>=MONETIZATION_REQUIRED_VIEWS && followers>=MONETIZATION_REQUIRED_FOLLOWERS;
$('monetizationContent').innerHTML=`${ready?'🎉 FlikZee Partner Ready':'FlikZee Creator Rewards & Ad-Share'}
Earn Creator Points from views, likes, tips, and sponsored ad-share. Full Partner threshold: ${MONETIZATION_REQUIRED_VIEWS.toLocaleString()} views and ${MONETIZATION_REQUIRED_FOLLOWERS.toLocaleString()} followers .
${formatCount(rewardPts)} Creator Pts ⭐
$${estEarnings} Est. Bonus 💵
${ready?'ACTIVE':'GROWING'} Ad-Share Status
Views ${formatCount(totalViews)} / ${formatCount(MONETIZATION_REQUIRED_VIEWS)}
Followers ${formatCount(followers)} / ${formatCount(MONETIZATION_REQUIRED_FOLLOWERS)}
🪙 Open Rewards Wallet
`;
}
window.openCreatorFund=()=>{
if(!window.userLoggedIn)return showAuthPanel('login');
const p=window.profileCache[window.currentUser.uid]||{};
renderMonetization(currentProfileVideos.length && window.currentViewingCreatorUid===window.currentUser.uid ? currentProfileVideos : (window.allVideos.filter(v=>v.userId===window.currentUser.uid)),p);
$('creatorFundModal').classList.add('open'); document.body.classList.add('modal-open'); $('settingsMenu').classList.remove('open');
};
function renderCoinPackages(){
const box=$('coinPackages'); if(!box)return; box.innerHTML='';
COIN_PACKAGES.forEach(pkg=>{
const b=document.createElement('button'); b.className='coin-pack'; b.type='button';
b.innerHTML=`🪙 ${pkg.coins.toLocaleString()} Coins ${pkg.price} · Top-Up `;
b.onclick=()=>startCoinPurchase(pkg); box.appendChild(b);
});
}
function updateWalletModalUI(userData = {}) {
const coins = Number(userData.coins) || 0;
const pts = Number(userData.rewardPoints) || 0;
const usd = (pts * 0.005).toFixed(2);
if ($('walletCoinBalance')) $('walletCoinBalance').innerText = formatCount(coins);
if ($('walletRewardPoints')) $('walletRewardPoints').innerText = formatCount(pts);
if ($('walletEstUsd')) $('walletEstUsd').innerText = `$${usd}`;
}
function startWalletListeners(uid){
if(walletUnsubscribe){walletUnsubscribe();walletUnsubscribe=null;}
walletUnsubscribe = onSnapshot(doc(db, 'users', uid), snap => {
if (snap.exists()) {
const data = snap.data();
window.profileCache[uid] = { ...(window.profileCache[uid] || {}), ...data };
updateWalletModalUI(data);
}
}, err => console.warn('Wallet sync:', err));
}
window.openWallet=()=>{
if(!window.userLoggedIn)return showAuthPanel('login');
const p = window.profileCache[window.currentUser.uid] || {};
updateWalletModalUI(p);
$('walletModal').classList.add('open'); document.body.classList.add('modal-open');
};
window.claimDailyReward = async () => {
if (!window.userLoggedIn || !window.currentUser) return showAuthPanel('login');
const uid = window.currentUser.uid;
const today = new Date().toISOString().slice(0, 10);
const key = `flikzee_daily_${uid}_${today}`;
if (localStorage.getItem(key)) {
return showToastMsg('You already claimed today\'s +25 FlikCoins!');
}
try {
localStorage.setItem(key, '1');
await setDoc(doc(db, 'users', uid), { coins: increment(25), rewardPoints: increment(5) }, { merge: true });
showToastMsg('🎉 +25 FlikCoins & +5 Creator Pts added!');
} catch (err) {
localStorage.removeItem(key);
console.error('Daily reward error:', err);
showToastMsg('Could not claim reward right now.');
}
};
window.sendCreatorTip = async () => {
if (!window.userLoggedIn || !window.currentUser) return showAuthPanel('login');
const targetUid = window.currentViewingCreatorUid;
if (!targetUid || targetUid === window.currentUser.uid) return showToastMsg('Choose another creator to tip.');
const myData = window.profileCache[window.currentUser.uid] || {};
if ((Number(myData.coins) || 0) < 10) {
openWallet();
return showToastMsg('Need at least 10 FlikCoins. Claim daily reward or top-up!');
}
try {
await setDoc(doc(db, 'users', window.currentUser.uid), { coins: increment(-10) }, { merge: true });
await setDoc(doc(db, 'users', targetUid), { coins: increment(10), rewardPoints: increment(10) }, { merge: true });
await addNotification(targetUid, {
fromUserId: window.currentUser.uid,
fromUsername: getUsername(window.currentUser),
fromUserAvatar: window.currentUser.photoURL || '',
message: 'sent you a 10 FlikCoins creator tip! 🪙'
});
showToastMsg('🪙 Sent 10 FlikCoins tip to creator!');
} catch (err) {
console.error('Tip error:', err);
showToastMsg('Could not send tip.');
}
};
window.boostMyLatestVideo = async () => {
if (!window.userLoggedIn || !window.currentUser) return showAuthPanel('login');
const uid = window.currentUser.uid;
const myVideos = window.allVideos.filter(v => v.userId === uid);
if (!myVideos.length) return showToastMsg('Upload a video first to boost it on the feed!');
const myData = window.profileCache[uid] || {};
if ((Number(myData.coins) || 0) < 50) {
return showToastMsg('Need 50 FlikCoins to boost a video. Claim daily rewards or top-up!');
}
try {
const targetVideo = myVideos[0];
await setDoc(doc(db, 'users', uid), { coins: increment(-50) }, { merge: true });
await updateDoc(doc(db, 'videos', targetVideo.id), { isBoosted: true });
showToastMsg('🚀 Your latest video is now Sponsored on FlikZee!');
closeModal('walletModal');
} catch (err) {
console.error('Boost error:', err);
showToastMsg('Boost failed. Try again.');
}
};
window.requestCreatorPayout = async () => {
if (!window.userLoggedIn || !window.currentUser) return showAuthPanel('login');
const uid = window.currentUser.uid;
const myData = window.profileCache[uid] || {};
const pts = Number(myData.rewardPoints) || 0;
if (pts < 1000) {
return showToastMsg(`Minimum 1,000 Creator Pts required for payout (Current: ${pts} Pts).`);
}
try {
await addDoc(collection(db, 'payments'), {
userId: uid,
userEmail: window.currentUser.email || '',
packageId: 'Creator Payout Request',
amount: `$${(pts * 0.005).toFixed(2)}`,
status: 'pending_review',
createdAt: serverTimestamp()
});
showToastMsg('💸 Payout request submitted to FlikZee Admin!');
} catch (err) {
console.error('Payout request:', err);
showToastMsg('Payout request logged.');
}
};
window.startCoinPurchase=async(pkg)=>{
if(!window.userLoggedIn)return showAuthPanel('login');
if(!PAYMENT_API_URL){
try {
await addDoc(collection(db, 'payments'), {
userId: window.currentUser.uid,
userEmail: window.currentUser.email || '',
packageId: `${pkg.coins} FlikCoins`,
amount: pkg.price,
status: 'checkout_requested',
createdAt: serverTimestamp()
});
} catch (_) {}
showToastMsg(`Selected ${pkg.coins} FlikCoins (${pkg.price}) — Merchant checkout ready.`);
return;
}
try{
const token=await window.currentUser.getIdToken();
const res=await fetch(PAYMENT_API_URL,{method:'POST',headers:{'Content-Type':'application/json','Authorization':`Bearer ${token}`},body:JSON.stringify({action:'create_checkout',packageId:pkg.id})});
const data=await res.json(); if(!res.ok||!data.checkoutUrl)throw new Error(data.error||'Could not create payment checkout.');
location.href=data.checkoutUrl;
}catch(err){console.error('Coin purchase:',err);alert(err.message||'Payment could not be started.');}
};
function renderLiveHub(){
const box=$('liveHubContent'); if(!box) return;
box.innerHTML='FlikZee Live Live broadcasting is hidden during initial launch phase.
';
}
window.openLiveHub=()=>{ showToastMsg('FlikZee Live will unlock in the next phase.'); };
renderCoinPackages();
function populateFilters(){
const row=$('filterRow'); row.innerHTML='';
Object.keys(FILTERS).forEach(name=>{const b=document.createElement('button');b.type='button';b.className='filter-btn'+(name===selectedFilterName?' active':'');b.innerText=name;b.onclick=()=>{selectedFilterName=name;document.querySelectorAll('#filterRow .filter-btn').forEach(x=>x.classList.remove('active'));b.classList.add('active');applyUploadFilter(name);};row.appendChild(b);});
}
function populateEditFilters(){
const row=$('editFilterRow'); row.innerHTML='';
Object.keys(FILTERS).forEach(name=>{const b=document.createElement('button');b.type='button';b.className='filter-btn'+(name===selectedEditFilterName?' active':'');b.innerText=name;b.onclick=()=>{selectedEditFilterName=name;row.querySelectorAll('.filter-btn').forEach(x=>x.classList.remove('active'));b.classList.add('active');};row.appendChild(b);});
}
function applyUploadFilter(name){
$('uploadPreview').style.filter=FILTERS[name]; $('uploadImagePreview').style.filter=FILTERS[name];
}
window.openCameraPicker=()=>{if(!window.userLoggedIn)return showAuthPanel('login');$('videoCameraInput').click();};
window.openGalleryPicker=()=>{if(!window.userLoggedIn)return showAuthPanel('login');$('videoGalleryInput').click();};
window.handleFileSelect=(e)=>{
const file=e.target.files?.[0]; if(!file)return;
if(!file.type.startsWith('video/') && !file.type.startsWith('image/')) return alert('Please choose a video or image.');
uploadSelectedFile=file; uploadSelectedMediaType=file.type.startsWith('image/')?'image':'video';
$('fileNameDisplay').innerText=`${file.name} ✔️`; $('fileNameDisplay').style.color='var(--primary)';
const url=URL.createObjectURL(file);
if(uploadSelectedMediaType==='image'){ $('uploadPreview').style.display='none'; $('uploadImagePreview').style.display='block'; $('uploadImagePreview').src=url; }
else { $('uploadImagePreview').style.display='none'; $('uploadPreview').style.display='block'; $('uploadPreview').src=url; $('uploadPreview').play().catch(()=>{}); }
$('uploadPreviewWrap').style.display='block'; applyUploadFilter(selectedFilterName);
e.target.value='';
};
function renderSoundCategories(){
const row=$('soundCategoryRow'); if(!row) return; row.innerHTML='';
SOUND_CATEGORIES.forEach(cat=>{
const b=document.createElement('button');
b.type='button';
b.className='sound-cat'+(cat===selectedSoundCategory?' active':'');
b.innerText=cat;
b.onclick=()=>{selectedSoundCategory=cat;renderSoundCategories();renderSoundLibrary();};
row.appendChild(b);
});
}
let soundPreviewAudio=null;
let soundPreviewTimer=null;
window.playSoundPreview = function(soundId){
const sound=SOUND_LIBRARY.find(x=>x.id===soundId); if(!sound)return;
const soundUrl=sound.soundUrl||getSoundUrl(sound.id,sound.category);
try{
if(soundPreviewAudio){soundPreviewAudio.pause();soundPreviewAudio.currentTime=0;}
if(soundPreviewTimer) clearTimeout(soundPreviewTimer);
soundPreviewAudio=new Audio(soundUrl);
soundPreviewAudio.volume=.85;
soundPreviewAudio.play().catch(()=>{});
showToastMsg(`🎵 Playing: ${sound.name}`);
soundPreviewTimer=setTimeout(()=>{if(soundPreviewAudio) soundPreviewAudio.pause();},6500);
}catch(e){console.warn('Sound preview:',e);}
};
window.renderSoundLibrary = function(){
const q=String($('soundSearchInput')?.value||'').trim().toLowerCase(); const list=$('soundList'); if(!list)return;
const sounds=SOUND_LIBRARY.filter(s=>(q ? (s.name.toLowerCase().includes(q) || s.category.toLowerCase().includes(q)) : s.category===selectedSoundCategory));
list.innerHTML='';
if(!sounds.length){list.innerHTML='No sounds found in this category.
';return;}
const frag=document.createDocumentFragment();
sounds.forEach(sound=>{
const card=document.createElement('div');
card.className='sound-card'+(selectedSound?.id===sound.id?' selected':'');
card.onclick=()=>{selectedSound={...sound};renderSoundLibrary();showToastMsg(`Selected: ${sound.name}`);};
card.innerHTML=`🎵
${escapeHtml(sound.name)} ${escapeHtml(sound.category)} • FlikZee Audio
`;
const play=document.createElement('button');play.type='button';play.className='sound-play';play.innerText='▶';
play.onclick=e=>{e.stopPropagation();playSoundPreview(sound.id);};
card.appendChild(play);frag.appendChild(card);
});
list.appendChild(frag);
};
window.clearSelectedSound=()=>{
selectedSound=null;
if(soundPreviewAudio){soundPreviewAudio.pause();soundPreviewAudio.currentTime=0;}
renderSoundLibrary();
showToastMsg('Using Original Video Audio');
};
renderCategories();
populateFilters(); populateEditFilters(); renderSoundCategories(); renderSoundLibrary();
window.processUpload=async()=>{
if(!window.userLoggedIn){showAuthPanel('login');return;}
if(!uploadSelectedFile)return alert('Please choose a video or image first.');
if(soundPreviewAudio){soundPreviewAudio.pause();soundPreviewAudio.currentTime=0;}
const title=$('videoTitle').value.trim();
const desc=$('videoDesc').value.trim();
const hashtags=normalizeHashtags($('videoHashtags').value);
const sponsoredUrl=safeUrl(($('videoSponsoredUrl')?.value||'').trim(),'');
const btn=$('uploadBtn'); const status=$('uploadStatus'); const fill=$('progressFill');
btn.disabled=true; $('progressBg').style.display='block'; fill.style.width='5%'; status.innerText='Uploading media to FlikZee...';
try{
const endpoint=uploadSelectedMediaType==='image'?CLOUDINARY_IMAGE_URL:CLOUDINARY_URL;
const formData=new FormData(); formData.append('file',uploadSelectedFile); formData.append('upload_preset',CLOUDINARY_PRESET);
const res=await fetch(endpoint,{method:'POST',body:formData});
const data=await res.json(); if(!data.secure_url)throw new Error(data.error?.message||'Cloudinary upload failed');
fill.style.width='75%'; status.innerText='Publishing post...';
const userDoc=window.profileCache[window.currentUser.uid]||{};
const payload={
userId:window.currentUser.uid,
username:userDoc.username||getUsername(window.currentUser),
userAvatar:userDoc.avatar||window.currentUser.photoURL||defaultAvatar(window.currentUser.uid),
title, caption:desc, hashtags,
sponsoredUrl,
mediaType:uploadSelectedMediaType,
mediaUrl:data.secure_url,
likes:[], commentsCount:0, views:0, shares:0,
videoFilter:selectedFilterName,
soundId:selectedSound?.id||'',
soundName:selectedSound?.name||'',
soundCategory:selectedSound?.category||'Original',
soundUrl:'',
timestamp:serverTimestamp()
};
if(uploadSelectedMediaType==='video') payload.videoUrl=data.secure_url;
await addDoc(collection(db,'videos'),payload);
await setDoc(doc(db,'users',window.currentUser.uid),{rewardPoints:increment(10),coins:increment(5)},{merge:true});
fill.style.width='100%'; status.innerText='Posted successfully! (+10 Creator Pts)'; showToastMsg('🎉 Your FlikZee post is live!');
setTimeout(()=>{
switchTab('home'); btn.disabled=false; $('progressBg').style.display='none'; fill.style.width='0'; status.innerText=''; uploadSelectedFile=null;
$('videoTitle').value=''; $('videoDesc').value=''; $('videoHashtags').value=''; if($('videoSponsoredUrl')) $('videoSponsoredUrl').value='';
$('hashtagPreview').innerHTML=''; $('fileNameDisplay').innerText='No media selected'; $('fileNameDisplay').style.color='#fff';
$('uploadPreviewWrap').style.display='none'; $('uploadPreview').src=''; $('uploadImagePreview').src='';
selectedFilterName='Original'; selectedSound=null; applyUploadFilter('Original'); populateFilters(); renderSoundLibrary();
},600);
}catch(err){console.error('Upload:',err);status.innerText='Upload error: '+(err.message||'Failed');btn.disabled=false;}
};
window.openSingleMedia=(url,mediaId=null)=>{
pauseAllFeedVideosExcept(null);
const view=$('singleVideoView'); const wrap=$('singleVideoWrapper'); wrap.innerHTML='';
const current=mediaId?window.allVideos.find(x=>x.id===mediaId):null; const type=current?.mediaType||'video';
if(type==='image'){const img=document.createElement('img');img.src=safeUrl(url);img.alt=current?.title||'FlikZee post';img.style.cssText='width:100%;height:100%;object-fit:contain;background:#000;';wrap.appendChild(img);}
else if(type==='text'){const d=document.createElement('div');d.style.cssText='width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:32px;text-align:center;font-size:1.35rem;font-weight:800;background:#111;color:#fff;';d.innerText=current?.caption||'';wrap.appendChild(d);}
else {const video=document.createElement('video');video.src=safeUrl(url);video.controls=true;video.autoplay=true;video.playsInline=true;video.loop=true;video.style.cssText='width:100%;height:100%;object-fit:cover;';video.muted=window.isGlobalMuted;wrap.appendChild(video);}
const singleSoundUrl=current?.soundUrl||((current?.soundId||current?.soundCategory)?getSoundUrl(current.soundId,current.soundCategory):'');
if(singleSoundUrl && current?.soundCategory !== 'Original'){const audio=document.createElement('audio');audio.src=safeUrl(singleSoundUrl);audio.loop=true;audio.preload='none';audio.muted=window.isGlobalMuted;audio.style.display='none';audio.volume=.78;wrap.appendChild(audio);if(!window.isGlobalMuted)audio.play().catch(()=>{});}
view.style.display='block';
if(mediaId) recordVideoView(mediaId,current);
};
window.openSingleVideo=(url,videoId=null)=>openSingleMedia(url,videoId);
window.handleSearch=(queryStr)=>renderDiscoverSearch(queryStr);
function renderDiscoverSearch(queryStr){
const q=String(queryStr||'').toLowerCase().trim(); const container=$('searchResults');
if(!q){container.innerHTML='';return;}
const matchedVideos=window.allVideos.filter(v=>(v.caption||'').toLowerCase().includes(q)||(v.title||'').toLowerCase().includes(q)||(v.username||'').toLowerCase().includes(q)||(v.hashtags||[]).some(h=>String(h).toLowerCase().includes(q)));
const creators=[]; const seen=new Set();
window.allVideos.forEach(v=>{if((v.username||'').toLowerCase().includes(q)&&!seen.has(v.userId)){seen.add(v.userId);creators.push(v);}});
let html='';
if(creators.length){html+='Accounts
'+creators.slice(0,8).map(v=>`
@${escapeHtml(v.username||'creator')} ${formatCount((v.likes||[]).length)} likes on loaded videos
`).join('')+'
';}
if(matchedVideos.length){html+='Videos
'+matchedVideos.slice(0,30).map(v=>{const src=escapeHtml(v.mediaUrl||v.videoUrl||'');const filter=escapeHtml(FILTERS[v.videoFilter]||FILTERS.Original);const media=(v.mediaType||'video')==='image'?`
`:`
`;return `
${media}
❤️ ${formatCount((v.likes||[]).length)}
${formatCount(v.views||0)} views
`;}).join('')+'
';}
if(!html)html='No matching videos or accounts found.
';
container.innerHTML=html;
container.querySelectorAll('.creator-result').forEach(el=>el.onclick=()=>openCreatorProfile(el.dataset.uid));
container.querySelectorAll('[data-video-id]').forEach(el=>el.onclick=()=>{const v=window.allVideos.find(x=>x.id===el.dataset.videoId);if(v)openSingleMedia(v.mediaUrl||v.videoUrl,v.id);});
}
window.searchTag=(tag)=>{switchTab('discover');$('searchInput').value=tag;renderDiscoverSearch(tag);};
window.openEditVideo=(videoId)=>{
if(!window.userLoggedIn)return showAuthPanel('login');
const video=window.allVideos.find(v=>v.id===videoId); if(!video || video.userId!==window.currentUser.uid)return showToastMsg('You can only edit your own posts.');
currentEditingVideoId=videoId; selectedEditFilterName=video.videoFilter||'Original';
$('editVideoTitle').value=video.title||''; $('editVideoDesc').value=video.caption||''; $('editVideoHashtags').value=(video.hashtags||[]).join(', '); renderEditHashtagPreview($('editVideoHashtags').value); populateEditFilters();
const prev=$('editVideoPreview'); prev.innerHTML=''; if((video.mediaType||'video')==='image'){const img=document.createElement('img');img.src=safeUrl(video.mediaUrl||video.videoUrl);prev.appendChild(img);} else {const v=document.createElement('video');v.src=safeUrl(video.mediaUrl||video.videoUrl);v.muted=true;v.playsInline=true;v.controls=true;prev.appendChild(v);}
$('editVideoModal').classList.add('open'); document.body.classList.add('modal-open');
};
window.saveVideoEdit=async()=>{
if(!currentEditingVideoId||!window.currentUser)return; const video=window.allVideos.find(v=>v.id===currentEditingVideoId); if(!video||video.userId!==window.currentUser.uid)return;
const btn=$('saveVideoEditBtn'); btn.disabled=true; btn.innerText='Saving...';
try{
const title=$('editVideoTitle').value.trim(); const caption=$('editVideoDesc').value.trim(); const hashtags=normalizeHashtags($('editVideoHashtags').value);
await updateDoc(doc(db,'videos',currentEditingVideoId),{title,caption,hashtags,videoFilter:selectedEditFilterName,updatedAt:serverTimestamp()});
closeModal('editVideoModal'); showToastMsg('Video updated successfully.');
}catch(err){console.error('Edit video:',err);showToastMsg('Could not update video.');}
finally{btn.disabled=false;btn.innerText='Save Changes';}
};
window.deleteVideo=async(videoId)=>{
if(!window.userLoggedIn)return showAuthPanel('login'); const video=window.allVideos.find(v=>v.id===videoId); if(!video||video.userId!==window.currentUser.uid)return showToastMsg('You can only delete your own posts.');
if(!confirm('Delete this post? This will remove it from FlikZee.'))return;
try{await deleteDoc(doc(db,'videos',videoId));showToastMsg('Post deleted.');}catch(err){console.error('Delete video:',err);showToastMsg('Could not delete this post.');}
};
window.handleHomeSearch=(value)=>{
const q=String(value||'').trim().toLowerCase(); const box=$('homeSearchResults'); if(!box)return;
if(!q){box.classList.remove('open');box.innerHTML='';return;}
const rows=[]; const seen=new Set();
window.allVideos.forEach(v=>{
const hay=[v.username,v.title,v.caption,v.soundName,...(v.hashtags||[])].join(' ').toLowerCase();
if(hay.includes(q)){const key=v.userId+'|'+v.id;if(!seen.has(key)){seen.add(key);rows.push(v);}}
});
const creators=[]; const cseen=new Set();
window.allVideos.forEach(v=>{if((v.username||'').toLowerCase().includes(q)&&!cseen.has(v.userId)){cseen.add(v.userId);creators.push(v);}});
let html='';
creators.slice(0,5).forEach(v=>{html+=`@${escapeHtml(v.username||'creator')} Creator profile
`;});
rows.slice(0,10).forEach(v=>{html+=`${escapeHtml(v.title||v.soundName||'FlikZee video')} @${escapeHtml(v.username||'creator')} • ${formatCount(v.views||0)} views
`;});
box.innerHTML=html||'No matching results.
'; box.classList.add('open');
box.querySelectorAll('[data-home-profile]').forEach(el=>el.onclick=()=>{box.classList.remove('open');openCreatorProfile(el.dataset.homeProfile);});
box.querySelectorAll('[data-home-video]').forEach(el=>el.onclick=()=>{box.classList.remove('open');const v=window.allVideos.find(x=>x.id===el.dataset.homeVideo);if(v)openSingleMedia(v.mediaUrl||v.videoUrl,v.id);});
};
document.addEventListener('click',e=>{if(!e.target.closest('.home-search-wrap'))$('homeSearchResults')?.classList.remove('open');});
window.setCameraMode=(mode)=>{
creatorCameraMode=mode; document.querySelectorAll('.camera-mode').forEach(b=>b.classList.toggle('active',b.dataset.cameraMode===mode));
const text=$('textCreateBox'), editor=$('studioUploadEditor'), record=$('cameraRecordBtn'), note=$('cameraLimitNote');
if(mode==='text'){closeCreatorCamera(); if(text)text.style.display='block'; if(editor)editor.style.display='none'; return;}
if(text)text.style.display='none'; if(mode==='photo'){note.innerText='Photo capture mode';record.innerText='PHOTO';} else {note.innerText=`${mode} second maximum`;record.innerText='';}
};
window.createTextPost=async()=>{
if(!window.userLoggedIn)return showAuthPanel('login'); const text=String($('textPostContent')?.value||'').trim(); if(!text)return;
try{await addDoc(collection(db,'videos'),{userId:window.currentUser.uid,username:getUsername(window.currentUser),userAvatar:window.currentUser.photoURL||defaultAvatar(window.currentUser.uid),title:'',caption:text,hashtags:normalizeHashtags(text),mediaType:'text',mediaUrl:'',videoUrl:'',likes:[],commentsCount:0,views:0,shares:0,videoFilter:'Original',soundId:'',soundName:'Original Sound',soundCategory:'Original',soundUrl:'',timestamp:serverTimestamp()});$('textPostContent').value='';showToastMsg('Text post published!');switchTab('home');}catch(err){console.error(err);showToastMsg('Could not publish text post.');}
};
window.switchStudioPanel=(panel)=>{
document.querySelectorAll('.studio-tab').forEach(b=>b.classList.toggle('active',b.dataset.studioTab===panel));
['create','templates','sounds','editor'].forEach(x=>$('studio'+x.charAt(0).toUpperCase()+x.slice(1)+'Panel')?.classList.toggle('active',x===panel));
if(panel==='templates')startTemplateListener();
if(panel==='sounds'){renderStudioSoundCategories();renderStudioSounds();}
};
window.openCreatorCamera=async()=>{
if(!window.userLoggedIn)return showAuthPanel('login');
try{
if(creatorCameraStream)creatorCameraStream.getTracks().forEach(t=>t.stop());
creatorCameraStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:creatorFacingMode,width:{ideal:1080},height:{ideal:1920}},audio:true});
setCameraMode(creatorCameraMode);
$('creatorCameraPreview').srcObject=creatorCameraStream;$('cameraCard').style.display='block';$('studioUploadEditor').style.display='none';$('cameraStatus').innerText='Ready — tap the circle to record';
}catch(err){console.error(err);alert('Camera/microphone access was blocked. Please allow permissions and try again.');}
};
window.closeCreatorCamera=()=>{if(creatorMediaRecorder&&creatorMediaRecorder.state!=='inactive')creatorMediaRecorder.stop();if(creatorCameraStream){creatorCameraStream.getTracks().forEach(t=>t.stop());creatorCameraStream=null;}$('creatorCameraPreview').srcObject=null;$('cameraCard').style.display='none';$('studioUploadEditor').style.display='block';creatorRecording=false;};
window.flipCreatorCamera=async()=>{creatorFacingMode=creatorFacingMode==='environment'?'user':'environment';await openCreatorCamera();};
window.toggleCreatorFlash=()=>showToastMsg('Flash control depends on the browser/device camera API.');
window.toggleCreatorTimer=()=>{creatorTimerSeconds=creatorTimerSeconds?0:3;$('cameraStatus').innerText=creatorTimerSeconds?'3 second timer enabled':'Timer disabled';};
window.captureCreatorMedia=async()=>{
if(creatorCameraMode==='text'){return;}
if(creatorCameraMode==='photo'){
if(!creatorCameraStream)return; const video=$('creatorCameraPreview'); const canvas=document.createElement('canvas'); canvas.width=video.videoWidth||1080; canvas.height=video.videoHeight||1920; const ctx=canvas.getContext('2d'); if(creatorFacingMode==='user'){ctx.translate(canvas.width,0);ctx.scale(-1,1);} ctx.drawImage(video,0,0,canvas.width,canvas.height); canvas.toBlob(blob=>{if(!blob)return;uploadSelectedFile=new File([blob],`flikzee-photo-${Date.now()}.jpg`,{type:'image/jpeg'});uploadSelectedMediaType='image';$('fileNameDisplay').innerText='Camera photo ✔️';$('uploadImagePreview').src=URL.createObjectURL(blob);$('uploadImagePreview').style.display='block';$('uploadPreview').style.display='none';$('uploadPreviewWrap').style.display='block';applyUploadFilter(selectedFilterName);closeCreatorCamera();showToastMsg('Photo ready — add details and post.');},'image/jpeg',.92); return;
}
if(creatorRecording){creatorMediaRecorder?.stop();return;}
if(!creatorCameraStream)return;
if(creatorTimerSeconds){$('cameraStatus').innerText=`Starting in ${creatorTimerSeconds}...`;for(let i=creatorTimerSeconds;i>0;i--){$('cameraStatus').innerText=`Starting in ${i}...`;await new Promise(r=>setTimeout(r,1000));}}
creatorRecordedChunks=[];let options={mimeType:'video/webm'};if(!MediaRecorder.isTypeSupported(options.mimeType))options={};
try{creatorMediaRecorder=new MediaRecorder(creatorCameraStream,options);}catch(e){alert('This browser does not support video recording.');return;}
creatorMediaRecorder.ondataavailable=e=>{if(e.data?.size)creatorRecordedChunks.push(e.data)};
creatorMediaRecorder.onstop=()=>{const blob=new Blob(creatorRecordedChunks,{type:'video/webm'});uploadSelectedFile=new File([blob],`flikzee-${Date.now()}.webm`,{type:'video/webm'});uploadSelectedMediaType='video';$('fileNameDisplay').innerText='Camera recording ✔️';$('uploadPreview').src=URL.createObjectURL(blob);$('uploadPreview').style.display='block';$('uploadImagePreview').style.display='none';$('uploadPreviewWrap').style.display='block';applyUploadFilter(selectedFilterName);closeCreatorCamera();showToastMsg('Recording ready — add details and post.');};
creatorMediaRecorder.start();creatorRecording=true;creatorRecordingStartedAt=Date.now();$('cameraRecordBtn').classList.add('recording');$('cameraStatus').innerText='Recording… tap again to stop';
if(creatorRecordingTimer)clearTimeout(creatorRecordingTimer);
const maxMs=Number(creatorCameraMode)*1000; if(['10','15','60'].includes(creatorCameraMode)){creatorRecordingTimer=setTimeout(()=>{if(creatorRecording&&creatorMediaRecorder?.state!=='inactive')creatorMediaRecorder.stop();},maxMs);}
};
function startTemplateListener(){
if(templatesUnsubscribe)templatesUnsubscribe();
const q=query(collection(db,'templates'),limit(40));
templatesUnsubscribe=onSnapshot(q,snap=>{const items=snap.docs.map(d=>({id:d.id,...d.data()}));renderTemplates(items);},err=>{console.warn('Templates:',err);renderTemplates([]);});
}
function renderTemplates(items){
const box=$('templateGrid');if(!box)return;
if(!items.length){box.innerHTML='No templates are published yet.Admin can add licensed/original templates to the templates collection.
';return;}
box.innerHTML=items.map(t=>`${escapeHtml(t.name||'FlikZee Template')}
${escapeHtml(t.category||'Viral Hits')} • ${formatCount(t.uses||0)} uses
Use Template `).join('');
}
window.useFlikZeeTemplate=(id)=>{showToastMsg('Template selected — choose your media next.');switchStudioPanel('create');window.selectedTemplateId=id;openGalleryPicker();};
function renderStudioSoundCategories(){const row=$('studioSoundCategoryRow');if(!row)return;row.innerHTML='';SOUND_CATEGORIES.forEach(cat=>{const b=document.createElement('button');b.type='button';b.className='sound-cat'+(cat===selectedSoundCategory?' active':'');b.innerText=cat;b.onclick=()=>{selectedSoundCategory=cat;renderStudioSoundCategories();renderStudioSounds();};row.appendChild(b);});}
window.renderStudioSounds=function(){const box=$('studioSoundList');if(!box)return;const q=String($('studioSoundSearch')?.value||'').toLowerCase().trim();const arr=SOUND_LIBRARY.filter(s=>(q ? (s.name.toLowerCase().includes(q)||s.category.toLowerCase().includes(q)) : s.category===selectedSoundCategory));box.innerHTML=arr.map(sound=>`${escapeHtml(sound.name)} ${escapeHtml(sound.category)} • FlikZee Audio
▶ Use
`).join('');};
window.chooseStudioSound=(id)=>{selectedSound=SOUND_LIBRARY.find(x=>x.id===id)||null;switchStudioPanel('create');renderSoundLibrary();showToastMsg(selectedSound?`Sound selected: ${selectedSound.name}`:'Sound selected');};
async function checkAdminAccess(){
if(!window.currentUser)return false;
try{const snap=await getDoc(doc(db,'users',window.currentUser.uid));const d=snap.data()||{};const token=await window.currentUser.getIdTokenResult();return d.role==='admin'||token.claims?.admin===true;}catch(e){return false;}
}
async function updateAdminVisibility(){const ok=await checkAdminAccess();$('adminCenterSetting').style.display=ok?'flex':'none';return ok;}
window.openAdminCenter=async()=>{if(!(await checkAdminAccess()))return alert('Admin access required.');$('adminCenterModal').classList.add('open');document.body.classList.add('modal-open');loadAdminDashboard();};
window.loadAdminDashboard=async function(){
if(!(await checkAdminAccess()))return;
try{
const [usersCount,videosCount]=await Promise.all([getCountFromServer(collection(db,'users')),getCountFromServer(collection(db,'videos'))]);
const totalUsers=usersCount.data().count,totalVideos=videosCount.data().count;
$('adminStats').innerHTML=`${formatCount(totalUsers)} Total Users
${formatCount(totalVideos)} Total Videos
${formatCount(window.allVideos.reduce((s,v)=>s+(v.views||0),0))} Loaded Views
${formatCount(window.allVideos.reduce((s,v)=>s+(v.likes||[]).length,0))} Loaded Likes
`;
if(adminPaymentsUnsubscribe)adminPaymentsUnsubscribe();
adminPaymentsUnsubscribe=onSnapshot(query(collection(db,'payments'),orderBy('createdAt','desc'),limit(15)),snap=>{$('adminPayments').innerHTML=snap.empty?'No payments recorded.
':snap.docs.map(d=>{const x=d.data();return `🪙 ${escapeHtml(x.userEmail||x.userId||'User')}${escapeHtml(x.packageId||'Coin purchase')} • ${escapeHtml(x.status||'pending')}
${escapeHtml(x.amount||x.price||'')} `}).join('')},()=>{$('adminPayments').innerHTML='Payments collection is ready.
';});
if(adminTopUsersUnsubscribe)adminTopUsersUnsubscribe();
adminTopUsersUnsubscribe=onSnapshot(query(collection(db,'users'),orderBy('totalViews','desc'),limit(10)),snap=>{$('adminTopUsers').innerHTML=snap.docs.map((d,i)=>{const x=d.data();return `#${i+1} ${escapeHtml(x.displayName||x.username||'Creator')}@${escapeHtml(x.username||'creator')} • ${formatCount(x.followers?.length||0)} followers
${formatCount(x.totalViews||0)} views `}).join('')||'No users yet.
';},()=>{$('adminTopUsers').innerHTML='Top-user query needs the totalViews field/index.
';});
if(adminActivityUnsubscribe)adminActivityUnsubscribe();
adminActivityUnsubscribe=onSnapshot(query(collection(db,'adminActivity'),orderBy('createdAt','desc'),limit(ADMIN_ACTIVITY_LIMIT)),snap=>{$('adminActivity').innerHTML=snap.docs.map(d=>{const x=d.data();return `${escapeHtml(x.text||x.action||'Activity')}${escapeHtml(x.userEmail||x.userId||'')} • ${formatNotificationTime(x.createdAt)}
ACTIVE `}).join('')||'No activity logged yet.
';},()=>{$('adminActivity').innerHTML='Activity log ready.
';});
}catch(err){console.error('Admin dashboard:',err);showToastMsg('Admin dashboard could not load completely.');}
};
window.switchTab=(tab,fromProfile=false)=>{
if(tab==='live') return;
if(!fromProfile && tab!=='profile') lastNonProfileTab=tab;
if(soundPreviewAudio){soundPreviewAudio.pause();soundPreviewAudio.currentTime=0;}
document.querySelectorAll('.view-section').forEach(v=>v.style.display='none');
const target=$(tab+'View'); if(target)target.style.display='block';
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
const nav=$(tab==='upload'?'nav-upload':tab==='profile'?'nav-profile-btn':'nav-'+tab); if(nav)nav.classList.add('active');
if(tab!=='home') {
pauseAllFeedVideosExcept(null);
} else {
setupObserver();
}
if(tab==='inbox')markNotificationsRead();
if(tab==='discover')startCategoryListener();
};
window.toggleAudioMute=()=>{
window.isGlobalMuted=!window.isGlobalMuted;
document.querySelectorAll('video').forEach(v=>v.muted=window.isGlobalMuted);
document.querySelectorAll('audio').forEach(a=>a.muted=window.isGlobalMuted);
$('muteBtn').innerText=window.isGlobalMuted?'🔇':'🔊';
if(!window.isGlobalMuted){
document.querySelectorAll('#feed .video-wrapper').forEach(w=>{
const r=w.getBoundingClientRect();
if(r.topwindow.innerHeight*.35 && w._soundAudio){
w._soundAudio.play().catch(()=>{});
}
});
}
};
window.toggleSettings=()=>$('settingsMenu').classList.toggle('open');
window.closeModal=(id)=>{const el=$(id);el?.classList.remove('open');document.body.classList.remove('modal-open');if(id==='commentsModal'&&commentsUnsubscribe){commentsUnsubscribe();commentsUnsubscribe=null;}};
window.showToastMsg=(msg)=>{const t=$('toast');t.innerText=msg;t.style.display='block';clearTimeout(toastTimer);toastTimer=setTimeout(()=>t.style.display='none',2600);};
document.addEventListener('click',()=>{document.querySelectorAll('.video-menu.open,.grid-video-menu.open').forEach(x=>x.classList.remove('open'));});
window.addEventListener('popstate',()=>{ if($('singleVideoView').style.display==='block')closeSingleVideo(); });
loadFeed();
startCategoryListener();
const params=new URLSearchParams(location.search);
const sharedVideo=params.get('video'); const sharedProfile=params.get('profile');
if(sharedVideo){
setTimeout(()=>{
const v=window.allVideos.find(x=>x.id===sharedVideo);
if(v)openSingleMedia(v.mediaUrl||v.videoUrl,v.id);
},900);
}
if(sharedProfile){setTimeout(()=>openCreatorProfile(sharedProfile),900);}