diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..128d114 --- /dev/null +++ b/css/styles.css @@ -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; +} + + diff --git a/icons/android-chrome-192x192.png b/icons/android-chrome-192x192.png new file mode 100644 index 0000000..ad38971 Binary files /dev/null and b/icons/android-chrome-192x192.png differ diff --git a/icons/android-chrome-512x512.png b/icons/android-chrome-512x512.png new file mode 100644 index 0000000..fb1a751 Binary files /dev/null and b/icons/android-chrome-512x512.png differ diff --git a/icons/apple-touch-icon.png b/icons/apple-touch-icon.png new file mode 100644 index 0000000..ae28f8a Binary files /dev/null and b/icons/apple-touch-icon.png differ diff --git a/icons/favicon-16x16.png b/icons/favicon-16x16.png new file mode 100644 index 0000000..546259e Binary files /dev/null and b/icons/favicon-16x16.png differ diff --git a/icons/favicon-32x32.png b/icons/favicon-32x32.png new file mode 100644 index 0000000..0cb7049 Binary files /dev/null and b/icons/favicon-32x32.png differ diff --git a/icons/favicon.ico b/icons/favicon.ico new file mode 100644 index 0000000..0021910 Binary files /dev/null and b/icons/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..74fdfc6 --- /dev/null +++ b/index.html @@ -0,0 +1,40 @@ + + + + + + Backlog of unwatched anime + + + + + + + + +
+
+
+
+
+
+ +
+ +
+
+ + + + + + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..e69b541 --- /dev/null +++ b/js/main.js @@ -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 ` +

Edit Record

+ + + +
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + `; +} + + +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 = `

${names.join('
')}

`; + 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 = ''; + 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'; + } + }); +}); diff --git a/php/add_record.php b/php/add_record.php new file mode 100644 index 0000000..857b6db --- /dev/null +++ b/php/add_record.php @@ -0,0 +1,68 @@ + '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(); + +?> + diff --git a/php/check_allowed_ip.php b/php/check_allowed_ip.php new file mode 100644 index 0000000..1767ddc --- /dev/null +++ b/php/check_allowed_ip.php @@ -0,0 +1,26 @@ + -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]); diff --git a/php/db_connect.example.php b/php/db_connect.example.php new file mode 100644 index 0000000..e8ec69a --- /dev/null +++ b/php/db_connect.example.php @@ -0,0 +1,19 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} catch(PDOException $e) { + echo "Connection failed: " . $e->getMessage(); + exit(); +} +?> diff --git a/php/delete_record.php b/php/delete_record.php new file mode 100644 index 0000000..f221bda --- /dev/null +++ b/php/delete_record.php @@ -0,0 +1,52 @@ + '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(); +?> + diff --git a/php/edit_record.php b/php/edit_record.php new file mode 100644 index 0000000..7cf865a --- /dev/null +++ b/php/edit_record.php @@ -0,0 +1,70 @@ + '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(); + +?> + diff --git a/php/fetch_data.php b/php/fetch_data.php new file mode 100644 index 0000000..6441944 --- /dev/null +++ b/php/fetch_data.php @@ -0,0 +1,90 @@ + -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'; diff --git a/php/fetch_record.php b/php/fetch_record.php new file mode 100644 index 0000000..7173902 --- /dev/null +++ b/php/fetch_record.php @@ -0,0 +1,12 @@ +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); +?> diff --git a/php/get_image.php b/php/get_image.php new file mode 100644 index 0000000..d514305 --- /dev/null +++ b/php/get_image.php @@ -0,0 +1,136 @@ + '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; +?> + diff --git a/php/get_logs.php b/php/get_logs.php new file mode 100644 index 0000000..d846792 --- /dev/null +++ b/php/get_logs.php @@ -0,0 +1,55 @@ +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); diff --git a/php/get_options.php b/php/get_options.php new file mode 100644 index 0000000..8b28695 --- /dev/null +++ b/php/get_options.php @@ -0,0 +1,28 @@ +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'; +} diff --git a/php/get_stats.php b/php/get_stats.php new file mode 100644 index 0000000..0fb9b08 --- /dev/null +++ b/php/get_stats.php @@ -0,0 +1,66 @@ +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; + +?> diff --git a/php/options_helper.php b/php/options_helper.php new file mode 100644 index 0000000..d026684 --- /dev/null +++ b/php/options_helper.php @@ -0,0 +1,11 @@ +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; +} + diff --git a/php/panel.php b/php/panel.php new file mode 100644 index 0000000..5694463 --- /dev/null +++ b/php/panel.php @@ -0,0 +1,49 @@ + 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'; diff --git a/php/set_complete.php b/php/set_complete.php new file mode 100644 index 0000000..fb25692 --- /dev/null +++ b/php/set_complete.php @@ -0,0 +1,67 @@ + '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']); +?> diff --git a/php/set_currently_watching.php b/php/set_currently_watching.php new file mode 100644 index 0000000..2068b3d --- /dev/null +++ b/php/set_currently_watching.php @@ -0,0 +1,60 @@ + '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']); diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..0a4cf84 --- /dev/null +++ b/schema.sql @@ -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 diff --git a/tmpl/fetch_data_view.php b/tmpl/fetch_data_view.php new file mode 100644 index 0000000..e664fb4 --- /dev/null +++ b/tmpl/fetch_data_view.php @@ -0,0 +1,391 @@ + + +

+ + +

+ Total entries in : , Completed: (%) +

+ +Suggest'; +} + +if ($mode === 'non-season-table') { + if (!empty($records)) { + renderPre2009Table($records, $yearTotal, $yearCompleted, $yearPercent); + } else { + echo '

No records found.

'; + } + renderAddRecordForm($yearValue, $seasons); + +} else if ($mode === 'manga') { + if (!empty($records)) { + renderMangaTable($records, $yearTotal, $yearCompleted, $yearPercent); + } + else { + echo '

No records found.

'; + } + 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. + ?> + + + + + + + + + + + + + + + + + + + + + + + + data-id="" data-name=""> + + + + + + + + + +
#NameTypeCommentDate CompletedActions
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + data-id="" data-name=""> + + + + + + + + +
#NameCommentDate CompletedActions
+ + + + + + + + + + + + + + +
+ 0 ? round(($seasonCompleted / $seasonTotal) * 100) : 0; + + echo '

' . ucfirst($season) . ' ' . get_season_emoji($season) . '

'; + // Display season stats: + echo "

Completed: $seasonCompleted ($seasonPercent%)

"; + + if (!empty($seasonRecordsThis)) { + ?> + + + + + + + + + + + + + + + + + + + data-id="" data-name=""> + + + + + + + + + +
#NameTypeCommentDate CompletedActions
+ + + + + + + + + + + + + + +
+ No records for this season.

'; + } + } +} + +function renderAddMangaForm($yearValue, $seasons) { + ?> +
+

Add New Record

+ +
+ + +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+
Control panel | Downloaded anime
+ +
+

Add New Record

+ +
+ + +
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+
Control panel | Downloaded anime
+ 0) ? ceil($total_logs / $per_page) : 1; + +?> + + + + Control Panel + + + + + +

Action Logs

+ + + + +
+ + + +
+ + + + + + + + + 0): ?> + + + + + + + + + + + + + +
Date and Time (GMT+5)IP AddressAnime NameAction
+ +
No logs available.
+
+ + + 0 && $total_pages > 1): ?> + + + +

Stats

+

Total Entries:

+

Completed: (%)

+

Database size: Kb.

+

Image cache size: Mb.

+

Number of cached images:

+ +

Count by Type

+ + + + +

Most Recent Completion Date: + format('F j, Y'); + else: + echo "No date found."; + endif; ?> +

+ +

Options

+
+
+ +
+ +
+ + +