Spaces:
Sleeping
Sleeping
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.9.0/dist/index.min.js"; | |
| // Read/write tokens from sessionStorage | |
| function getReadToken() { | |
| return sessionStorage.getItem('hf_read_token') || ''; | |
| } | |
| function getWriteToken() { | |
| return sessionStorage.getItem('hf_write_token') || ''; | |
| } | |
| // Helper to add tokens to every API call | |
| function withTokens(obj = {}) { | |
| return { | |
| ...obj, | |
| read_token: getReadToken(), | |
| write_token: getWriteToken(), | |
| }; | |
| } | |
| // Helper to call secure server-side proxy | |
| async function callSecureApi(fn, args) { | |
| const resp = await fetch('/api/proxy', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ fn, args }) | |
| }); | |
| if (!resp.ok) throw new Error('Proxy error: ' + resp.status); | |
| return await resp.json(); | |
| } | |
| let client; | |
| let currentUser = null; | |
| let currentPassword = null; | |
| let files = []; | |
| let selected = new Set(); | |
| let currentView = 'grid'; | |
| let previewFile = null; | |
| let isLoadingFiles = false; | |
| const iconMap = { | |
| 'pdf': 'π', 'doc': 'π', 'docx': 'π', | |
| 'png': 'πΌοΈ', 'jpg': 'πΌοΈ', 'jpeg': 'πΌοΈ', 'gif': 'πΌοΈ', | |
| 'mp3': 'π΅', 'wav': 'π΅', 'flac': 'π΅', | |
| 'mp4': 'πΉ', 'avi': 'πΉ', 'mov': 'πΉ', | |
| 'zip': 'ποΈ', 'rar': 'ποΈ', '7z': 'ποΈ', | |
| 'txt': 'π', 'md': 'π', | |
| 'py': 'π', 'js': 'π', 'html': 'π', 'css': 'π', | |
| 'psd': 'π¨', 'ai': 'π¨', | |
| 'sql': 'ποΈ', 'xlsx': 'π', 'csv': 'π', | |
| 'pptx': 'π', 'ppt': 'π', | |
| }; | |
| // Initialize dashboard on page load | |
| async function initDashboard() { | |
| console.log('=== Dashboard Init Started ==='); | |
| currentUser = sessionStorage.getItem('pockit_user'); | |
| currentPassword = sessionStorage.getItem('pockit_pass'); | |
| if (!currentUser || !currentPassword) { | |
| console.warn('No session found, redirecting to login'); | |
| window.location.href = 'index.html'; | |
| return; | |
| } | |
| try { | |
| // Update account info first | |
| const initials = currentUser.substring(0, 2).toUpperCase(); | |
| document.querySelector('.avatar').textContent = initials; | |
| document.querySelector('.account-name').textContent = currentUser; | |
| // Load data using proxy | |
| await loadUserData(); | |
| await loadFiles(); | |
| console.log('β Dashboard loaded'); | |
| } catch (err) { | |
| console.error('Init error:', err); | |
| showToast('Connection failed'); | |
| setTimeout(() => window.location.href = 'index.html', 2000); | |
| } | |
| } | |
| // Load user info and update cards | |
| async function loadUserData() { | |
| try { | |
| console.log('Loading user data...'); | |
| const result = await callSecureApi("/search_user", withTokens({ user_id: currentUser })); | |
| const [log, isDev, isPro, isTester, capacityUsed, usedTotal] = result.data; | |
| // Determine role | |
| let role = 'free'; | |
| if (isDev) role = 'dev'; | |
| else if (isPro) role = 'pro'; | |
| else if (isTester) role = 'tester'; | |
| const tag = document.getElementById('role-tag'); | |
| tag.className = 'role-tag ' + role; | |
| tag.textContent = role.charAt(0).toUpperCase() + role.slice(1); | |
| document.getElementById('tier-val').textContent = tag.textContent; | |
| // Parse capacity | |
| const capacityPercent = Math.round(capacityUsed); | |
| let usedDisplay = usedTotal || '0 / 5 GB'; | |
| let usedParts = usedDisplay.split('/'); | |
| let usedAmount = usedParts[0].trim(); | |
| let maxAmount = usedParts[1] ? usedParts[1].trim() : '5 GB'; | |
| // Update storage displays | |
| document.querySelector('.card-bar-fill').style.width = capacityPercent + '%'; | |
| document.querySelector('.stor-label span:last-child').textContent = usedDisplay; | |
| document.querySelector('.sbar-fill').style.width = capacityPercent + '%'; | |
| const storageCard = document.querySelectorAll('.card')[0]; | |
| storageCard.querySelector('.card-val').innerHTML = usedAmount + ' <span style="font-size:.38em;font-weight:400;color:rgba(0,0,0,.38)">(' + capacityPercent + '%)</span>'; | |
| storageCard.querySelector('.card-sub').textContent = usedDisplay + ' used'; | |
| document.querySelectorAll('.card')[3].querySelector('.card-sub').textContent = maxAmount + ' max capacity'; | |
| console.log('β User data loaded'); | |
| } catch (err) { | |
| console.error('User data error:', err); | |
| } | |
| } | |
| // Load files from API | |
| async function loadFiles() { | |
| // Prevent concurrent loads | |
| if (isLoadingFiles) return; | |
| isLoadingFiles = true; | |
| try { | |
| console.log('Loading files...'); | |
| loading(true); | |
| const result = await callSecureApi("/get_files_secure", withTokens({ | |
| user_id: currentUser, | |
| password: currentPassword, | |
| })); | |
| // API returns [dropdown, status] | |
| const dropdownData = result.data[0]; | |
| console.log('Raw dropdownData:', dropdownData); | |
| let fileNames = []; | |
| if (dropdownData && typeof dropdownData === 'object' && dropdownData.choices) { | |
| // Flatten all sub-arrays in choices | |
| fileNames = dropdownData.choices.flat ? dropdownData.choices.flat() : [].concat(...dropdownData.choices); | |
| } else if (Array.isArray(dropdownData)) { | |
| fileNames = dropdownData; | |
| } else if (typeof dropdownData === 'string') { | |
| fileNames = [dropdownData]; | |
| } | |
| // Convert all to string, trim, and filter empty | |
| fileNames = fileNames.map(f => String(f).trim()).filter(Boolean); | |
| console.log('Extracted fileNames from dropdown:', fileNames); | |
| // Deduplicate file names (case-insensitive, trim) | |
| const seen = new Set(); | |
| const deduped = []; | |
| for (const name of fileNames) { | |
| const key = name.toLowerCase(); | |
| if (!key || seen.has(key)) continue; | |
| seen.add(key); | |
| deduped.push(name); | |
| } | |
| files = deduped.map((name, idx) => { | |
| const ext = name.includes('.') ? name.split('.').pop().toLowerCase() : ''; | |
| return { | |
| id: idx, | |
| name: name, | |
| icon: iconMap[ext] || 'π', | |
| size: 'β', | |
| date: 'Recently', | |
| type: ext ? ext.toUpperCase() : '', | |
| preview: null // for preview content | |
| }; | |
| }); | |
| console.log('Final files array before render:', files); | |
| selected.clear(); | |
| updateStatsCards(); | |
| render(files); | |
| loading(false); | |
| console.log('β Files loaded and rendered'); | |
| preloadTextPreviews(); | |
| } catch (err) { | |
| console.error('Files load error:', err); | |
| loading(false); | |
| files = []; | |
| selected.clear(); | |
| render([]); | |
| showToast('Failed to load files'); | |
| } finally { | |
| isLoadingFiles = false; | |
| } | |
| } | |
| // Preload short text previews for text files so grid view | |
| // can show the first few words instead of just an icon. | |
| async function preloadTextPreviews() { | |
| // Only attempt after files are loaded and user is known | |
| if (!currentUser || !currentPassword || !Array.isArray(files)) return; | |
| for (const file of files) { | |
| // Simple heuristic: only fetch preview for obvious text formats | |
| const name = (file && file.name) ? String(file.name).toLowerCase() : ''; | |
| if (!name.endsWith('.txt') && !name.endsWith('.md')) continue; | |
| try { | |
| const result = await callSecureApi("/get_preview_text_action", withTokens({ | |
| user_id: currentUser, | |
| password: currentPassword, | |
| filename: file.name, | |
| })); | |
| const text = Array.isArray(result.data) ? result.data[0] : result.data; | |
| if (typeof text === 'string' && text.trim()) { | |
| file.preview = text; | |
| } | |
| } catch (err) { | |
| console.error('Text preview preload error for', file.name, err); | |
| } | |
| } | |
| render(files); | |
| } | |
| // Update stats cards | |
| function updateStatsCards() { | |
| const cards = document.querySelectorAll('.card'); | |
| if (cards.length < 3) return; | |
| // Files count | |
| cards[1].querySelector('.card-val').textContent = files.length; | |
| // File types | |
| const fileTypes = new Set(files.map(f => f.type)); | |
| cards[1].querySelector('.card-sub').textContent = `Across ${fileTypes.size} file type${fileTypes.size !== 1 ? 's' : ''}`; | |
| // Upload card | |
| cards[2].querySelector('.card-val').textContent = files.length > 0 ? '1' : '0'; | |
| cards[2].querySelector('.card-sub').textContent = files.length > 0 ? 'Last upload: Recently' : 'No uploads yet'; | |
| } | |
| // Render grid and table | |
| function render(list) { | |
| console.log('Rendering', list.length, 'files:', list.map(f => f.name)); | |
| const grid = document.getElementById('file-grid'); | |
| const tbody = document.getElementById('file-tbody'); | |
| if (!grid) { | |
| console.error('file-grid missing'); | |
| return; | |
| } | |
| if (!tbody) { | |
| console.error('file-tbody missing'); | |
| return; | |
| } | |
| // Clear everything first | |
| grid.innerHTML = ''; | |
| tbody.innerHTML = ''; | |
| // Only render if there are files | |
| if (!list || list.length === 0) { | |
| console.log('No files to render'); | |
| return; | |
| } | |
| // Grid HTML | |
| const gridHTML = list.map((f, i) => { | |
| let iconOrPreview = f.icon; | |
| if (f.preview && typeof f.preview === 'string' && f.preview.trim()) { | |
| const words = f.preview.trim().split(/\s+/).slice(0, 5).join(' '); | |
| const snippet = words + (f.preview.trim().length > words.length ? '...' : ''); | |
| iconOrPreview = escapeHtml(snippet); | |
| } | |
| return ` | |
| <div class="file-card ${selected.has(i) ? 'selected' : ''}" onclick="toggleSelect(${i})" ondblclick="openPreview(${i})"> | |
| <span class="fc-check"><i class="fa-solid fa-check"></i></span> | |
| <span class="fc-icon">${iconOrPreview}</span> | |
| <span class="fc-name">${f.name}</span> | |
| <span class="fc-meta">${f.size}</span> | |
| </div> | |
| `; | |
| }).join(''); | |
| grid.innerHTML = gridHTML; | |
| console.log('Grid rendered with', list.length, 'items'); | |
| // Table HTML | |
| const tableHTML = list.map((f, i) => ` | |
| <tr onclick="openPreview(${i})"> | |
| <td class="td-name"><span>${f.icon}</span> ${f.name}</td> | |
| <td class="td-muted">${f.size}</td> | |
| <td class="td-muted">${f.date}</td> | |
| <td class="td-muted">${f.type}</td> | |
| <td class="td-actions"> | |
| <button class="tbl-btn" onclick="event.stopPropagation();openPreview(${i})"><i class="fa-solid fa-eye"></i></button> | |
| <button class="tbl-btn" onclick="event.stopPropagation();downloadFile(${i})"><i class="fa-solid fa-download"></i></button> | |
| <button class="tbl-btn del" onclick="event.stopPropagation();deleteFile(${i})"><i class="fa-solid fa-trash"></i></button> | |
| </td> | |
| </tr> | |
| `).join(''); | |
| tbody.innerHTML = tableHTML; | |
| console.log('Table rendered with', list.length, 'items'); | |
| } | |
| // Select/deselect file | |
| function toggleSelect(id) { | |
| if (id < 0 || id >= files.length) return; | |
| selected.has(id) ? selected.delete(id) : selected.add(id); | |
| render(currentView === 'grid' ? files : files); | |
| } | |
| function clearSelection() { | |
| selected.clear(); | |
| render(files); | |
| } | |
| function handleSelectAll(checked) { | |
| if (checked) { | |
| files.forEach((_, i) => selected.add(i)); | |
| } else { | |
| selected.clear(); | |
| } | |
| render(files); | |
| } | |
| // Open file preview | |
| function openPreview(id) { | |
| if (id < 0 || id >= files.length) return; | |
| previewFile = files[id]; | |
| document.getElementById('preview-thumb').textContent = previewFile.icon; | |
| document.getElementById('preview-name').textContent = previewFile.name; | |
| document.getElementById('preview-name').style.color = '#1a1a2e'; | |
| document.getElementById('pv-size').textContent = previewFile.size; | |
| document.getElementById('pv-date').textContent = previewFile.date; | |
| document.getElementById('pv-type').textContent = previewFile.type; | |
| // Load preview content from API | |
| loadPreviewContent(previewFile); | |
| selected.clear(); | |
| selected.add(id); | |
| render(files); | |
| } | |
| // Load preview content for a file using /handle_preview_secure and /get_preview_text_action | |
| async function loadPreviewContent(file) { | |
| if (!file || !file.name) return; | |
| const previewContainer = document.getElementById('preview-content'); | |
| if (!previewContainer) return; | |
| previewContainer.innerHTML = '<span style="color:#aaa">Loading preview...</span>'; | |
| try { | |
| // Try /handle_preview_secure first | |
| const result = await callSecureApi("/handle_preview_secure", withTokens({ | |
| user_id: currentUser, | |
| password: currentPassword, | |
| filename: file.name, | |
| })); | |
| // result.data: [file, localPath, html, image, code] | |
| let previewHtml = ''; | |
| if (result.data[3] && typeof result.data[3] === 'string' && result.data[3].startsWith('data:image')) { | |
| // Image preview | |
| previewHtml = `<img src="${result.data[3]}" alt="Preview" style="max-width:100%;max-height:200px;">`; | |
| } else if (result.data[2] && typeof result.data[2] === 'string' && result.data[2].length > 0) { | |
| // HTML preview | |
| previewHtml = `<div class="pv-html">${result.data[2]}</div>`; | |
| } else if (result.data[4] && typeof result.data[4] === 'string' && result.data[4].length > 0) { | |
| // Code preview | |
| previewHtml = `<pre class="pv-code">${escapeHtml(result.data[4])}</pre>`; | |
| } else { | |
| // Try /get_preview_text_action for text preview | |
| const textResult = await callSecureApi("/get_preview_text_action", withTokens({ | |
| user_id: currentUser, | |
| password: currentPassword, | |
| filename: file.name, | |
| })); | |
| if (textResult.data && typeof textResult.data[0] === 'string' && textResult.data[0].length > 0) { | |
| previewHtml = `<pre class="pv-text">${escapeHtml(textResult.data[0])}</pre>`; | |
| } else { | |
| previewHtml = '<span style="color:#aaa">No preview available.</span>'; | |
| } | |
| } | |
| previewContainer.innerHTML = previewHtml; | |
| } catch (err) { | |
| console.error('Preview error:', err); | |
| previewContainer.innerHTML = '<span style="color:#f55">Failed to load preview.</span>'; | |
| } | |
| } | |
| // Helper to escape HTML for code/text preview | |
| function escapeHtml(str) { | |
| return String(str) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| } | |
| // Delete single file | |
| async function deleteFile(id) { | |
| if (id < 0 || id >= files.length) return; | |
| const file = files[id]; | |
| try { | |
| loading(true); | |
| const result = await callSecureApi("/delete_file_secure", withTokens({ | |
| user_id: currentUser, | |
| password: currentPassword, | |
| filename: file.name, | |
| })); | |
| console.log('Deleted:', file.name); | |
| // Update storage | |
| const [status, capacityUsed, usedTotal] = result.data; | |
| const percent = Math.round(capacityUsed); | |
| document.querySelector('.card-bar-fill').style.width = percent + '%'; | |
| document.querySelector('.sbar-fill').style.width = percent + '%'; | |
| document.querySelector('.stor-label span:last-child').textContent = usedTotal; | |
| // Remove from array | |
| files.splice(id, 1); | |
| selected.delete(id); | |
| updateStatsCards(); | |
| render(files); | |
| loading(false); | |
| showToast(file.name + ' deleted'); | |
| } catch (err) { | |
| console.error('Delete error:', err); | |
| loading(false); | |
| showToast('Delete failed'); | |
| } | |
| } | |
| // Delete selected files | |
| async function deleteSelected() { | |
| if (selected.size === 0) { | |
| showToast('No files selected'); | |
| return; | |
| } | |
| const ids = Array.from(selected).sort((a, b) => b - a); | |
| for (const id of ids) { | |
| await deleteFile(id); | |
| } | |
| } | |
| // Delete preview file | |
| async function deletePreviewFile() { | |
| if (!previewFile) return; | |
| const idx = files.indexOf(previewFile); | |
| if (idx >= 0) await deleteFile(idx); | |
| previewFile = null; | |
| document.getElementById('preview-name').textContent = 'Select a file'; | |
| document.getElementById('preview-name').style.color = 'rgba(0,0,0,.3)'; | |
| } | |
| // Download file | |
| async function downloadFile(id) { | |
| if (id < 0 || id >= files.length) return; | |
| const file = files[id]; | |
| try { | |
| loading(true); | |
| const resp = await fetch('/api/download-link', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| user_id: currentUser, | |
| password: currentPassword, | |
| filename: file.name, | |
| }), | |
| }); | |
| if (!resp.ok) throw new Error('Download-link HTTP ' + resp.status); | |
| const payload = await resp.json(); | |
| const fileTarget = Array.isArray(payload.data) ? payload.data[0] : payload.data; | |
| if (typeof fileTarget === 'string' && fileTarget) { | |
| const a = document.createElement('a'); | |
| a.href = '/api/download?file=' + encodeURIComponent(fileTarget); | |
| a.download = file.name; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| }, 100); | |
| showToast('Download started: ' + file.name); | |
| } else { | |
| showToast('Download link error'); | |
| } | |
| loading(false); | |
| } catch (err) { | |
| console.error('Download error:', err); | |
| loading(false); | |
| showToast('Download failed'); | |
| } | |
| } | |
| async function downloadPreviewFile() { | |
| if (!previewFile) return; | |
| const idx = files.indexOf(previewFile); | |
| if (idx >= 0) { | |
| await downloadFile(idx); | |
| } | |
| } | |
| // Upload file (single file) via server-side endpoint so private HF token stays on the server | |
| async function handleUpload(input) { | |
| if (!input.files || !input.files[0]) return; | |
| const file = input.files[0]; | |
| try { | |
| loading(true); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| formData.append('user_id', currentUser); | |
| formData.append('password', currentPassword); | |
| formData.append('custom_name', file.name); | |
| const resp = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| if (!resp.ok) throw new Error('Upload HTTP ' + resp.status); | |
| const result = await resp.json(); | |
| console.log('Uploaded:', file.name); | |
| // Update storage | |
| const [status, capacityUsed, usedTotal] = result.data; | |
| const percent = Math.round(capacityUsed); | |
| document.querySelector('.card-bar-fill').style.width = percent + '%'; | |
| document.querySelector('.sbar-fill').style.width = percent + '%'; | |
| document.querySelector('.stor-label span:last-child').textContent = usedTotal; | |
| // Reload files | |
| await loadFiles(); | |
| showToast(file.name + ' uploaded'); | |
| input.value = ''; | |
| } catch (err) { | |
| console.error('Upload error:', err); | |
| showToast('Upload failed'); | |
| } finally { | |
| loading(false); | |
| } | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| document.getElementById('drop-zone').classList.remove('drag'); | |
| if (e.dataTransfer.files) { | |
| handleUpload({ files: e.dataTransfer.files }); | |
| } | |
| } | |
| // Filter files by search | |
| function filterFiles(q) { | |
| const query = q.toLowerCase(); | |
| const filtered = files.filter(f => f.name.toLowerCase().includes(query)); | |
| render(filtered); | |
| } | |
| // Toggle view | |
| function setView(v) { | |
| currentView = v; | |
| document.getElementById('file-grid').style.display = v === 'grid' ? 'grid' : 'none'; | |
| document.getElementById('file-table-wrap').style.display = v === 'list' ? 'block' : 'none'; | |
| document.getElementById('vt-grid').classList.toggle('active', v === 'grid'); | |
| document.getElementById('vt-list').classList.toggle('active', v === 'list'); | |
| } | |
| // Navigation sections | |
| function setNav(el, section) { | |
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); | |
| el.classList.add('active'); | |
| if (section === 'recent') { | |
| document.getElementById('page-title').textContent = 'Recent Files'; | |
| render(files.slice(0, 5)); | |
| } else { | |
| document.getElementById('page-title').textContent = 'My Files'; | |
| render(files); | |
| } | |
| } | |
| // Change password | |
| async function changePassword() { | |
| const p1 = document.getElementById('new-pw').value; | |
| const p2 = document.getElementById('conf-pw').value; | |
| if (!p1 || p1 !== p2) { | |
| showToast('Passwords do not match'); | |
| return; | |
| } | |
| try { | |
| loading(true); | |
| const result = await callSecureApi("/update_password", withTokens({ | |
| user_id: currentUser, | |
| new_password: p1, | |
| })); | |
| if (result.data[0].toLowerCase().includes('success') || result.data[0].toLowerCase().includes('updated')) { | |
| currentPassword = p1; | |
| sessionStorage.setItem('pockit_pass', p1); | |
| closeModal('pw-modal'); | |
| showToast('Password updated'); | |
| document.getElementById('new-pw').value = ''; | |
| document.getElementById('conf-pw').value = ''; | |
| } else { | |
| showToast('Password update failed'); | |
| } | |
| loading(false); | |
| } catch (err) { | |
| console.error('Password error:', err); | |
| loading(false); | |
| showToast('Password update failed'); | |
| } | |
| } | |
| // Logout | |
| function logoutUser() { | |
| sessionStorage.clear(); | |
| showToast('Logged out'); | |
| setTimeout(() => window.location.href = 'index.html', 1500); | |
| } | |
| // Modal helpers | |
| function openModal(id) { | |
| document.getElementById(id).classList.add('open'); | |
| } | |
| function closeModal(id) { | |
| document.getElementById(id).classList.remove('open'); | |
| } | |
| // UI helpers | |
| function loading(on) { | |
| document.getElementById('loader').classList.toggle('on', on); | |
| } | |
| let toastTimer; | |
| function showToast(msg) { | |
| const toast = document.getElementById('toast'); | |
| document.getElementById('toast-msg').textContent = msg; | |
| toast.classList.add('show'); | |
| clearTimeout(toastTimer); | |
| toastTimer = setTimeout(() => toast.classList.remove('show'), 3000); | |
| } | |
| // Initialize on page load | |
| document.addEventListener('DOMContentLoaded', initDashboard); | |
| // Expose to global scope | |
| window.initDashboard = initDashboard; | |
| window.loadUserData = loadUserData; | |
| window.loadFiles = loadFiles; | |
| window.updateStatsCards = updateStatsCards; | |
| window.render = render; | |
| window.toggleSelect = toggleSelect; | |
| window.clearSelection = clearSelection; | |
| window.handleSelectAll = handleSelectAll; | |
| window.openPreview = openPreview; | |
| window.loadPreviewContent = loadPreviewContent; | |
| window.deleteFile = deleteFile; | |
| window.deleteSelected = deleteSelected; | |
| window.deletePreviewFile = deletePreviewFile; | |
| window.downloadFile = downloadFile; | |
| window.downloadPreviewFile = downloadPreviewFile; | |
| window.handleUpload = handleUpload; | |
| window.handleDrop = handleDrop; | |
| window.filterFiles = filterFiles; | |
| window.setView = setView; | |
| window.setNav = setNav; | |
| window.changePassword = changePassword; | |
| window.logoutUser = logoutUser; | |
| window.openModal = openModal; | |
| window.closeModal = closeModal; | |
| window.loading = loading; | |
| window.showToast = showToast; | |