Inital commit

This commit is contained in:
Bernd 2025-01-07 12:23:54 +05:00
parent a5b08abada
commit 2c532c6555
28 changed files with 2564 additions and 0 deletions

487
css/styles.css Normal file
View File

@ -0,0 +1,487 @@
/* styles.css */
html {
font-size: 14px; /* Reduce from default 16px */
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.tabs-container {
position: sticky;
top: 0;
z-index: 1000; /* Ensure it stays above other content */
background-color: #f1f1f1; /* Match the background color */
}
.tabs {
display: flex;
flex-direction: column; /* Stack tabs vertically */
width: 150px; /* Adjust width of the tabs */
border-right: 1px solid #ddd; /* Add a vertical separator */
background-color: #f5f5f5;
height: 100vh; /* Make tabs stretch vertically to fill the viewport */
overflow-y: auto; /* Enable scrolling if the tabs exceed the height */
position: fixed; /* Keep tabs fixed on the left side */
left: 0; /* Align tabs to the left edge */
top: 0;
z-index: 1000; /* Ensure tabs are above other content */
}
/* Individual Tab */
.tab {
padding: 15px 20px;
font-size: 16px;
background: none;
border: none;
text-align: left;
cursor: pointer;
transition: background-color 0.3s;
border-bottom: 1px solid #ddd; /* Add a separator between tabs */
width: 100%;
}
/* Active Tab */
.tab.active {
background-color: #ddd;
color: #000;
font-weight: bold;
}
/* Hover Effect */
.tab:hover {
background-color: #eee;
}
/* Optional: Remove last tab's margin */
.tabs .tab:last-child {
margin-right: 0;
}
#content {
margin-left: 150px; /* Match the width of the tabs */
padding: 20px;
background-color: #fff;
border: 1px solid #ccc;
border-top: none;
padding-top: 20px;
}
.modal {
display: none; /* Hidden by default */
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
.modal-content {
background-color: #fefefe;
margin: 5% auto; /* Reduced top margin for more vertical space */
padding: 15px; /* Reduced padding */
border: 1px solid #888;
width: 35%; /* Reduced width from 50% to 35% */
max-width: 500px; /* Added max-width for larger screens */
position: relative;
border-radius: 8px; /* Optional: Rounded corners for a modern look */
}
#close-modal {
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;
}
/* Add padding and spacing for form fields inside the Edit Modal */
#edit-modal .form-container {
display: flex;
flex-direction: column;
gap: 10px; /* Reduced gap between form elements */
padding: 0; /* Removed extra padding */
background-color: transparent; /* Remove background to match modal */
box-shadow: none; /* Remove shadow inside modal */
}
/* Ensure form fields are styled properly */
#edit-modal .form-container .form-group {
margin-bottom: 10px; /* Add spacing between individual form groups */
}
#edit-modal .form-container label {
margin-bottom: 3px;
font-weight: bold;
}
#edit-modal .form-container input[type="text"],
#edit-modal .form-container input[type="date"],
#edit-modal .form-container input[type="url"],
#edit-modal .form-container textarea,
#edit-modal .form-container select {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
#edit-modal .form-container textarea {
resize: vertical;
min-height: 80px;
}
/* Buttons at the bottom of the form */
#edit-modal .form-container .form-buttons {
display: flex;
justify-content: flex-end;
gap: 10px; /* Space between buttons */
}
#edit-modal .form-container .form-buttons button {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
#edit-modal .form-container .form-buttons button[type="submit"] {
padding: 8px 12px; /* Reduced padding */
font-size: 14px; /* Reduced font size */
background-color: #007bff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
#edit-modal .form-container .form-buttons button[type="button"] {
background-color: #dc3545;
color: #fff;
}
#edit-modal .form-container .form-buttons button:hover {
opacity: 0.9;
}
/* Style for the Add Record form */
#add-form {
margin-bottom: 40px;
padding: 10px;
background-color: #f9f9f9;
}
tr.completed {
background-color: lightgreen !important;
}
.suggested {
background-color: yellow;
}
.watching {
background-color: yellow;
}
/* Form Container */
.form-container-horizontal {
max-width: 100%;
margin: 20px auto;
padding: 20px;
background-color: #f1f1f1; /* Light background */
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.form-container-horizontal h3 {
text-align: center;
margin-bottom: 20px;
}
/* Form Row */
.form-row {
display: flex;
gap: 10px; /* space between the columns */
}
/* Optional: make each form-group share space evenly */
.form-row .form-group {
flex: 1;
}
/* Remove margin from the last form group in a row */
.form-row .form-group:last-child {
margin-right: 0;
}
/* Labels */
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
/* Input fields */
.form-control {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
/* Checkbox and Buttons Row */
#edit-modal .form-container .form-check {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.form-check-input {
margin-right: 5px;
}
.btn {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 4px;
text-decoration: none;
}
.btn-primary {
background-color: #007bff;
color: #fff;
}
.btn-primary:hover {
background-color: #0069d9;
}
.set-complete-button {
background-color: #00eb0c;
}
.set-complete-button:hover {
background-color: #038c0a;
}
.set-currently-watching-button {
background-color: #f6fa02;
}
.set-currently-watching-button:hover {
background-color: #bcbf02;
}
.delete-button {
background-color: #f44336;
}
.delete-button:hover {
background-color: #d32f2f;
}
.close-button {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px; /* Reduced font size */
color: #aaa;
cursor: pointer;
}
/* Responsive Design */
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
.form-row .form-group {
margin-right: 0;
margin-bottom: 15px;
}
.form-check {
margin-bottom: 15px;
}
.tabs {
flex-direction: column;
}
.tab {
margin-bottom: 5px;
}
}
/* Close Button */
.close-button {
position: absolute;
top: 10px;
right: 20px;
font-size: 30px;
font-weight: bold;
color: #aaa;
cursor: pointer;
}
.close-button:hover {
color: #000;
}
.record-table {
width: 100%;
border-collapse: collapse;
font-family: Arial, sans-serif;
}
.record-table {
width: 100%;
border-collapse: collapse;
font-family: Arial, sans-serif;
border: 1px solid #000; /* Added black border to the table */
}
/* Column widths using CSS selectors */
.record-table colgroup col:nth-child(1) { width: 3%; } /* # */
.record-table colgroup col:nth-child(2) { width: 35%; } /* Name */
.record-table colgroup col:nth-child(3) { width: 4%; }/* Type */
.record-table colgroup col:nth-child(4) { width: 37%; }/* Comment */
.record-table colgroup col:nth-child(5) { width: 6%; }/* Date completed */
.record-table colgroup col:nth-child(6) { width: 15%; }/* Actions */
/* Column widths using CSS selectors */
.record-table colgroup col:nth-child(1) #rewatch { width: 3%; } /* # */
.record-table colgroup col:nth-child(2) #rewatch { width: 40%; } /* Name */
.record-table colgroup col:nth-child(3) #rewatch { width: 47%; }/* Comment */
.record-table colgroup col:nth-child(4) #rewatch { width: 10%; }/* Actions */
/* Manga table */
.record-table colgroup col:nth-child(1) #manga { width: 3%; } /* # */
.record-table colgroup col:nth-child(2) #manga { width: 35%; } /* Name */
.record-table colgroup col:nth-child(4) #manga { width: 41%; }/* Comment */
.record-table colgroup col:nth-child(5) #manga { width: 6%; }/* Date completed */
.record-table colgroup col:nth-child(6) #manga { width: 15%; }/* Actions */
/* Styling table headers and cells */
.record-table th, .record-table td {
border: 1px solid #000; /* Changed border color to black */
padding: 8px;
}
/* Style for table headers */
.record-table th {
background-color: #f2f2f2;
font-weight: bold;
}
.record-table tbody tr:hover {
background-color: #e9e9e9;
}
/* Text alignment in columns */
.record-table th:nth-child(1), .record-table td:nth-child(1),
.record-table th:nth-child(3), .record-table td:nth-child(3),
.record-table th:nth-child(5), .record-table td:nth-child(5),
.record-table th:nth-child(6), .record-table td:nth-child(6) {
text-align: center;
}
/* Styling action buttons */
.record-table td button {
padding: 4px 6px; /* Reduce button padding */
margin: 0; /* Remove margins */
font-size: 12px; /* Reduce font size */
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 5px;
}
th, td {
padding: 1px 2px; /* Reduced padding */
line-height: 0.8; /* Reduced line height */
font-size: 14px; /* Reduced font size */
vertical-align: middle; /* Center content vertically */
text-align: left; /* Existing alignment */
white-space: nowrap; /* Prevent text from wrapping */
overflow: hidden; /* Hide overflow */
text-overflow: ellipsis; /* Add ellipsis for overflowing text */
}
th {
background-color: #f2f2f2;
cursor: pointer;
background: linear-gradient(to bottom, lightgrey, white);
}
.record-table table, .record-table th, .record-table td {
border: 1px solid gray;
}
#image-tooltip {
position: absolute;
border: 1px solid #ccc;
background: #fff;
padding: 5px;
z-index: 1000;
max-width: 200px;
max-height: 300px;
overflow: hidden;
display: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

BIN
icons/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
icons/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

BIN
icons/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

40
index.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Backlog of unwatched anime</title>
<link rel="stylesheet" href="css/styles.css?v=71">
<link rel="apple-touch-icon" sizes="180x180" href="./icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="./icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="./icons/favicon-16x16.png">
</head>
<body>
<!-- Tabs for Years -->
<div class="tabs">
<div class="tabs-container">
<div id="year-tabs" class="tabs"></div>
</div>
</div>
<div>
<!-- Content Area -->
<div id="content">
<!-- Tables and Forms will be loaded here -->
</div>
</div>
<!-- Edit Modal -->
<div id="edit-modal" class="modal">
<div class="modal-content">
<span id="close-modal" class="close-button">&times;</span>
<form id="edit-form" class="form-container">
<!-- Form fields will be loaded here -->
</form>
</div>
</div>
<script src="js/main.js?v=72"></script>
</body>
</html>

540
js/main.js Normal file
View File

@ -0,0 +1,540 @@
let currentYear;
//Adding new element in the array creates new tab
const years = ['pre-2009', '2009', '2010', '2011', '2012', '2013', '2014', '2015',
'2016','2017','2018','2019','2020','2021','2022','2023', '2024', 're-watch', 'manga'];
function loadYearTabs() {
const yearTabs = document.getElementById('year-tabs');
// Populate the tabs
years.forEach(year => {
const li = document.createElement('li');
li.textContent = year;
li.classList.add('tab');
li.dataset.year = year; // Set data-year attribute for each tab
// Add a click event listener to each tab
li.addEventListener('click', () => {
loadYearContent(year);
});
yearTabs.appendChild(li);
// Now check if all entries are completed for this year
fetch(`php/check_completed_year.php?year=${encodeURIComponent(year)}`)
.then(response => response.json())
.then(data => {
if (data.all_completed) {
// Strike through the tab if all are completed
li.style.textDecoration = 'line-through';
}
})
.catch(error => console.error('Error checking completed status:', error));
});
// Load the last active tab or default to 'pre-2009'
const activeYear = localStorage.getItem('activeYear') || 'pre-2009';
loadYearContent(activeYear);
}
function loadYearContent(year) {
currentYear = year;
// Save the current year in localStorage for persistence
localStorage.setItem('activeYear', year);
// Highlight the active tab
const tabs = document.querySelectorAll('#year-tabs .tab');
tabs.forEach(tab => {
// Add or remove the active class based on the tab's data-year
if (tab.dataset.year === year) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
// Fetch and display content for the selected year
fetch(`php/fetch_data.php?year=${encodeURIComponent(year)}`)
.then(response => response.text())
.then(html => {
document.getElementById('content').innerHTML = html;
setupFormSubmission();
setupEditButtons();
setupDeleteButtons();
setupSuggestButton();
setupSetCompleteButtons();
setupSetCurrentlyWatchingButtons();
})
.catch(error => console.error('Error loading content:', error));
}
function setupFormSubmission() {
const form = document.getElementById('add-form');
function handleFormSubmit(event) {
event.preventDefault();
const formData = new FormData(form);
fetch('php/add_record.php', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
// Если сервер вернул ошибку (например, 403)
return response.json().then(errorData => {
if (response.status === 403) {
alert(errorData.message || 'Access denied: You are not authorized to perform this action.');
}
throw new Error(errorData.message || 'An error occurred.');
});
}
return response.text();
})
.then(() => {
// Если запись добавлена успешно
loadYearContent(currentYear);
})
.catch(error => {
console.error('Error:', error);
});
}
form.addEventListener('submit', handleFormSubmit);
form.addEventListener('keydown', event => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
handleFormSubmit(event);
}
});
}
function setupEditButtons() {
const editButtons = document.querySelectorAll('.edit-button');
editButtons.forEach(button => {
button.addEventListener('click', () => {
const recordId = button.dataset.id;
openEditModal(recordId);
});
});
}
function setupDeleteButtons() {
const deleteButtons = document.querySelectorAll('.delete-button');
deleteButtons.forEach(button => {
button.addEventListener('click', () => {
const recordId = button.dataset.id;
if (confirm('Are you sure you want to delete this record?')) {
fetch(`php/delete_record.php?id=${recordId}`, {
method: 'DELETE' // Используем метод DELETE для удаления
})
.then(response => {
if (!response.ok) {
// Если сервер вернул ошибку (например, 403)
return response.json().then(errorData => {
if (response.status === 403) {
alert(errorData.message || 'Access denied: You are not authorized to delete this record.');
}
throw new Error(errorData.message || 'An error occurred while deleting the record.');
});
}
return response.text();
})
.then(() => {
// Если удаление прошло успешно
loadYearContent(currentYear);
})
.catch(error => {
console.error('Error:', error);
});
}
});
});
}
function openEditModal(id) {
// Fetch record data
fetch(`php/fetch_record.php?id=${id}`)
.then(response => response.json())
.then(data => {
const modal = document.getElementById('edit-modal');
const form = document.getElementById('edit-form');
form.innerHTML = generateEditForm(data);
modal.style.display = 'block';
setupEditFormSubmission();
});
}
function setupModal() {
const modal = document.getElementById('edit-modal');
const closeModal = document.getElementById('close-modal');
closeModal.addEventListener('click', () => modal.style.display = 'none');
window.addEventListener('keydown', event => {
if (event.key === 'Escape') {
modal.style.display = 'none';
}
});
// Close modal when clicking outside the modal content
modal.addEventListener('click', event => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
}
function mapLabelToYear(label) {
if (label === 'pre-2009') return -1;
if (label === 're-watch') return -2;
return parseInt(label, 10);
}
function setupEditFormSubmission() {
const form = document.getElementById('edit-form');
function handleEditFormSubmit(event) {
event.preventDefault();
const yearLabelSelect = form.querySelector('#year_label');
const numericYearInput = form.querySelector('#year_numeric');
const selectedLabel = yearLabelSelect.value;
// Use the mapLabelToYear function
numericYearInput.value = mapLabelToYear(selectedLabel);
const formData = new FormData(form);
fetch('php/edit_record.php', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
if (response.status === 403) {
alert(errorData.message || 'Access denied: You are not authorized to edit this record.');
}
throw new Error(errorData.message || 'An error occurred while editing the record.');
});
}
return response.text();
})
.then(() => {
const modal = document.getElementById('edit-modal');
modal.style.display = 'none';
loadYearContent(currentYear);
})
.catch(error => {
console.error('Error:', error);
});
}
form.addEventListener('submit', handleEditFormSubmit);
form.addEventListener('keydown', event => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
handleEditFormSubmit(event);
}
});
}
function generateEditForm(data) {
function mapYearToLabel(yearValue) {
if (yearValue == -1) return 'pre-2009';
if (yearValue == -2) return 're-watch';
if (yearValue == -3) return 'manga';
return yearValue.toString();
}
const selectedYearLabel = mapYearToLabel(data.year);
return `
<h3>Edit Record</h3>
<input type="hidden" name="id" value="${data.id}">
<input type="hidden" name="year" id="year_numeric" value="${data.year}">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" name="name" id="name" class="form-control" value="${data.name || ''}" required>
</div>
<!-- Wrap season, year, and type fields in a single container -->
<div class="form-row">
<div class="form-group">
<label for="season">Season:</label>
<select name="season" id="season" class="form-control">
${['winter', 'spring', 'summer', 'fall', 'omake'].map(season => `
<option value="${season}" ${data.season === season ? 'selected' : ''}>
${season.charAt(0).toUpperCase() + season.slice(1)}
</option>
`).join('')}
</select>
</div>
<div class="form-group">
<label for="year_label">Year:</label>
<select name="year_label" id="year_label" class="form-control">
${years.map(y => `
<option value="${y}" ${y === selectedYearLabel ? 'selected' : ''}>${y}</option>
`).join('')}
</select>
</div>
<div class="form-group">
<label for="type">Type:</label>
<select name="type" id="type" class="form-control">
${['tv', 'special', 'short', 'ova', 'movie', 'other'].map(type => `
<option value="${type}" ${data.type === type ? 'selected' : ''}>${type}</option>
`).join('')}
</select>
</div>
</div>
<div class="form-group">
<label for="comment">Comment:</label>
<input type="text" name="comment" id="comment" class="form-control" value="${data.comment || ''}">
</div>
<div class="form-group">
<label for="date_completed">Date Completed:</label>
<input type="date" name="date_completed" id="date_completed" class="form-control" value="${data.date_completed || ''}">
</div>
<div class="form-group">
<label for="url">URL:</label>
<input type="url" name="url" id="url" class="form-control" value="${data.url || ''}">
</div>
<div class="form-group form-check">
<input type="checkbox" name="is_completed" id="is_completed" class="form-check-input" ${parseInt(data.is_completed) ? 'checked' : ''}>
<label for="is_completed" class="form-check-label">Is Completed</label>
</div>
<div class="form-group form-check">
<input type="checkbox" name="currently_watching" id="currently_watching" class="form-check-input" ${parseInt(data.currently_watching) ? 'checked' : ''}>
<label for="currently_watching" class="form-check-label">Currently Watching</label>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
`;
}
function setupSuggestButton() {
const suggestButton = document.getElementById('suggest-button');
if (suggestButton) {
suggestButton.addEventListener('click', () => {
makeSuggestion();
});
}
}
function makeSuggestion() {
// Clear any previous suggestions
const previousSuggestion = document.getElementById('suggestion-display');
if (previousSuggestion) {
previousSuggestion.remove();
}
// Get all table rows that are not completed
const rows = Array.from(document.querySelectorAll('table tbody tr')).filter(row => {
return !row.classList.contains('completed');
});
// Exclude header rows and ensure there are at least two entries
if (rows.length < 1) {
alert('Not enough uncompleted entries to make a suggestion.');
return;
}
// Randomly select two different rows
const indices = [];
while (indices.length < 1) {
const index = Math.floor(Math.random() * rows.length);
if (!indices.includes(index)) {
indices.push(index);
}
}
const selectedRows = [rows[indices[0]]];
// Get the names of the selected records
const names = selectedRows.map(row => row.getAttribute('data-name'));
// Display the names at the top of the content
const content = document.getElementById('content');
const suggestionDisplayDiv = document.createElement('div');
suggestionDisplayDiv.id = 'suggestion-display';
suggestionDisplayDiv.innerHTML = `<p>${names.join('<br> ')}</p>`;
content.insertBefore(suggestionDisplayDiv, content.querySelector('button')); // Insert before the add form
}
function setupStickyTabs() {
const tabsContainer = document.querySelector('.tabs-container');
const observer = new IntersectionObserver(
([e]) => e.target.classList.toggle('is-sticky', e.intersectionRatio < 1),
{ threshold: [1] }
);
observer.observe(tabsContainer);
}
function setupSetCompleteButtons() {
const setCompleteButtons = document.querySelectorAll('.set-complete-button');
setCompleteButtons.forEach(button => {
button.addEventListener('click', () => {
const recordId = button.dataset.id;
const formData = new FormData();
formData.append('id', recordId);
fetch('php/set_complete.php', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
if (response.status === 403) {
alert(errorData.message || 'Access denied: You are not authorized to perform this action.');
}
throw new Error(errorData.message || 'An error occurred while setting the record as complete.');
});
}
return response.json();
})
.then(() => {
// Reload the table to reflect the changes
loadYearContent(currentYear);
})
.catch(error => {
console.error('Error:', error);
});
});
});
}
function setupSetCurrentlyWatchingButtons() {
const setCurrentlyWatchingButtons = document.querySelectorAll('.set-currently-watching-button');
setCurrentlyWatchingButtons.forEach(button => {
button.addEventListener('click', () => {
const recordId = button.dataset.id;
const formData = new FormData();
formData.append('id', recordId);
fetch('php/set_currently_watching.php', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
if (response.status === 403) {
alert(errorData.message || 'Access denied: You are not authorized to perform this action.');
}
throw new Error(errorData.message || 'An error occurred while setting the record as currently watching.');
});
}
return response.json();
})
.then(() => {
// Reload the table to reflect the changes
loadYearContent(currentYear);
})
.catch(error => {
console.error('Error:', error);
});
});
});
}
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM fully loaded and parsed');
loadYearTabs();
setupModal();
var imageCache = {};
var currentHoverTarget = null;
function showImageTooltip(event, imageUrl) {
var tooltip = document.getElementById('image-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.id = 'image-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.border = '1px solid #ccc';
tooltip.style.background = '#fff';
tooltip.style.padding = '5px';
tooltip.style.zIndex = 1000;
tooltip.style.maxWidth = '200px';
tooltip.style.maxHeight = '300px';
tooltip.style.overflow = 'hidden';
tooltip.style.display = 'none';
document.body.appendChild(tooltip);
}
tooltip.innerHTML = '<img src="' + imageUrl + '" style="max-width: 100%; max-height: 100%;">';
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY + 15) + 'px';
tooltip.style.display = 'block';
}
function hideImageTooltip() {
var tooltip = document.getElementById('image-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
}
// Добавляем обработчики событий на родительский элемент
var contentDiv = document.getElementById('content');
contentDiv.addEventListener('mouseover', function (event) {
var target = event.target;
// Проверяем, является ли целевой элемент ссылкой с атрибутом data-url
if (target.tagName.toLowerCase() === 'a' && target.hasAttribute('data-url')) {
var url = target.getAttribute('data-url');
currentHoverTarget = target; // Set the current hover target
if (imageCache[url]) {
showImageTooltip(event, imageCache[url]);
} else {
var xhr = new XMLHttpRequest();
xhr.open('GET', './php/get_image.php?url=' + encodeURIComponent(url), true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response.image_url) {
imageCache[url] = response.image_url;
if (currentHoverTarget === target) {
showImageTooltip(event, response.image_url);
}
} else if (response.error) {
console.error(response.error);
}
}
};
xhr.send();
}
}
});
contentDiv.addEventListener('mouseout', function (event) {
var target = event.target;
if (target.tagName.toLowerCase() === 'a' && target.hasAttribute('data-url')) {
hideImageTooltip();
if (currentHoverTarget === target) {
currentHoverTarget = null; // Clear the current hover target
}
}
});
contentDiv.addEventListener('mousemove', function (event) {
var tooltip = document.getElementById('image-tooltip');
if (tooltip && tooltip.style.display === 'block') {
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY + 15) + 'px';
}
});
});

68
php/add_record.php Normal file
View File

@ -0,0 +1,68 @@
<?php
include_once 'db_connect.php';
include_once 'check_allowed_ip.php';
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!isAllowedIP($clientIP, $allowedSubnets)) {
http_response_code(403); // Устанавливаем код ответа 403
header('Content-Type: application/json'); // Указываем, что возвращаем JSON
echo json_encode([
'status' => 'error',
'message' => 'Access denied: Your IP is not authorized to add records.'
]);
exit;
}
$name = $_POST['name'];
$year = $_POST['year'];
$season = $_POST['season'];
$type = $_POST['type'];
$comment = $_POST['comment'];
$is_completed = isset($_POST['is_completed']) ? 1 : 0;
$currently_watching = isset($_POST['currently_watching']) ? 1 : 0;
$date_completed = $_POST['date_completed'];
$url = $_POST['url'];
// Handle empty date_completed
if (empty($date_completed)) {
$date_completed = NULL;
}
$stmt = $conn->prepare("INSERT INTO anime_list (name, year, season, type, comment, is_completed, date_completed, url, currently_watching) VALUES (:name, :year, :season, :type, :comment, :is_completed, :date_completed, :url, :currently_watching)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':year', $year, PDO::PARAM_INT);
$stmt->bindParam(':season', $season);
$stmt->bindParam(':type', $type);
$stmt->bindParam(':comment', $comment);
$stmt->bindParam(':is_completed', $is_completed, PDO::PARAM_INT);
$stmt->bindParam(':currently_watching', $currently_watching, PDO::PARAM_INT);
$stmt->bindParam(':url', $url);
// Use bindValue with PDO::PARAM_NULL if date_completed is NULL
if ($date_completed === NULL) {
$stmt->bindValue(':date_completed', NULL, PDO::PARAM_NULL);
} else {
$stmt->bindParam(':date_completed', $date_completed);
}
$stmt->execute();
// Log the action
// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
// $ip_address = $_SERVER['REMOTE_ADDR'];
// $anime_name = $name;
// $anime_year = $year;
// $action_type = 'adding';
// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
// $log_stmt->bindParam(':action_time', $action_time_formatted);
// $log_stmt->bindParam(':ip_address', $ip_address);
// $log_stmt->bindParam(':anime_name', $anime_name);
// $log_stmt->bindParam(':action_type', $action_type);
// $log_stmt->bindParam(':anime_year', $anime_year);
// $log_stmt->execute();
?>

26
php/check_allowed_ip.php Normal file
View File

@ -0,0 +1,26 @@
<?php
$allowedSubnets = [
'192.168.1.0/24',
'10.0.0.0/8',
'172.16.0.0/12',
'77.91.72.48/32',
'93.140.0.0/12',
'94.140.0.0/12'
];
function isAllowedIP($ip, $subnets) {
foreach ($subnets as $subnet) {
list($subnetBase, $bits) = explode('/', $subnet);
$ipLong = ip2long($ip);
$subnetLong = ip2long($subnetBase);
$mask = -1 << (32 - $bits);
$subnetLong &= $mask;
if (($ipLong & $mask) === $subnetLong) {
return true; // Совпадение найдено
}
}
return false; // Нет совпадений
}

View File

@ -0,0 +1,26 @@
<?php
include_once 'db_connect.php';
$year = $_GET['year'];
$yearMap = [
'pre-2009' => -1,
're-watch' => -2,
'manga' => -3
];
$yearValue = $yearMap[$year] ?? (int)$year;
// Count total records and completed records for that year
$stmt = $conn->prepare("SELECT COUNT(*) AS total_records, SUM(is_completed) AS total_completed FROM anime_list WHERE year = :year");
$stmt->bindParam(':year', $yearValue, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$all_completed = false;
if ($row && $row['total_records'] > 0 && $row['total_completed'] == $row['total_records']) {
$all_completed = true;
}
header('Content-Type: application/json');
echo json_encode(['all_completed' => $all_completed]);

View File

@ -0,0 +1,19 @@
<?php
session_start();
error_reporting(E_ALL);
ini_set('display_errors', 0); // Set to 1 for debugging purposes
$servername = "localhost";
$username = "username";
$password = "definetelysecurepwd123";
$dbname = "plan_to_watch";
try {
$conn = new PDO("mysql:host=$servername;dbname=$dbname;charset=utf8mb4", $username, $password);
// Set PDO error mode to exception
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
echo "Connection failed: " . $e->getMessage();
exit();
}
?>

52
php/delete_record.php Normal file
View File

@ -0,0 +1,52 @@
<?php
include_once 'db_connect.php';
include_once 'check_allowed_ip.php';
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!isAllowedIP($clientIP, $allowedSubnets)) {
http_response_code(403); // Set response code to 403
header('Content-Type: application/json');
echo json_encode([
'status' => 'error',
'message' => 'Access denied: Your IP is not authorized to delete records.'
]);
exit;
}
$id = $_GET['id'];
// Fetch the anime name and year before deleting
$stmt = $conn->prepare("SELECT name, `year` FROM anime_list WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$anime = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$anime) {
echo 'No anime found with id ' . htmlspecialchars($id);
exit;
}
$anime_name = $anime['name'];
$anime_year = $anime['year'];
// Delete the record
$stmt = $conn->prepare("DELETE FROM anime_list WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
// Log the action
// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
// $ip_address = $_SERVER['REMOTE_ADDR'];
// $action_type = 'deleting';
// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, `year`) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
// $log_stmt->bindParam(':action_time', $action_time_formatted);
// $log_stmt->bindParam(':ip_address', $ip_address);
// $log_stmt->bindParam(':anime_name', $anime_name);
// $log_stmt->bindParam(':action_type', $action_type);
// $log_stmt->bindParam(':anime_year', $anime_year, PDO::PARAM_INT);
// $log_stmt->execute();
?>

70
php/edit_record.php Normal file
View File

@ -0,0 +1,70 @@
<?php
include_once 'db_connect.php';
include_once 'check_allowed_ip.php';
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!isAllowedIP($clientIP, $allowedSubnets)) {
http_response_code(403); // Устанавливаем код ответа 403
header('Content-Type: application/json'); // Указываем, что возвращаем JSON
echo json_encode([
'status' => 'error',
'message' => 'Access denied: Your IP is not authorized to modify records.'
]);
exit;
}
$id = $_POST['id'];
$name = $_POST['name'];
$year = $_POST['year'];
$season = $_POST['season'];
$type = $_POST['type'];
$comment = $_POST['comment'];
$is_completed = isset($_POST['is_completed']) ? 1 : 0;
$currently_watching = isset($_POST['currently_watching']) ? 1 : 0;
$date_completed = $_POST['date_completed'];
$url = $_POST['url'];
// Handle empty date_completed
if (empty($date_completed)) {
$date_completed = NULL;
}
$stmt = $conn->prepare("UPDATE anime_list SET name = :name, year = :year, season = :season, type = :type, comment = :comment, is_completed = :is_completed, date_completed = :date_completed, url = :url, currently_watching = :currently_watching WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':year', $year, PDO::PARAM_INT);
$stmt->bindParam(':season', $season);
$stmt->bindParam(':type', $type);
$stmt->bindParam(':comment', $comment);
$stmt->bindParam(':is_completed', $is_completed, PDO::PARAM_INT);
$stmt->bindParam(':currently_watching', $currently_watching, PDO::PARAM_INT);
$stmt->bindParam(':url', $url);
// Use bindValue with PDO::PARAM_NULL if date_completed is NULL
if ($date_completed === NULL) {
$stmt->bindValue(':date_completed', NULL, PDO::PARAM_NULL);
} else {
$stmt->bindParam(':date_completed', $date_completed);
}
$stmt->execute();
// // Log the action
// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
// $ip_address = $_SERVER['REMOTE_ADDR'];
// $anime_name = $name;
// $anime_year = $year;
// $action_type = 'editing';
// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
// $log_stmt->bindParam(':action_time', $action_time_formatted);
// $log_stmt->bindParam(':ip_address', $ip_address);
// $log_stmt->bindParam(':anime_name', $anime_name);
// $log_stmt->bindParam(':anime_year', $anime_year);
// $log_stmt->bindParam(':action_type', $action_type);
// $log_stmt->execute();
?>

90
php/fetch_data.php Normal file
View File

@ -0,0 +1,90 @@
<?php
include_once 'db_connect.php';
$year = $_GET['year'] ?? 'pre-2009';
// Map special tabs to year values
$yearMap = [
'pre-2009' => -1,
're-watch' => -2,
'manga' => -3
];
$yearValue = $yearMap[$year] ?? (int)$year;
// Seasons array to organize records
$seasons = ['winter', 'spring', 'summer', 'fall', 'omake'];
// Fetch records from DB based on mode
if ($yearValue == -1 || $yearValue == -2 || $yearValue == -3) {
// For pre-2009, re-watch, or manga, we order by name (or differently if you prefer)
$stmt = $conn->prepare("SELECT * FROM anime_list WHERE year = :year ORDER BY name ASC");
} else {
// Normal year mode
$stmt = $conn->prepare("SELECT * FROM anime_list WHERE year = :year ORDER BY
CASE
WHEN season = 'winter' THEN 1
WHEN season = 'spring' THEN 2
WHEN season = 'summer' THEN 3
WHEN season = 'fall' THEN 4
WHEN season = 'omake' THEN 5
ELSE 6
END, name ASC");
}
$stmt->bindParam(':year', $yearValue, PDO::PARAM_INT);
$stmt->execute();
$records = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($yearValue == -1 || $yearValue == -2) {
$mode = 'non-season-table';
} else if ($yearValue == -3) {
$mode = 'manga';
} else {
$mode = 'normal';
}
// If normal mode, group records by season
$seasonRecords = [];
if ($mode === 'normal') {
// Initialize season arrays
foreach ($seasons as $s) {
$seasonRecords[$s] = [];
}
// Place records into their respective season
foreach ($records as $record) {
$recordSeason = strtolower($record['season']);
if (in_array($recordSeason, $seasons)) {
$seasonRecords[$recordSeason][] = $record;
} else {
// If season is unknown, treat as 'omake'
$seasonRecords['omake'][] = $record;
}
}
}
$yearTotal = count($records);
$yearCompleted = 0;
foreach ($records as $r) {
if ($r['is_completed']) {
$yearCompleted++;
}
}
$yearPercent = $yearTotal > 0 ? round(($yearCompleted / $yearTotal) * 100) : 0;
// Prepare data for the view
$viewData = [
'year' => $year,
'yearValue' => $yearValue,
'mode' => $mode, // either 'non-season-table' or 'normal'
'records' => $records,
'seasonRecords' => $seasonRecords ?? [],
'seasons' => $seasons,
'showSuggestButton' => ($mode === 'normal'),
'yearTotal' => $yearTotal,
'yearCompleted' => $yearCompleted,
'yearPercent' => $yearPercent
];
// Include the view
include '../tmpl/fetch_data_view.php';

12
php/fetch_record.php Normal file
View File

@ -0,0 +1,12 @@
<?php
include_once 'db_connect.php';
$id = $_GET['id'];
$stmt = $conn->prepare("SELECT * FROM anime_list WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$record = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode($record);
?>

136
php/get_image.php Normal file
View File

@ -0,0 +1,136 @@
<?php
// Включение отображения ошибок для отладки (удалите или закомментируйте в продакшене)
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
if (!isset($_GET['url'])) {
echo json_encode(['error' => 'No URL provided']);
exit;
}
$url = $_GET['url'];
if (strpos($url, 'myanimelist.net') === false) {
echo json_encode(['error' => 'Invalid URL']);
exit;
}
// Используем абсолютные пути для директорий кэша
$cache_dir = __DIR__ . '/cache/images/';
$cache_meta_dir = __DIR__ . '/cache/meta/';
// Проверяем и создаём директории, если они не существуют
if (!is_dir($cache_dir)) {
if (!mkdir($cache_dir, 0755, true)) {
echo json_encode(['error' => 'Failed to create images cache directory']);
exit;
}
}
if (!is_dir($cache_meta_dir)) {
if (!mkdir($cache_meta_dir, 0755, true)) {
echo json_encode(['error' => 'Failed to create meta cache directory']);
exit;
}
}
$hash = md5($url);
$cache_meta_file = $cache_meta_dir . $hash . '.json';
$cache_image_file = $cache_dir . $hash . '.jpg'; // Предполагаем, что изображения в формате JPG
// Проверяем, существует ли кэшированное изображение
if (file_exists($cache_image_file)) {
// Возвращаем путь к локальному изображению
$image_url_local = '/plan-to-watch/php/cache/images/' . $hash . '.jpg';
echo json_encode(['image_url' => $image_url_local]);
exit;
}
// Если кэшированного изображения нет, получаем URL изображения
// Используем cURL для получения страницы
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0');
$html = curl_exec($ch);
curl_close($ch);
if ($html === false) {
echo json_encode(['error' => 'Could not fetch the page']);
exit;
}
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($doc);
// Ищем мета-тег og:image
$image_nodes = $xpath->query("//meta[@property='og:image']");
if ($image_nodes->length > 0) {
$image_url = $image_nodes->item(0)->getAttribute('content');
// Проверяем, получили ли мы URL изображения
if (!empty($image_url)) {
// Скачиваем изображение
$image_data = file_get_contents($image_url);
if ($image_data === false) {
echo json_encode(['error' => 'Could not download the image']);
exit;
}
// Определяем тип изображения
$image_info = getimagesizefromstring($image_data);
if ($image_info === false) {
echo json_encode(['error' => 'Invalid image data']);
exit;
}
// Определяем расширение файла
$mime = $image_info['mime'];
switch ($mime) {
case 'image/jpeg':
$extension = '.jpg';
break;
case 'image/png':
$extension = '.png';
break;
case 'image/gif':
$extension = '.gif';
break;
default:
echo json_encode(['error' => 'Unsupported image type']);
exit;
}
// Обновляем путь к кэшированному изображению с правильным расширением
$cache_image_file = $cache_dir . $hash . $extension;
$image_url_local = '/plan-to-watch/php/cache/images/' . $hash . $extension;
// Сохраняем изображение на сервере
if (file_put_contents($cache_image_file, $image_data) === false) {
echo json_encode(['error' => 'Failed to save the image']);
exit;
}
// Сохраняем метаданные (опционально)
$meta_data = json_encode(['image_url' => $image_url_local]);
if (file_put_contents($cache_meta_file, $meta_data) === false) {
echo json_encode(['error' => 'Failed to save meta data']);
exit;
}
echo json_encode(['image_url' => $image_url_local]);
exit;
}
}
echo json_encode(['error' => 'Image not found']);
exit;
?>

55
php/get_logs.php Normal file
View File

@ -0,0 +1,55 @@
<?php
include_once 'db_connect.php';
include_once 'check_allowed_ip.php';
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!isAllowedIP($clientIP, $allowedSubnets)) {
http_response_code(403);
die("Access denied for {$clientIP}");
}
// Generate CSRF token if not set
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
// Handle clear logs action
if (isset($_GET['action']) && $_GET['action'] === 'clear_logs') {
// Verify CSRF token
if (!isset($_GET['token']) || $_GET['token'] !== $_SESSION['csrf_token']) {
// Invalid CSRF token
die('Invalid CSRF token');
}
// Clear the logs
$stmt = $conn->prepare("TRUNCATE TABLE action_logs");
$stmt->execute();
// Regenerate CSRF token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// Redirect back to panel.php
header("Location: panel.php");
exit();
}
// Handle pagination parameters passed from logs.php
$per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 10;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
// Count total logs
$count_stmt = $conn->query("SELECT COUNT(*) AS total FROM action_logs");
$total_logs = (int)$count_stmt->fetchColumn();
// Calculate offset
$offset = ($page - 1) * $per_page;
// Fetch logs with LIMIT and OFFSET
$stmt = $conn->prepare("SELECT * FROM action_logs ORDER BY action_time DESC LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $per_page, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);

28
php/get_options.php Normal file
View File

@ -0,0 +1,28 @@
<?php
include_once 'db_connect.php'; // ensure you have a PDO $conn
// Check if form is submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_options'])) {
// Get checkbox value, 'on' if checked, otherwise null
$autoAdd = isset($_POST['auto_add_completed_date']) ? '1' : '0';
// Update the option in the database
$stmt = $conn->prepare("INSERT INTO options (option_name, option_value) VALUES ('auto_add_completed_date', :val)
ON DUPLICATE KEY UPDATE option_value = :val");
$stmt->bindParam(':val', $autoAdd, PDO::PARAM_STR);
$stmt->execute();
// Redirect to panel.php to avoid form resubmission
header("Location: panel.php");
exit;
}
// Fetch the current option value
$stmt = $conn->prepare("SELECT option_value FROM options WHERE option_name = 'auto_add_completed_date'");
$stmt->execute();
$autoAddCompletedDate = $stmt->fetchColumn();
// If not found in DB, default to '0'
if ($autoAddCompletedDate === false) {
$autoAddCompletedDate = '0';
}

66
php/get_stats.php Normal file
View File

@ -0,0 +1,66 @@
<?php
function getDirectorySize($dir) {
$size = 0;
if (is_dir($dir)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)) as $file) {
$size += $file->getSize();
}
}
return $size;
}
// Функция для подсчета количества файлов в директории
function getFileCount($dir) {
$count = 0;
if (is_dir($dir)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)) as $file) {
if ($file->isFile()) {
$count++;
}
}
}
return $count;
}
// Определяем абсолютные пути к директориям кэша для расчета статистики
$cache_images_dir = __DIR__ . '/cache/images/';
$cache_meta_dir = __DIR__ . '/cache/meta/';
// Расчет размера кэша (только изображения)
$cache_size_bytes = getDirectorySize($cache_images_dir);
$cache_size_mb = round($cache_size_bytes / (1024 * 1024), 2);
// Подсчет количества изображений в кэше
$image_count = getFileCount($cache_images_dir);
// Fetch statistics
$stats = [];
// Total number of entries
$query = "SELECT COUNT(*) AS total_entries FROM anime_list";
$stats['total_entries'] = $conn->query($query)->fetch(PDO::FETCH_ASSOC)['total_entries'];
// Total number of completed anime
$query = "SELECT COUNT(*) AS completed_anime FROM anime_list WHERE is_completed = 1";
$stats['completed_anime'] = $conn->query($query)->fetch(PDO::FETCH_ASSOC)['completed_anime'];
// Count by type
$query = "SELECT type, COUNT(*) AS count FROM anime_list GROUP BY type";
$stats['by_type'] = $conn->query($query)->fetchAll(PDO::FETCH_ASSOC);
// Most recent completion date
$query = "SELECT MAX(date_completed) AS most_recent_completion FROM anime_list WHERE date_completed IS NOT NULL";
$stats['most_recent_completion'] = $conn->query($query)->fetch(PDO::FETCH_ASSOC)['most_recent_completion'];
// DB size
$query = "SELECT SUM(data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = :dbname";
$stmt = $conn->prepare($query);
$stmt->execute(['dbname' => $dbname]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$size_in_bytes = $row['size'];
$size_in_kilobytes = $size_in_bytes / 1024;
?>

11
php/options_helper.php Normal file
View File

@ -0,0 +1,11 @@
<?php
include_once 'db_connect.php';
function getOptionValue($conn, $optionName, $default = '0') {
$stmt = $conn->prepare("SELECT option_value FROM options WHERE option_name = :option_name");
$stmt->bindParam(':option_name', $optionName, PDO::PARAM_STR);
$stmt->execute();
$value = $stmt->fetchColumn();
return $value !== false ? $value : $default;
}

49
php/panel.php Normal file
View File

@ -0,0 +1,49 @@
<?php
include_once 'db_connect.php'; // Ensure session and DB are available
include_once 'check_allowed_ip.php';
include 'get_options.php';
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!isAllowedIP($clientIP, $allowedSubnets)) {
http_response_code(403);
die("Access denied for {$clientIP}");
}
// Default per_page and allowed values
$allowed_per_page = [5, 10, 50, 100];
$default_per_page = 10;
// Get the chosen per_page from GET, default if not valid
$per_page = isset($_GET['per_page']) && in_array((int)$_GET['per_page'], $allowed_per_page)
? (int)$_GET['per_page']
: $default_per_page;
// Get the current page from GET, default to 1 if not valid
$page = isset($_GET['page']) && (int)$_GET['page'] > 0 ? (int)$_GET['page'] : 1;
// Pass these parameters to get_logs.php for the query
$_GET['per_page'] = $per_page;
$_GET['page'] = $page;
include 'get_logs.php';
include 'get_stats.php';
// Prepare data for the view
$viewData = [
'per_page' => $per_page,
'page' => $page,
'allowed_per_page' => $allowed_per_page,
'logs' => $logs,
'total_logs' => $total_logs,
'csrf_token' => $csrf_token,
'stats' => $stats,
'size_in_kilobytes' => $size_in_kilobytes,
'cache_size_mb' => $cache_size_mb,
'image_count' => $image_count,
'autoAddCompletedDate' => $autoAddCompletedDate
];
include '../tmpl/panel_view.php';

67
php/set_complete.php Normal file
View File

@ -0,0 +1,67 @@
<?php
include_once 'db_connect.php';
include_once 'check_allowed_ip.php';
include 'options_helper.php';
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!isAllowedIP($clientIP, $allowedSubnets)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode([
'status' => 'error',
'message' => 'Access denied: Your IP is not authorized to modify records.'
]);
exit;
}
$id = $_POST['id'];
// Fetch the anime name and year for logging
$stmt = $conn->prepare("SELECT name, year FROM anime_list WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$record = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$record) {
http_response_code(404);
echo json_encode([
'status' => 'error',
'message' => 'Record not found.'
]);
exit;
}
$anime_name = $record['name'];
$anime_year = $record['year'];
// Update the record to set is_completed to 1
$auto_add_complelete_date = getOptionValue($conn, 'auto_add_completed_date', '0');
if ($auto_add_complelete_date == 1) {
$current_date = date('Y-m-d');
$stmt = $conn->prepare("UPDATE anime_list SET is_completed = 1, date_completed = :current_date WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->bindParam(':current_date', $current_date);
} else {
$stmt = $conn->prepare("UPDATE anime_list SET is_completed = 1 WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
}
$stmt->execute();
// Log the action
$action_time = new DateTime('now', new DateTimeZone('GMT+5'));
$action_time_formatted = $action_time->format('Y-m-d H:i:s');
$ip_address = $_SERVER['REMOTE_ADDR'];
$action_type = 'set_complete';
$log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
$log_stmt->bindParam(':action_time', $action_time_formatted);
$log_stmt->bindParam(':ip_address', $ip_address);
$log_stmt->bindParam(':anime_name', $anime_name);
$log_stmt->bindParam(':anime_year', $anime_year);
$log_stmt->bindParam(':action_type', $action_type);
$log_stmt->execute();
echo json_encode(['status' => 'success']);
?>

View File

@ -0,0 +1,60 @@
<?php
include_once 'db_connect.php';
include_once 'check_allowed_ip.php';
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!isAllowedIP($clientIP, $allowedSubnets)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode([
'status' => 'error',
'message' => 'Access denied: Your IP is not authorized to modify records.'
]);
exit;
}
$id = $_POST['id'];
// Fetch the current state of currently_watching
$stmt = $conn->prepare("SELECT name, year, currently_watching FROM anime_list WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$record = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$record) {
http_response_code(404);
echo json_encode([
'status' => 'error',
'message' => 'Record not found.'
]);
exit;
}
$anime_name = $record['name'];
$anime_year = $record['year'];
$currentState = (int)$record['currently_watching'];
// Toggle currently_watching
$newState = $currentState === 1 ? 0 : 1;
$stmt = $conn->prepare("UPDATE anime_list SET currently_watching = :newState WHERE id = :id");
$stmt->bindParam(':newState', $newState, PDO::PARAM_INT);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
// Log the action
// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
// $ip_address = $_SERVER['REMOTE_ADDR'];
// $action_type = $newState === 1 ? 'set_currently_watching' : 'unset_currently_watching';
// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
// $log_stmt->bindParam(':action_time', $action_time_formatted);
// $log_stmt->bindParam(':ip_address', $ip_address);
// $log_stmt->bindParam(':anime_name', $anime_name);
// $log_stmt->bindParam(':anime_year', $anime_year);
// $log_stmt->bindParam(':action_type', $action_type);
// $log_stmt->execute();
echo json_encode(['status' => 'success']);

84
schema.sql Normal file
View File

@ -0,0 +1,84 @@
-- MySQL dump 10.19 Distrib 10.3.39-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: plan_to_watch
-- ------------------------------------------------------
-- Server version 10.3.39-MariaDB-0+deb10u2
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `action_logs`
--
DROP TABLE IF EXISTS `action_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `action_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`action_time` datetime DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`anime_name` varchar(255) DEFAULT NULL,
`action_type` varchar(50) DEFAULT NULL,
`year` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `anime_list`
--
DROP TABLE IF EXISTS `anime_list`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `anime_list` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`year` int(11) DEFAULT NULL,
`season` varchar(50) DEFAULT NULL,
`type` varchar(50) DEFAULT NULL,
`comment` text DEFAULT NULL,
`is_completed` tinyint(1) DEFAULT 0,
`date_completed` date DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
`currently_watching` tinyint(1) NOT NULL DEFAULT 0,
`table_type` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=985 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `options`
--
DROP TABLE IF EXISTS `options`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `options` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`option_name` varchar(255) NOT NULL,
`option_value` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `option_name` (`option_name`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-01-06 22:57:21

391
tmpl/fetch_data_view.php Normal file
View File

@ -0,0 +1,391 @@
<?php
extract($viewData);
?>
<h2><?php echo htmlspecialchars($year); ?></h2>
<!-- Display year stats -->
<p>
Total entries in <?php echo $year; ?>: <strong><?php echo $yearTotal; ?></strong>, Completed: <strong><?php echo $yearCompleted; ?> (<?php echo $yearPercent; ?>%)</strong>
</p>
<?php
if ($showSuggestButton) {
echo '<button id="suggest-button">Suggest</button>';
}
if ($mode === 'non-season-table') {
if (!empty($records)) {
renderPre2009Table($records, $yearTotal, $yearCompleted, $yearPercent);
} else {
echo '<p>No records found.</p>';
}
renderAddRecordForm($yearValue, $seasons);
} else if ($mode === 'manga') {
if (!empty($records)) {
renderMangaTable($records, $yearTotal, $yearCompleted, $yearPercent);
}
else {
echo '<p>No records found.</p>';
}
renderAddMangaForm($yearValue, $seasons);
}
else {
renderSeasonTables($seasons, $seasonRecords);
renderAddRecordForm($yearValue, $seasons);
}
// Renders table for pre-2009 records
function renderPre2009Table($records, $yearTotal, $yearCompleted, $yearPercent) {
// Since pre-2009 mode doesn't have seasons, we've already shown year stats above.
?>
<table class="record-table">
<colgroup>
<col>
<col>
<col>
<col>
<col>
<col>
</colgroup>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Type</th>
<th>Comment</th>
<th>Date Completed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php $rowNumber = 1; ?>
<?php foreach ($records as $record): ?>
<?php
$rowClasses = [];
if ($record['is_completed']) {
$rowClasses[] = 'completed';
}
if (!empty($record['currently_watching']) && $record['currently_watching']) {
$rowClasses[] = 'watching';
}
$classAttr = !empty($rowClasses) ? ' class="'.implode(' ', $rowClasses).'"' : '';
?>
<tr <?php echo $classAttr; ?> data-id="<?php echo $record['id']; ?>" data-name="<?php echo htmlspecialchars($record['name'], ENT_QUOTES); ?>">
<td><?php echo $rowNumber++; ?></td>
<td>
<?php if (!empty($record['url'])): ?>
<a href="<?php echo htmlspecialchars($record['url']); ?>" target="_blank" data-url="<?php echo htmlspecialchars($record['url'], ENT_QUOTES); ?>">
<?php echo htmlspecialchars($record['name']); ?>
</a>
<?php else: ?>
<?php echo htmlspecialchars($record['name']); ?>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($record['type']); ?></td>
<td><?php echo htmlspecialchars($record['comment']); ?></td>
<td><?php echo htmlspecialchars($record['date_completed']); ?></td>
<td>
<?php if (!$record['is_completed']): ?>
<button class="set-complete-button" data-id="<?php echo $record['id']; ?>">+</button>
<?php endif; ?>
<button class="set-currently-watching-button" data-id="<?php echo $record['id']; ?>">W</button>
<button class="edit-button" data-id="<?php echo $record['id']; ?>">Edit</button>
<button class="delete-button" data-id="<?php echo $record['id']; ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
}
// Renders table for manga
function renderMangaTable($records, $yearTotal, $yearCompleted, $yearPercent) {
// Since pre-2009 mode doesn't have seasons, we've already shown year stats above.
?>
<table class="record-table" id="manga">
<colgroup>
<col>
<col>
<col>
<col>
<col>
</colgroup>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Comment</th>
<th>Date Completed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php $rowNumber = 1; ?>
<?php foreach ($records as $record): ?>
<?php
$rowClasses = [];
if ($record['is_completed']) {
$rowClasses[] = 'completed';
}
if (!empty($record['currently_watching']) && $record['currently_watching']) {
$rowClasses[] = 'watching';
}
$classAttr = !empty($rowClasses) ? ' class="'.implode(' ', $rowClasses).'"' : '';
?>
<tr <?php echo $classAttr; ?> data-id="<?php echo $record['id']; ?>" data-name="<?php echo htmlspecialchars($record['name'], ENT_QUOTES); ?>">
<td><?php echo $rowNumber++; ?></td>
<td>
<?php if (!empty($record['url'])): ?>
<a href="<?php echo htmlspecialchars($record['url']); ?>" target="_blank" data-url="<?php echo htmlspecialchars($record['url'], ENT_QUOTES); ?>">
<?php echo htmlspecialchars($record['name']); ?>
</a>
<?php else: ?>
<?php echo htmlspecialchars($record['name']); ?>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($record['comment']); ?></td>
<td><?php echo htmlspecialchars($record['date_completed']); ?></td>
<td>
<?php if (!$record['is_completed']): ?>
<button class="set-complete-button" data-id="<?php echo $record['id']; ?>">+</button>
<?php endif; ?>
<button class="set-currently-watching-button" data-id="<?php echo $record['id']; ?>">CR</button>
<button class="edit-button" data-id="<?php echo $record['id']; ?>">Edit</button>
<button class="delete-button" data-id="<?php echo $record['id']; ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
}
// Renders season tables for normal mode
function renderSeasonTables($seasons, $seasonRecords) {
foreach ($seasons as $season) {
// Compute season stats:
$seasonRecordsThis = $seasonRecords[$season] ?? [];
$seasonTotal = count($seasonRecordsThis);
$seasonCompleted = 0;
foreach ($seasonRecordsThis as $r) {
if ($r['is_completed']) {
$seasonCompleted++;
}
}
$seasonPercent = $seasonTotal > 0 ? round(($seasonCompleted / $seasonTotal) * 100) : 0;
echo '<h3>' . ucfirst($season) . ' ' . get_season_emoji($season) . '</h3>';
// Display season stats:
echo "<p>Completed: <strong>$seasonCompleted ($seasonPercent%)</strong></p>";
if (!empty($seasonRecordsThis)) {
?>
<table class="record-table">
<colgroup>
<col><col><col><col><col><col>
</colgroup>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Type</th>
<th>Comment</th>
<th>Date Completed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php $rowNumber = 1; ?>
<?php foreach ($seasonRecordsThis as $record): ?>
<?php
$rowClasses = [];
if ($record['is_completed']) {
$rowClasses[] = 'completed';
}
if (!empty($record['currently_watching']) && $record['currently_watching']) {
$rowClasses[] = 'watching';
}
$classAttr = !empty($rowClasses) ? ' class="' . implode(' ', $rowClasses) . '"' : '';
?>
<tr<?php echo $classAttr; ?> data-id="<?php echo $record['id']; ?>" data-name="<?php echo htmlspecialchars($record['name'], ENT_QUOTES); ?>">
<td><?php echo $rowNumber++; ?></td>
<td>
<?php if (!empty($record['url'])): ?>
<a href="<?php echo htmlspecialchars($record['url']); ?>" target="_blank" data-url="<?php echo htmlspecialchars($record['url'], ENT_QUOTES); ?>">
<?php echo htmlspecialchars($record['name']); ?>
</a>
<?php else: ?>
<?php echo htmlspecialchars($record['name']); ?>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($record['type']); ?></td>
<td><?php echo htmlspecialchars($record['comment']); ?></td>
<td><?php echo htmlspecialchars($record['date_completed']); ?></td>
<td>
<?php if (!$record['is_completed']): ?>
<button class="set-complete-button" data-id="<?php echo $record['id']; ?>">+</button>
<?php endif; ?>
<button class="set-currently-watching-button" data-id="<?php echo $record['id']; ?>">CW</button>
<button class="edit-button" data-id="<?php echo $record['id']; ?>">Edit</button>
<button class="delete-button" data-id="<?php echo $record['id']; ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
} else {
echo '<p>No records for this season.</p>';
}
}
}
function renderAddMangaForm($yearValue, $seasons) {
?>
<form id="add-form" class="form-container-horizontal">
<h3>Add New Record</h3>
<input type="hidden" name="year" value="<?php echo htmlspecialchars($yearValue); ?>">
<div class="form-row">
<?php if ($yearValue == -1 || $yearValue == -2 || $yearValue == -3): ?>
<input type="hidden" name="season" value="N/A">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" name="name" id="name" class="form-control" required>
</div>
<?php else: ?>
<div class="form-group">
<label for="name">Name:</label>
<input type="text" name="name" id="name" class="form-control" required>
</div>
<?php endif; ?>
</div> <!-- End of first form row -->
<div class="form-row">
<div class="form-group">
<label for="comment">Comment:</label>
<input type="text" name="comment" id="comment" class="form-control">
</div>
<div class="form-group">
<label for="date_completed">Date Completed:</label>
<input type="date" name="date_completed" id="date_completed" class="form-control">
</div>
<div class="form-group">
<label for="url">URL:</label>
<input type="url" name="url" id="url" class="form-control">
</div>
</div> <!-- End of second form row -->
<div class="form-row">
<div class="form-group form-check">
<input type="checkbox" name="is_completed" id="is_completed" class="form-check-input">
<label for="is_completed" class="form-check-label">Is Completed</label>
</div>
<div class="form-group form-check">
<input type="checkbox" name="currently_watching" id="currently_watching" class="form-check-input">
<label for="currently_watching" class="form-check-label">Currently reading</label>
</div>
</div> <!-- End of third form row -->
<div class="form-row">
<div class="form-group">
<button type="submit" class="btn btn-primary">Add Record</button>
</div>
</div> <!-- End of fourth form row -->
</form>
<div><a href="./php/panel.php">Control panel</a> | <a href="/docs/animu-tree.txt">Downloaded anime</a></div>
<?php
}
function renderAddRecordForm($yearValue, $seasons) {
?>
<form id="add-form" class="form-container-horizontal">
<h3>Add New Record</h3>
<input type="hidden" name="year" value="<?php echo htmlspecialchars($yearValue); ?>">
<div class="form-row">
<?php if ($yearValue == -1 || $yearValue == -2 || $yearValue == -3): ?>
<input type="hidden" name="season" value="N/A">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" name="name" id="name" class="form-control" required>
</div>
<?php else: ?>
<div class="form-group">
<label for="name">Name:</label>
<input type="text" name="name" id="name" class="form-control" required>
</div>
<div class="form-group">
<label for="season">Season:</label>
<select name="season" id="season" class="form-control">
<?php foreach ($seasons as $season): ?>
<option value="<?php echo $season; ?>"><?php echo ucfirst($season); ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="form-group">
<label for="type">Type:</label>
<select name="type" id="type" class="form-control">
<option value="tv">TV</option>
<option value="special">Special</option>
<option value="short">Short</option>
<option value="ova">OVA</option>
<option value="movie">Movie</option>
<option value="other">Other</option>
</select>
</div>
</div> <!-- End of first form row -->
<div class="form-row">
<div class="form-group">
<label for="comment">Comment:</label>
<input type="text" name="comment" id="comment" class="form-control">
</div>
<div class="form-group">
<label for="date_completed">Date Completed:</label>
<input type="date" name="date_completed" id="date_completed" class="form-control">
</div>
<div class="form-group">
<label for="url">URL:</label>
<input type="url" name="url" id="url" class="form-control">
</div>
</div> <!-- End of second form row -->
<div class="form-row">
<div class="form-group form-check">
<input type="checkbox" name="is_completed" id="is_completed" class="form-check-input">
<label for="is_completed" class="form-check-label">Is Completed</label>
</div>
<div class="form-group form-check">
<input type="checkbox" name="currently_watching" id="currently_watching" class="form-check-input">
<label for="currently_watching" class="form-check-label">Currently Watching</label>
</div>
</div> <!-- End of third form row -->
<div class="form-row">
<div class="form-group">
<button type="submit" class="btn btn-primary">Add Record</button>
</div>
</div> <!-- End of fourth form row -->
</form>
<div><a href="./php/panel.php">Control panel</a> | <a href="/docs/animu-tree.txt">Downloaded anime</a></div>
<?php
}
function get_season_emoji($season) {
switch($season) {
case 'winter':
return '❄️';
case 'spring':
return '🌱';
case 'summer':
return '☀️';
case 'fall':
return '🍂';
default:
return '🎇';
}
}

187
tmpl/panel_view.php Normal file
View File

@ -0,0 +1,187 @@
<?php
// panel_view.php
// Extract all the variables we need from $viewData
extract($viewData);
function get_anime_year($raw_year) {
switch($raw_year) {
case -1:
return 'pre-2009';
case -2:
return 're-watch';
case -3:
return 'manga';
default:
return $raw_year;
}
}
// Calculate pagination details
$total_pages = ($total_logs > 0) ? ceil($total_logs / $per_page) : 1;
?>
<!DOCTYPE html>
<html>
<head>
<title>Control Panel</title>
<style>
/* Existing styles */
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
}
th {
background-color: #ddd;
text-align: left;
}
/* Style for the links */
.action-links {
margin-bottom: 15px;
}
.action-links a {
margin-right: 15px;
text-decoration: none;
font-weight: bold;
}
.clear-logs-link {
color: red;
}
/* Pagination styles */
.pagination {
margin-top: 15px;
}
.pagination a {
margin: 0 5px;
text-decoration: none;
font-weight: bold;
}
.pagination span.current-page {
font-weight: bold;
margin: 0 5px;
}
/* Per-page form */
.per-page-form {
margin-bottom: 15px;
}
</style>
<script>
function confirmClearLogs() {
return confirm('Are you sure you want to clear all logs?');
}
</script>
</head>
<body>
<h1>Action Logs</h1>
<div class="action-links">
<!-- Link to refresh logs -->
<a href="panel.php?per_page=<?php echo $per_page; ?>" class="refresh-logs-link">Refresh</a>
<!-- Link to clear logs -->
<a href="panel.php?action=clear_logs&token=<?php echo $csrf_token; ?>&per_page=<?php echo $per_page; ?>" class="clear-logs-link" onclick="return confirmClearLogs();">Clear Logs</a>
</div>
<!-- Per-page selection form -->
<form class="per-page-form" method="get" action="panel.php">
<label for="per_page">Show per page:</label>
<select name="per_page" id="per_page">
<?php foreach ($allowed_per_page as $option): ?>
<option value="<?php echo $option; ?>" <?php if ($option == $per_page) echo 'selected'; ?>><?php echo $option; ?></option>
<?php endforeach; ?>
</select>
<button type="submit">Apply</button>
</form>
<table>
<tr>
<th>Date and Time (GMT+5)</th>
<th>IP Address</th>
<th>Anime Name</th>
<th>Action</th>
</tr>
<?php if (count($logs) > 0): ?>
<?php foreach ($logs as $log): ?>
<tr>
<td><?php echo htmlspecialchars($log['action_time']); ?></td>
<td><?php echo htmlspecialchars($log['ip_address']); ?></td>
<td>
<?php
echo htmlspecialchars($log['anime_name']);
echo ' (' . get_anime_year(htmlspecialchars($log['year'])) . ')';
?>
</td>
<td><?php echo htmlspecialchars($log['action_type']); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4">No logs available.</td>
</tr>
<?php endif; ?>
</table>
<hr>
<!-- Pagination Links -->
<?php if ($total_logs > 0 && $total_pages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="panel.php?page=<?php echo $page - 1; ?>&per_page=<?php echo $per_page; ?>">&laquo; Prev</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<?php if ($i == $page): ?>
<span class="current-page"><?php echo $i; ?></span>
<?php else: ?>
<a href="panel.php?page=<?php echo $i; ?>&per_page=<?php echo $per_page; ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($page < $total_pages): ?>
<a href="panel.php?page=<?php echo $page + 1; ?>&per_page=<?php echo $per_page; ?>">Next &raquo;</a>
<?php endif; ?>
</div>
<?php endif; ?>
<h1>Stats</h1>
<p><strong>Total Entries:</strong> <?php echo $stats['total_entries']; ?></p>
<p><strong>Completed:</strong> <?php echo $stats['completed_anime']; ?> (<?php echo ceil(($stats['completed_anime'] / $stats['total_entries']) * 100); ?>%)</p>
<p><strong>Database size: </strong><?php echo number_format($size_in_kilobytes, 2); ?> Kb.</p>
<p><strong>Image cache size: </strong><?php echo $cache_size_mb; ?>Mb.</p>
<p><strong>Number of cached images: </strong><?php echo $image_count; ?></p>
<h3>Count by Type</h3>
<?php if (!empty($stats['by_type'])): ?>
<ul>
<?php foreach ($stats['by_type'] as $type): ?>
<li><?php echo htmlspecialchars($type['type'] ?: 'Unknown'); ?>: <?php echo $type['count']; ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<p><strong>Most Recent Completion Date:</strong>
<?php if ($stats['most_recent_completion']):
$date = new DateTime($stats['most_recent_completion']);
echo $date->format('F j, Y');
else:
echo "No date found.";
endif; ?>
</p>
<h1>Options</h1>
<form method="post" action="panel.php">
<div>
<label>
<input type="checkbox" name="auto_add_completed_date" <?php if ($autoAddCompletedDate === '1') echo 'checked'; ?>>
Automatically add current date when anime is completed
</label>
</div>
<button type="submit" name="update_options">Save</button>
</form>
</body>
</html>