diff --git a/src/model/card.py b/src/model/card.py index ba34567..8e2f599 100644 --- a/src/model/card.py +++ b/src/model/card.py @@ -1,8 +1,19 @@ import dataclasses +from PyQt5 import QtCore + @dataclasses.dataclass class Card: identifier: int question: str - answer: str + _answer: str + + def set_answer(self, answer: str) -> None: + self._answer = answer.replace(QtCore.QStandardPaths.writableLocation( + QtCore.QStandardPaths.AppDataLocation), '') + + def get_answer(self) -> str: + return self._answer.replace('', + QtCore.QStandardPaths.writableLocation( + QtCore.QStandardPaths.AppDataLocation)) diff --git a/src/model/deck.py b/src/model/deck.py index 2378919..9ed2271 100644 --- a/src/model/deck.py +++ b/src/model/deck.py @@ -4,6 +4,7 @@ import os import pickle import shutil +import tarfile from model import card import config @@ -24,6 +25,18 @@ def __init__(self, name: str) -> None: self._img_id = 0 os.makedirs(f'{config.DECKS_DIR}{name}/{config.IMG_DIR}') + @staticmethod + def load_deck(filepath: str) -> Deck: + with open(filepath, 'rb') as f: + d: Deck = pickle.load(f) + n = min((datetime.date.today() - d.last_study_day).days, + len(d._queues)-1) + for i in range(n): + d._queues[0].extend(d._queues[i+1]) + del d._queues[1:n+1] + d.last_study_day = datetime.date.today() + return d + def add_card(self, question: str, answer: str) -> None: self.cards.append(card.Card(self._current_id, question, answer)) self._cards_strengths[self._current_id] = 1 @@ -100,16 +113,9 @@ def dump(self) -> None: def delete(self) -> None: shutil.rmtree(f'{config.DECKS_DIR}{self.name}') - -def load_deck(filepath: str) -> Deck: - with open(filepath, 'rb') as f: - d: Deck = pickle.load(f) - n = min((datetime.date.today() - d.last_study_day).days, len(d._queues)-1) - for i in range(n): - d._queues[0].extend(d._queues[i+1]) - del d._queues[1:n+1] - d.last_study_day = datetime.date.today() - return d + def export(self, file_name: str) -> None: + with tarfile.open(file_name, 'w') as tar: + tar.add(f'{config.DECKS_DIR}{self.name}', self.name) class EmptyQueuesException(Exception): diff --git a/src/view/editor_widget.py b/src/view/editor_widget.py index 1e05d64..d70f085 100644 --- a/src/view/editor_widget.py +++ b/src/view/editor_widget.py @@ -112,10 +112,10 @@ def move_card(self, up: bool) -> None: def show_card(self) -> None: try: - self._deck.cards[self._old_select].question\ + self._deck.cards[self._old_select].question \ = self._question_edit.text() - self._deck.cards[self._old_select].answer\ - = self._answer_edit.toHtml() + self._deck.cards[self._old_select].set_answer( + self._answer_edit.toHtml()) self._cards_list.item(self._old_select).setText( self.question_prefix + self._deck.cards[self._old_select].question) @@ -135,11 +135,12 @@ def show_card(self) -> None: self._question_edit.setText( self._deck.cards[self._cards_list.currentRow()].question) self._answer_edit.setText( - self._deck.cards[self._cards_list.currentRow()].answer) + self._deck.cards[self._cards_list.currentRow()]._answer) self._old_select = self._cards_list.currentRow() def add_image(self) -> None: - file_name = QtWidgets.QFileDialog.getOpenFileName(parent=self.window(), + file_name = QtWidgets.QFileDialog.getOpenFileName( + parent=self.window(), filter='Images (*.png *.jpeg *.jpg)') try: path = self._deck.add_image( @@ -161,9 +162,9 @@ def disable(self) -> None: def exit(self) -> None: try: - self._deck.cards[self._old_select].question\ + self._deck.cards[self._old_select].question \ = self._question_edit.text() - self._deck.cards[self._old_select].answer\ + self._deck.cards[self._old_select]._answer \ = self._answer_edit.toHtml() except TypeError: pass diff --git a/src/view/home_widget.py b/src/view/home_widget.py index 0e41775..c58975f 100644 --- a/src/view/home_widget.py +++ b/src/view/home_widget.py @@ -1,4 +1,5 @@ import os +import tarfile from PyQt5 import QtWidgets, QtCore, QtGui @@ -29,11 +30,14 @@ def __init__(self, *args, **kwargs) -> None: outer_layout.addWidget(self._scroll_area) new_deck_btn = QtWidgets.QPushButton( - QtGui.QIcon(f'{config.ICONS_DIR}plus.png'), 'Create new deck') + QtGui.QIcon(f'{config.ICONS_DIR}plus.png'), 'Create new deck') new_deck_btn.released.connect(self.window().create_deck) outer_layout.addWidget(new_deck_btn) self.window().action_new_deck.setVisible(True) + self.window().action_delete_deck.setVisible(True) + self.window().action_import.setVisible(True) + self.window().action_export.setVisible(True) def populate_list(self) -> None: self._scroll_area_widget_contents = QtWidgets.QWidget() @@ -68,7 +72,8 @@ def populate_list(self) -> None: QtGui.QIcon(f'{config.ICONS_DIR}cross.png'), '') delete_btn.setFixedWidth(32) delete_btn.setToolTip('Delete deck') - delete_btn.released.connect(lambda d=deck_name: self.delete_deck(d)) + delete_btn.released.connect( + lambda d=deck_name: self.delete_deck(d)) horizontal_layout.addWidget(study_btn) horizontal_layout.addWidget(edit_btn) horizontal_layout.addWidget(delete_btn) @@ -76,12 +81,12 @@ def populate_list(self) -> None: self._scroll_area.setWidget(self._scroll_area_widget_contents) def edit_deck(self, name: str) -> None: - d = deck.load_deck(f'{config.DECKS_DIR}{name}/' - + config.DECK_FILE) + d = deck.Deck.load_deck(f'{config.DECKS_DIR}{name}/' + + config.DECK_FILE) editor_widget.EditorWidget(d, parent=self.window()) def study_deck(self, name: str) -> None: - study_widget.StudyWidget(deck.load_deck( + study_widget.StudyWidget(deck.Deck.load_deck( f'{config.DECKS_DIR}{name}/{config.DECK_FILE}'), parent=self.window()) @@ -90,15 +95,51 @@ def delete_deck(self, deck_name: str = None) -> None: dlg = SelectDeckDialog('delete', self) if dlg.exec(): deck_name = dlg.deck_name - if deck_name is not None: - dialog = QtWidgets.QMessageBox(parent=self.window()) - dialog.setText(f'Are you sure you want delete {deck_name} deck?') - dialog.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) - if dialog.exec() == QtWidgets.QMessageBox.Ok: - d = deck.load_deck( - f'{config.DECKS_DIR}{deck_name}/' - + config.DECK_FILE) - d.delete() + if deck_name is None or QtWidgets.QMessageBox.question( + self.window(), 'Delete deck', + f'Are you sure you want delete {repr(deck_name)} deck?') \ + == QtWidgets.QMessageBox.No: + return + d = deck.Deck.load_deck( + f'{config.DECKS_DIR}{deck_name}/{config.DECK_FILE}') + d.delete() + self.populate_list() + + def export_deck(self) -> None: + dialog = SelectDeckDialog('export', self) + if not dialog.exec(): + return + deck_name = dialog.deck_name + directory = QtWidgets.QFileDialog.getExistingDirectory(self.window()) + path = f'{directory}/{deck_name}.tar' + if os.access(path, os.F_OK) and QtWidgets.QMessageBox.question( + self.window(), 'File already exists', + f'A file called "{deck_name}.tar" already exists in the ' + + 'selected directory. Do you want to overwrite it?') \ + == QtWidgets.QMessageBox.No: + return + d = deck.Deck.load_deck( + f'{config.DECKS_DIR}{deck_name}/{config.DECK_FILE}') + d.export(path) + + def import_deck(self) -> None: + file_name = QtWidgets.QFileDialog.getOpenFileName( + parent=self.window(), filter='Archive (*.tar)')[0] + try: + with tarfile.open(file_name) as tar: + print(tar.getnames()[0]) + if os.access(f'{config.DECKS_DIR}{tar.getnames()[0]}', + os.F_OK) \ + and QtWidgets.QMessageBox.question( + self.window(), 'Deck already exists', + f'A deck called {repr(tar.getnames()[0])} already ' + + 'exists. Do you want to overwrite it?') \ + == QtWidgets.QMessageBox.No: + return + tar.extractall(config.DECKS_DIR) + except ValueError: + pass + else: self.populate_list() def exit(self) -> None: @@ -122,7 +163,8 @@ def __init__(self, action: str, *args, **kwargs) -> None: self._combo_box.addItem(deck_name) layout.addWidget(self._combo_box) - q_btn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + q_btn = QtWidgets.QDialogButtonBox.Ok \ + | QtWidgets.QDialogButtonBox.Cancel button_box = QtWidgets.QDialogButtonBox(q_btn, parent=self) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) diff --git a/src/view/main_window.py b/src/view/main_window.py index 62d7f3d..e361be0 100644 --- a/src/view/main_window.py +++ b/src/view/main_window.py @@ -1,3 +1,4 @@ +import os import re from PyQt5 import QtWidgets, QtGui, QtCore @@ -20,19 +21,15 @@ def __init__(self, *args, **kwargs) -> None: menubar = QtWidgets.QMenuBar(self) menu_file = QtWidgets.QMenu(menubar) - # menu_export_deck = QtWidgets.QMenu(menu_file) self.setMenuBar(menubar) self.action_new_deck = QtWidgets.QAction(self) action_quit = QtWidgets.QAction(self) - # action_import_deck = QtWidgets.QAction(self) - # action_local_export = QtWidgets.QAction(self) - # action_remote_export = QtWidgets.QAction(self) + self.action_import = QtWidgets.QAction(self) + self.action_export = QtWidgets.QAction(self) self.action_delete_deck = QtWidgets.QAction(self) - # menu_export_deck.addAction(action_local_export) - # menu_export_deck.addAction(action_remote_export) menu_file.addAction(self.action_new_deck) - # menu_file.addAction(action_import_deck) - # menu_file.addAction(menu_export_deck.menuAction()) + menu_file.addAction(self.action_export) + menu_file.addAction(self.action_import) menu_file.addAction(self.action_delete_deck) menu_file.addAction(action_quit) menubar.addAction(menu_file.menuAction()) @@ -43,8 +40,6 @@ def __init__(self, *args, **kwargs) -> None: menu_file.setTitle(QtCore.QCoreApplication.translate( 'Study and Repeat', 'File')) - # menu_export_deck.setTitle(QtCore.QCoreApplication.translate( - # 'Study and Repeat', 'Export deck')) # menu_help.setTitle(QtCore.QCoreApplication.translate( # 'Study and Repeat', 'Help')) self.action_new_deck.setText(QtCore.QCoreApplication.translate( @@ -53,12 +48,10 @@ def __init__(self, *args, **kwargs) -> None: # 'Study and Repeat', 'About')) action_quit.setText(QtCore.QCoreApplication.translate( 'Study and Repeat', 'Quit')) - # action_import_deck.setText(QtCore.QCoreApplication.translate( - # 'Study and Repeat', 'Import deck')) - # action_local_export.setText(QtCore.QCoreApplication.translate( - # 'Study and Repeat', 'Local export')) - # action_remote_export.setText(QtCore.QCoreApplication.translate( - # 'Study and Repeat', 'Remote export')) + self.action_import.setText(QtCore.QCoreApplication.translate( + 'Study and Repeat', 'Import deck')) + self.action_export.setText(QtCore.QCoreApplication.translate( + 'Study and Repeat', 'Export deck')) self.action_delete_deck.setText(QtCore.QCoreApplication.translate( 'Study and Repeat', 'Delete deck')) @@ -66,6 +59,10 @@ def __init__(self, *args, **kwargs) -> None: action_quit.triggered.connect(self.close) self.action_delete_deck.triggered.connect( lambda: self.centralWidget().delete_deck()) + self.action_export.triggered.connect( + lambda: self.centralWidget().export_deck()) + self.action_import.triggered.connect( + lambda: self.centralWidget().import_deck()) self.setCentralWidget(home_widget.HomeWidget(parent=self)) @@ -89,11 +86,17 @@ def __init__(self, *args, **kwargs) -> None: layout = QtWidgets.QVBoxLayout() self.setLayout(layout) - self._error_label = QtWidgets.QLabel('The only permitted characters ' - + 'are a-z, A-Z, 0-9 and _') - self._error_label.setStyleSheet("color: #ff0000") - layout.addWidget(self._error_label) - self._error_label.hide() + self._syntax_error_label = QtWidgets.QLabel( + 'The only permitted characters are a-z, A-Z, 0-9 and _') + self._syntax_error_label.setStyleSheet("color: #ff0000") + layout.addWidget(self._syntax_error_label) + self._syntax_error_label.hide() + + self._exist_error_label = QtWidgets.QLabel( + 'Two decks cannot have the same name') + self._exist_error_label.setStyleSheet("color: #ff0000") + layout.addWidget(self._exist_error_label) + self._exist_error_label.hide() self._line_edit = QtWidgets.QLineEdit(self) self._line_edit.setPlaceholderText('Insert deck name') @@ -108,7 +111,9 @@ def __init__(self, *args, **kwargs) -> None: def accept(self, *args, **kwargs) -> None: if re.fullmatch('[a-zA-Z0-9_]+', self._line_edit.text()) is None: - self._error_label.show() + self._syntax_error_label.show() + elif os.access(f'{config.DECKS_DIR}{self._line_edit.text()}', os.F_OK): + self._exist_error_label.show() else: super().accept() self.deck = deck.Deck(self._line_edit.text()) diff --git a/src/view/secondary_widget.py b/src/view/secondary_widget.py index e7e7b55..36447e2 100644 --- a/src/view/secondary_widget.py +++ b/src/view/secondary_widget.py @@ -23,15 +23,15 @@ def __init__(self, d: deck.Deck, *args, **kwargs) -> None: back_btn.released.connect(self.back_home) self._central_widget = QtWidgets.QWidget() self._layout.addWidget(self._central_widget) - + self.window().setCentralWidget(self) + self.window().action_new_deck.setVisible(False) self.window().action_delete_deck.setVisible(False) + self.window().action_import.setVisible(False) + self.window().action_export.setVisible(False) def back_home(self) -> None: - self.window().action_new_deck.setVisible(True) - self.window().action_delete_deck.setVisible(True) self.window().setCentralWidget(home_widget.HomeWidget( parent=self.window())) self.exit() - \ No newline at end of file diff --git a/src/view/study_widget.py b/src/view/study_widget.py index c503fde..977ebec 100644 --- a/src/view/study_widget.py +++ b/src/view/study_widget.py @@ -48,7 +48,7 @@ def show_question(self) -> None: parent=self.window())) else: self._question_text.setText(self._current_card.question) - self._answer_text.setText(self._current_card.answer) + self._answer_text.setText(self._current_card.get_answer()) def show_answer(self) -> None: self._show_btn.hide()