This commit is contained in:
ZorahM 2025-05-08 23:07:03 +00:00
parent 869e2b4110
commit 293c638e2d

View File

@ -1,43 +1,4 @@
<script type="text/javascript"> <!DOCTYPE html>
var gk_isXlsx = false;
var gk_xlsxFileLookup = {};
var gk_fileData = {};
function filledCell(cell) {
return cell !== '' && cell != null;
}
function loadFileData(filename) {
if (gk_isXlsx && gk_xlsxFileLookup[filename]) {
try {
var workbook = XLSX.read(gk_fileData[filename], { type: 'base64' });
var firstSheetName = workbook.SheetNames[0];
var worksheet = workbook.Sheets[firstSheetName];
// Convert sheet to JSON to filter blank rows
var jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false, defval: '' });
// Filter out blank rows (rows where all cells are empty, null, or undefined)
var filteredData = jsonData.filter(row => row.some(filledCell));
// Heuristic to find the header row by ignoring rows with fewer filled cells than the next row
var headerRowIndex = filteredData.findIndex((row, index) =>
row.filter(filledCell).length >= filteredData[index + 1]?.filter(filledCell).length
);
// Fallback
if (headerRowIndex === -1 || headerRowIndex > 25) {
headerRowIndex = 0;
}
// Convert filtered JSON back to CSV
var csv = XLSX.utils.aoa_to_sheet(filteredData.slice(headerRowIndex)); // Create a new sheet from filtered array of arrays
csv = XLSX.utils.sheet_to_csv(csv, { header: 1 });
return csv;
} catch (e) {
console.error(e);
return "";
}
}
return gk_fileData[filename] || "";
}
</script><!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -48,6 +9,14 @@
body { body {
background: linear-gradient(to bottom, #fff5f5, #ffffff); background: linear-gradient(to bottom, #fff5f5, #ffffff);
} }
.password-cell {
-webkit-text-security: disc; /* Скрываем пароли по умолчанию */
font-family: monospace;
}
.reveal-btn {
cursor: pointer;
margin-left: 8px;
}
</style> </style>
</head> </head>
<body class="min-h-screen font-sans"> <body class="min-h-screen font-sans">
@ -58,7 +27,12 @@
<div class="bg-white p-6 rounded-3xl shadow-lg"> <div class="bg-white p-6 rounded-3xl shadow-lg">
<textarea id="inputData" class="w-full p-4 border rounded-2xl mb-4 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-red-400 text-sm resize-y" rows="5" placeholder="Введите данные в формате: Сервис Логин Пароль (каждая запись с новой строки)"></textarea> <textarea id="inputData" class="w-full p-4 border rounded-2xl mb-4 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-red-400 text-sm resize-y" rows="5" placeholder="Введите данные в формате: Сервис Логин Пароль (каждая запись с новой строки)"></textarea>
<button onclick="parsePasswords()" class="w-full bg-red-500 text-white p-3 rounded-2xl hover:bg-red-600 transition duration-300 font-semibold">Добавить</button> <div class="flex gap-2">
<button onclick="parsePasswords()" class="flex-1 bg-red-500 text-white p-3 rounded-2xl hover:bg-red-600 transition duration-300 font-semibold">Добавить</button>
<button onclick="clearAllPasswords()" class="bg-gray-200 text-gray-700 p-3 rounded-2xl hover:bg-gray-300 transition duration-300 font-semibold">Очистить всё</button>
<button onclick="exportPasswords()" class="bg-green-500 text-white p-3 rounded-2xl hover:bg-green-600 transition duration-300 font-semibold">Экспорт</button>
<button onclick="importPasswords()" class="bg-blue-500 text-white p-3 rounded-2xl hover:bg-blue-600 transition duration-300 font-semibold">Импорт</button>
</div>
<div id="output" class="mt-6"></div> <div id="output" class="mt-6"></div>
</div> </div>
</section> </section>
@ -68,39 +42,127 @@
// Load existing passwords from local storage on page load // Load existing passwords from local storage on page load
window.onload = function() { window.onload = function() {
const savedData = localStorage.getItem('passwords'); loadPasswords();
if (savedData) {
tableData = JSON.parse(savedData);
renderTable();
}
}; };
function loadPasswords() {
const savedData = localStorage.getItem('passwords');
if (savedData) {
try {
tableData = JSON.parse(savedData);
renderTable();
} catch (e) {
console.error("Ошибка загрузки паролей:", e);
alert("Ошибка загрузки сохранённых паролей");
}
}
}
function savePasswords() {
localStorage.setItem('passwords', JSON.stringify(tableData));
}
function parsePasswords() { function parsePasswords() {
const input = document.getElementById('inputData').value; const input = document.getElementById('inputData').value.trim();
const output = document.getElementById('output'); const output = document.getElementById('output');
output.innerHTML = ''; output.innerHTML = '';
const lines = input.trim().split('\n'); if (!input) {
if (lines.length === 0 || lines[0] === '') {
renderTable(); renderTable();
return; return;
} }
const lines = input.split('\n');
let addedCount = 0;
lines.forEach(line => { lines.forEach(line => {
const parts = line.trim().split(' '); const trimmedLine = line.trim();
if (!trimmedLine) return;
// Поддержка разделителей: пробел, табуляция или запятая
const parts = trimmedLine.split(/[\s\t,]+/).filter(part => part);
if (parts.length >= 3) { if (parts.length >= 3) {
const password = parts.pop(); const password = parts.pop();
const login = parts.pop(); const login = parts.pop();
const service = parts.join(' '); const service = parts.join(' ');
if (service && login && password) { if (service && login && password) {
tableData.push({ service, login, password }); // Проверка на дубликаты
const isDuplicate = tableData.some(item =>
item.service === service && item.login === login
);
if (!isDuplicate) {
tableData.push({ service, login, password });
addedCount++;
}
} }
} }
}); });
localStorage.setItem('passwords', JSON.stringify(tableData)); if (addedCount > 0) {
document.getElementById('inputData').value = ''; savePasswords();
renderTable(); document.getElementById('inputData').value = '';
renderTable();
alert(`Добавлено ${addedCount} новых записей`);
} else {
alert("Не удалось добавить новые записи. Проверьте формат ввода или возможно записи уже существуют.");
}
}
function clearAllPasswords() {
if (confirm("Вы уверены, что хотите удалить все сохранённые пароли? Это действие нельзя отменить.")) {
tableData = [];
savePasswords();
renderTable();
}
}
function exportPasswords() {
const dataStr = JSON.stringify(tableData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'passwords-backup.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
}
function importPasswords() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = event => {
try {
const importedData = JSON.parse(event.target.result);
if (Array.isArray(importedData) && importedData.length > 0) {
if (confirm(`Найдено ${importedData.length} записей. Добавить их к существующим?`)) {
tableData = [...tableData, ...importedData];
savePasswords();
renderTable();
alert(`Импортировано ${importedData.length} записей`);
}
} else {
alert("Файл не содержит данных для импорта");
}
} catch (error) {
alert("Ошибка при чтении файла. Убедитесь, что выбран правильный файл.");
console.error(error);
}
};
reader.readAsText(file);
};
fileInput.click();
} }
function renderTable() { function renderTable() {
@ -108,59 +170,150 @@
output.innerHTML = ''; output.innerHTML = '';
if (tableData.length === 0) { if (tableData.length === 0) {
output.innerHTML = '<p class="text-red-500 text-center font-medium">Пожалуйста, введите данные!</p>'; output.innerHTML = '<p class="text-red-500 text-center font-medium">Нет сохранённых паролей. Введите данные в поле выше.</p>';
return; return;
} }
// Создаем контейнер для таблицы и поиска
const tableContainer = document.createElement('div');
tableContainer.className = 'overflow-x-auto';
// Добавляем поле поиска
const searchDiv = document.createElement('div');
searchDiv.className = 'mb-4';
searchDiv.innerHTML = `
<input type="text" id="searchInput" placeholder="Поиск по сервису или логину..."
class="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-300">
<div class="text-sm text-gray-500 mt-1">Найдено записей: <span id="matchCount">${tableData.length}</span></div>
`;
tableContainer.appendChild(searchDiv);
const searchInput = searchDiv.querySelector('#searchInput');
searchInput.addEventListener('input', () => filterTable(searchInput.value.toLowerCase()));
const table = document.createElement('table'); const table = document.createElement('table');
table.className = 'w-full border-collapse rounded-2xl overflow-hidden shadow-md'; table.className = 'w-full border-collapse rounded-2xl overflow-hidden shadow-md';
let header = table.insertRow(); let header = table.insertRow();
const headers = ['Сервис', 'Логин', 'Пароль']; const headers = ['Сервис', 'Логин', 'Пароль', 'Действия'];
let sortDirection = { 0: 1, 1: 1, 2: 1 }; // 1 for ascending, -1 for descending let sortDirection = { 0: 1, 1: 1, 2: 1 }; // 1 for ascending, -1 for descending
headers.forEach((text, index) => { headers.forEach((text, index) => {
const th = document.createElement('th'); const th = document.createElement('th');
th.textContent = text; th.textContent = text;
th.className = 'border border-gray-200 p-4 bg-red-50 text-gray-700 font-semibold text-left cursor-pointer'; th.className = 'border border-gray-200 p-4 bg-red-50 text-gray-700 font-semibold text-left cursor-pointer';
th.addEventListener('click', () => { if (index < 3) {
sortTable(index, sortDirection[index]); th.addEventListener('click', () => {
sortDirection[index] *= -1; sortTable(index, sortDirection[index]);
renderTable(); sortDirection[index] *= -1;
}); renderTable();
});
}
header.appendChild(th); header.appendChild(th);
}); });
tableData.forEach((row, rowIndex) => { tableData.forEach((row, rowIndex) => {
const tableRow = table.insertRow(); const tableRow = table.insertRow();
let i = 0; tableRow.className = 'hover:bg-gray-50';
for (let key in row) { tableRow.dataset.service = row.service.toLowerCase();
const td = tableRow.insertCell(); tableRow.dataset.login = row.login.toLowerCase();
const text = row[key];
td.className = 'border border-gray-200 p-4 bg-white text-sm break-words max-w-xs'; // Ячейка сервиса
const copyBtn = document.createElement('button'); const serviceCell = tableRow.insertCell();
copyBtn.textContent = '📋'; serviceCell.className = 'border border-gray-200 p-4 bg-white text-sm break-words max-w-xs';
copyBtn.className = 'ml-2 text-blue-500 hover:text-blue-700'; serviceCell.textContent = row.service;
copyBtn.onclick = (e) => {
e.stopPropagation(); // Ячейка логина
navigator.clipboard.writeText(text).then(() => alert('Пароль скопирован!')); const loginCell = tableRow.insertCell();
}; loginCell.className = 'border border-gray-200 p-4 bg-white text-sm break-words max-w-xs';
td.appendChild(document.createTextNode(text)); loginCell.textContent = row.login;
td.appendChild(copyBtn);
i++; // Ячейка пароля
} const passwordCell = tableRow.insertCell();
passwordCell.className = 'border border-gray-200 p-4 bg-white text-sm break-words max-w-xs password-cell';
passwordCell.textContent = '••••••••';
passwordCell.dataset.password = row.password;
// Ячейка действий
const actionsCell = tableRow.insertCell();
actionsCell.className = 'border border-gray-200 p-4 bg-white text-sm flex gap-2';
// Кнопка показать/скрыть пароль
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '👁️';
toggleBtn.className = 'reveal-btn text-blue-500 hover:text-blue-700';
toggleBtn.onclick = () => {
if (passwordCell.classList.contains('password-cell')) {
passwordCell.textContent = passwordCell.dataset.password;
passwordCell.classList.remove('password-cell');
toggleBtn.textContent = '🙈';
} else {
passwordCell.textContent = '••••••••';
passwordCell.classList.add('password-cell');
toggleBtn.textContent = '👁️';
}
};
// Кнопка копирования пароля
const copyBtn = document.createElement('button');
copyBtn.textContent = '📋';
copyBtn.className = 'text-blue-500 hover:text-blue-700';
copyBtn.onclick = () => {
navigator.clipboard.writeText(row.password).then(() => {
const originalText = copyBtn.textContent;
copyBtn.textContent = '✓';
setTimeout(() => copyBtn.textContent = originalText, 2000);
});
};
// Кнопка удаления
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑️';
deleteBtn.className = 'text-red-500 hover:text-red-700';
deleteBtn.onclick = () => {
if (confirm(`Удалить запись для ${row.service}?`)) {
tableData.splice(rowIndex, 1);
savePasswords();
renderTable();
}
};
actionsCell.appendChild(toggleBtn);
actionsCell.appendChild(copyBtn);
actionsCell.appendChild(deleteBtn);
}); });
output.appendChild(table); tableContainer.appendChild(table);
output.appendChild(tableContainer);
}
function filterTable(searchTerm) {
const rows = document.querySelectorAll('tbody tr');
let matchCount = 0;
rows.forEach(row => {
const service = row.dataset.service;
const login = row.dataset.login;
const isVisible = service.includes(searchTerm) || login.includes(searchTerm);
row.style.display = isVisible ? '' : 'none';
if (isVisible) matchCount++;
});
document.getElementById('matchCount').textContent = matchCount;
} }
function sortTable(columnIndex, direction) { function sortTable(columnIndex, direction) {
const columns = ['service', 'login', 'password'];
const key = columns[columnIndex];
tableData.sort((a, b) => { tableData.sort((a, b) => {
const keys = ['service', 'login', 'password']; const valueA = a[key].toLowerCase();
const valueA = a[keys[columnIndex]]; const valueB = b[key].toLowerCase();
const valueB = b[keys[columnIndex]];
return direction * valueA.localeCompare(valueB); return direction * valueA.localeCompare(valueB);
}); });
localStorage.setItem('passwords', JSON.stringify(tableData));
savePasswords();
} }
</script> </script>
</body> </body>