859 lines
35 KiB
HTML
859 lines
35 KiB
HTML
{{template "base.html" .}}
|
|
|
|
{{define "title"}}Password Pusher - Secure Text Sharing{{end}}
|
|
|
|
{{define "head"}}
|
|
{{if .Success}}
|
|
<meta name="success-data" content='{"id":"{{.ID}}","url":"{{.PushURL}}","expiresAt":"{{.ExpiresAt}}","maxViews":"{{.MaxViews}}"}'>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="container">
|
|
<div class="text-center mb-8">
|
|
<a href="/pwpush" class="inline-block">
|
|
<h1 class="text-2xl md:text-3xl font-bold text-gray-100 hover:text-blue-400 transition-colors cursor-pointer mb-4">
|
|
🔐 Password Pusher
|
|
</h1>
|
|
</a>
|
|
<p class="text-lg text-gray-300 max-w-2xl mx-auto">
|
|
Share sensitive text securely with automatic expiration and view limits.
|
|
</p>
|
|
</div>
|
|
|
|
{{if .Success}}
|
|
<div class="bg-green-800 border border-green-600 rounded-lg p-6 mb-8 max-w-4xl mx-auto">
|
|
<h3 class="text-xl font-bold text-green-100 mb-4">✅ Secure Link Created!</h3>
|
|
<p class="text-green-200 mb-4">Your text has been encrypted and stored securely. Share this link:</p>
|
|
<div class="flex flex-col sm:flex-row gap-3 mb-4 p-4 bg-green-900/50 rounded-lg border border-green-700">
|
|
<input type="text"
|
|
id="pushUrl"
|
|
value="{{.PushURL}}"
|
|
readonly
|
|
onclick="this.select()"
|
|
class="flex-1 p-3 bg-gray-900 border-2 border-gray-600 rounded-lg text-gray-100 font-mono text-sm focus:border-green-500 focus:outline-none">
|
|
<div class="flex flex-row items-stretch justify-center gap-3">
|
|
<button onclick="copyToClipboard()"
|
|
class="copy-btn bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors whitespace-nowrap">
|
|
📋 Copy URL
|
|
</button>
|
|
<button onclick="copyAsMessage()"
|
|
class="copy-message-btn bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors whitespace-nowrap">
|
|
💬 Copy as Message
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p class="text-green-300 text-sm mb-4">🕒 Expires: {{.ExpiresAt}} or after {{.MaxViews}} views.</p>
|
|
<a href="/pwpush"
|
|
class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors">
|
|
Create Another Link
|
|
</a>
|
|
</div>
|
|
{{else}}
|
|
|
|
<form id="pushForm" method="post" class="bg-gray-800 rounded-lg p-6 mb-8 max-w-4xl mx-auto border border-gray-700 shadow-lg">
|
|
{{if .CSRFToken}}
|
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
|
{{end}}
|
|
|
|
<!-- Main content area -->
|
|
<div class="space-y-6 mb-8">
|
|
<div class="w-full">
|
|
<label for="text" class="block text-sm font-semibold text-gray-200 mb-2">📝 Text to Share:</label>
|
|
<textarea name="text" id="text"
|
|
class="w-full p-4 bg-gray-900 border-2 border-gray-600 rounded-lg text-gray-100 font-mono resize-y focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none transition-all"
|
|
rows="6"
|
|
placeholder="Enter your password, secret, or sensitive text here..."
|
|
required></textarea>
|
|
<small class="block mt-1 text-xs text-gray-400">Enter any sensitive information you want to share securely.</small>
|
|
</div>
|
|
|
|
<div class="w-full">
|
|
<label for="protection_password" class="block text-sm font-semibold text-gray-200 mb-2">🔑 Password Protection (Optional):</label>
|
|
<input type="password"
|
|
name="password"
|
|
id="protection_password"
|
|
class="w-full p-3 bg-gray-900 border-2 border-gray-600 rounded-lg text-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none transition-all"
|
|
placeholder="Leave empty for no password protection">
|
|
<small class="block mt-1 text-xs text-gray-400">If set, viewers must enter this password to access the content. This bypasses the click-to-reveal feature.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Settings Toggle -->
|
|
<div class="mb-8">
|
|
<button type="button"
|
|
onclick="toggleAdvancedSettings()"
|
|
class="flex items-center justify-between w-full p-2 bg-gray-900 rounded-lg border border-gray-700 hover:bg-gray-800 transition-colors">
|
|
<span class="flex items-center space-x-2 text-gray-200 font-medium">
|
|
<span class="text-lg">⚙️</span>
|
|
<span>Advanced Settings <small class="text-gray-500">(Expiry, Click to reveal, History, Deletion)</small></span>
|
|
</span>
|
|
<span id="advancedToggleIcon" class="text-gray-400 transform transition-transform duration-200">▼</span>
|
|
</button>
|
|
|
|
<div id="advancedSettings" class="hidden mt-4 space-y-6">
|
|
<!-- Settings grid -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 bg-gray-900 rounded-lg border border-gray-700">
|
|
<div class="space-y-3">
|
|
<label for="expiry_days" class="block text-sm font-semibold text-gray-200">⏰ Expires After:</label>
|
|
<input type="range"
|
|
name="expiry_days"
|
|
id="expiry_days"
|
|
min="1" max="90" value="7"
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
|
oninput="updateExpiryDisplay(this.value)"
|
|
onchange="saveSettingWithDelay('expiry_days', this.value)">
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span id="expiryDisplay" class="font-bold text-blue-400">7 days</span>
|
|
<small class="text-gray-500">Max: 3 months</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<label for="max_views" class="block text-sm font-semibold text-gray-200">👁️ Maximum Views:</label>
|
|
<input type="range"
|
|
name="max_views"
|
|
id="max_views"
|
|
min="1" max="100" value="10"
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
|
oninput="updateViewsDisplay(this.value)"
|
|
onchange="saveSettingWithDelay('max_views', this.value)">
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span id="viewsDisplay" class="font-bold text-blue-400">10 views</span>
|
|
<small class="text-gray-500">Max: 100 views</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Options -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 p-6 bg-gray-900 rounded-lg border border-gray-700">
|
|
<div class="space-y-2">
|
|
<label class="flex items-center space-x-3 cursor-pointer">
|
|
<input type="checkbox"
|
|
name="require_click"
|
|
checked
|
|
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
|
|
onchange="saveSettingImmediately('require_click', this.checked)">
|
|
<span class="text-sm font-medium text-gray-200">🛡️ Require click to reveal</span>
|
|
</label>
|
|
<small class="block text-xs text-gray-400 ml-8">Hides content from web crawlers and requires user interaction</small>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<label class="flex items-center space-x-3 cursor-pointer">
|
|
<input type="checkbox"
|
|
name="auto_delete"
|
|
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
|
|
onchange="saveSettingImmediately('auto_delete', this.checked)">
|
|
<span class="text-sm font-medium text-gray-200">🗑️ Allow manual deletion</span>
|
|
</label>
|
|
<small class="block text-xs text-gray-400 ml-8">Adds a delete button when content is viewed (viewer can choose to delete)</small>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<label class="flex items-center space-x-3 cursor-pointer">
|
|
<input type="checkbox"
|
|
name="track_history"
|
|
id="track_history"
|
|
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
|
|
onchange="saveSettingImmediately('track_history', this.checked)">
|
|
<span class="text-sm font-medium text-gray-200">📚 Save to my history</span>
|
|
</label>
|
|
<small class="block text-xs text-gray-400 ml-8">Keep a record of your created links (stored in browser)</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="text-center pt-6">
|
|
<button type="submit"
|
|
class="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-bold py-4 px-8 rounded-lg text-lg transition-all duration-200 transform hover:scale-105 hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-blue-500/50">
|
|
🔒 Create Secure Link
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="bg-gray-800 rounded-lg p-6 mb-8 max-w-4xl mx-auto border border-gray-700">
|
|
<h3 class="text-lg font-bold text-gray-200 mb-4">🔒 How it works:</h3>
|
|
<ul class="space-y-2 text-gray-300">
|
|
<li class="flex items-start space-x-2">
|
|
<span class="text-blue-400 font-bold">•</span>
|
|
<span><strong class="text-gray-200">Encryption:</strong> Do not remove <code>?k=...</code> from link, otherwise you cannot retrieve content.</span>
|
|
</li>
|
|
<li class="flex items-start space-x-2">
|
|
<span class="text-blue-400 font-bold">•</span>
|
|
<span><strong class="text-gray-200">Automatic Expiry:</strong> Links expire after set time or view limit is reached</span>
|
|
</li>
|
|
<li class="flex items-start space-x-2">
|
|
<span class="text-blue-400 font-bold">•</span>
|
|
<span><strong class="text-gray-200">One-time Use:</strong> Optional auto-delete after viewing with button click.</span>
|
|
</li>
|
|
<li class="flex items-start space-x-2">
|
|
<span class="text-blue-400 font-bold">•</span>
|
|
<span><strong class="text-gray-200">No Registration:</strong> No account required</span>
|
|
</li>
|
|
<li class="flex items-start space-x-2">
|
|
<span class="text-blue-400 font-bold">•</span>
|
|
<span><strong class="text-gray-200">Browser History:</strong> Optionally track your links locally, in web browser only.</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- History Section -->
|
|
<div class="bg-gray-800 rounded-lg p-6 max-w-6xl mx-auto border border-gray-700">
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6">
|
|
<h2 class="text-xl font-bold text-gray-200 mb-4 sm:mb-0">📚 My Recent Links</h2>
|
|
<div class="flex space-x-2">
|
|
<button onclick="refreshHistory()"
|
|
class="bg-gray-700 hover:bg-gray-600 text-gray-200 px-4 py-2 rounded-lg text-sm font-medium transition-colors">
|
|
🔄 Refresh
|
|
</button>
|
|
<button onclick="clearHistory()"
|
|
class="bg-red-700 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
|
|
🗑️ Clear All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="historyContainer">
|
|
<div class="flex flex-wrap gap-4 mb-6">
|
|
<span class="bg-gray-700 text-gray-300 px-3 py-1 rounded-full text-sm">
|
|
Total: <span id="totalCount" class="font-bold text-white">0</span>
|
|
</span>
|
|
<span class="bg-green-700 text-green-100 px-3 py-1 rounded-full text-sm">
|
|
Active: <span id="activeCount" class="font-bold text-white">0</span>
|
|
</span>
|
|
<span class="bg-red-700 text-red-100 px-3 py-1 rounded-full text-sm">
|
|
Expired: <span id="expiredCount" class="font-bold text-white">0</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table id="historyTable" class="w-full text-sm text-gray-300">
|
|
<thead class="bg-gray-900 text-gray-200 uppercase text-xs">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left">📅 Created</th>
|
|
<th class="px-4 py-3 text-left">⏰ Expires</th>
|
|
<th class="px-4 py-3 text-left">👁️ Views</th>
|
|
<th class="px-4 py-3 text-left">📝 Local Notes</th>
|
|
<th class="px-4 py-3 text-left">🔗 Status</th>
|
|
<th class="px-4 py-3 text-left">⚡ Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="historyTableBody" class="divide-y divide-gray-700">
|
|
<tr class="empty-row">
|
|
<td colspan="6" class="px-4 py-8 text-center text-gray-400">
|
|
📭 No history found. Enable "Save to my history" when creating links.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleAdvancedSettings() {
|
|
const advancedSettings = document.getElementById('advancedSettings');
|
|
const toggleIcon = document.getElementById('advancedToggleIcon');
|
|
|
|
if (advancedSettings.classList.contains('hidden')) {
|
|
advancedSettings.classList.remove('hidden');
|
|
toggleIcon.textContent = '▲';
|
|
toggleIcon.style.transform = 'rotate(180deg)';
|
|
} else {
|
|
advancedSettings.classList.add('hidden');
|
|
toggleIcon.textContent = '▼';
|
|
toggleIcon.style.transform = 'rotate(0deg)';
|
|
}
|
|
}
|
|
|
|
function updateExpiryDisplay(days) {
|
|
const display = document.getElementById('expiryDisplay');
|
|
if (days == 1) {
|
|
display.textContent = '1 day';
|
|
} else if (days <= 7) {
|
|
display.textContent = days + ' days';
|
|
} else if (days <= 30) {
|
|
const weeks = Math.floor(days / 7);
|
|
const remainingDays = days % 7;
|
|
if (remainingDays === 0) {
|
|
display.textContent = weeks + (weeks === 1 ? ' week' : ' weeks');
|
|
} else {
|
|
display.textContent = weeks + 'w ' + remainingDays + 'd';
|
|
}
|
|
} else {
|
|
const months = Math.floor(days / 30);
|
|
const remainingDays = days % 30;
|
|
if (remainingDays === 0) {
|
|
display.textContent = months + (months === 1 ? ' month' : ' months');
|
|
} else {
|
|
display.textContent = months + 'm ' + remainingDays + 'd';
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateViewsDisplay(views) {
|
|
const display = document.getElementById('viewsDisplay');
|
|
display.textContent = views + (views === '1' ? ' view' : ' views');
|
|
}
|
|
|
|
// Auto-save settings functionality
|
|
let saveTimeouts = {};
|
|
|
|
function saveSettingWithDelay(settingName, value) {
|
|
// Clear any existing timeout for this setting
|
|
if (saveTimeouts[settingName]) {
|
|
clearTimeout(saveTimeouts[settingName]);
|
|
}
|
|
|
|
// Set a new timeout to save after user stops changing the value
|
|
saveTimeouts[settingName] = setTimeout(() => {
|
|
saveSettingImmediately(settingName, value);
|
|
}, 1000); // Wait 1 second after user stops moving slider
|
|
}
|
|
|
|
function saveSettingImmediately(settingName, value) {
|
|
setCookie('pwpush_' + settingName, value, 30);
|
|
|
|
// Show success notification
|
|
if (typeof showPopup === 'function') {
|
|
showPopup('Setting "' + settingName.replace('_', ' ') + '" saved automatically!', 'success', 2000);
|
|
}
|
|
}
|
|
|
|
function copyToClipboard() {
|
|
const urlInput = document.getElementById('pushUrl');
|
|
const btn = document.querySelector('.copy-btn');
|
|
|
|
if (!urlInput) {
|
|
console.error('pushUrl input not found');
|
|
return;
|
|
}
|
|
|
|
if (!btn) {
|
|
console.error('copy-btn button not found');
|
|
return;
|
|
}
|
|
|
|
const originalText = btn.textContent;
|
|
|
|
// Use modern clipboard API if available, fallback to execCommand
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(urlInput.value).then(() => {
|
|
btn.textContent = '✅ Copied!';
|
|
btn.style.background = '#4caf50';
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.style.background = '';
|
|
}, 2000);
|
|
}).catch(err => {
|
|
console.error('Failed to copy: ', err);
|
|
fallbackCopy();
|
|
});
|
|
} else {
|
|
fallbackCopy();
|
|
}
|
|
|
|
function fallbackCopy() {
|
|
urlInput.select();
|
|
urlInput.setSelectionRange(0, 99999); // For mobile devices
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
btn.textContent = '✅ Copied!';
|
|
btn.style.background = '#4caf50';
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.style.background = '';
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy: ', err);
|
|
btn.textContent = '❌ Failed';
|
|
btn.style.background = '#f44336';
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.style.background = '';
|
|
}, 2000);
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyAsMessage() {
|
|
const urlInput = document.getElementById('pushUrl');
|
|
const btn = document.querySelector('.copy-message-btn');
|
|
|
|
if (!urlInput) {
|
|
console.error('pushUrl input not found');
|
|
return;
|
|
}
|
|
|
|
if (!btn) {
|
|
console.error('copy-message-btn button not found');
|
|
return;
|
|
}
|
|
|
|
// Get the data from meta tag
|
|
const successData = document.querySelector('meta[name="success-data"]');
|
|
if (!successData) {
|
|
console.error('No success data found');
|
|
return;
|
|
}
|
|
|
|
const data = JSON.parse(successData.content);
|
|
const url = data.url;
|
|
const expiresAt = data.expiresAt;
|
|
const maxViews = data.maxViews;
|
|
|
|
// Create the message
|
|
const message = `The secret has been shared via the link below, it will expire on ${expiresAt} or after ${maxViews} views, whichever comes first:
|
|
${url}`;
|
|
|
|
const originalText = btn.textContent;
|
|
|
|
// Use modern clipboard API if available, fallback to execCommand
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(message).then(() => {
|
|
btn.textContent = '✅ Message Copied!';
|
|
btn.style.background = '#4caf50';
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.style.background = '';
|
|
}, 2000);
|
|
}).catch(err => {
|
|
console.error('Failed to copy message: ', err);
|
|
fallbackCopyMessage();
|
|
});
|
|
} else {
|
|
fallbackCopyMessage();
|
|
}
|
|
|
|
function fallbackCopyMessage() {
|
|
// Create a temporary textarea to copy the message
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = message;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
btn.textContent = '✅ Message Copied!';
|
|
btn.style.background = '#4caf50';
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.style.background = '';
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy message: ', err);
|
|
btn.textContent = '❌ Failed';
|
|
btn.style.background = '#f44336';
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.style.background = '';
|
|
}, 2000);
|
|
} finally {
|
|
document.body.removeChild(textArea);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save settings to cookies when form is submitted
|
|
const pushForm = document.getElementById('pushForm');
|
|
if (pushForm) {
|
|
pushForm.addEventListener('submit', function() {
|
|
const expiryDays = document.getElementById('expiry_days').value;
|
|
const maxViews = document.getElementById('max_views').value;
|
|
const requireClick = document.querySelector('input[name="require_click"]').checked;
|
|
const autoDelete = document.querySelector('input[name="auto_delete"]').checked;
|
|
const trackHistory = document.querySelector('input[name="track_history"]').checked;
|
|
|
|
setCookie('pwpush_expiry_days', expiryDays, 30);
|
|
setCookie('pwpush_max_views', maxViews, 30);
|
|
setCookie('pwpush_require_click', requireClick, 30);
|
|
setCookie('pwpush_auto_delete', autoDelete, 30);
|
|
setCookie('pwpush_track_history', trackHistory, 30);
|
|
});
|
|
}
|
|
|
|
// History functionality
|
|
function refreshHistory() {
|
|
// Show loading state
|
|
const refreshBtn = document.querySelector('button[onclick="refreshHistory()"]');
|
|
const originalText = refreshBtn.textContent;
|
|
refreshBtn.textContent = '🔄 Validating...';
|
|
refreshBtn.disabled = true;
|
|
|
|
setTimeout(() => {
|
|
loadHistory();
|
|
refreshBtn.textContent = originalText;
|
|
refreshBtn.disabled = false;
|
|
}, 100);
|
|
}
|
|
|
|
function validateLinksWithServer(history) {
|
|
// Only validate links that are not already marked as expired/deleted
|
|
const activeLinks = history.filter(item => !item.isDeleted && new Date(item.expiresAt) > new Date());
|
|
|
|
if (activeLinks.length === 0) {
|
|
return Promise.resolve(history); // No active links to validate
|
|
}
|
|
|
|
const ids = activeLinks.map(item => item.id);
|
|
|
|
return fetch('/pwpush/api/status/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ ids: ids })
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to validate links');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(statuses => {
|
|
// Update history items with server status
|
|
const statusMap = {};
|
|
statuses.forEach(status => {
|
|
statusMap[status.id] = status;
|
|
});
|
|
|
|
return history.map(item => {
|
|
const serverStatus = statusMap[item.id];
|
|
if (serverStatus) {
|
|
// Update with server data
|
|
item.exists = serverStatus.exists;
|
|
item.isDeleted = serverStatus.is_deleted;
|
|
item.viewCount = serverStatus.current_views;
|
|
item.maxViews = serverStatus.max_views;
|
|
|
|
// Mark as expired if server says so
|
|
if (serverStatus.is_expired || !serverStatus.exists) {
|
|
item.isExpired = true;
|
|
}
|
|
|
|
// If link doesn't exist on server, mark for removal
|
|
if (!serverStatus.exists) {
|
|
item.shouldRemove = true;
|
|
}
|
|
}
|
|
return item;
|
|
}).filter(item => !item.shouldRemove); // Remove non-existent links
|
|
})
|
|
.catch(error => {
|
|
console.warn('Failed to validate links with server:', error);
|
|
return history; // Return original history if validation fails
|
|
});
|
|
}
|
|
|
|
function loadHistory() {
|
|
let history = getHistoryFromCookie();
|
|
|
|
// Always display existing history, but only validate with server if tracking is enabled
|
|
const trackHistory = getCookie('pwpush_track_history');
|
|
if (trackHistory === 'true' && history.length > 0) {
|
|
// Validate with server and update display
|
|
validateLinksWithServer(history).then(validatedHistory => {
|
|
// Save updated history back to cookies
|
|
if (validatedHistory.length !== history.length) {
|
|
saveHistoryToCookie(validatedHistory);
|
|
history = validatedHistory;
|
|
}
|
|
displayHistory(history);
|
|
});
|
|
} else {
|
|
// Just display the history without server validation
|
|
displayHistory(history);
|
|
}
|
|
}
|
|
|
|
function displayHistory(history) {
|
|
const tbody = document.getElementById('historyTableBody');
|
|
const emptyRow = tbody.querySelector('.empty-row');
|
|
|
|
// Always clear existing rows except empty row first
|
|
const existingRows = tbody.querySelectorAll('tr:not(.empty-row)');
|
|
existingRows.forEach(row => row.remove());
|
|
|
|
if (history.length === 0) {
|
|
if (emptyRow) emptyRow.style.display = '';
|
|
updateStats(0, 0, 0);
|
|
return;
|
|
}
|
|
|
|
if (emptyRow) emptyRow.style.display = 'none';
|
|
|
|
let activeCount = 0;
|
|
let expiredCount = 0;
|
|
|
|
history.forEach(item => {
|
|
const isExpired = item.isExpired || item.isDeleted || new Date(item.expiresAt) < new Date();
|
|
if (isExpired) expiredCount++;
|
|
else activeCount++;
|
|
|
|
const row = createHistoryRow(item, isExpired);
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
updateStats(history.length, activeCount, expiredCount);
|
|
}
|
|
|
|
function createHistoryRow(item, isExpired) {
|
|
const row = document.createElement('tr');
|
|
row.className = isExpired ? 'expired' : 'active';
|
|
|
|
// Determine status text and actions based on server state
|
|
let statusText = '✅ Active';
|
|
let statusClass = 'active';
|
|
let actions = '';
|
|
|
|
if (item.isDeleted) {
|
|
statusText = '🗑️ Deleted';
|
|
statusClass = 'deleted';
|
|
actions = '<span class="expired-text">Deleted on server</span>';
|
|
} else if (isExpired) {
|
|
statusText = '❌ Expired';
|
|
statusClass = 'expired';
|
|
actions = '<span class="expired-text">Expired</span>';
|
|
} else {
|
|
actions = `<button onclick="copyLink('${item.url}')" class="btn btn-secondary btn-xs" title="Copy Link">📋</button>
|
|
<a href="${item.url}" class="btn btn-primary btn-xs" target="_blank" title="View">👁️</a>
|
|
<button onclick="editNotes('${item.id}')" class="btn btn-secondary btn-xs" title="Edit Notes">✏️</button>`;
|
|
}
|
|
|
|
// Always show remove button for cleaning up history
|
|
actions += `<button onclick="removeFromHistory('${item.id}')" class="btn btn-danger btn-xs" title="Remove from browser history(only)">🗑️</button>`;
|
|
|
|
row.innerHTML = `
|
|
<td class="date-cell">
|
|
${formatDate(item.createdAt)}
|
|
</td>
|
|
<td class="date-cell">
|
|
${formatDate(item.expiresAt)}
|
|
</td>
|
|
<td class="views-cell">
|
|
${item.viewCount || 0}/${item.maxViews}
|
|
</td>
|
|
<td class="notes-cell">
|
|
<span class="notes-text">${item.notes ? item.notes.substring(0, 30) + (item.notes.length > 30 ? '...' : '') : '-'}</span>
|
|
</td>
|
|
<td class="status-cell">
|
|
<span class="status-badge ${statusClass}">
|
|
${statusText}
|
|
</span>
|
|
</td>
|
|
<td class="actions-cell">
|
|
${actions}
|
|
</td>
|
|
`;
|
|
|
|
return row;
|
|
}
|
|
|
|
function updateStats(total, active, expired) {
|
|
document.getElementById('totalCount').textContent = total;
|
|
document.getElementById('activeCount').textContent = active;
|
|
document.getElementById('expiredCount').textContent = expired;
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
}
|
|
|
|
function copyLink(url) {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = url;
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
|
|
// Show success feedback
|
|
const btn = event.target;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = '✅';
|
|
btn.style.background = '#4caf50';
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.style.background = '';
|
|
}, 1500);
|
|
}
|
|
|
|
function removeFromHistory(pushId) {
|
|
if (confirm('Remove this item from your history?')) {
|
|
const history = getHistoryFromCookie();
|
|
const updatedHistory = history.filter(h => h.id !== pushId);
|
|
saveHistoryToCookie(updatedHistory);
|
|
loadHistory();
|
|
}
|
|
}
|
|
|
|
function editNotes(pushId) {
|
|
const history = getHistoryFromCookie();
|
|
const item = history.find(h => h.id === pushId);
|
|
if (!item) return;
|
|
|
|
const currentNotes = item.notes || '';
|
|
const newNotes = prompt('Add notes for this link (for your reference only):', currentNotes);
|
|
|
|
if (newNotes !== null) { // User didn't cancel
|
|
item.notes = newNotes;
|
|
saveHistoryToCookie(history);
|
|
loadHistory();
|
|
}
|
|
}
|
|
|
|
function clearHistory() {
|
|
if (confirm('Clear all history? This cannot be undone.')) {
|
|
// Show loading state
|
|
const clearBtn = document.querySelector('button[onclick="clearHistory()"]');
|
|
const originalText = clearBtn.textContent;
|
|
clearBtn.textContent = '🗑️ Clearing...';
|
|
clearBtn.disabled = true;
|
|
|
|
// Clear the cookie
|
|
document.cookie = "pwpush_history=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
|
|
// Immediately update the display to show empty state
|
|
displayHistory([]);
|
|
|
|
// Show success feedback
|
|
clearBtn.textContent = '✅ Cleared!';
|
|
|
|
// Restore original text after showing success
|
|
setTimeout(() => {
|
|
clearBtn.textContent = originalText;
|
|
clearBtn.disabled = false;
|
|
}, 1500);
|
|
}
|
|
}
|
|
|
|
function getHistoryFromCookie() {
|
|
const cookie = document.cookie
|
|
.split('; ')
|
|
.find(row => row.startsWith('pwpush_history='));
|
|
|
|
if (!cookie) return [];
|
|
|
|
try {
|
|
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function saveHistoryToCookie(history) {
|
|
const expires = new Date();
|
|
expires.setTime(expires.getTime() + (30 * 24 * 60 * 60 * 1000)); // 30 days
|
|
document.cookie = `pwpush_history=${encodeURIComponent(JSON.stringify(history))};expires=${expires.toUTCString()};path=/`;
|
|
}
|
|
|
|
function getCookie(name) {
|
|
const value = "; " + document.cookie;
|
|
const parts = value.split("; " + name + "=");
|
|
if (parts.length === 2) return parts.pop().split(";").shift();
|
|
return null;
|
|
}
|
|
|
|
function setCookie(name, value, days) {
|
|
const expires = new Date();
|
|
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
document.cookie = name + "=" + value + ";expires=" + expires.toUTCString() + ";path=/";
|
|
}
|
|
|
|
// Load history on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Check if this is a success page with a new link created
|
|
const successData = document.querySelector('meta[name="success-data"]');
|
|
if (successData) {
|
|
const trackHistory = getCookie('pwpush_track_history');
|
|
if (trackHistory === 'true') {
|
|
// Add the new link to history
|
|
const data = JSON.parse(successData.content);
|
|
const newItem = {
|
|
id: data.id,
|
|
url: data.url,
|
|
createdAt: new Date().toISOString(),
|
|
expiresAt: data.expiresAt,
|
|
maxViews: parseInt(getCookie('pwpush_max_views')) || 10,
|
|
viewCount: 0,
|
|
previewText: 'Content hidden for security'
|
|
};
|
|
|
|
let history = getHistoryFromCookie();
|
|
|
|
// Check if this item already exists to prevent duplicates
|
|
const existingIndex = history.findIndex(item => item.id === newItem.id);
|
|
if (existingIndex === -1) {
|
|
history.unshift(newItem); // Add to beginning only if it doesn't exist
|
|
|
|
// Keep only last 50 items
|
|
if (history.length > 50) {
|
|
history = history.slice(0, 50);
|
|
}
|
|
|
|
saveHistoryToCookie(history);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load and display history (this will show the updated history including any new item)
|
|
loadHistory();
|
|
|
|
// Load saved preferences
|
|
const savedExpiry = getCookie('pwpush_expiry_days');
|
|
const savedViews = getCookie('pwpush_max_views');
|
|
const savedRequireClick = getCookie('pwpush_require_click');
|
|
const savedAutoDelete = getCookie('pwpush_auto_delete');
|
|
const savedTrackHistory = getCookie('pwpush_track_history');
|
|
|
|
const expiryDaysElement = document.getElementById('expiry_days');
|
|
const maxViewsElement = document.getElementById('max_views');
|
|
const requireClickElement = document.querySelector('input[name="require_click"]');
|
|
const autoDeleteElement = document.querySelector('input[name="auto_delete"]');
|
|
const trackHistoryElement = document.querySelector('input[name="track_history"]');
|
|
|
|
if (savedExpiry && expiryDaysElement) {
|
|
expiryDaysElement.value = savedExpiry;
|
|
updateExpiryDisplay(savedExpiry);
|
|
}
|
|
|
|
if (savedViews && maxViewsElement) {
|
|
maxViewsElement.value = savedViews;
|
|
updateViewsDisplay(savedViews);
|
|
}
|
|
|
|
if (savedRequireClick !== null && requireClickElement) {
|
|
requireClickElement.checked = savedRequireClick === 'true';
|
|
}
|
|
|
|
if (savedAutoDelete !== null && autoDeleteElement) {
|
|
autoDeleteElement.checked = savedAutoDelete === 'true';
|
|
}
|
|
|
|
if (savedTrackHistory !== null && trackHistoryElement) {
|
|
trackHistoryElement.checked = savedTrackHistory === 'true';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
|
|
{{end}}
|