Compare commits

...

10 Commits

Author SHA1 Message Date
Lukas Brübach
b718c28dd5 Size things correctly.
Took 32 minutes

Took 10 seconds
2026-01-13 10:53:49 +01:00
Lukas Brübach
b20d0bcb4f Stash
Took 50 minutes

Took 2 minutes


Took 45 seconds
2026-01-13 10:53:38 +01:00
Lukas Brübach
2c2c2a639b tutorialize deck editors
Took 8 seconds

Took 12 minutes

Took 2 minutes
2026-01-13 10:53:36 +01:00
Lukas Brübach
f7fe686634 tutorialize home
Took 14 seconds

Took 2 minutes
2026-01-13 10:52:54 +01:00
Lukas Brübach
6d44b00260 Bubbling.
Took 16 seconds

# Commit time for manual adjustment:
# Took 8 seconds

Took 16 seconds

# Commit time for manual adjustment:
# Took 6 seconds

Took 9 seconds

# Commit time for manual adjustment:
# Took 8 seconds


Took 14 seconds
2026-01-13 10:52:54 +01:00
Lukas Brübach
fdb547c0e2 Improve with sequencing and better rendering.
Took 3 minutes


Took 18 seconds
2026-01-13 10:52:54 +01:00
Brübach, Lukas
fe61f5c7d4 [App] First-run tutorial
Took 14 minutes

Took 19 seconds
2026-01-13 10:52:54 +01:00
transifex-integration[bot]
52547bbfe8 Updates for project Cockatrice and language de (#6508)
* Translate oracle/oracle_en@source.ts in de

100% translated source file: 'oracle/oracle_en@source.ts'
on 'de'.

* Translate oracle/oracle_en@source.ts in de

100% translated source file: 'oracle/oracle_en@source.ts'
on 'de'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-11 14:41:16 +01:00
BruebachL
9ab398f08d [Doxygen] Add documentation for beta channel. (#6510)
* [Doxygen] Add documentation for beta channel.

Took 36 minutes

* [Doxygen] Use default theme.

Took 13 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-11 12:48:31 +01:00
RickyRister
0deaa9d9b4 [DeckEditor] Don't change widget focus when adding card (#6503) 2026-01-09 18:27:54 -08:00
22 changed files with 930 additions and 101 deletions

View File

@@ -197,6 +197,9 @@ set(cockatrice_SOURCES
src/interface/widgets/general/layout_containers/flow_widget.cpp
src/interface/widgets/general/layout_containers/overlap_control_widget.cpp
src/interface/widgets/general/layout_containers/overlap_widget.cpp
src/interface/widgets/general/tutorial/tutorial_bubble_widget.cpp
src/interface/widgets/general/tutorial/tutorial_controller.cpp
src/interface/widgets/general/tutorial/tutorial_overlay.cpp
src/interface/widgets/menus/deck_editor_menu.cpp
src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp
src/interface/widgets/printing_selector/card_amount_widget.cpp

View File

@@ -459,12 +459,15 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
}
}
void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex)
void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus)
{
deckView->clearSelection();
deckView->setCurrentIndex(newCardIndex);
recursiveExpand(newCardIndex);
deckView->setFocus(Qt::FocusReason::MouseFocusReason);
if (!preserveWidgetFocus) {
deckView->setFocus(Qt::FocusReason::MouseFocusReason);
}
}
void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()

View File

@@ -100,7 +100,7 @@ private slots:
void writeComments();
void writeBannerCard(int);
void applyActiveGroupCriteria();
void setSelectedIndex(const QModelIndex &newCardIndex);
void setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus);
void updateHash();
void refreshShortcuts();
void updateShowBannerCardComboBox(bool visible);

View File

@@ -182,7 +182,7 @@ QModelIndex DeckStateManager::addCard(const ExactCard &card, const QString &zone
QModelIndex idx = modifyDeck(reason, [&card, &zone](auto model) { return model->addCard(card, zone); });
if (idx.isValid()) {
emit focusIndexChanged(idx);
emit focusIndexChanged(idx, true);
}
return idx;
@@ -208,7 +208,7 @@ QModelIndex DeckStateManager::decrementCard(const ExactCard &card, const QString
}
if (idx.isValid()) {
emit focusIndexChanged(idx);
emit focusIndexChanged(idx, true);
}
return idx;

View File

@@ -290,8 +290,9 @@ signals:
/**
* The selected card on any views connected to this deck should be changed to this index.
* @param index The model index
* @param preserveWidgetFocus Whether to keep the widget focus unchanged
*/
void focusIndexChanged(QModelIndex index);
void focusIndexChanged(QModelIndex index, bool preserveWidgetFocus);
};
#endif // COCKATRICE_DECK_STATE_MANAGER_H

View File

@@ -5,6 +5,7 @@
#include "../../window_main.h"
#include "background_sources.h"
#include "home_styled_button.h"
#include "tutorial/tutorial_controller.h"
#include <QGroupBox>
#include <QPainter>
@@ -44,6 +45,30 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
&HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::homeTabBackgroundShuffleFrequencyChanged, this,
&HomeWidget::onBackgroundShuffleFrequencyChanged);
tutorialController = new TutorialController(this);
auto sequence = TutorialSequence();
sequence.addStep({connectButton, "Connect to a server to play here!"});
sequence.addStep({visualDeckEditorButton, "Create a new deck from cards in the database here!"});
sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."});
sequence.addStep({visualDatabaseDisplayButton, "View the card database here."});
sequence.addStep(
{edhrecButton, "Browse EDHRec, an external service designed to provide card recommendations for decks."});
sequence.addStep({archidektButton, "Browse Archidekt, an external service that allows users to store "
"decklists and import them to your local collection."});
sequence.addStep({replaybutton, "View replays of your past games here."});
sequence.addStep({exitButton, "Exit the application."});
tutorialController->addSequence(sequence);
}
void HomeWidget::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (!tutorialStarted) {
tutorialStarted = true;
// Start on next event loop iteration so everything is fully painted
QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); });
}
}
void HomeWidget::initializeBackgroundFromSource()
@@ -184,29 +209,29 @@ QGroupBox *HomeWidget::createButtons()
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
boxLayout->addWidget(connectButton);
auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
boxLayout->addWidget(visualDeckEditorButton);
auto visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
boxLayout->addWidget(visualDeckStorageButton);
auto visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
&TabSupervisor::addVisualDatabaseDisplayTab);
boxLayout->addWidget(visualDatabaseDisplayButton);
auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
boxLayout->addWidget(edhrecButton);
auto archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
boxLayout->addWidget(archidektButton);
auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
boxLayout->addWidget(replaybutton);
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
connect(exitButton, &QPushButton::clicked, qobject_cast<MainWindow *>(tabSupervisor->parentWidget()),
&MainWindow::actExit);
boxLayout->addWidget(exitButton);

View File

@@ -24,9 +24,18 @@ public:
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
void updateRandomCard();
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
HomeStyledButton *connectButton;
HomeStyledButton *visualDeckEditorButton;
HomeStyledButton *visualDeckStorageButton;
HomeStyledButton *visualDatabaseDisplayButton;
HomeStyledButton *edhrecButton;
HomeStyledButton *archidektButton;
HomeStyledButton *replaybutton;
HomeStyledButton *exitButton;
public slots:
void paintEvent(QPaintEvent *event) override;
void showEvent(QShowEvent *event) override;
void initializeBackgroundFromSource();
void onBackgroundShuffleFrequencyChanged();
void updateBackgroundProperties();
@@ -39,11 +48,12 @@ private:
QTimer *cardChangeTimer;
TabSupervisor *tabSupervisor;
QPixmap background;
TutorialController *tutorialController;
bool tutorialStarted = false;
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
DeckList backgroundSourceDeck;
QPixmap overlay;
QPair<QColor, QColor> gradientColors;
HomeStyledButton *connectButton;
void loadBackgroundSourceDeck();
};

View File

@@ -0,0 +1,39 @@
#include "tutorial_bubble_widget.h"
BubbleWidget::BubbleWidget(QWidget *parent) : QFrame(parent)
{
setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
setStyleSheet("QFrame { background:white; border-radius:8px; }"
"QLabel { color:black; }");
QGridLayout *layout = new QGridLayout(this);
layout->setContentsMargins(12, 10, 12, 10);
layout->setHorizontalSpacing(8);
layout->setVerticalSpacing(8);
counterLabel = new QLabel(this);
counterLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
textLabel = new QLabel(this);
textLabel->setWordWrap(true);
textLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
textLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
textLabel->setStyleSheet("color:black;"); // guard against global styles
// Layout
layout->addWidget(counterLabel, 0, 0, Qt::AlignLeft | Qt::AlignVCenter);
layout->addItem(new QSpacerItem(10, 10, QSizePolicy::Expanding, QSizePolicy::Minimum), 0, 1);
layout->addWidget(textLabel, 1, 0, 1, 3);
// Make column 1 take extra space so text gets room to expand/wrap
layout->setColumnStretch(1, 1);
// sensible default maximum width for bubble so text will wrap
setMaximumWidth(420);
}
void BubbleWidget::setText(const QString &text)
{
textLabel->setText(text);
update();
}

View File

@@ -0,0 +1,19 @@
#ifndef COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
#define COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
#include <QFrame>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
class BubbleWidget : public QFrame
{
Q_OBJECT
public:
QLabel *textLabel;
QLabel *counterLabel;
BubbleWidget(QWidget *parent);
void setText(const QString &text);
};
#endif // COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H

View File

@@ -0,0 +1,181 @@
#include "tutorial_controller.h"
#include <QTimer>
TutorialController::TutorialController(QWidget *_tutorializedWidget)
: QObject(_tutorializedWidget), tutorializedWidget(_tutorializedWidget)
{
tutorialOverlay = new TutorialOverlay(tutorializedWidget->window());
// Make it frameless + translucent
tutorialOverlay->setWindowFlags(tutorialOverlay->windowFlags() | Qt::FramelessWindowHint);
tutorialOverlay->setAttribute(Qt::WA_TranslucentBackground);
// hide until start
tutorialOverlay->hide();
connect(tutorialOverlay, &TutorialOverlay::nextStep, this, &TutorialController::nextStep);
connect(tutorialOverlay, &TutorialOverlay::prevStep, this, &TutorialController::prevStep);
connect(tutorialOverlay, &TutorialOverlay::nextSequence, this, &TutorialController::nextSequence);
connect(tutorialOverlay, &TutorialOverlay::prevSequence, this, &TutorialController::prevSequence);
connect(tutorialOverlay, &TutorialOverlay::skipTutorial, this, &TutorialController::exitTutorial);
}
void TutorialController::addSequence(const TutorialSequence &seq)
{
sequences.append(seq);
}
void TutorialController::start()
{
if (sequences.isEmpty()) {
return;
}
QTimer::singleShot(0, this, [this]() {
QWidget *win = tutorializedWidget->window();
tutorialOverlay->parentResized();
tutorialOverlay->setGeometry(QRect(QPoint(0, 0), win->size()));
tutorialOverlay->show();
tutorialOverlay->raise();
tutorialOverlay->parentResized();
currentSequence = 0;
currentStep = 0;
showStep();
});
}
void TutorialController::nextStep()
{
// advance within sequence
currentStep++;
if (currentSequence < 0) {
return; // defensive in case we haven't started yet
}
if (currentStep >= sequences[currentSequence].steps.size()) {
// advance to next sequence
nextSequence();
return;
}
showStep();
}
void TutorialController::prevStep()
{
if (currentSequence < 0) {
return; // defensive in case we haven't started yet
}
if (currentStep == 0) {
prevSequence();
return;
}
currentStep--;
showStep();
}
void TutorialController::nextSequence()
{
if (currentSequence < 0) {
return;
}
// run exit for the last step of the current sequence (showStep handles previous onExit,
// but ensure we run it here because we're jumping sequence)
// We'll increment sequence and then call showStep which will call the onEnter for the new step.
currentSequence++;
currentStep = 0;
if (currentSequence >= sequences.size()) {
exitTutorial();
return;
}
showStep();
}
void TutorialController::prevSequence()
{
if (currentSequence <= 0) {
// already at first sequence -> stay
currentStep = 0;
showStep();
return;
}
currentSequence--;
currentStep = 0;
showStep();
}
void TutorialController::exitTutorial()
{
// Run onExit for the current step if present
if (currentSequence >= 0 && currentStep >= 0 && currentSequence < sequences.size() &&
currentStep < sequences[currentSequence].steps.size()) {
const auto &curStep = sequences[currentSequence].steps[currentStep];
if (curStep.onExit) {
curStep.onExit();
}
}
tutorialOverlay->hide();
// reset indices so start() can be called again cleanly
currentSequence = -1;
currentStep = -1;
}
void TutorialController::showStep()
{
// bounds checks
if (currentSequence < 0 || currentSequence >= sequences.size()) {
return;
}
const auto &seq = sequences[currentSequence];
if (currentStep < 0 || currentStep >= seq.steps.size()) {
return;
}
// run onExit for the previous step (including if previous step was in previous sequence)
if (!(currentSequence == 0 && currentStep == 0)) {
int prevSeq = currentSequence;
int prevStepIndex = currentStep - 1;
if (prevStepIndex < 0) {
// previous is last step of previous sequence
prevSeq = currentSequence - 1;
if (prevSeq >= 0) {
prevStepIndex = sequences[prevSeq].steps.size() - 1;
} else {
prevStepIndex = -1;
}
}
if (prevSeq >= 0 && prevStepIndex >= 0) {
const auto &previousStep = sequences[prevSeq].steps[prevStepIndex];
if (previousStep.onExit) {
previousStep.onExit();
}
}
}
// current step
const auto &step = seq.steps[currentStep];
// Run any action associated with this step
if (step.onEnter) {
step.onEnter();
}
tutorialOverlay->setTargetWidget(step.targetWidget);
tutorialOverlay->setText(step.text);
tutorialOverlay->parentResized();
tutorialOverlay->raise();
tutorialOverlay->update();
}

View File

@@ -0,0 +1,57 @@
#ifndef COCKATRICE_TUTORIAL_CONTROLLER_H
#define COCKATRICE_TUTORIAL_CONTROLLER_H
#include "tutorial_overlay.h"
#include <QObject>
#include <QVector>
#include <functional>
struct TutorialStep
{
QWidget *targetWidget;
QString text;
std::function<void()> onEnter = nullptr; // Optional function to run when this step starts
std::function<void()> onExit = nullptr; // Optional function to run when step ends
};
struct TutorialSequence
{
QString name;
QVector<TutorialStep> steps;
void addStep(const TutorialStep &step)
{
steps.append(step);
}
};
class TutorialController : public QObject
{
Q_OBJECT
public slots:
void start();
void nextStep();
void prevStep();
void nextSequence();
void prevSequence();
void exitTutorial();
public:
explicit TutorialController(QWidget *_tutorializedWidget);
void addSequence(const TutorialSequence &step);
private:
QWidget *tutorializedWidget;
QVector<TutorialSequence> sequences;
int currentSequence = -1;
int currentStep = -1;
TutorialOverlay *tutorialOverlay;
void showStep();
};
#endif // COCKATRICE_TUTORIAL_CONTROLLER_H

View File

@@ -0,0 +1,261 @@
#include "tutorial_overlay.h"
#include "tutorial_bubble_widget.h"
#include <QEvent>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QResizeEvent>
TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent, Qt::Window)
{
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_TransparentForMouseEvents, false);
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint);
if (parent) {
parent->installEventFilter(this);
parentResized();
}
// ---- Control bar -------------------------------------------------
controlBar = new QFrame(this);
controlBar->setStyleSheet(
"QFrame { background: rgba(30,30,30,200); border-radius: 6px; }"
"QPushButton { padding: 6px 10px; border: 1px solid #aaa; border-radius: 4px; background:#f5f5f5; }"
"QPushButton:hover { background:#eaeaea; }");
QHBoxLayout *barLayout = new QHBoxLayout(controlBar);
barLayout->setContentsMargins(8, 4, 8, 4);
titleLabel = new QLabel("Tutorial", controlBar);
titleLabel->setStyleSheet("color:white; font-weight:bold;");
barLayout->addWidget(titleLabel);
barLayout->addStretch();
auto mkBtn = [&](const QString &t, const QString &tip) {
QPushButton *b = new QPushButton(t, controlBar);
b->setToolTip(tip);
return b;
};
QPushButton *prevSeq = mkBtn("", "Previous chapter");
QPushButton *prev = mkBtn("", "Previous step");
QPushButton *next = mkBtn("", "Next step");
QPushButton *nextSeq = mkBtn("", "Next chapter");
QPushButton *close = mkBtn("", "Exit tutorial");
barLayout->addWidget(prevSeq);
barLayout->addWidget(prev);
barLayout->addWidget(next);
barLayout->addWidget(nextSeq);
barLayout->addWidget(close);
connect(prev, &QPushButton::clicked, this, &TutorialOverlay::prevStep);
connect(next, &QPushButton::clicked, this, &TutorialOverlay::nextStep);
connect(prevSeq, &QPushButton::clicked, this, &TutorialOverlay::prevSequence);
connect(nextSeq, &QPushButton::clicked, this, &TutorialOverlay::nextSequence);
connect(close, &QPushButton::clicked, this, &TutorialOverlay::skipTutorial);
// ---- Bubble ------------------------------------------------------
bubble = new BubbleWidget(this);
bubble->hide();
controlBar->hide();
}
void TutorialOverlay::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
// Queue geometry correction after the window is fully shown
QMetaObject::invokeMethod(this, [this]() { parentResized(); }, Qt::QueuedConnection);
}
void TutorialOverlay::setTitle(const QString &title)
{
titleLabel->setText(title);
}
void TutorialOverlay::setBlocking(bool block)
{
blockInput = block;
setAttribute(Qt::WA_TransparentForMouseEvents, !blockInput);
}
void TutorialOverlay::setTargetWidget(QWidget *w)
{
if (targetWidget)
targetWidget->removeEventFilter(this);
targetWidget = w;
if (targetWidget)
targetWidget->installEventFilter(this);
recomputeLayout();
}
void TutorialOverlay::setText(const QString &t)
{
tutorialText = t;
bubble->setText(t);
bubble->adjustSize();
recomputeLayout();
}
QRect TutorialOverlay::currentHoleRect() const
{
if (!targetWidget || !targetWidget->isVisible())
return QRect();
QPoint globalTopLeft = targetWidget->mapToGlobal(QPoint(0, 0));
QPoint localTopLeft = mapFromGlobal(globalTopLeft);
return QRect(localTopLeft, targetWidget->size()).adjusted(-6, -6, 6, 6);
}
void TutorialOverlay::resizeEvent(QResizeEvent *)
{
recomputeLayout();
}
bool TutorialOverlay::eventFilter(QObject *obj, QEvent *event)
{
if (obj == parentWidget() && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) {
parentResized();
}
if (obj == targetWidget) {
if (event->type() == QEvent::Show) {
// Defer layout recalculation to give Qt time to finalize geometry
QMetaObject::invokeMethod(this, [this]() { recomputeLayout(); }, Qt::QueuedConnection);
} else if (event->type() == QEvent::Hide || event->type() == QEvent::Move || event->type() == QEvent::Resize) {
recomputeLayout();
}
}
return QWidget::eventFilter(obj, event);
}
void TutorialOverlay::parentResized()
{
if (!parentWidget())
return;
QWidget *w = parentWidget();
// Get the parent rect in local coordinates
QRect r = w->rect();
// Map top-left to global coordinates
QPoint globalTopLeft = w->mapToGlobal(QPoint(0, 0));
// Set overlay geometry in screen coords, exactly matching the parent widget
r.moveTopLeft(globalTopLeft);
setGeometry(r);
recomputeLayout();
}
void TutorialOverlay::recomputeLayout()
{
QRect hole = currentHoleRect();
if (hole.isEmpty()) {
if (bubble) {
bubble->hide();
}
if (controlBar) {
controlBar->hide();
}
hide();
return;
}
show();
raise();
// ---- Bubble ----
QSize bsize = bubble->sizeHint().expandedTo(QSize(160, 60));
highlightBubbleRect = computeBubbleRect(hole, bsize);
bubble->setGeometry(highlightBubbleRect);
bubble->show();
bubble->raise();
// ---- Control bar ----
controlBar->adjustSize();
controlBar->show();
const int margin = 8;
QRect r = rect();
QList<QPoint> positions = {{r.right() - controlBar->width() - margin, r.bottom() - controlBar->height() - margin},
{r.right() - controlBar->width() - margin, margin},
{margin, r.bottom() - controlBar->height() - margin},
{margin, margin}};
for (const QPoint &pos : positions) {
QRect proposed(pos, controlBar->size());
if (!proposed.intersects(hole)) {
controlBar->move(pos);
break;
}
}
controlBar->raise();
update();
}
QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const
{
const int margin = 16;
QRect r = rect();
QRect bubble;
if (hole.isEmpty()) {
bubble = QRect(r.center() - QPoint(bubbleSize.width() / 2, bubbleSize.height() / 2), bubbleSize);
} else {
bubble = QRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height());
if (!r.contains(bubble))
bubble.moveLeft(hole.left() - margin - bubbleSize.width());
if (!r.contains(bubble)) {
bubble.moveLeft(hole.center().x() - bubbleSize.width() / 2);
bubble.moveTop(hole.top() - margin - bubbleSize.height());
}
if (!r.contains(bubble))
bubble.moveTop(hole.bottom() + margin);
}
// Final clamp to overlay bounds - ensure min <= max for qBound
int maxLeft = qMax(r.left(), r.right() - bubble.width());
int maxTop = qMax(r.top(), r.bottom() - bubble.height());
bubble.moveLeft(qBound(r.left(), bubble.left(), maxLeft));
bubble.moveTop(qBound(r.top(), bubble.top(), maxTop));
return bubble;
}
void TutorialOverlay::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
p.fillRect(rect(), QColor(0, 0, 0, 160));
QRect hole = currentHoleRect();
if (!hole.isEmpty()) {
QPainterPath holePath;
holePath.addRoundedRect(hole, 8, 8);
p.setCompositionMode(QPainter::CompositionMode_Clear);
p.fillPath(holePath, Qt::transparent);
}
}

View File

@@ -0,0 +1,59 @@
#ifndef COCKATRICE_TUTORIAL_OVERLAY_H
#define COCKATRICE_TUTORIAL_OVERLAY_H
#include <QWidget>
class BubbleWidget;
class QLabel;
class QFrame;
class TutorialOverlay : public QWidget
{
Q_OBJECT
public slots:
void showEvent(QShowEvent *event) override;
public:
explicit TutorialOverlay(QWidget *parent = nullptr);
void setTargetWidget(QWidget *w);
void updateHoleRect();
void setText(const QString &t);
QRect currentHoleRect() const;
void setTitle(const QString &title);
void setBlocking(bool blockInput);
void parentResized();
signals:
void nextStep();
void prevStep();
void nextSequence();
void prevSequence();
void skipTutorial();
protected:
void paintEvent(QPaintEvent *) override;
void resizeEvent(QResizeEvent *) override;
bool eventFilter(QObject *obj, QEvent *event) override;
private:
QRect computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const;
void recomputeLayout();
QWidget *targetWidget = nullptr;
BubbleWidget *bubble = nullptr;
QFrame *controlBar = nullptr;
QLabel *titleLabel = nullptr;
QString tutorialText;
QRect cachedHoleRect;
QRect highlightBubbleRect;
bool blockInput = true;
};
#endif // COCKATRICE_TUTORIAL_OVERLAY_H

View File

@@ -198,7 +198,7 @@ void CardAmountWidget::addPrinting(const QString &zone)
});
if (newCardIndex.isValid()) {
emit deckStateManager->focusIndexChanged(newCardIndex);
emit deckStateManager->focusIndexChanged(newCardIndex, false);
}
}

View File

@@ -6,6 +6,7 @@
#include "../../interface/pixel_map_generator.h"
#include "../../interface/widgets/cards/card_info_frame_widget.h"
#include "../../interface/widgets/deck_analytics/deck_analytics_widget.h"
#include "../../interface/widgets/general/tutorial/tutorial_controller.h"
#include "../../interface/widgets/visual_deck_editor/visual_deck_editor_widget.h"
#include "../tab_deck_editor.h"
#include "../tab_supervisor.h"
@@ -50,7 +51,31 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra
refreshShortcuts();
loadLayout();
cardDatabaseDockWidget->setHidden(true);
cardDatabaseDockWidget->setHidden(true); tutorialController = new TutorialController(this);
auto sequence = TutorialSequence();
sequence.addStep({tabContainer->visualDeckView, "View your deck here.",
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDeckView); }});
sequence.addStep({printingSelectorDockWidget, "Change the printings in your deck here."});
tutorialController->addSequence(sequence);
auto vddSequence = tabContainer->visualDatabaseDisplay->addTutorialSteps();
vddSequence.steps.prepend({tabContainer->visualDatabaseDisplay, "View the database here",
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDatabaseDisplay); }});
tutorialController->addSequence(vddSequence);
}
void TabDeckEditorVisual::showEvent(QShowEvent *ev)
{
QWidget::showEvent(ev);
if (!tutorialStarted) {
tutorialStarted = true;
// Start on next event loop iteration so everything is fully painted
QTimer::singleShot(0, tutorialController, [this] { tutorialController->start(); });
}
}
/** @brief Creates the central frame containing the tab container. */

View File

@@ -4,6 +4,7 @@
#include "../tab.h"
#include "tab_deck_editor_visual_tab_widget.h"
class TutorialController;
/**
* @class TabDeckEditorVisual
* @ingroup DeckEditorTabs
@@ -55,7 +56,12 @@ class TabDeckEditorVisual : public AbstractTabDeckEditor
{
Q_OBJECT
private:
TutorialController *tutorialController = nullptr;
bool tutorialStarted = false;
protected slots:
void showEvent(QShowEvent *ev) override;
/**
* @brief Load the editor layout from settings.
*/

View File

@@ -5,6 +5,7 @@
#include "../../../filters/syntax_help.h"
#include "../../pixel_map_generator.h"
#include "../cards/card_info_picture_with_text_overlay_widget.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../quick_settings/settings_button_widget.h"
#include "../utility/custom_line_edit.h"
#include "visual_database_display_color_filter_widget.h"
@@ -194,6 +195,28 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
retranslateUi();
}
TutorialSequence VisualDatabaseDisplayWidget::addTutorialSteps()
{
auto sequence = TutorialSequence();
sequence.addStep({colorFilterWidget, "Filter the database by colors with these controls"});
sequence.addStep({displayModeButton, "You can change back to the old table display-style with this button."});
sequence.addStep({filterContainer, "Use these controls for quick access to common filters."});
sequence.addStep(
{quickFilterSaveLoadWidget, "This button will let you save and load all currently applied filters to files."});
sequence.addStep({quickFilterNameWidget,
"This button will let you apply name filters. Optionally, you can import every card in "
"your deck as a name filter and then save this as a filter using the save/load button "
"to make your own quick access collections!"});
sequence.addStep({mainTypeFilterWidget, "Use these buttons to quickly filter by card types."});
sequence.addStep({quickFilterSubTypeWidget, "This button will let you apply filters for card sub-types."});
sequence.addStep(
{quickFilterSetWidget,
"This button will let you apply filters for card sets. You can also filter to the X most recent sets. "
"Filtering to a set will display all printings of a card within that set."});
return sequence;
}
void VisualDatabaseDisplayWidget::initialize()
{
databaseLoadIndicator->setVisible(false);

View File

@@ -14,6 +14,7 @@
#include "../cards/card_size_widget.h"
#include "../general/layout_containers/flow_widget.h"
#include "../general/layout_containers/overlap_control_widget.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../utility/custom_line_edit.h"
#include "visual_database_display_color_filter_widget.h"
#include "visual_database_display_filter_save_load_widget.h"
@@ -31,6 +32,7 @@
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <qscrollarea.h>
class TutorialController;
inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display");
class VisualDatabaseDisplayWidget : public QWidget
@@ -60,6 +62,7 @@ public:
VisualDatabaseDisplayColorFilterWidget *colorFilterWidget;
public slots:
TutorialSequence addTutorialSteps();
void searchModelChanged();
signals:

View File

@@ -1,9 +1,17 @@
@page user_reference User Reference
- @subpage search_syntax_help
- @subpage deck_search_syntax_help
# Deck Management
- @subpage creating_decks
- @subpage importing_decks
- @subpage editing_decks
- @subpage exporting_decks
# Release Channels
- @subpage beta_release
# Syntax Help
- @subpage search_syntax_help
- @subpage deck_search_syntax_help

View File

@@ -0,0 +1,54 @@
@page beta_release Beta Release
The beta version of Cockatrice features all the newest features before they make their way into the latest stable
release.
It allows us to test these features ahead of time and you to give feedback on them.
We release new betas very frequently and usually release a follow-up beta with a fix within a day or two if we break
anything. In contrast, stable releases are released much less frequently but ensured to be stable.
# What to Expect from the Beta
Using the beta and giving feedback on it helps us improve Cockatrice a lot.
We do not perform any automatic data collection so your voluntary feedback is the only way for us to get a sense of what
you enjoy, expect, or would like to change.
We highly appreciate your active participation and feedback.
That being said, some words of warning should be issued:
The beta release may contain unfinished features, bugs, or performance issues.
It is intended for testing and feedback and may be less stable than the default release.
We recommend using the beta only if you are comfortable encountering issues and helping us improve Cockatrice.
# Switching to the Beta
To switch to the Beta, navigate to your settings, using the Tab "Cockatrice → Settings" or Ctrl + Shift + P (default
shortcut).
Within the settings, in the first tab 'General', the first section 'Personal Settings' contains the setting 'Release
Channel'.
\image html release_channel_beta.png
Switch this to 'Beta'.
Afterwards, close your settings, and use the 'Help' tab to search for a client update. You should be prompted to install
the beta version.
# Switching back to Stable
Follow the same steps as above, but set Release Channel to 'Default'.
# Giving Feedback
If you encounter a bug or unexpected behavior while using the beta, please report it on:
- GitHub: https://github.com/Cockatrice/Cockatrice/issues
- Discord: https://discord.gg/4hNswvHeFd in #beta-testing
When reporting issues, please include:
- Your operating system
- The beta version number (found in 'Help → About Cockatrice')
- Steps to reproduce the problem, if possible

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -2,22 +2,22 @@
<context>
<name>IntroPage</name>
<message>
<location filename="src/oraclewizard.cpp" line="215"/>
<location filename="src/pages.cpp" line="127"/>
<source>Introduction</source>
<translation>Einführung</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="216"/>
<location filename="src/pages.cpp" line="128"/>
<source>This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice.</source>
<translation>Dieser Assistent wird eine Liste aller Editionen, Karten und Spielsteine importieren, die von Cockatrice genutzt werden.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="218"/>
<location filename="src/pages.cpp" line="130"/>
<source>Interface language:</source>
<translation>Interfacesprache:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="219"/>
<location filename="src/pages.cpp" line="131"/>
<source>Version:</source>
<translation>Version:</translation>
</message>
@@ -25,134 +25,134 @@
<context>
<name>LoadSetsPage</name>
<message>
<location filename="src/oraclewizard.cpp" line="288"/>
<location filename="src/pages.cpp" line="200"/>
<source>Source selection</source>
<translation>Quellenauswahl</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="289"/>
<location filename="src/pages.cpp" line="201"/>
<source>Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer.</source>
<translation>Bitte geben Sie eine kompatible Quelle für die Liste der Editionen und Karten an. Sie können eine URL-Adresse zum Herunterladen angeben oder eine existierende Datei von Ihrem Computer verwenden.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="293"/>
<location filename="src/pages.cpp" line="205"/>
<source>Download URL:</source>
<translation>Download URL:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="294"/>
<location filename="src/pages.cpp" line="206"/>
<source>Local file:</source>
<translation>Lokale Datei:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="295"/>
<location filename="src/pages.cpp" line="207"/>
<source>Restore default URL</source>
<translation>Standard-URL wiederherstellen</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="296"/>
<location filename="src/pages.cpp" line="208"/>
<source>Choose file...</source>
<translation>Datei auswählen...</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="306"/>
<location filename="src/pages.cpp" line="218"/>
<source>Load sets file</source>
<translation>Editionsdatei wird geladen</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="316"/>
<location filename="src/pages.cpp" line="228"/>
<source>Sets file (%1)</source>
<oldsource>Sets JSON file (%1)</oldsource>
<translation>Sets Datei (%1)</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="346"/>
<location filename="src/oraclewizard.cpp" line="365"/>
<location filename="src/oraclewizard.cpp" line="370"/>
<location filename="src/oraclewizard.cpp" line="438"/>
<location filename="src/oraclewizard.cpp" line="569"/>
<location filename="src/oraclewizard.cpp" line="582"/>
<location filename="src/oraclewizard.cpp" line="603"/>
<location filename="src/pages.cpp" line="258"/>
<location filename="src/pages.cpp" line="277"/>
<location filename="src/pages.cpp" line="282"/>
<location filename="src/pages.cpp" line="354"/>
<location filename="src/pages.cpp" line="485"/>
<location filename="src/pages.cpp" line="498"/>
<location filename="src/pages.cpp" line="519"/>
<source>Error</source>
<translation>Fehler</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="346"/>
<location filename="src/pages.cpp" line="258"/>
<source>The provided URL is not valid.</source>
<translation>Die eingegebene URL ist nicht gültig.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="350"/>
<location filename="src/pages.cpp" line="262"/>
<source>Downloading (0MB)</source>
<translation>Herunterladen (0MB)</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="365"/>
<location filename="src/pages.cpp" line="277"/>
<source>Please choose a file.</source>
<translation>Bitte wählen Sie eine Datei.</translation>
<translation>Bitte wähle eine Datei aus.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="370"/>
<location filename="src/pages.cpp" line="282"/>
<source>Cannot open file &apos;%1&apos;.</source>
<translation>Datei &apos;%1&apos; kann nicht geöffnet werden.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="429"/>
<location filename="src/pages.cpp" line="345"/>
<source>Downloading (%1MB)</source>
<translation>Herunterladen (%1MB)</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="438"/>
<location filename="src/pages.cpp" line="354"/>
<source>Network error: %1.</source>
<translation>Netzwerkfehler: %1.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="476"/>
<location filename="src/pages.cpp" line="392"/>
<source>Parsing file</source>
<translation>Datei wird verarbeitet</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="498"/>
<location filename="src/pages.cpp" line="414"/>
<source>Xz extraction failed.</source>
<translation>Fehler beim Extrahieren der xz Datei.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="505"/>
<location filename="src/pages.cpp" line="421"/>
<source>Sorry, this version of Oracle does not support xz compressed files.</source>
<translation>Es tut uns Leid, diese Version von Oracle unterstützt keine xz komprimierten Dateien.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="525"/>
<location filename="src/pages.cpp" line="441"/>
<source>Failed to open Zip archive: %1.</source>
<translation>Fehler beim Öffnen des Zip Archivs: %1.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="530"/>
<location filename="src/pages.cpp" line="446"/>
<source>Zip extraction failed: the Zip archive doesn&apos;t contain exactly one file.</source>
<translation>Fehler beim Extrahieren: Das Zip Archiv enthält mehr als eine Datei.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="538"/>
<location filename="src/pages.cpp" line="454"/>
<source>Zip extraction failed: %1.</source>
<translation>Fehler beim Extrahieren: %1.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="546"/>
<location filename="src/pages.cpp" line="462"/>
<source>Sorry, this version of Oracle does not support zipped files.</source>
<translation>Es tut uns Leid, diese Version von Oracle unterstützt keine Zip Archive.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="569"/>
<location filename="src/pages.cpp" line="485"/>
<source>Failed to interpret downloaded data.</source>
<translation>Interpretation der heruntergeladenen Daten fehlgeschlagen.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="582"/>
<location filename="src/pages.cpp" line="498"/>
<source>Do you want to download the uncompressed file instead?</source>
<translation>Möchten Sie stattdessen die unkomprimierte Datei herunterladen?</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="604"/>
<location filename="src/pages.cpp" line="520"/>
<source>The file was retrieved successfully, but it does not contain any sets data.</source>
<translation>Die Datei wurde erfolgreich abgerufen, sie enthält aber keine Editionsdaten.</translation>
</message>
@@ -160,42 +160,57 @@
<context>
<name>LoadSpoilersPage</name>
<message>
<location filename="src/oraclewizard.cpp" line="790"/>
<location filename="src/pages.cpp" line="716"/>
<source>Save spoiler database</source>
<translation>Speichere Spoilerdatenbank</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="795"/>
<location filename="src/pages.cpp" line="721"/>
<source>XML; spoiler database (*.xml)</source>
<translation>XML; Spoilerdatenbank (*.xml)</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="800"/>
<location filename="src/pages.cpp" line="726"/>
<source>spoiler</source>
<translation>Spoiler</translation>
</message>
<message>
<location filename="src/pages.cpp" line="731"/>
<source>Spoilers import</source>
<translation>Spoilerimport</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="801"/>
<location filename="src/pages.cpp" line="732"/>
<source>Please specify a compatible source for spoiler data.</source>
<translation>Bitte geben Sie eine kompatible Quelle für Spoilerdaten an.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="803"/>
<location filename="src/pages.cpp" line="734"/>
<source>Download URL:</source>
<translation>Download URL:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="804"/>
<location filename="src/pages.cpp" line="735"/>
<source>Local file:</source>
<translation>Lokale Datei:</translation>
</message>
<message>
<location filename="src/pages.cpp" line="736"/>
<source>Restore default URL</source>
<translation>Standard-URL wiederherstellen</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="805"/>
<location filename="src/pages.cpp" line="737"/>
<source>Choose file...</source>
<translation>Datei auswählen...</translation>
</message>
<message>
<location filename="src/pages.cpp" line="739"/>
<source>The spoiler database will be saved at the following location:</source>
<translation>Die Spoilerdatenbank wird in folgendem Pfad gespeichert:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="807"/>
<location filename="src/pages.cpp" line="741"/>
<source>Save to a custom path (not recommended)</source>
<translation>Speichere in benutzerdefiniertem Pfad (nicht empfohlen)</translation>
</message>
@@ -203,42 +218,57 @@
<context>
<name>LoadTokensPage</name>
<message>
<location filename="src/oraclewizard.cpp" line="753"/>
<location filename="src/pages.cpp" line="671"/>
<source>Save token database</source>
<translation>Speichere Spielsteindatenbank</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="758"/>
<location filename="src/pages.cpp" line="676"/>
<source>XML; token database (*.xml)</source>
<translation>XML; Spielsteindatenbank (*.xml)</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="763"/>
<location filename="src/pages.cpp" line="681"/>
<source>tokens</source>
<translation>Spielsteine</translation>
</message>
<message>
<location filename="src/pages.cpp" line="686"/>
<source>Tokens import</source>
<translation>Spielsteinimport</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="764"/>
<location filename="src/pages.cpp" line="687"/>
<source>Please specify a compatible source for token data.</source>
<translation>Bitte geben Sie eine kompatible Quelle für Spielsteindaten an.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="766"/>
<location filename="src/pages.cpp" line="689"/>
<source>Download URL:</source>
<translation>Download URL:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="767"/>
<location filename="src/pages.cpp" line="690"/>
<source>Local file:</source>
<translation>Lokale Datei:</translation>
</message>
<message>
<location filename="src/pages.cpp" line="691"/>
<source>Restore default URL</source>
<translation>Standard-URL wiederherstellen</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="768"/>
<location filename="src/pages.cpp" line="692"/>
<source>Choose file...</source>
<translation>Datei auswählen...</translation>
</message>
<message>
<location filename="src/pages.cpp" line="694"/>
<source>The token database will be saved at the following location:</source>
<translation>Die Spielsteindatenbank wird in folgendem Pfad gespeichert:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="770"/>
<location filename="src/pages.cpp" line="696"/>
<source>Save to a custom path (not recommended)</source>
<translation>Speichere in benutzerdefiniertem Pfad (nicht empfohlen)</translation>
</message>
@@ -246,7 +276,7 @@
<context>
<name>OracleImporter</name>
<message>
<location filename="src/oracleimporter.cpp" line="466"/>
<location filename="src/oracleimporter.cpp" line="541"/>
<source>Dummy set containing tokens</source>
<translation>Platzhalter Edition mit Spielsteinen</translation>
</message>
@@ -254,7 +284,7 @@
<context>
<name>OracleWizard</name>
<message>
<location filename="src/oraclewizard.cpp" line="106"/>
<location filename="src/oraclewizard.cpp" line="70"/>
<source>Oracle Importer</source>
<translation>Oracle Importer</translation>
</message>
@@ -262,22 +292,22 @@
<context>
<name>OutroPage</name>
<message>
<location filename="src/oraclewizard.cpp" line="224"/>
<location filename="src/pages.cpp" line="136"/>
<source>Finished</source>
<translation>Fertig</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="225"/>
<location filename="src/pages.cpp" line="137"/>
<source>The wizard has finished.</source>
<translation>Der Wizard ist fertig.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="226"/>
<location filename="src/pages.cpp" line="138"/>
<source>You can now start using Cockatrice with the newly updated cards.</source>
<translation>Sie können nun Cockatrice mit den aktuellen Karten verwenden.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="227"/>
<location filename="src/pages.cpp" line="139"/>
<source>If the card databases don&apos;t reload automatically, restart the Cockatrice client.</source>
<translation>Falls die Datenbanken nicht automatisch neu geladen werden, starten Sie bitte Cockatrice neu.</translation>
</message>
@@ -285,73 +315,73 @@
<context>
<name>SaveSetsPage</name>
<message>
<location filename="src/oraclewizard.cpp" line="646"/>
<location filename="src/oraclewizard.cpp" line="720"/>
<location filename="src/pages.cpp" line="563"/>
<location filename="src/pages.cpp" line="638"/>
<source>Error</source>
<translation>Fehler</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="646"/>
<location filename="src/pages.cpp" line="563"/>
<source>No set has been imported.</source>
<translation>Es wurden keine Editionen importiert.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="656"/>
<location filename="src/pages.cpp" line="574"/>
<source>Sets imported</source>
<translation>Editionen wurden importiert</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="658"/>
<location filename="src/pages.cpp" line="576"/>
<source>A cockatrice database file of %1 MB has been downloaded.</source>
<translation>Eine Cockatrice-Datenbankdatei der Größe %1 MB wurde heruntergeladen.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="661"/>
<location filename="src/pages.cpp" line="579"/>
<source>The following sets have been found:</source>
<translation>Die folgenden Sets wurden gefunden:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="664"/>
<location filename="src/pages.cpp" line="582"/>
<source>Press &quot;Save&quot; to store the imported cards in the Cockatrice database.</source>
<translation>Drücken Sie &quot;Speichern&quot;, um die importierten Karten in der Datenbank zu speichern.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="665"/>
<location filename="src/pages.cpp" line="583"/>
<source>The card database will be saved at the following location:</source>
<translation>Die Kartendatenbank wird in folgendem Pfad gespeichert:</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="667"/>
<location filename="src/pages.cpp" line="585"/>
<source>Save to a custom path (not recommended)</source>
<translation>Speichere in benutzerdefiniertem Pfad (nicht empfohlen)</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="669"/>
<location filename="src/pages.cpp" line="587"/>
<source>&amp;Save</source>
<translation>&amp;Speichern</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="675"/>
<location filename="src/pages.cpp" line="593"/>
<source>Import finished: %1 cards.</source>
<translation>Importieren abgeschlossen: %1 Karten.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="678"/>
<location filename="src/pages.cpp" line="596"/>
<source>%1: %2 cards imported</source>
<translation>%1: %2 Karten importiert.</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="687"/>
<location filename="src/pages.cpp" line="605"/>
<source>Save card database</source>
<translation>Kartendatenbank speichern</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="688"/>
<location filename="src/pages.cpp" line="606"/>
<source>XML; card database (*.xml)</source>
<translation>XML; Kartendatenbank (*.xml)</translation>
</message>
<message>
<location filename="src/oraclewizard.cpp" line="720"/>
<location filename="src/pages.cpp" line="638"/>
<source>The file could not be saved to %1</source>
<translation>Die Datei konnte nicht gespeichert werden:
%1 </translation>
@@ -360,34 +390,56 @@
<context>
<name>SimpleDownloadFilePage</name>
<message>
<location filename="src/pagetemplates.cpp" line="73"/>
<location filename="src/pagetemplates.cpp" line="113"/>
<location filename="src/pagetemplates.cpp" line="169"/>
<location filename="src/pagetemplates.cpp" line="72"/>
<source>Load %1 file</source>
<translation>Lade %1 Datei</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="82"/>
<source>%1 file (%1)</source>
<translation>%1 Datei (%1)</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="111"/>
<location filename="src/pagetemplates.cpp" line="129"/>
<location filename="src/pagetemplates.cpp" line="134"/>
<location filename="src/pagetemplates.cpp" line="168"/>
<location filename="src/pagetemplates.cpp" line="224"/>
<source>Error</source>
<translation>Fehler</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="73"/>
<location filename="src/pagetemplates.cpp" line="111"/>
<source>The provided URL is not valid: </source>
<translation>Die bereitgestellte URL ist nicht gültig:</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="77"/>
<location filename="src/pagetemplates.cpp" line="115"/>
<source>Downloading (0MB)</source>
<translation>Herunterladen (0MB)</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="104"/>
<location filename="src/pagetemplates.cpp" line="129"/>
<source>Please choose a file.</source>
<translation>Bitte wähle eine Datei aus.</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="134"/>
<source>Cannot open file &apos;%1&apos;.</source>
<translation>Datei &apos;%1&apos; kann nicht geöffnet werden.</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="159"/>
<source>Downloading (%1MB)</source>
<translation>Herunterladen (%1MB)</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="113"/>
<location filename="src/pagetemplates.cpp" line="168"/>
<source>Network error: %1.</source>
<translation>Netzwerkfehler: %1.</translation>
</message>
<message>
<location filename="src/pagetemplates.cpp" line="169"/>
<location filename="src/pagetemplates.cpp" line="224"/>
<source>The file could not be saved to %1</source>
<translation>Die Datei konnte nicht in %1 gespeichert werden</translation>
</message>
@@ -536,7 +588,7 @@
<context>
<name>i18n</name>
<message>
<location filename="src/oraclewizard.cpp" line="58"/>
<location filename="src/oraclewizard.cpp" line="22"/>
<source>English</source>
<translation>Deutsch (German)</translation>
</message>