Improved save and load system, added description and sort options
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16
-6
@@ -1,11 +1,21 @@
|
|||||||
from .. import db
|
from .. import db
|
||||||
from flask_login import UserMixin
|
|
||||||
|
|
||||||
class GameState(UserMixin, db.Model):
|
class GameState(db.Model):
|
||||||
|
__tablename__ = "game_state"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, nullable=False)
|
user_id = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
board = db.Column(db.Text, nullable=False) # JSON String
|
board = db.Column(db.Text, nullable=False)
|
||||||
mode = db.Column(db.String(10), nullable=False) # queens / rooks
|
mode = db.Column(db.String(10), nullable=False)
|
||||||
|
board_size = db.Column(db.Integer, nullable=False, default=8)
|
||||||
created_at = db.Column(db.DateTime, default=db.func.now())
|
save_name = db.Column(db.String(80), nullable=False, default="Unnamed Save")
|
||||||
|
save_note = db.Column(db.Text, nullable=False, default="")
|
||||||
|
is_favorite = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||||
|
updated_at = db.Column(
|
||||||
|
db.DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=db.func.now(),
|
||||||
|
onupdate=db.func.now()
|
||||||
|
)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+86
-20
@@ -23,6 +23,10 @@ def parse_board_size(raw_size, default=DEFAULT_BOARD_SIZE):
|
|||||||
return max(MIN_BOARD_SIZE, min(MAX_BOARD_SIZE, board_size))
|
return max(MIN_BOARD_SIZE, min(MAX_BOARD_SIZE, board_size))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_favorite_flag(raw_value):
|
||||||
|
return str(raw_value).lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
def solve_queens(board_size):
|
def solve_queens(board_size):
|
||||||
queen_positions = [-1] * board_size
|
queen_positions = [-1] * board_size
|
||||||
queen_solutions = []
|
queen_solutions = []
|
||||||
@@ -67,12 +71,43 @@ def serialize_game_state(game_state):
|
|||||||
"id": game_state.id,
|
"id": game_state.id,
|
||||||
"board": board,
|
"board": board,
|
||||||
"mode": game_state.mode,
|
"mode": game_state.mode,
|
||||||
"board_size": len(board),
|
"board_size": game_state.board_size,
|
||||||
|
"save_name": game_state.save_name,
|
||||||
|
"save_note": game_state.save_note,
|
||||||
|
"is_favorite": game_state.is_favorite,
|
||||||
"pieces_placed": sum(1 for column in board if column != -1),
|
"pieces_placed": sum(1 for column in board if column != -1),
|
||||||
"created_at": game_state.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
"created_at": game_state.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"updated_at": game_state.updated_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_save_point_query():
|
||||||
|
query = GameState.query.filter_by(user_id=current_user.id)
|
||||||
|
|
||||||
|
selected_mode = request.args.get("mode")
|
||||||
|
board_size = request.args.get("board_size")
|
||||||
|
favorites_only = request.args.get("favorites_only")
|
||||||
|
sort_by = request.args.get("sort", "newest")
|
||||||
|
|
||||||
|
if selected_mode in {"queens", "rooks"}:
|
||||||
|
query = query.filter_by(mode=selected_mode)
|
||||||
|
|
||||||
|
if board_size not in {None, "", "all"}:
|
||||||
|
query = query.filter_by(board_size=parse_board_size(board_size))
|
||||||
|
|
||||||
|
if parse_favorite_flag(favorites_only):
|
||||||
|
query = query.filter_by(is_favorite=True)
|
||||||
|
|
||||||
|
sort_options = {
|
||||||
|
"newest": GameState.created_at.desc(),
|
||||||
|
"oldest": GameState.created_at.asc(),
|
||||||
|
"updated": GameState.updated_at.desc(),
|
||||||
|
"favorites": GameState.is_favorite.desc()
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.order_by(sort_options.get(sort_by, GameState.created_at.desc()), GameState.id.desc())
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route("/")
|
@main_bp.route("/")
|
||||||
def home():
|
def home():
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
@@ -127,29 +162,63 @@ def save_game():
|
|||||||
|
|
||||||
board = data["board"]
|
board = data["board"]
|
||||||
piece_mode = data["mode"]
|
piece_mode = data["mode"]
|
||||||
|
board_size = parse_board_size(data.get("boardSize"), default=len(board))
|
||||||
|
save_name = (data.get("saveName") or "").strip() or f"{piece_mode.title()} Save"
|
||||||
|
save_note = (data.get("saveNote") or "").strip()
|
||||||
|
is_favorite = bool(data.get("isFavorite"))
|
||||||
|
|
||||||
game = GameState(
|
game_state = GameState(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
board=json.dumps(board),
|
board=json.dumps(board),
|
||||||
mode=piece_mode
|
mode=piece_mode,
|
||||||
|
board_size=board_size,
|
||||||
|
save_name=save_name,
|
||||||
|
save_note=save_note,
|
||||||
|
is_favorite=is_favorite
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(game)
|
db.session.add(game_state)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"status": "saved"})
|
return jsonify({
|
||||||
|
"status": "saved",
|
||||||
|
"save_point": serialize_game_state(game_state)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route("/save_points")
|
@main_bp.route("/save_points")
|
||||||
@login_required
|
@login_required
|
||||||
def list_save_points():
|
def list_save_points():
|
||||||
saved_games = (
|
save_points = build_save_point_query().all()
|
||||||
GameState.query.filter_by(user_id=current_user.id)
|
return jsonify([serialize_game_state(game_state) for game_state in save_points])
|
||||||
.order_by(GameState.id.desc())
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify([serialize_game_state(game_state) for game_state in saved_games])
|
|
||||||
|
@main_bp.route("/save_points/<int:game_id>/favorite", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def toggle_save_point_favorite(game_id):
|
||||||
|
game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first()
|
||||||
|
|
||||||
|
if not game_state:
|
||||||
|
return jsonify({"error": "Save point not found"}), 404
|
||||||
|
|
||||||
|
game_state.is_favorite = not game_state.is_favorite
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(serialize_game_state(game_state))
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route("/save_points/<int:game_id>", methods=["DELETE"])
|
||||||
|
@login_required
|
||||||
|
def delete_save_point(game_id):
|
||||||
|
game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first()
|
||||||
|
|
||||||
|
if not game_state:
|
||||||
|
return jsonify({"error": "Save point not found"}), 404
|
||||||
|
|
||||||
|
db.session.delete(game_state)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"status": "deleted"})
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route("/load")
|
@main_bp.route("/load")
|
||||||
@@ -157,25 +226,22 @@ def list_save_points():
|
|||||||
def load_game():
|
def load_game():
|
||||||
latest_game = (
|
latest_game = (
|
||||||
GameState.query.filter_by(user_id=current_user.id)
|
GameState.query.filter_by(user_id=current_user.id)
|
||||||
.order_by(GameState.id.desc())
|
.order_by(GameState.created_at.desc(), GameState.id.desc())
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
if not latest_game:
|
if not latest_game:
|
||||||
return jsonify({"board": [-1] * DEFAULT_BOARD_SIZE, "mode": "queens"})
|
return jsonify({"board": [-1] * DEFAULT_BOARD_SIZE, "mode": "queens"})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify(serialize_game_state(latest_game))
|
||||||
"board": json.loads(latest_game.board),
|
|
||||||
"mode": latest_game.mode
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route("/load/<int:game_id>")
|
@main_bp.route("/load/<int:game_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def load_specific_game(game_id):
|
def load_specific_game(game_id):
|
||||||
selected_game = GameState.query.filter_by(id=game_id, user_id=current_user.id).first()
|
game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first()
|
||||||
|
|
||||||
if not selected_game:
|
if not game_state:
|
||||||
return jsonify({"error": "Save point not found"}), 404
|
return jsonify({"error": "Save point not found"}), 404
|
||||||
|
|
||||||
return jsonify(serialize_game_state(selected_game))
|
return jsonify(serialize_game_state(game_state))
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ body {
|
|||||||
width: min(380px, 92vw);
|
width: min(380px, 92vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-sidebar {
|
||||||
|
width: min(360px, 90vw);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -212,6 +216,29 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
font: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 16px 0 18px;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-size-row {
|
.sidebar-size-row {
|
||||||
margin: 12px 0 18px;
|
margin: 12px 0 18px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -228,6 +255,14 @@ body {
|
|||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-point-filters {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 18px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
.save-point-card {
|
.save-point-card {
|
||||||
border: 1px solid #dbe4ee;
|
border: 1px solid #dbe4ee;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -253,6 +288,32 @@ body {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-point-note {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-point-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-point-actions .btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.ghost {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.ghost:hover {
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-backdrop {
|
.sidebar-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
+150
-15
@@ -15,6 +15,8 @@ const btnQueens = document.getElementById("btnQueens");
|
|||||||
const btnRooks = document.getElementById("btnRooks");
|
const btnRooks = document.getElementById("btnRooks");
|
||||||
const settingsSidebar = document.getElementById("settingsSidebar");
|
const settingsSidebar = document.getElementById("settingsSidebar");
|
||||||
const sidebarBackdrop = document.getElementById("sidebarBackdrop");
|
const sidebarBackdrop = document.getElementById("sidebarBackdrop");
|
||||||
|
const saveSidebar = document.getElementById("saveSidebar");
|
||||||
|
const saveBackdrop = document.getElementById("saveBackdrop");
|
||||||
const savePointsSidebar = document.getElementById("savePointsSidebar");
|
const savePointsSidebar = document.getElementById("savePointsSidebar");
|
||||||
const savePointsBackdrop = document.getElementById("savePointsBackdrop");
|
const savePointsBackdrop = document.getElementById("savePointsBackdrop");
|
||||||
const boardSizeInput = document.getElementById("boardSizeInput");
|
const boardSizeInput = document.getElementById("boardSizeInput");
|
||||||
@@ -23,6 +25,13 @@ const boardSizeValueMirror = document.getElementById("boardSizeValueMirror");
|
|||||||
const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue");
|
const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue");
|
||||||
const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror");
|
const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror");
|
||||||
const savePointsList = document.getElementById("savePointsList");
|
const savePointsList = document.getElementById("savePointsList");
|
||||||
|
const saveNameInput = document.getElementById("saveNameInput");
|
||||||
|
const saveNoteInput = document.getElementById("saveNoteInput");
|
||||||
|
const saveFavoriteInput = document.getElementById("saveFavoriteInput");
|
||||||
|
const savePointModeFilter = document.getElementById("savePointModeFilter");
|
||||||
|
const savePointSizeFilter = document.getElementById("savePointSizeFilter");
|
||||||
|
const savePointSortFilter = document.getElementById("savePointSortFilter");
|
||||||
|
const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly");
|
||||||
|
|
||||||
function createEmptyBoard(size) {
|
function createEmptyBoard(size) {
|
||||||
return Array(size).fill(EMPTY_CELL);
|
return Array(size).fill(EMPTY_CELL);
|
||||||
@@ -67,6 +76,12 @@ function setMode(newMode) {
|
|||||||
updateModeButtons();
|
updateModeButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeAllPanels() {
|
||||||
|
toggleSidebar(false);
|
||||||
|
toggleSavePanel(false);
|
||||||
|
toggleSavePointsPanel(false);
|
||||||
|
}
|
||||||
|
|
||||||
function getConflicts() {
|
function getConflicts() {
|
||||||
const conflicts = [];
|
const conflicts = [];
|
||||||
|
|
||||||
@@ -178,6 +193,7 @@ function toggleSidebar(forceOpen) {
|
|||||||
const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !settingsSidebar.classList.contains("open");
|
const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !settingsSidebar.classList.contains("open");
|
||||||
|
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
|
toggleSavePanel(false);
|
||||||
toggleSavePointsPanel(false);
|
toggleSavePointsPanel(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,9 +202,27 @@ function toggleSidebar(forceOpen) {
|
|||||||
settingsSidebar.setAttribute("aria-hidden", String(!shouldOpen));
|
settingsSidebar.setAttribute("aria-hidden", String(!shouldOpen));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSavePanel(forceOpen) {
|
||||||
|
const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !saveSidebar.classList.contains("open");
|
||||||
|
|
||||||
|
if (shouldOpen) {
|
||||||
|
toggleSidebar(false);
|
||||||
|
toggleSavePointsPanel(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSidebar.classList.toggle("open", shouldOpen);
|
||||||
|
saveBackdrop.classList.toggle("visible", shouldOpen);
|
||||||
|
saveSidebar.setAttribute("aria-hidden", String(!shouldOpen));
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSavePointsPanel(forceOpen) {
|
function toggleSavePointsPanel(forceOpen) {
|
||||||
const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !savePointsSidebar.classList.contains("open");
|
const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !savePointsSidebar.classList.contains("open");
|
||||||
|
|
||||||
|
if (shouldOpen) {
|
||||||
|
toggleSidebar(false);
|
||||||
|
toggleSavePanel(false);
|
||||||
|
}
|
||||||
|
|
||||||
savePointsSidebar.classList.toggle("open", shouldOpen);
|
savePointsSidebar.classList.toggle("open", shouldOpen);
|
||||||
savePointsBackdrop.classList.toggle("visible", shouldOpen);
|
savePointsBackdrop.classList.toggle("visible", shouldOpen);
|
||||||
savePointsSidebar.setAttribute("aria-hidden", String(!shouldOpen));
|
savePointsSidebar.setAttribute("aria-hidden", String(!shouldOpen));
|
||||||
@@ -198,6 +232,21 @@ function getPieceLabel(selectedMode) {
|
|||||||
return selectedMode === "queens" ? "Dame" : "Turm";
|
return selectedMode === "queens" ? "Dame" : "Turm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlacedPieceCount() {
|
||||||
|
return board.filter((col) => col !== EMPTY_CELL).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSaveForm() {
|
||||||
|
saveNameInput.value = `${getPieceLabel(mode)} ${boardSize}x${boardSize} - ${getPlacedPieceCount()} gesetzt`;
|
||||||
|
saveNoteInput.value = "";
|
||||||
|
saveFavoriteInput.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSavePanel() {
|
||||||
|
resetSaveForm();
|
||||||
|
toggleSavePanel(true);
|
||||||
|
}
|
||||||
|
|
||||||
function getBoardPreview(savedBoard, limit = 4) {
|
function getBoardPreview(savedBoard, limit = 4) {
|
||||||
const preview = [];
|
const preview = [];
|
||||||
|
|
||||||
@@ -216,11 +265,40 @@ function getBoardPreview(savedBoard, limit = 4) {
|
|||||||
return preview.length > 0 ? preview.join(", ") : "Noch keine Figuren gesetzt";
|
return preview.length > 0 ? preview.join(", ") : "Noch keine Figuren gesetzt";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSavePointFilters() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (savePointModeFilter.value !== "all") {
|
||||||
|
params.set("mode", savePointModeFilter.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savePointSizeFilter.value !== "all") {
|
||||||
|
params.set("board_size", savePointSizeFilter.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set("sort", savePointSortFilter.value);
|
||||||
|
|
||||||
|
if (savePointFavoritesOnly.checked) {
|
||||||
|
params.set("favorites_only", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSavePointActionButton(label, className, onClick) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = className;
|
||||||
|
button.type = "button";
|
||||||
|
button.textContent = label;
|
||||||
|
button.onclick = onClick;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
function renderSavePoints(savePoints) {
|
function renderSavePoints(savePoints) {
|
||||||
savePointsList.innerHTML = "";
|
savePointsList.innerHTML = "";
|
||||||
|
|
||||||
if (savePoints.length === 0) {
|
if (savePoints.length === 0) {
|
||||||
savePointsList.innerHTML = '<p class="sidebar-hint">Noch keine Speicherstaende gefunden.</p>';
|
savePointsList.innerHTML = '<p class="sidebar-hint">Keine Speicherstaende fuer diese Filter gefunden.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,42 +306,54 @@ function renderSavePoints(savePoints) {
|
|||||||
const item = document.createElement("article");
|
const item = document.createElement("article");
|
||||||
const title = document.createElement("h4");
|
const title = document.createElement("h4");
|
||||||
const createdAt = document.createElement("p");
|
const createdAt = document.createElement("p");
|
||||||
|
const updatedAt = document.createElement("p");
|
||||||
const boardMeta = document.createElement("p");
|
const boardMeta = document.createElement("p");
|
||||||
const preview = document.createElement("p");
|
const preview = document.createElement("p");
|
||||||
const loadButton = document.createElement("button");
|
const note = document.createElement("p");
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
|
||||||
item.className = "save-point-card";
|
item.className = "save-point-card";
|
||||||
title.textContent = `Save Point #${savePoint.id}`;
|
title.textContent = `${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`;
|
||||||
createdAt.className = "save-point-meta";
|
createdAt.className = "save-point-meta";
|
||||||
createdAt.textContent = `Gespeichert: ${savePoint.created_at}`;
|
createdAt.textContent = `Erstellt: ${savePoint.created_at}`;
|
||||||
|
updatedAt.className = "save-point-meta";
|
||||||
|
updatedAt.textContent = `Aktualisiert: ${savePoint.updated_at}`;
|
||||||
boardMeta.className = "save-point-meta";
|
boardMeta.className = "save-point-meta";
|
||||||
boardMeta.textContent = `${getPieceLabel(savePoint.mode)} | ${savePoint.board_size} x ${savePoint.board_size} | ${savePoint.pieces_placed}/${savePoint.board_size} gesetzt`;
|
boardMeta.textContent = `${getPieceLabel(savePoint.mode)} | ${savePoint.board_size} x ${savePoint.board_size} | ${savePoint.pieces_placed}/${savePoint.board_size} gesetzt`;
|
||||||
preview.className = "save-point-preview";
|
preview.className = "save-point-preview";
|
||||||
preview.textContent = `Vorschau: ${getBoardPreview(savePoint.board)}`;
|
preview.textContent = `Vorschau: ${getBoardPreview(savePoint.board)}`;
|
||||||
|
note.className = "save-point-note";
|
||||||
|
note.textContent = savePoint.save_note ? `Notiz: ${savePoint.save_note}` : "Notiz: Keine Notiz";
|
||||||
|
actions.className = "save-point-actions";
|
||||||
|
|
||||||
loadButton.className = "btn";
|
actions.appendChild(createSavePointActionButton("Laden", "btn", () => loadSavePoint(savePoint.id)));
|
||||||
loadButton.type = "button";
|
actions.appendChild(createSavePointActionButton(
|
||||||
loadButton.textContent = "Diesen laden";
|
savePoint.is_favorite ? "Favorit entfernen" : "Als Favorit",
|
||||||
loadButton.onclick = () => loadSavePoint(savePoint.id);
|
"btn secondary",
|
||||||
|
() => toggleSavePointFavorite(savePoint.id)
|
||||||
|
));
|
||||||
|
actions.appendChild(createSavePointActionButton("Loeschen", "btn ghost", () => deleteSavePoint(savePoint.id)));
|
||||||
|
|
||||||
item.appendChild(title);
|
item.appendChild(title);
|
||||||
item.appendChild(createdAt);
|
item.appendChild(createdAt);
|
||||||
|
item.appendChild(updatedAt);
|
||||||
item.appendChild(boardMeta);
|
item.appendChild(boardMeta);
|
||||||
item.appendChild(preview);
|
item.appendChild(preview);
|
||||||
item.appendChild(loadButton);
|
item.appendChild(note);
|
||||||
|
item.appendChild(actions);
|
||||||
savePointsList.appendChild(item);
|
savePointsList.appendChild(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshSavePoints() {
|
async function refreshSavePoints() {
|
||||||
const response = await fetch("/save_points");
|
const queryString = getSavePointFilters();
|
||||||
|
const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`);
|
||||||
const savePoints = await response.json();
|
const savePoints = await response.json();
|
||||||
renderSavePoints(savePoints);
|
renderSavePoints(savePoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSavePointsPanel() {
|
async function openSavePointsPanel() {
|
||||||
await refreshSavePoints();
|
await refreshSavePoints();
|
||||||
toggleSidebar(false);
|
|
||||||
toggleSavePointsPanel(true);
|
toggleSavePointsPanel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +406,7 @@ function updateQueenList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveGame() {
|
async function saveGame() {
|
||||||
await fetch("/save", {
|
const response = await fetch("/save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -324,12 +414,23 @@ async function saveGame() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
board,
|
board,
|
||||||
mode,
|
mode,
|
||||||
boardSize
|
boardSize,
|
||||||
|
saveName: saveNameInput.value,
|
||||||
|
saveNote: saveNoteInput.value,
|
||||||
|
isFavorite: saveFavoriteInput.checked
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setStatus("❌ Der Speicherstand konnte nicht gespeichert werden.", "red");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
toggleSavePanel(false);
|
||||||
await refreshSavePoints();
|
await refreshSavePoints();
|
||||||
alert("💾 Spiel gespeichert!");
|
setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadGame() {
|
async function loadGame() {
|
||||||
@@ -349,10 +450,44 @@ async function loadSavePoint(savePointId) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
applyLoadedGame(data);
|
applyLoadedGame(data);
|
||||||
setStatus(`Speicherstand #${savePointId} geladen.`, "green");
|
setStatus(`Speicherpunkt "${data.save_name}" geladen.`, "green");
|
||||||
toggleSavePointsPanel(false);
|
toggleSavePointsPanel(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleSavePointFavorite(savePointId) {
|
||||||
|
const response = await fetch(`/save_points/${savePointId}/favorite`, {
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setStatus("❌ Favorit konnte nicht aktualisiert werden.", "red");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSavePoints();
|
||||||
|
setStatus("Favoritenstatus aktualisiert.", "#2563eb");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSavePoint(savePointId) {
|
||||||
|
const confirmed = window.confirm("Diesen Speicherpunkt wirklich loeschen?");
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/save_points/${savePointId}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setStatus("❌ Speicherpunkt konnte nicht geloescht werden.", "red");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSavePoints();
|
||||||
|
setStatus("Speicherpunkt geloescht.", "#2563eb");
|
||||||
|
}
|
||||||
|
|
||||||
pendingBoardSize = boardSize;
|
pendingBoardSize = boardSize;
|
||||||
updateModeButtons();
|
updateModeButtons();
|
||||||
updateBoardSizeDisplays(boardSize);
|
updateBoardSizeDisplays(boardSize);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
<button class="btn" onclick="checkSolution()">Check</button>
|
<button class="btn" onclick="checkSolution()">Check</button>
|
||||||
<button class="btn secondary" onclick="resetBoard()">Reset</button>
|
<button class="btn secondary" onclick="resetBoard()">Reset</button>
|
||||||
|
|
||||||
<button class="btn" onclick="saveGame()">💾 Save</button>
|
<button class="btn" onclick="openSavePanel()">💾 Save</button>
|
||||||
<button class="btn" onclick="openSavePointsPanel()">📂 Load</button>
|
<button class="btn" onclick="openSavePointsPanel()">📂 Load</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,6 +95,29 @@
|
|||||||
<p class="sidebar-hint">Das Brett wird beim Ändern zur Sicherheit zurückgesetzt.</p>
|
<p class="sidebar-hint">Das Brett wird beim Ändern zur Sicherheit zurückgesetzt.</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<aside class="sidebar save-sidebar" id="saveSidebar" aria-hidden="true">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>Speicherpunkt</h3>
|
||||||
|
<button class="btn secondary sidebar-close" onclick="toggleSavePanel(false)" type="button">Schliessen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Gib dem Spielstand einen klaren Namen und optional eine kurze Notiz.</p>
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="saveNameInput">Name</label>
|
||||||
|
<input class="sidebar-input" id="saveNameInput" type="text" maxlength="80" placeholder="z. B. Fast geloeste Dame 8x8">
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="saveNoteInput">Notiz</label>
|
||||||
|
<textarea class="sidebar-input sidebar-textarea" id="saveNoteInput" rows="4" maxlength="240"
|
||||||
|
placeholder="z. B. Gute Startposition fuer Tuermetest"></textarea>
|
||||||
|
|
||||||
|
<label class="sidebar-checkbox">
|
||||||
|
<input id="saveFavoriteInput" type="checkbox">
|
||||||
|
Als Favorit markieren
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="btn" onclick="saveGame()" type="button">Jetzt speichern</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<aside class="sidebar save-points-sidebar" id="savePointsSidebar" aria-hidden="true">
|
<aside class="sidebar save-points-sidebar" id="savePointsSidebar" aria-hidden="true">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>Save Points</h3>
|
<h3>Save Points</h3>
|
||||||
@@ -102,12 +125,44 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Waehle einen Speicherstand aus der Liste.</p>
|
<p>Waehle einen Speicherstand aus der Liste.</p>
|
||||||
|
|
||||||
|
<div class="save-point-filters">
|
||||||
|
<label class="sidebar-label" for="savePointModeFilter">Modus</label>
|
||||||
|
<select class="sidebar-input" id="savePointModeFilter" onchange="refreshSavePoints()">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
<option value="queens">Damen</option>
|
||||||
|
<option value="rooks">Tuerme</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="savePointSizeFilter">Brettgroesse</label>
|
||||||
|
<select class="sidebar-input" id="savePointSizeFilter" onchange="refreshSavePoints()">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
{% for board_size in range(4, 14) %}
|
||||||
|
<option value="{{ board_size }}">{{ board_size }} x {{ board_size }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="savePointSortFilter">Sortierung</label>
|
||||||
|
<select class="sidebar-input" id="savePointSortFilter" onchange="refreshSavePoints()">
|
||||||
|
<option value="newest">Neueste zuerst</option>
|
||||||
|
<option value="oldest">Aelteste zuerst</option>
|
||||||
|
<option value="updated">Zuletzt bearbeitet</option>
|
||||||
|
<option value="favorites">Favoriten zuerst</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="sidebar-checkbox">
|
||||||
|
<input id="savePointFavoritesOnly" type="checkbox" onchange="refreshSavePoints()">
|
||||||
|
Nur Favoriten anzeigen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="save-points-list" id="savePointsList">
|
<div class="save-points-list" id="savePointsList">
|
||||||
<p class="sidebar-hint">Noch keine Speicherstaende gefunden.</p>
|
<p class="sidebar-hint">Noch keine Speicherstaende gefunden.</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="sidebar-backdrop" id="sidebarBackdrop" onclick="toggleSidebar(false)"></div>
|
<div class="sidebar-backdrop" id="sidebarBackdrop" onclick="toggleSidebar(false)"></div>
|
||||||
|
<div class="sidebar-backdrop" id="saveBackdrop" onclick="toggleSavePanel(false)"></div>
|
||||||
<div class="sidebar-backdrop" id="savePointsBackdrop" onclick="toggleSavePointsPanel(false)"></div>
|
<div class="sidebar-backdrop" id="savePointsBackdrop" onclick="toggleSavePointsPanel(false)"></div>
|
||||||
|
|
||||||
<script src="../static/js/dashboard.js"></script>
|
<script src="../static/js/dashboard.js"></script>
|
||||||
|
|||||||
+6
-1
@@ -11,5 +11,10 @@ CREATE TABLE game_state (
|
|||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
board TEXT NOT NULL, -- JSON String (z.B. "[0,4,7,5,2,6,1,3]")
|
board TEXT NOT NULL, -- JSON String (z.B. "[0,4,7,5,2,6,1,3]")
|
||||||
mode TEXT NOT NULL, -- "queens" oder "rooks"
|
mode TEXT NOT NULL, -- "queens" oder "rooks"
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
board_size INTEGER NOT NULL DEFAULT 8,
|
||||||
|
save_name VARCHAR(80) NOT NULL DEFAULT 'Unnamed Save',
|
||||||
|
save_note TEXT NOT NULL DEFAULT '',
|
||||||
|
is_favorite BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user