Compare commits
10 Commits
b5314497da
...
540b5a3194
Author | SHA1 | Date | |
---|---|---|---|
|
540b5a3194 | ||
|
1dd69955d4 | ||
|
6cad2d4f8a | ||
|
c091c3b699 | ||
|
3e8329e4b8 | ||
|
1741024090 | ||
|
8403e7b7c5 | ||
|
25df9c5a9a | ||
|
df9085f479 | ||
|
bee297bf19 |
6
.gitignore
vendored
@ -4,3 +4,9 @@ __pycache__/
|
|||||||
anime_tracker.log
|
anime_tracker.log
|
||||||
todo.txt
|
todo.txt
|
||||||
*.csv
|
*.csv
|
||||||
|
anime_backlog.db.bk
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
anime_env/
|
||||||
|
BIN
anime_backlog.db-shm
Normal file
BIN
anime_backlog.db-wal
Normal file
129
backend.py
@ -1,6 +1,7 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
|
import html
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR,
|
logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR,
|
||||||
@ -8,7 +9,8 @@ logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR,
|
|||||||
|
|
||||||
class AnimeBackend:
|
class AnimeBackend:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.db = sqlite3.connect('anime_backlog.db')
|
self.db = sqlite3.connect('anime_backlog.db', isolation_level=None) # Autocommit mode to prevent locks
|
||||||
|
self.db.execute('PRAGMA journal_mode=WAL') # Use WAL mode for better concurrency
|
||||||
self.create_table()
|
self.create_table()
|
||||||
|
|
||||||
def create_table(self):
|
def create_table(self):
|
||||||
@ -30,28 +32,10 @@ class AnimeBackend:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error creating table: {e}")
|
logging.error(f"Error creating table: {e}")
|
||||||
|
|
||||||
def get_pre_2010_entries(self, status_filter=None, type_filter=None, search=None, year_filter=None):
|
def get_pre_2010_entries(self):
|
||||||
try:
|
try:
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
sql = "SELECT * FROM anime WHERE year < 2010"
|
cursor.execute("SELECT * FROM anime WHERE year < 2010 ORDER BY year DESC, name")
|
||||||
add_where = []
|
|
||||||
params = []
|
|
||||||
if year_filter is not None:
|
|
||||||
add_where.append("year = ?")
|
|
||||||
params.append(year_filter)
|
|
||||||
if status_filter is not None:
|
|
||||||
add_where.append("status = ?")
|
|
||||||
params.append(status_filter)
|
|
||||||
if type_filter is not None:
|
|
||||||
add_where.append("type = ?")
|
|
||||||
params.append(type_filter)
|
|
||||||
if search is not None:
|
|
||||||
add_where.append("(name LIKE ? OR comment LIKE ?)")
|
|
||||||
params.append(f"%{search}%")
|
|
||||||
params.append(f"%{search}%")
|
|
||||||
where_clause = " AND " + " AND ".join(add_where) if add_where else ""
|
|
||||||
sql += where_clause + " ORDER BY year DESC, name"
|
|
||||||
cursor.execute(sql, params)
|
|
||||||
return cursor.fetchall()
|
return cursor.fetchall()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting pre-2010 entries: {e}")
|
logging.error(f"Error getting pre-2010 entries: {e}")
|
||||||
@ -66,25 +50,10 @@ class AnimeBackend:
|
|||||||
logging.error(f"Error getting years: {e}")
|
logging.error(f"Error getting years: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_entries_for_season(self, year, season, status_filter=None, type_filter=None, search=None):
|
def get_entries_for_season(self, year, season):
|
||||||
try:
|
try:
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
sql = "SELECT * FROM anime WHERE year = ? AND season = ?"
|
cursor.execute("SELECT * FROM anime WHERE year = ? AND season = ? ORDER BY name", (year, season))
|
||||||
params = [year, season]
|
|
||||||
add_where = []
|
|
||||||
if status_filter is not None:
|
|
||||||
add_where.append("status = ?")
|
|
||||||
params.append(status_filter)
|
|
||||||
if type_filter is not None:
|
|
||||||
add_where.append("type = ?")
|
|
||||||
params.append(type_filter)
|
|
||||||
if search is not None:
|
|
||||||
add_where.append("(name LIKE ? OR comment LIKE ?)")
|
|
||||||
params.append(f"%{search}%")
|
|
||||||
params.append(f"%{search}%")
|
|
||||||
where_clause = " AND " + " AND ".join(add_where) if add_where else ""
|
|
||||||
sql += where_clause + " ORDER BY name"
|
|
||||||
cursor.execute(sql, params)
|
|
||||||
return cursor.fetchall()
|
return cursor.fetchall()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting entries for season {season} in year {year}: {e}")
|
logging.error(f"Error getting entries for season {season} in year {year}: {e}")
|
||||||
@ -101,25 +70,64 @@ class AnimeBackend:
|
|||||||
|
|
||||||
def add_anime(self, data):
|
def add_anime(self, data):
|
||||||
try:
|
try:
|
||||||
|
# Sanitize string inputs
|
||||||
|
sanitized_data = {
|
||||||
|
'name': html.escape(data['name'].strip()) if data['name'] else '',
|
||||||
|
'year': data['year'],
|
||||||
|
'season': data['season'].strip() if data['season'] else '',
|
||||||
|
'status': data['status'].strip() if data['status'] else 'unwatched',
|
||||||
|
'type': data['type'].strip() if data['type'] else '',
|
||||||
|
'comment': html.escape(data['comment'].strip()) if data['comment'] else '',
|
||||||
|
'url': data['url'].strip() if data['url'] else ''
|
||||||
|
}
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO anime (name, year, season, status, type, comment, url) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO anime (name, year, season, status, type, comment, url) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
(data['name'], data['year'], data['season'], data['status'], data['type'], data['comment'], data['url'])
|
(
|
||||||
|
sanitized_data['name'],
|
||||||
|
sanitized_data['year'],
|
||||||
|
sanitized_data['season'],
|
||||||
|
sanitized_data['status'],
|
||||||
|
sanitized_data['type'],
|
||||||
|
sanitized_data['comment'],
|
||||||
|
sanitized_data['url']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error adding anime: {e}")
|
logging.error(f"Error adding anime: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def edit_anime(self, anime_id, data):
|
def edit_anime(self, anime_id, data):
|
||||||
try:
|
try:
|
||||||
|
# Sanitize string inputs
|
||||||
|
sanitized_data = {
|
||||||
|
'name': html.escape(data['name'].strip()) if data['name'] else '',
|
||||||
|
'year': data['year'],
|
||||||
|
'season': data['season'].strip() if data['season'] else '',
|
||||||
|
'status': data['status'].strip() if data['status'] else 'unwatched',
|
||||||
|
'type': data['type'].strip() if data['type'] else '',
|
||||||
|
'comment': html.escape(data['comment'].strip()) if data['comment'] else '',
|
||||||
|
'url': data['url'].strip() if data['url'] else ''
|
||||||
|
}
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE anime SET name=?, year=?, season=?, status=?, type=?, comment=?, url=? WHERE id=?",
|
"UPDATE anime SET name=?, year=?, season=?, status=?, type=?, comment=?, url=? WHERE id=?",
|
||||||
(data['name'], data['year'], data['season'], data['status'], data['type'], data['comment'], data['url'], anime_id)
|
(
|
||||||
|
sanitized_data['name'],
|
||||||
|
sanitized_data['year'],
|
||||||
|
sanitized_data['season'],
|
||||||
|
sanitized_data['status'],
|
||||||
|
sanitized_data['type'],
|
||||||
|
sanitized_data['comment'],
|
||||||
|
sanitized_data['url'],
|
||||||
|
anime_id
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error editing anime id {anime_id}: {e}")
|
logging.error(f"Error editing anime id {anime_id}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def delete_anime(self, anime_id):
|
def delete_anime(self, anime_id):
|
||||||
try:
|
try:
|
||||||
@ -128,6 +136,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deleting anime id {anime_id}: {e}")
|
logging.error(f"Error deleting anime id {anime_id}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def change_status(self, anime_id, new_status):
|
def change_status(self, anime_id, new_status):
|
||||||
try:
|
try:
|
||||||
@ -136,6 +145,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error changing status for anime id {anime_id}: {e}")
|
logging.error(f"Error changing status for anime id {anime_id}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def add_placeholders_for_year(self, year):
|
def add_placeholders_for_year(self, year):
|
||||||
try:
|
try:
|
||||||
@ -148,6 +158,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error adding placeholders for year {year}: {e}")
|
logging.error(f"Error adding placeholders for year {year}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def import_from_csv(self, file_name):
|
def import_from_csv(self, file_name):
|
||||||
try:
|
try:
|
||||||
@ -167,6 +178,13 @@ class AnimeBackend:
|
|||||||
year = int(year_str)
|
year = int(year_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
# Sanitize CSV inputs
|
||||||
|
name = html.escape(name.strip()) if name else ''
|
||||||
|
season = season.strip() if season else ''
|
||||||
|
status = status.strip() if status else 'unwatched'
|
||||||
|
type_ = type_.strip() if type_ else ''
|
||||||
|
comment = html.escape(comment.strip()) if comment else ''
|
||||||
|
url = url.strip() if url else ''
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT id FROM anime WHERE name = ? AND year = ? AND season = ?",
|
"SELECT id FROM anime WHERE name = ? AND year = ? AND season = ?",
|
||||||
(name, year, season)
|
(name, year, season)
|
||||||
@ -179,6 +197,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error importing from CSV {file_name}: {e}")
|
logging.error(f"Error importing from CSV {file_name}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def export_to_csv(self, file_name):
|
def export_to_csv(self, file_name):
|
||||||
try:
|
try:
|
||||||
@ -186,7 +205,7 @@ class AnimeBackend:
|
|||||||
cursor.execute("SELECT * FROM anime")
|
cursor.execute("SELECT * FROM anime")
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
with open(file_name, 'w', newline='') as f:
|
with open(file_name, 'w', newline='') as f:
|
||||||
writer = csv.writer(f)
|
writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
|
||||||
writer.writerow(['id', 'name', 'year', 'season', 'status', 'type', 'comment', 'url'])
|
writer.writerow(['id', 'name', 'year', 'season', 'status', 'type', 'comment', 'url'])
|
||||||
writer.writerows(rows)
|
writer.writerows(rows)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -199,3 +218,31 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
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()
|
||||||
|
|
||||||
|
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 []
|
55
build.spec
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['frontend.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[
|
||||||
|
('icons', 'icons'),
|
||||||
|
('anime_backlog.db', '.')
|
||||||
|
],
|
||||||
|
hiddenimports=[
|
||||||
|
'backend',
|
||||||
|
'sqlite3',
|
||||||
|
*collect_submodules('PyQt5')
|
||||||
|
],
|
||||||
|
hookspath=['.'],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='AnimeTracker',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_tracker=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon='icons/anime-app-icon.png'
|
||||||
|
)
|
||||||
|
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
name='AnimeTracker',
|
||||||
|
)
|
225
frontend.py
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
||||||
QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox,
|
QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox,
|
||||||
QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox,
|
QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox,
|
||||||
QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView, QPushButton
|
QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QSettings
|
from PyQt5.QtCore import Qt, QSettings
|
||||||
from PyQt5.QtGui import QColor, QIcon, QFont
|
from PyQt5.QtGui import QColor, QIcon, QFont
|
||||||
@ -25,6 +25,7 @@ class AnimeDialog(QDialog):
|
|||||||
self.setWindowTitle("Add Anime" if entry is None else "Edit Anime")
|
self.setWindowTitle("Add Anime" if entry is None else "Edit Anime")
|
||||||
layout = QFormLayout(self)
|
layout = QFormLayout(self)
|
||||||
self.name_edit = QLineEdit()
|
self.name_edit = QLineEdit()
|
||||||
|
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(1900, 2100)
|
||||||
@ -39,20 +40,23 @@ class AnimeDialog(QDialog):
|
|||||||
self.status_combo.addItems(['unwatched', 'watching', 'completed'])
|
self.status_combo.addItems(['unwatched', 'watching', 'completed'])
|
||||||
layout.addRow("Status", self.status_combo)
|
layout.addRow("Status", self.status_combo)
|
||||||
self.comment_edit = QTextEdit()
|
self.comment_edit = QTextEdit()
|
||||||
|
self.comment_edit.setAcceptRichText(False) # Prevent HTML injection
|
||||||
layout.addRow("Comment", self.comment_edit)
|
layout.addRow("Comment", self.comment_edit)
|
||||||
self.url_edit = QLineEdit()
|
self.url_edit = QLineEdit()
|
||||||
|
self.url_edit.setMaxLength(2048) # Reasonable limit for URLs
|
||||||
layout.addRow("MAL URL", self.url_edit)
|
layout.addRow("MAL URL", self.url_edit)
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addRow(buttons)
|
layout.addRow(buttons)
|
||||||
if entry:
|
if entry:
|
||||||
self.name_edit.setText(entry[1])
|
# Unescape for display in input fields
|
||||||
|
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])
|
||||||
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.setText(entry[6] or '')
|
self.comment_edit.setPlainText(html.unescape(entry[6] or ''))
|
||||||
self.url_edit.setText(entry[7] or '')
|
self.url_edit.setText(entry[7] or '')
|
||||||
else:
|
else:
|
||||||
if default_year is not None:
|
if default_year is not None:
|
||||||
@ -70,14 +74,15 @@ class AnimeDialog(QDialog):
|
|||||||
self.season_combo.setEnabled(True)
|
self.season_combo.setEnabled(True)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
|
# Sanitize inputs by escaping special characters
|
||||||
return {
|
return {
|
||||||
'name': self.name_edit.text(),
|
'name': html.escape(self.name_edit.text().strip()),
|
||||||
'year': self.year_spin.value(),
|
'year': self.year_spin.value(),
|
||||||
'season': self.season_combo.currentText(),
|
'season': html.escape(self.season_combo.currentText().strip()),
|
||||||
'status': self.status_combo.currentText(),
|
'status': html.escape(self.status_combo.currentText().strip()),
|
||||||
'type': self.type_combo.currentText(),
|
'type': html.escape(self.type_combo.currentText().strip()),
|
||||||
'comment': self.comment_edit.toPlainText(),
|
'comment': html.escape(self.comment_edit.toPlainText().strip()),
|
||||||
'url': self.url_edit.text()
|
'url': html.escape(self.url_edit.text().strip())
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShortcutsDialog(QDialog):
|
class ShortcutsDialog(QDialog):
|
||||||
@ -92,7 +97,7 @@ Global Shortcuts:
|
|||||||
- A: Add an anime
|
- A: Add an anime
|
||||||
- Page Down: Next tab
|
- Page Down: Next tab
|
||||||
- Page Up: Previous tab
|
- Page Up: Previous tab
|
||||||
- Q: Quit the program
|
- Ctrl + Q: Quit the program
|
||||||
- R: Pick random anime
|
- R: Pick random anime
|
||||||
- Ctrl + + : Increase table scale
|
- Ctrl + + : Increase table scale
|
||||||
- Ctrl + - : Decrease table scale
|
- Ctrl + - : Decrease table scale
|
||||||
@ -145,7 +150,12 @@ class AnimeTracker(QMainWindow):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.settings = QSettings("xAI", "AnimeBacklogTracker")
|
self.settings = QSettings("xAI", "AnimeBacklogTracker")
|
||||||
|
self.last_used_season = self.settings.value("lastUsedSeason", "winter")
|
||||||
self.setWindowTitle("Anime Backlog Tracker")
|
self.setWindowTitle("Anime Backlog Tracker")
|
||||||
|
# Add application icon
|
||||||
|
icon_path = os.path.join(os.path.dirname(__file__), './icons/anime-app-icon.png')
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
self.setWindowIcon(QIcon(icon_path))
|
||||||
self.resize(800, 600)
|
self.resize(800, 600)
|
||||||
self.backend = AnimeBackend()
|
self.backend = AnimeBackend()
|
||||||
self.tab_widget = QTabWidget()
|
self.tab_widget = QTabWidget()
|
||||||
@ -153,45 +163,18 @@ class AnimeTracker(QMainWindow):
|
|||||||
central_layout = QVBoxLayout(self.central_widget)
|
central_layout = QVBoxLayout(self.central_widget)
|
||||||
self.filter_bar = QWidget()
|
self.filter_bar = QWidget()
|
||||||
filter_layout = QHBoxLayout(self.filter_bar)
|
filter_layout = QHBoxLayout(self.filter_bar)
|
||||||
label_year = QLabel("Year:")
|
|
||||||
self.year_spin = QSpinBox()
|
|
||||||
self.year_spin.setRange(0, 2100)
|
|
||||||
self.year_spin.setValue(0)
|
|
||||||
self.year_spin.setSpecialValueText("All")
|
|
||||||
filter_layout.addWidget(label_year)
|
|
||||||
filter_layout.addWidget(self.year_spin)
|
|
||||||
label_season = QLabel("Season:")
|
|
||||||
self.season_combo = QComboBox()
|
|
||||||
self.season_combo.addItems(['All', 'winter', 'spring', 'summer', 'fall', 'Other'])
|
|
||||||
filter_layout.addWidget(label_season)
|
|
||||||
filter_layout.addWidget(self.season_combo)
|
|
||||||
label_status = QLabel("Status:")
|
|
||||||
self.status_combo = QComboBox()
|
|
||||||
self.status_combo.addItems(['All', 'unwatched', 'watching', 'completed'])
|
|
||||||
filter_layout.addWidget(label_status)
|
|
||||||
filter_layout.addWidget(self.status_combo)
|
|
||||||
label_type = QLabel("Type:")
|
|
||||||
self.type_combo = QComboBox()
|
|
||||||
self.type_combo.addItems(['All', 'TV', 'Movie', 'OVA', 'Special', 'Short TV', 'Other'])
|
|
||||||
filter_layout.addWidget(label_type)
|
|
||||||
filter_layout.addWidget(self.type_combo)
|
|
||||||
label_search = QLabel("Search:")
|
label_search = QLabel("Search:")
|
||||||
self.search_edit = QLineEdit()
|
self.search_edit = QLineEdit()
|
||||||
self.search_edit.setPlaceholderText("Search name or comment")
|
self.search_edit.setPlaceholderText("Search name")
|
||||||
|
self.search_edit.textChanged.connect(self.filter_tables)
|
||||||
filter_layout.addWidget(label_search)
|
filter_layout.addWidget(label_search)
|
||||||
filter_layout.addWidget(self.search_edit)
|
filter_layout.addWidget(self.search_edit)
|
||||||
apply_btn = QPushButton("Apply")
|
|
||||||
apply_btn.clicked.connect(self.apply_filters)
|
|
||||||
filter_layout.addWidget(apply_btn)
|
|
||||||
central_layout.addWidget(self.filter_bar)
|
central_layout.addWidget(self.filter_bar)
|
||||||
central_layout.addWidget(self.tab_widget)
|
central_layout.addWidget(self.tab_widget)
|
||||||
self.setCentralWidget(self.central_widget)
|
self.setCentralWidget(self.central_widget)
|
||||||
self.year_filter = None
|
self.search_text = ''
|
||||||
self.season_filter = None
|
|
||||||
self.status_filter = None
|
|
||||||
self.type_filter = None
|
|
||||||
self.search_text = None
|
|
||||||
self.table_scale = self.settings.value("tableScale", 1.0, type=float)
|
self.table_scale = self.settings.value("tableScale", 1.0, type=float)
|
||||||
|
self.tables = []
|
||||||
self.create_menu()
|
self.create_menu()
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.restoreGeometry(self.settings.value("geometry", self.saveGeometry()))
|
self.restoreGeometry(self.settings.value("geometry", self.saveGeometry()))
|
||||||
@ -200,18 +183,14 @@ 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 apply_filters(self):
|
def filter_tables(self, text):
|
||||||
year_val = self.year_spin.value()
|
self.search_text = text.strip().lower()
|
||||||
self.year_filter = year_val if year_val > 0 else None
|
for table in self.tables:
|
||||||
season_val = self.season_combo.currentText()
|
for row in range(table.rowCount()):
|
||||||
self.season_filter = None if season_val == 'All' else ('' if season_val == 'Other' else season_val)
|
name_col = 2 if table.is_pre else 1
|
||||||
status_val = self.status_combo.currentText()
|
name_text = table.cellWidget(row, name_col).text()
|
||||||
self.status_filter = None if status_val == 'All' else status_val
|
clean_name = re.sub(r'<[^>]+>', '', name_text).lower()
|
||||||
type_val = self.type_combo.currentText()
|
table.setRowHidden(row, self.search_text not in clean_name if self.search_text else False)
|
||||||
self.type_filter = None if type_val == 'All' else type_val
|
|
||||||
search_val = self.search_edit.text().strip()
|
|
||||||
self.search_text = search_val if search_val else None
|
|
||||||
self.load_tabs()
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
self.settings.setValue("geometry", self.saveGeometry())
|
self.settings.setValue("geometry", self.saveGeometry())
|
||||||
@ -242,6 +221,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)
|
||||||
@ -281,23 +263,22 @@ class AnimeTracker(QMainWindow):
|
|||||||
|
|
||||||
def load_tabs(self):
|
def load_tabs(self):
|
||||||
self.tab_widget.clear()
|
self.tab_widget.clear()
|
||||||
show_pre = True
|
self.tables = []
|
||||||
if self.season_filter is not None and self.season_filter != '':
|
# Pre-2010 tab
|
||||||
show_pre = False
|
pre_entries = self.backend.get_pre_2010_entries()
|
||||||
if self.year_filter is not None and self.year_filter >= 2010:
|
|
||||||
show_pre = False
|
|
||||||
if show_pre:
|
|
||||||
pre_entries = self.backend.get_pre_2010_entries(self.status_filter, self.type_filter, self.search_text, self.year_filter if self.year_filter and self.year_filter < 2010 else None)
|
|
||||||
pre_tab = QScrollArea()
|
pre_tab = QScrollArea()
|
||||||
pre_tab.setWidgetResizable(True)
|
pre_tab.setWidgetResizable(True)
|
||||||
pre_content = QWidget()
|
pre_content = QWidget()
|
||||||
pre_layout = QVBoxLayout(pre_content)
|
pre_layout = QVBoxLayout(pre_content)
|
||||||
if pre_entries:
|
if pre_entries:
|
||||||
table = CustomTableWidget(self, is_pre=True)
|
table = CustomTableWidget(self, is_pre=True)
|
||||||
|
table.is_pre = True
|
||||||
|
self.tables.append(table)
|
||||||
table.setRowCount(len(pre_entries))
|
table.setRowCount(len(pre_entries))
|
||||||
table.setColumnCount(7)
|
table.setColumnCount(7)
|
||||||
headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions']
|
headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions']
|
||||||
table.setHorizontalHeaderLabels(headers)
|
table.setHorizontalHeaderLabels(headers)
|
||||||
|
table.setColumnHidden(4, True) # Hide Status column
|
||||||
table.setColumnHidden(0, True)
|
table.setColumnHidden(0, True)
|
||||||
table.setAlternatingRowColors(True)
|
table.setAlternatingRowColors(True)
|
||||||
table.setShowGrid(True)
|
table.setShowGrid(True)
|
||||||
@ -322,6 +303,15 @@ class AnimeTracker(QMainWindow):
|
|||||||
table.setFont(font)
|
table.setFont(font)
|
||||||
table.horizontalHeader().setFont(header_font)
|
table.horizontalHeader().setFont(header_font)
|
||||||
table.verticalHeader().setFont(font)
|
table.verticalHeader().setFont(font)
|
||||||
|
# Add vertical header styling for index numbers
|
||||||
|
vheader = table.verticalHeader()
|
||||||
|
vheader.setDefaultAlignment(Qt.AlignCenter)
|
||||||
|
vheader.setStyleSheet("""
|
||||||
|
QHeaderView::section {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
for row, entry in enumerate(pre_entries):
|
for row, entry in enumerate(pre_entries):
|
||||||
col = 0
|
col = 0
|
||||||
# ID
|
# ID
|
||||||
@ -349,7 +339,6 @@ class AnimeTracker(QMainWindow):
|
|||||||
name_label.setOpenExternalLinks(True)
|
name_label.setOpenExternalLinks(True)
|
||||||
else:
|
else:
|
||||||
name_label.setText(html.escape(name))
|
name_label.setText(html.escape(name))
|
||||||
name_label.setStyleSheet("padding-left: 10px;")
|
|
||||||
table.setCellWidget(row, col, name_label)
|
table.setCellWidget(row, col, name_label)
|
||||||
col += 1
|
col += 1
|
||||||
# Type
|
# Type
|
||||||
@ -399,7 +388,7 @@ class AnimeTracker(QMainWindow):
|
|||||||
item.setBackground(color)
|
item.setBackground(color)
|
||||||
widget = table.cellWidget(r, c)
|
widget = table.cellWidget(r, c)
|
||||||
if widget:
|
if widget:
|
||||||
widget.setStyleSheet(f"background-color: {color.name()};")
|
widget.setStyleSheet(f"padding-left: 2px;background-color: {color.name()};")
|
||||||
pre_layout.addWidget(table)
|
pre_layout.addWidget(table)
|
||||||
pre_tab.setWidget(pre_content)
|
pre_tab.setWidget(pre_content)
|
||||||
tab_text = "Pre-2010"
|
tab_text = "Pre-2010"
|
||||||
@ -416,8 +405,6 @@ class AnimeTracker(QMainWindow):
|
|||||||
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
||||||
# Years >= 2010
|
# Years >= 2010
|
||||||
years = self.backend.get_years()
|
years = self.backend.get_years()
|
||||||
if self.year_filter is not None:
|
|
||||||
years = [self.year_filter] if self.year_filter in years else []
|
|
||||||
for year in years:
|
for year in years:
|
||||||
year_tab = QScrollArea()
|
year_tab = QScrollArea()
|
||||||
year_tab.setWidgetResizable(True)
|
year_tab.setWidgetResizable(True)
|
||||||
@ -426,25 +413,51 @@ class AnimeTracker(QMainWindow):
|
|||||||
total_entries = 0
|
total_entries = 0
|
||||||
comp_entries = 0
|
comp_entries = 0
|
||||||
for season in ['winter', 'spring', 'summer', 'fall', '']:
|
for season in ['winter', 'spring', 'summer', 'fall', '']:
|
||||||
show_section = True
|
entries = self.backend.get_entries_for_season(year, season)
|
||||||
if self.season_filter is not None:
|
# Database schema order:
|
||||||
if season != self.season_filter:
|
# 0: id
|
||||||
show_section = False
|
# 1: name
|
||||||
if show_section:
|
# 2: year
|
||||||
entries = self.backend.get_entries_for_season(year, season, self.status_filter, self.type_filter, self.search_text)
|
# 3: season
|
||||||
|
# 4: status
|
||||||
|
# 5: type
|
||||||
|
# 6: comment
|
||||||
|
# 7: url
|
||||||
if entries:
|
if entries:
|
||||||
|
# Season title
|
||||||
s_name = season.capitalize() if season else 'Other'
|
s_name = season.capitalize() if season else 'Other'
|
||||||
label = QLabel(s_name)
|
label = QLabel()
|
||||||
season_font = QFont()
|
season_font = QFont()
|
||||||
season_font.setPointSize(int(12 * self.table_scale))
|
season_font.setPointSize(int(12 * self.table_scale))
|
||||||
season_font.setBold(True)
|
season_font.setBold(True)
|
||||||
label.setFont(season_font)
|
label.setFont(season_font)
|
||||||
|
if season: # Only create links for actual seasons, not 'Other'
|
||||||
|
mal_url = f"https://myanimelist.net/anime/season/{year}/{season}"
|
||||||
|
label.setText(f'<a href="{mal_url}" style="color: #0000FF; text-decoration: none; font-weight: bold;">{s_name}</a>')
|
||||||
|
label.setTextFormat(Qt.RichText)
|
||||||
|
label.setOpenExternalLinks(True) # This makes the link clickable and open in browser
|
||||||
|
else:
|
||||||
|
label.setText(s_name) # No link for 'Other' section
|
||||||
|
|
||||||
layout.addWidget(label)
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
# Season's stats
|
||||||
|
total_anime = len(entries)
|
||||||
|
completed_anime = sum(1 for entry in entries if entry[4] == 'completed')
|
||||||
|
completion_percentage = (completed_anime / total_anime * 100) if total_anime > 0 else 0
|
||||||
|
s_stat = QLabel(f"Completed: <b>{completed_anime}/{total_anime} ({completion_percentage:.0f}%)</b>")
|
||||||
|
s_stat_font = QFont()
|
||||||
|
s_stat_font.setPointSize(int(10 * self.table_scale)) # Slightly smaller than season label
|
||||||
|
s_stat.setFont(s_stat_font)
|
||||||
|
layout.addWidget(s_stat)
|
||||||
table = CustomTableWidget(self, is_pre=False)
|
table = CustomTableWidget(self, is_pre=False)
|
||||||
|
table.is_pre = False
|
||||||
|
self.tables.append(table)
|
||||||
table.setRowCount(len(entries))
|
table.setRowCount(len(entries))
|
||||||
table.setColumnCount(6)
|
table.setColumnCount(6)
|
||||||
headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions']
|
headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions']
|
||||||
table.setHorizontalHeaderLabels(headers)
|
table.setHorizontalHeaderLabels(headers)
|
||||||
|
table.setColumnHidden(3, True) # Hide Status column
|
||||||
table.setColumnHidden(0, True)
|
table.setColumnHidden(0, True)
|
||||||
table.setAlternatingRowColors(True)
|
table.setAlternatingRowColors(True)
|
||||||
table.setShowGrid(True)
|
table.setShowGrid(True)
|
||||||
@ -468,6 +481,15 @@ class AnimeTracker(QMainWindow):
|
|||||||
table.setFont(font)
|
table.setFont(font)
|
||||||
table.horizontalHeader().setFont(header_font)
|
table.horizontalHeader().setFont(header_font)
|
||||||
table.verticalHeader().setFont(font)
|
table.verticalHeader().setFont(font)
|
||||||
|
# Add vertical header styling for index numbers
|
||||||
|
vheader = table.verticalHeader()
|
||||||
|
vheader.setDefaultAlignment(Qt.AlignCenter)
|
||||||
|
vheader.setStyleSheet("""
|
||||||
|
QHeaderView::section {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
for row, entry in enumerate(entries):
|
for row, entry in enumerate(entries):
|
||||||
col = 0
|
col = 0
|
||||||
# ID
|
# ID
|
||||||
@ -490,7 +512,6 @@ class AnimeTracker(QMainWindow):
|
|||||||
name_label.setOpenExternalLinks(True)
|
name_label.setOpenExternalLinks(True)
|
||||||
else:
|
else:
|
||||||
name_label.setText(html.escape(name))
|
name_label.setText(html.escape(name))
|
||||||
name_label.setStyleSheet("padding-left: 10px;")
|
|
||||||
table.setCellWidget(row, col, name_label)
|
table.setCellWidget(row, col, name_label)
|
||||||
col += 1
|
col += 1
|
||||||
# Type
|
# Type
|
||||||
@ -540,18 +561,19 @@ class AnimeTracker(QMainWindow):
|
|||||||
item.setBackground(color)
|
item.setBackground(color)
|
||||||
widget = table.cellWidget(r, c)
|
widget = table.cellWidget(r, c)
|
||||||
if widget:
|
if widget:
|
||||||
widget.setStyleSheet(f"background-color: {color.name()};")
|
widget.setStyleSheet(f"padding-left: 2px; background-color: {color.name()};")
|
||||||
layout.addWidget(table)
|
layout.addWidget(table)
|
||||||
total_entries += len(entries)
|
total_entries += len(entries)
|
||||||
comp_entries += sum(1 for e in entries if e[4] == 'completed')
|
comp_entries += sum(1 for e in entries if e[4] == 'completed')
|
||||||
year_tab.setWidget(content)
|
year_tab.setWidget(content)
|
||||||
if total_entries > 0 or (self.year_filter == year):
|
if total_entries > 0:
|
||||||
perc = (comp_entries / total_entries * 100) if total_entries > 0 else 0
|
perc = (comp_entries / total_entries * 100) if total_entries > 0 else 0
|
||||||
tab_text = f"{year} ({perc:.0f}%)"
|
tab_text = f"{year} ({perc:.0f}%)"
|
||||||
completed = (comp_entries == total_entries)
|
completed = (comp_entries == total_entries)
|
||||||
index = self.tab_widget.addTab(year_tab, tab_text)
|
index = self.tab_widget.addTab(year_tab, tab_text)
|
||||||
if completed:
|
if completed:
|
||||||
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
||||||
|
self.filter_tables(self.search_text) # Apply search filter after loading tabs
|
||||||
|
|
||||||
def create_actions_widget(self, anime_id, status):
|
def create_actions_widget(self, anime_id, status):
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
@ -606,10 +628,16 @@ 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 = ''
|
||||||
dialog = AnimeDialog(self, None, default_year, default_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()
|
current_id = self.get_current_tab_identifier()
|
||||||
data = dialog.get_data()
|
data = dialog.get_data()
|
||||||
|
if data['season']:
|
||||||
|
self.last_used_season = data['season']
|
||||||
|
self.settings.setValue("lastUsedSeason", self.last_used_season)
|
||||||
|
if not data['name']:
|
||||||
|
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
||||||
|
return
|
||||||
self.backend.add_anime(data)
|
self.backend.add_anime(data)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
@ -620,7 +648,11 @@ class AnimeTracker(QMainWindow):
|
|||||||
current_id = self.get_current_tab_identifier()
|
current_id = self.get_current_tab_identifier()
|
||||||
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']:
|
||||||
|
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
||||||
|
return
|
||||||
self.backend.edit_anime(anime_id, data)
|
self.backend.edit_anime(anime_id, data)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
@ -684,6 +716,41 @@ 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:
|
||||||
@ -711,10 +778,11 @@ class AnimeTracker(QMainWindow):
|
|||||||
current = self.tab_widget.currentIndex()
|
current = self.tab_widget.currentIndex()
|
||||||
if current > 0:
|
if current > 0:
|
||||||
self.tab_widget.setCurrentIndex(current - 1)
|
self.tab_widget.setCurrentIndex(current - 1)
|
||||||
elif key == Qt.Key_Q:
|
|
||||||
self.close()
|
|
||||||
elif key == Qt.Key_R:
|
elif key == Qt.Key_R:
|
||||||
self.random_pick()
|
self.random_pick()
|
||||||
|
elif key == Qt.Key_Q:
|
||||||
|
self.close()
|
||||||
|
|
||||||
elif modifiers == Qt.ControlModifier:
|
elif modifiers == Qt.ControlModifier:
|
||||||
if key == Qt.Key_Plus or key == Qt.Key_Equal:
|
if key == Qt.Key_Plus or key == Qt.Key_Equal:
|
||||||
current_id = self.get_current_tab_identifier()
|
current_id = self.get_current_tab_identifier()
|
||||||
@ -726,6 +794,9 @@ class AnimeTracker(QMainWindow):
|
|||||||
self.table_scale = max(self.table_scale - 0.1, 0.5)
|
self.table_scale = max(self.table_scale - 0.1, 0.5)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
|
elif key == Qt.Key_Q:
|
||||||
|
self.close()
|
||||||
|
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
BIN
icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 306 KiB |
BIN
icons/anime-app-icon.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 924 B |
BIN
icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
icons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
8
readme.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# How to build this app:
|
||||||
|
`python -m venv anime_env`
|
||||||
|
`source anime_env/bin/activate`
|
||||||
|
`pip install pyinstaller`
|
||||||
|
`pyinstaller build.spec --clean`
|
||||||
|
|
||||||
|
# How to run this app without building:
|
||||||
|
`python frontend.py`
|
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
PyQt5==5.15.10
|