Compare commits
7 Commits
1dd69955d4
...
v1.0.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c0a37c4b45 | ||
![]() |
86e7b72323 | ||
![]() |
8900b7b11d | ||
![]() |
8be501dd46 | ||
![]() |
8067343a74 | ||
![]() |
a4901ba2a4 | ||
![]() |
540b5a3194 |
Binary file not shown.
Binary file not shown.
27
backend.py
27
backend.py
@@ -219,3 +219,30 @@ class AnimeBackend:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deleting year {year}: {e}")
|
logging.error(f"Error deleting year {year}: {e}")
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
|
|
||||||
|
def get_total_entries(self):
|
||||||
|
try:
|
||||||
|
cursor = self.db.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM anime")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error getting total entries: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_completed_entries(self):
|
||||||
|
try:
|
||||||
|
cursor = self.db.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM anime WHERE status = 'completed'")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error getting completed entries: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_entries_by_type(self):
|
||||||
|
try:
|
||||||
|
cursor = self.db.cursor()
|
||||||
|
cursor.execute("SELECT type, COUNT(*) FROM anime GROUP BY type ORDER BY 2 DESC")
|
||||||
|
return cursor.fetchall() # Returns list of tuples: (type, count)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error getting entries by type: {e}")
|
||||||
|
return []
|
108
frontend.py
108
frontend.py
@@ -28,10 +28,20 @@ class AnimeDialog(QDialog):
|
|||||||
self.name_edit.setMaxLength(255) # Prevent overly long inputs
|
self.name_edit.setMaxLength(255) # Prevent overly long inputs
|
||||||
layout.addRow("Name", self.name_edit)
|
layout.addRow("Name", self.name_edit)
|
||||||
self.year_spin = QSpinBox()
|
self.year_spin = QSpinBox()
|
||||||
self.year_spin.setRange(1900, 2100)
|
self.year_spin.setRange(1960, 2073)
|
||||||
layout.addRow("Year", self.year_spin)
|
layout.addRow("Year", self.year_spin)
|
||||||
self.season_combo = QComboBox()
|
self.season_combo = QComboBox()
|
||||||
self.season_combo.addItems(['', 'winter', 'spring', 'summer', 'fall'])
|
self.season_combo.addItems(['Winter', 'Spring', 'Summer', 'Fall', 'Other'])
|
||||||
|
# Map display names to database values
|
||||||
|
self.season_map = {
|
||||||
|
'Winter': 'winter',
|
||||||
|
'Spring': 'spring',
|
||||||
|
'Summer': 'summer',
|
||||||
|
'Fall': 'fall',
|
||||||
|
'Other': ''
|
||||||
|
}
|
||||||
|
# Reverse map for setting current selection
|
||||||
|
self.reverse_season_map = {v: k for k, v in self.season_map.items()}
|
||||||
layout.addRow("Season", self.season_combo)
|
layout.addRow("Season", self.season_combo)
|
||||||
self.type_combo = QComboBox()
|
self.type_combo = QComboBox()
|
||||||
self.type_combo.addItems(['TV', 'Movie', 'OVA', 'Special', 'Short TV', 'Other'])
|
self.type_combo.addItems(['TV', 'Movie', 'OVA', 'Special', 'Short TV', 'Other'])
|
||||||
@@ -53,7 +63,8 @@ class AnimeDialog(QDialog):
|
|||||||
# Unescape for display in input fields
|
# Unescape for display in input fields
|
||||||
self.name_edit.setText(html.unescape(entry[1]))
|
self.name_edit.setText(html.unescape(entry[1]))
|
||||||
self.year_spin.setValue(entry[2])
|
self.year_spin.setValue(entry[2])
|
||||||
self.season_combo.setCurrentText(entry[3])
|
self.season_combo.setCurrentText((entry[3] or "Other").capitalize())
|
||||||
|
print("entry[3] = " + entry[3])
|
||||||
self.status_combo.setCurrentText(entry[4])
|
self.status_combo.setCurrentText(entry[4])
|
||||||
self.type_combo.setCurrentText(entry[5] or '')
|
self.type_combo.setCurrentText(entry[5] or '')
|
||||||
self.comment_edit.setPlainText(html.unescape(entry[6] or ''))
|
self.comment_edit.setPlainText(html.unescape(entry[6] or ''))
|
||||||
@@ -62,23 +73,28 @@ class AnimeDialog(QDialog):
|
|||||||
if default_year is not None:
|
if default_year is not None:
|
||||||
self.year_spin.setValue(default_year)
|
self.year_spin.setValue(default_year)
|
||||||
if default_season is not None:
|
if default_season is not None:
|
||||||
self.season_combo.setCurrentText(default_season)
|
# Map database season value to display name for default
|
||||||
|
display_season = self.reverse_season_map.get(default_season, 'Other')
|
||||||
self.year_spin.valueChanged.connect(self.update_season)
|
self.year_spin.valueChanged.connect(self.update_season)
|
||||||
self.update_season(self.year_spin.value())
|
self.update_season(self.year_spin.value())
|
||||||
|
|
||||||
def update_season(self, year):
|
def update_season(self, year):
|
||||||
if year < 2010:
|
if year < 2010:
|
||||||
self.season_combo.setEnabled(False)
|
self.season_combo.setEnabled(False)
|
||||||
self.season_combo.setCurrentText('')
|
self.season_combo.setCurrentText('Other')
|
||||||
else:
|
else:
|
||||||
self.season_combo.setEnabled(True)
|
self.season_combo.setEnabled(True)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
|
# Convert display season name back to database value
|
||||||
|
display_season = self.season_combo.currentText().strip()
|
||||||
|
db_season = self.season_map.get(display_season, '')
|
||||||
|
|
||||||
# Sanitize inputs by escaping special characters
|
# Sanitize inputs by escaping special characters
|
||||||
return {
|
return {
|
||||||
'name': html.escape(self.name_edit.text().strip()),
|
'name': html.escape(self.name_edit.text().strip()),
|
||||||
'year': self.year_spin.value(),
|
'year': self.year_spin.value(),
|
||||||
'season': html.escape(self.season_combo.currentText().strip()),
|
'season': html.escape(db_season), # Use mapped database value
|
||||||
'status': html.escape(self.status_combo.currentText().strip()),
|
'status': html.escape(self.status_combo.currentText().strip()),
|
||||||
'type': html.escape(self.type_combo.currentText().strip()),
|
'type': html.escape(self.type_combo.currentText().strip()),
|
||||||
'comment': html.escape(self.comment_edit.toPlainText().strip()),
|
'comment': html.escape(self.comment_edit.toPlainText().strip()),
|
||||||
@@ -183,6 +199,17 @@ class AnimeTracker(QMainWindow):
|
|||||||
self.set_current_tab_by_identifier(last_tab)
|
self.set_current_tab_by_identifier(last_tab)
|
||||||
self.tab_widget.setFocus()
|
self.tab_widget.setFocus()
|
||||||
|
|
||||||
|
def get_current_scroll_pos(self):
|
||||||
|
widget = self.tab_widget.currentWidget()
|
||||||
|
if widget:
|
||||||
|
return widget.verticalScrollBar().value()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def set_current_scroll_pos(self, pos):
|
||||||
|
widget = self.tab_widget.currentWidget()
|
||||||
|
if widget:
|
||||||
|
widget.verticalScrollBar().setValue(pos)
|
||||||
|
|
||||||
def filter_tables(self, text):
|
def filter_tables(self, text):
|
||||||
self.search_text = text.strip().lower()
|
self.search_text = text.strip().lower()
|
||||||
for table in self.tables:
|
for table in self.tables:
|
||||||
@@ -221,6 +248,9 @@ class AnimeTracker(QMainWindow):
|
|||||||
random_act = QAction('Random Pick', self)
|
random_act = QAction('Random Pick', self)
|
||||||
random_act.triggered.connect(self.random_pick)
|
random_act.triggered.connect(self.random_pick)
|
||||||
tools_menu.addAction(random_act)
|
tools_menu.addAction(random_act)
|
||||||
|
statistics_act = QAction('Statistics', self)
|
||||||
|
statistics_act.triggered.connect(self.show_statistics)
|
||||||
|
tools_menu.addAction(statistics_act)
|
||||||
help_menu = menubar.addMenu('Help')
|
help_menu = menubar.addMenu('Help')
|
||||||
shortcuts_act = QAction('Shortcuts', self)
|
shortcuts_act = QAction('Shortcuts', self)
|
||||||
shortcuts_act.triggered.connect(self.show_shortcuts)
|
shortcuts_act.triggered.connect(self.show_shortcuts)
|
||||||
@@ -625,9 +655,10 @@ class AnimeTracker(QMainWindow):
|
|||||||
parts = tab_text.split(' (')
|
parts = tab_text.split(' (')
|
||||||
default_year = int(parts[0])
|
default_year = int(parts[0])
|
||||||
default_season = ''
|
default_season = ''
|
||||||
|
current_id = self.get_current_tab_identifier()
|
||||||
|
current_scroll = self.get_current_scroll_pos()
|
||||||
dialog = AnimeDialog(self, None, default_year, self.last_used_season)
|
dialog = AnimeDialog(self, None, default_year, self.last_used_season)
|
||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
current_id = self.get_current_tab_identifier()
|
|
||||||
data = dialog.get_data()
|
data = dialog.get_data()
|
||||||
if data['season']:
|
if data['season']:
|
||||||
self.last_used_season = data['season']
|
self.last_used_season = data['season']
|
||||||
@@ -636,36 +667,50 @@ class AnimeTracker(QMainWindow):
|
|||||||
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
||||||
return
|
return
|
||||||
self.backend.add_anime(data)
|
self.backend.add_anime(data)
|
||||||
|
new_year = data['year']
|
||||||
|
new_id = "pre" if new_year < 2010 else new_year
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(new_id)
|
||||||
|
if self.get_current_tab_identifier() == current_id:
|
||||||
|
self.set_current_scroll_pos(current_scroll)
|
||||||
|
|
||||||
def edit_anime(self, anime_id):
|
def edit_anime(self, anime_id):
|
||||||
entry = self.backend.get_anime_by_id(anime_id)
|
entry = self.backend.get_anime_by_id(anime_id)
|
||||||
if entry:
|
if entry:
|
||||||
current_id = self.get_current_tab_identifier()
|
current_id = self.get_current_tab_identifier()
|
||||||
|
current_scroll = self.get_current_scroll_pos()
|
||||||
dialog = AnimeDialog(self, entry)
|
dialog = AnimeDialog(self, entry)
|
||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
current_id = self.get_current_tab_identifier()
|
|
||||||
data = dialog.get_data()
|
data = dialog.get_data()
|
||||||
if not data['name']:
|
if not data['name']:
|
||||||
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
||||||
return
|
return
|
||||||
self.backend.edit_anime(anime_id, data)
|
self.backend.edit_anime(anime_id, data)
|
||||||
|
new_year = data['year']
|
||||||
|
new_id = "pre" if new_year < 2010 else new_year
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(new_id)
|
||||||
|
if self.get_current_tab_identifier() == current_id:
|
||||||
|
self.set_current_scroll_pos(current_scroll)
|
||||||
|
|
||||||
def delete_anime(self, anime_id):
|
def delete_anime(self, anime_id):
|
||||||
if QMessageBox.question(self, "Confirm Delete", "Are you sure you want to delete this entry?") == QMessageBox.Yes:
|
if QMessageBox.question(self, "Confirm Delete", "Are you sure you want to delete this entry?") == QMessageBox.Yes:
|
||||||
current_id = self.get_current_tab_identifier()
|
current_id = self.get_current_tab_identifier()
|
||||||
|
current_scroll = self.get_current_scroll_pos()
|
||||||
self.backend.delete_anime(anime_id)
|
self.backend.delete_anime(anime_id)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
|
if self.get_current_tab_identifier() == current_id:
|
||||||
|
self.set_current_scroll_pos(current_scroll)
|
||||||
|
|
||||||
def change_status(self, anime_id, new_status):
|
def change_status(self, anime_id, new_status):
|
||||||
current_id = self.get_current_tab_identifier()
|
current_id = self.get_current_tab_identifier()
|
||||||
|
current_scroll = self.get_current_scroll_pos()
|
||||||
self.backend.change_status(anime_id, new_status)
|
self.backend.change_status(anime_id, new_status)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
|
if self.get_current_tab_identifier() == current_id:
|
||||||
|
self.set_current_scroll_pos(current_scroll)
|
||||||
|
|
||||||
def add_new_year(self):
|
def add_new_year(self):
|
||||||
current_year = datetime.now().year
|
current_year = datetime.now().year
|
||||||
@@ -686,8 +731,13 @@ class AnimeTracker(QMainWindow):
|
|||||||
QMessageBox.warning(self, "Error", f"Year {year} does not exist.")
|
QMessageBox.warning(self, "Error", f"Year {year} does not exist.")
|
||||||
return
|
return
|
||||||
if QMessageBox.question(self, "Confirm Delete", f"Are you sure you want to delete all entries for {year}?") == QMessageBox.Yes:
|
if QMessageBox.question(self, "Confirm Delete", f"Are you sure you want to delete all entries for {year}?") == QMessageBox.Yes:
|
||||||
|
current_id = self.get_current_tab_identifier()
|
||||||
|
current_scroll = self.get_current_scroll_pos()
|
||||||
self.backend.delete_year(year)
|
self.backend.delete_year(year)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
|
self.set_current_tab_by_identifier(current_id)
|
||||||
|
if self.get_current_tab_identifier() == current_id:
|
||||||
|
self.set_current_scroll_pos(current_scroll)
|
||||||
|
|
||||||
def random_pick(self):
|
def random_pick(self):
|
||||||
if self.tab_widget.currentIndex() == -1:
|
if self.tab_widget.currentIndex() == -1:
|
||||||
@@ -713,13 +763,51 @@ class AnimeTracker(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
QMessageBox.information(self, "Random Pick", "No unwatched anime in this tab.")
|
QMessageBox.information(self, "Random Pick", "No unwatched anime in this tab.")
|
||||||
|
|
||||||
|
def show_statistics(self):
|
||||||
|
dialog = QDialog(self)
|
||||||
|
dialog.setWindowTitle("Anime Statistics")
|
||||||
|
layout = QVBoxLayout(dialog)
|
||||||
|
|
||||||
|
total = self.backend.get_total_entries()
|
||||||
|
completed = self.backend.get_completed_entries()
|
||||||
|
percentage = (completed / total * 100) if total > 0 else 0
|
||||||
|
|
||||||
|
layout.addWidget(QLabel(f"Total anime entries: <b>{total}</b>"))
|
||||||
|
layout.addWidget(QLabel(f"Total completed entries: <b>{completed}</b>"))
|
||||||
|
layout.addWidget(QLabel(f"Percentage of completed entries: <b>{percentage:.2f}%</b>"))
|
||||||
|
|
||||||
|
layout.addWidget(QLabel("Number of anime entries by type:"))
|
||||||
|
types_data = self.backend.get_entries_by_type()
|
||||||
|
if types_data:
|
||||||
|
for typ, count in types_data:
|
||||||
|
display_type = typ if typ else "None" # Handle empty type as 'None'
|
||||||
|
layout.addWidget(QLabel(f" {display_type}: <b>{count}</b>"))
|
||||||
|
else:
|
||||||
|
layout.addWidget(QLabel(" No data available"))
|
||||||
|
|
||||||
|
db_path = 'anime_backlog.db'
|
||||||
|
if os.path.exists(db_path):
|
||||||
|
db_size_kb = os.path.getsize(db_path) / 1024
|
||||||
|
layout.addWidget(QLabel(f"Size of the database: <b>{db_size_kb:.2f} Kb</b>"))
|
||||||
|
else:
|
||||||
|
layout.addWidget(QLabel("Size of the database: N/A (database file not found)"))
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||||
|
buttons.accepted.connect(dialog.accept)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
def import_csv(self):
|
def import_csv(self):
|
||||||
file_name, _ = QFileDialog.getOpenFileName(self, "Import CSV", "", "CSV Files (*.csv)")
|
file_name, _ = QFileDialog.getOpenFileName(self, "Import CSV", "", "CSV Files (*.csv)")
|
||||||
if file_name:
|
if file_name:
|
||||||
current_id = self.get_current_tab_identifier()
|
current_id = self.get_current_tab_identifier()
|
||||||
|
current_scroll = self.get_current_scroll_pos()
|
||||||
self.backend.import_from_csv(file_name)
|
self.backend.import_from_csv(file_name)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
|
if self.get_current_tab_identifier() == current_id:
|
||||||
|
self.set_current_scroll_pos(current_scroll)
|
||||||
|
|
||||||
def export_csv(self):
|
def export_csv(self):
|
||||||
file_name, _ = QFileDialog.getSaveFileName(self, "Export CSV", "anime_backlog.csv", "CSV Files (*.csv)")
|
file_name, _ = QFileDialog.getSaveFileName(self, "Export CSV", "anime_backlog.csv", "CSV Files (*.csv)")
|
||||||
|
13
readme.md
13
readme.md
@@ -1,8 +1,17 @@
|
|||||||
# How to build this app:
|
## How to build this app:
|
||||||
`python -m venv anime_env`
|
`python -m venv anime_env`
|
||||||
|
|
||||||
`source anime_env/bin/activate`
|
`source anime_env/bin/activate`
|
||||||
|
|
||||||
`pip install pyinstaller`
|
`pip install pyinstaller`
|
||||||
|
|
||||||
`pyinstaller build.spec --clean`
|
`pyinstaller build.spec --clean`
|
||||||
|
|
||||||
# How to run this app without building:
|
Then run a binary:
|
||||||
|
`./dist/AnimeTracker/AnimeTracker`
|
||||||
|
|
||||||
|
## How to update the binary file
|
||||||
|
cp ~/Documents/programs/python/anime-tracker/dist/AnimeTracker/AnimeTracker ~/Applications/AnimeTracker/AnimeTracker
|
||||||
|
|
||||||
|
## How to run this app without building:
|
||||||
`python frontend.py`
|
`python frontend.py`
|
Reference in New Issue
Block a user