Compare commits

..

66 Commits

Author SHA1 Message Date
c477355fe9 Ajouter README.md 2025-05-21 10:38:18 +02:00
f52c970bef meta 2025-05-09 11:50:32 +02:00
cf0a5a465e fix 2025-05-09 11:13:29 +02:00
310a1883d2 declare properties, use session without cookies 2025-05-06 18:20:08 +02:00
4b92464c94 format 2025-05-05 23:18:26 +02:00
c09ed80a52 serviceWorker in html 2024-05-12 11:17:03 +02:00
b145ae566a hover 2024-04-09 01:38:42 +02:00
21e8f4134f auto import 2023-12-23 13:25:04 +01:00
31a40a7e93 format 2023-12-23 13:24:44 +01:00
ad3992ac30 navbar 2023-12-23 13:24:22 +01:00
5feaa65955 global $sudokuGridSolutions 2023-12-23 13:24:12 +01:00
ed5795a6cc save directly nbsolutions in $_SESSION 2023-12-12 18:36:37 +01:00
bdb1a094c3 back to bootstrap dark 2023-12-11 09:13:35 +01:00
85efaca248 color: inherit 2023-12-08 01:17:20 +01:00
39c564fb89 $_SESSION["sudokuGridSolutions"] 2023-12-08 01:17:06 +01:00
ba07a531d0 save nbSolutions in session 2023-12-06 15:10:06 +01:00
f510310549 use bootstrap default color theme switcher 2023-12-06 12:08:45 +01:00
5091e6a888 update 400.php style 2023-12-06 11:02:46 +01:00
67cd3594a6 fix context-menu z-index 2023-12-06 10:38:57 +01:00
4027a8b36f md 2023-12-05 17:36:00 +01:00
e1fc974372 links on aside 2023-12-05 17:32:40 +01:00
853d93f8ff fixes 2023-11-25 22:38:03 +01:00
ddc1a51899 sight fixes 2023-11-25 22:37:44 +01:00
77886a6878 fix area on showDuplicates 2023-11-25 21:51:56 +01:00
80bc9f083d refactor with bind 2023-11-25 21:40:35 +01:00
1365ef65dd optimize with sort 2023-11-25 19:55:59 +01:00
ad414d0da8 refactor 2023-11-25 19:03:21 +01:00
57d06c3b53 fix check on onpopstate 2023-11-25 18:47:11 +01:00
84d9222a1c undo button is back 2023-11-25 11:58:18 +01:00
fa979cb973 use window.history 2023-11-25 04:40:40 +01:00
7245e0f073 ternary accessKeyModifiers 2023-11-02 10:37:07 +01:00
2429845dd6 navigator.platform is depreciated 2023-11-02 10:28:39 +01:00
956966fbe9 html format 2023-11-02 08:45:59 +01:00
ddcabbdd39 html format 2023-11-02 08:40:13 +01:00
743f7fa72d format 2023-10-31 03:29:02 +01:00
3a243c38f0 color picker icon 2023-10-23 19:28:03 +02:00
beebb14464 improve color picker 2023-10-23 17:25:21 +02:00
24806a289f improve color picker 2023-10-23 16:43:19 +02:00
4a0d03f445 add color picker 2023-10-23 09:16:46 +02:00
2e5125c298 uncheck radio if clicked twice 2023-10-17 23:33:39 +02:00
959deb5e14 icon margin 2023-06-21 08:38:19 +02:00
562cd7964b negative icon margin 2023-06-21 08:25:20 +02:00
e437a01e30 more button padding 2023-06-18 12:32:00 +02:00
f811000ea0 less button padding 2023-06-18 12:08:43 +02:00
35f1bee053 update thumbnail 2023-04-30 10:59:58 +02:00
79a20f323a update manifest 2023-04-30 10:27:41 +02:00
e80fef6c08 force favicon and manifest reload 2023-04-30 03:43:18 +02:00
7c65480bdd CDN 2023-04-22 16:14:46 +02:00
7043fb8bb6 CDN 2023-04-22 16:09:00 +02:00
15ed790caf smartphone friendly 2023-04-21 03:40:11 +02:00
c0c815e757 new thumbnail 2023-04-21 01:53:16 +02:00
5bd46082d5 move insertRadio0 2023-04-16 16:11:09 +02:00
de7b013716 text cursor 2023-04-16 15:14:08 +02:00
dab898c391 focus 2023-04-16 03:37:24 +02:00
f53e09b52e remix icons 2023-04-16 03:36:30 +02:00
176e72c465 small changes 2023-04-05 21:54:42 +02:00
cedd63dd90 source code 2023-03-31 02:33:57 +02:00
60b5f74e94 restart on no hash 2023-03-31 02:22:07 +02:00
c47cd498c0 fix onhashchange 2023-03-31 02:15:39 +02:00
6ee740cebd onhashchange 2023-03-31 01:55:12 +02:00
785f1460fb save to location.hash 2023-03-31 01:47:22 +02:00
80fa25c092 fix disable insertRadio 2023-03-31 01:35:47 +02:00
ac91453013 table-success 2023-03-30 21:26:14 +02:00
8c62d6bb0e text insert radio to the right 2023-03-30 19:38:36 +02:00
8f412bdd0f keyboard button 2023-03-30 03:42:32 +02:00
dee27c9ca7 ui 2023-03-30 02:55:15 +02:00
20 changed files with 382 additions and 2436 deletions

29
400.php Normal file → Executable file
View File

@ -6,17 +6,22 @@
<title>Requête incorrecte</title>
</head>
<body>
<header>
<h1>Requête incorrecte</h1>
</header>
L'adresse URL doit être de la forme :<br/>
<?=$dirUrl?>/?<em>grille</em><br/>
<em>grille</em> étant une suite de 81 caractères représentant la grille de gauche à droite puis de haut en bas, soit :
<ul>
<li>un chiffre entre 1 et 9 pour les cases connues</li>
<li>un point pour les case vides</li>
</ul>
Exemple :<br/>
<a href='<?=$newGridUrl?>'><?=$newGridUrl?></a>
<nav class="navbar mb-4">
<h1 class="display-4 text-center m-auto">Sudoku</h1>
</nav>
<main class="container my-4">
<header>
<h1 class="mb-4">Requête incorrecte</h1>
</header>
L'adresse URL doit être de la forme :<br/>
<?=$dirUrl?>/?<em>grille</em><br/>
<em>grille</em> étant une suite de 81 caractères représentant la grille de gauche à droite puis de haut en bas, soit :
<ul>
<li>un chiffre entre 1 et 9 pour les cases connues</li>
<li>un tiret (-) pour les case vides</li>
</ul>
Exemple :<br/>
<a href='<?=$newGridUrl?>'><?=$newGridUrl?></a>
</main>
</body>
</html>

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Sudoku
Web sudoku assistant
![screenshot](https://git.malingrey.fr/adrien/sudoku/raw/branch/master/thumbnail.png)

29
classes.php Normal file → Executable file
View File

@ -1,5 +1,5 @@
<?php
const UNKNOWN = ".";
const UNKNOWN = "-";
$validGrids = array();
@ -27,15 +27,19 @@
class Box {
public $values = array('1', '2', '3', '4', '5', '6', '7', '8', '9');
public $value = UNKNOWN;
public $rowId;
public $columnId;
public $regionId;
public $candidates;
public $candidateRemoved = array();
public $neighbourhood = array();
function __construct($rowId, $columnId, $regionId) {
$this->value = UNKNOWN;
$this->rowId = $rowId;
$this->columnId = $columnId;
$this->regionId = $regionId;
$this->candidates = $this->values;
$this->candidateRemoved = array();
$this->neighbourhood = array();
}
function searchCandidates() {
@ -48,8 +52,13 @@
}
class Grid {
function __construct() {
$this->boxes = array();
private $boxes = array();
private $rows;
private $columns;
private $regions;
function __construct($gridStr="") {
$this->rows = array_fill(0, 9, array());
$this->columns = array_fill(0, 9, array());
$this->regions = array_fill(0, 9, array());
@ -74,6 +83,12 @@
if ($box != $neighbour && !in_array($neighbour, $box->neighbourhood))
$box->neighbourhood[] = $neighbour;
}
if ($gridStr) {
$this->import($gridStr);
} else {
$this->generate();
}
}
function import($gridStr) {

File diff suppressed because one or more lines are too long

2018
css/bootstrap-icons.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

0
favicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

Binary file not shown.

68
head.php Normal file → Executable file
View File

@ -1,36 +1,32 @@
<meta charset="utf-8" />
<title>Sudoku</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="css/bootstrap-dark.min.css" rel="stylesheet" type="text/css" title="Automatique" />
<link href="css/bootstrap.min.css" rel="alternate stylesheet" type="text/css" title="Clair">
<link href="css/bootstrap-night.min.css" rel="alternate stylesheet" type="text/css" title="Sombre">
<link rel="stylesheet" type="text/css" href="css/bootstrap-icons.css" />
<link rel="stylesheet" type="text/css" href="css/style.css" />
<link rel="apple-touch-icon" href="thumbnail.php?size=57" sizes="57x57">
<link rel="apple-touch-icon" href="thumbnail.php?size=114" sizes="114x114">
<link rel="apple-touch-icon" href="thumbnail.php?size=72" sizes="72x72">
<link rel="apple-touch-icon" href="thumbnail.php?size=144" sizes="144x144">
<link rel="apple-touch-icon" href="thumbnail.php?size=60" sizes="60x60">
<link rel="apple-touch-icon" href="thumbnail.php?size=120" sizes="120x120">
<link rel="apple-touch-icon" href="thumbnail.php?size=76" sizes="76x76">
<link rel="apple-touch-icon" href="thumbnail.php?size=152" sizes="152x152">
<link rel="icon" type="image/png" href="thumbnail.php?size=196" sizes="196x196">
<link rel="icon" type="image/png" href="thumbnail.php?size=160" sizes="160x160">
<link rel="icon" type="image/png" href="thumbnail.php?size=96" sizes="96x96">
<link rel="icon" type="image/png" href="thumbnail.php?size=16" sizes="16x16">
<link rel="icon" type="image/png" href="thumbnail.php?size=32" sizes="32x32">
<link rel="manifest" href="manifest.php">
<meta property="og:title" content="Sudoku" />
<meta property="og:type" content="website" />
<meta property="og:url"
content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"]?>" />
<meta property="og:image"
content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])?>/thumbnail.php?size=200" />
<meta property="og:image:width" content="200" />
<meta property="og:image:height" content="200" />
<meta property="og:description"
content="Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9." />
<meta property="og:locale" content="fr_FR" />
<meta property="og:site_name" content="<?=$_SERVER["HTTP_HOST"]?>" />
<script src='js/sudoku.js'></script>
<script src="js/bootstrap.bundle.min.js" integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" crossorigin="anonymous"></script>
<meta charset="utf-8" />
<title>Sudoku</title><meta property="og:title" content="Sudoku" />
<meta property="og:type" content="game" />
<meta name="description" property="og:description" content="Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9." />
<link rel="canonical" href="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])?>" />
<meta property="og:url" content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"]?>" />
<meta property="og:image" content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])?>/thumbnail.php?size=200&grid=<?=$currentGrid?>" />
<meta property="og:image:width" content="200" />
<meta property="og:image:height" content="200" />
<meta name="Language" CONTENT="fr" /><meta property="og:locale" content="fr_FR" />
<meta property="og:site_name" content="<?=$_SERVER["HTTP_HOST"]?>" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-dark.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.2.0/fonts/remixicon.css" rel="stylesheet">
<link href="style.css" rel="stylesheet" type="text/css" />
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=196" sizes="196x196" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=160" sizes="160x160" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=96" sizes="96x96" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=16" sizes="16x16" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=32" sizes="32x32" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=152" sizes="152x152" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=144" sizes="144x144" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=120" sizes="120x120" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=114" sizes="114x114" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=57" sizes="57x57" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=72" sizes="72x72" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=60" sizes="60x60" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=76" sizes="76x76" rel="apple-touch-icon">
<link href="manifest.php?grid=<?=$currentGrid?>" rel="manifest">

57
index.php Normal file → Executable file
View File

@ -1,40 +1,45 @@
<?php
require("classes.php");
session_start();
$fullUrl = $_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"];
$dirUrl = dirname($fullUrl);
$currentGrid = strip_tags($_SERVER['QUERY_STRING']);
if (preg_match("/^[1-9.]{81}$/", $currentGrid)) {
if (!isset($_SESSION[$currentGrid]) || $_SESSION[$currentGrid] != "checked") {
$grid = new Grid();
$grid->import($currentGrid);
if ($grid->containsDuplicates()) {
if (preg_match("/^[1-9-]{81}$/", $currentGrid)) {
session_id($currentGrid);
session_start(["use_cookies" => false]);
if (!array_key_exists("nbSolutions", $_SESSION)) {
$grid = new Grid($currentGrid);
$_SESSION["nbSolutions"] = $grid->containsDuplicates() ? -1 : $grid->countSolutions(2);
}
switch($_SESSION["nbSolutions"]) {
case -1:
$warning = "Cette grille contient des doublons.";
} else {
switch($grid->countSolutions(2)) {
case 0:
$warning = "Cette grille n'a pas de solution.";
break;
case 1:
$validGrids[] = $currentGrid;
break;
default:
$warning = "Cette grille a plusieurs solutions.";
}
}
break;
case 0:
$warning = "Cette grille n'a pas de solution.";
break;
case 1:
break;
default:
$warning = "Cette grille a plusieurs solutions.";
}
require("sudoku.php");
} else {
$grid = new Grid();
$grid->generate();
$gridAsString = $grid->toString();
$newGridUrl = "$dirUrl/?$gridAsString";
$_SESSION[$gridAsString] = "checked";
if (!$currentGrid) {
header("Location: $newGridUrl");
} else {
if ($currentGrid) {
require("400.php");
} else {
$grid = new Grid();
$gridAsString = $grid->toString();
$newGridUrl = "$dirUrl/?$gridAsString";
session_id($gridAsString);
session_start(["use_cookies" => false]);
$_SESSION["nbSolutions"] = 1;
header("Location: $newGridUrl");
}
}
?>

File diff suppressed because one or more lines are too long

53
manifest.php Normal file → Executable file
View File

@ -1,7 +1,6 @@
<?php
session_start();
if (isset($_SESSION["currentGrid"]))
$currentGrid = $_SESSION["currentGrid"];
if (isset($_GET["grid"]))
$currentGrid = $_GET["grid"];
else
$currentGrid = ".";
?>
@ -10,27 +9,27 @@
"name": "Sudoku",
"description": "Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.",
"icons": [{
"src": "thumbnail.php?size=48",
"src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72",
"src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96",
"src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144",
"src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168",
"src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192",
"src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
"sizes": "192x192",
"type": "image/png"
}],
@ -47,27 +46,27 @@
"description": "Continuer cette grille de sudoku",
"url": "<?=$currentGrid?>",
"icons": [{
"src": "thumbnail.php?size=48",
"src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72",
"src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96",
"src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144",
"src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168",
"src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192",
"src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
"sizes": "192x192",
"type": "image/png"
}]
@ -78,27 +77,27 @@
"description": "Grille de sudoku vierge",
"url": ".................................................................................",
"icons": [{
"src": "thumbnail.php?size=48",
"src": "thumbnail.php?size=48&grid=.................................................................................",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72",
"src": "thumbnail.php?size=72&grid=.................................................................................",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96",
"src": "thumbnail.php?size=96&grid=.................................................................................",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144",
"src": "thumbnail.php?size=144&grid=.................................................................................",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168",
"src": "thumbnail.php?size=168&grid=.................................................................................",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192",
"src": "thumbnail.php?size=192&grid=.................................................................................",
"sizes": "192x192",
"type": "image/png"
}]
@ -109,27 +108,27 @@
"description": "Nouvelle grille de sudoku",
"url": ".",
"icons": [{
"src": "thumbnail.php?size=48",
"src": "thumbnail.php?size=48&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72",
"src": "thumbnail.php?size=72&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96",
"src": "thumbnail.php?size=96&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144",
"src": "thumbnail.php?size=144&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168",
"src": "thumbnail.php?size=168&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192",
"src": "thumbnail.php?size=192&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "192x192",
"type": "image/png"
}]

10
service-worker.php → service-worker.js Normal file → Executable file
View File

@ -1,11 +1,3 @@
<?php
session_start();
if (isset($_SESSION["currentGrid"]))
$currentGrid = $_SESSION["currentGrid"];
else
$currentGrid = ".";
header ("Content-type: application/javascript");
?>
/*
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
@ -24,7 +16,7 @@ Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "<?=$currentGrid?>";
const OFFLINE_URL = ".";
self.addEventListener("install", (event) => {
event.waitUntil(

55
css/style.css → style.css Normal file → Executable file
View File

@ -1,8 +1,3 @@
body {
width: min-content;
margin: auto;
}
input[type="number"]::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
@ -25,14 +20,25 @@ table input {
padding: 0 !important;
margin: 0 !important;
text-align: center;
-moz-appearance: textfield !important;
appearance: textfield !important;
border-radius: 0 !important;
}
table td.table-primary input,
table td.table-active input,
table.table-success input,
td.table-danger input:disabled,
table input:not([disabled]) {
background: transparent !important;
color: inherit !important;
}
table input:not([disabled]):hover {
background: #9fb9b945 !important;
}
table input:disabled {
background-position: center !important;
}
tr:nth-child(3n+1) td input {
@ -86,10 +92,14 @@ tr:last-child td:last-child input {
td {
padding: 0 !important;
margin: 0 !important;
transition: background-color .4s, box-shadow .4s !important;
border: 0 !important;
}
td,
table input {
transition: background-color .4s, box-shadow .4s !important;
}
.context-menu li {
cursor: default;
}
@ -98,7 +108,7 @@ td {
cursor: inherit !important;
}
.table-danger {
.not-allowed {
cursor: not-allowed !important;
}
@ -108,10 +118,15 @@ table {
button,
label {
padding: .375rem .6rem !important;
/*! padding: .375rem .65rem !important; */
cursor: pointer;
}
button i,
label i {
margin: 0 -.125rem;
}
button:disabled,
:disabled+label {
cursor: not-allowed !important;
@ -121,20 +136,26 @@ table input:enabled {
cursor: inherit;
}
.modal-content {
z-index: 1000;
}
.bi::before {
vertical-align: 0;
}
.pencil {
color: var(--bs-secondary-color) !important;
}
#colorPickerInput{
width: 2.3rem;
height: auto;
padding: .375rem;
}
#colorPickerLabel {
color: var(--bs-body-color);
}
@media (prefers-color-scheme:dark) {
.pencil {
color: #5a5a5a !important;
}
}
.context-menu {
z-index: 100;
}

View File

@ -1,16 +1,18 @@
const VALUES = "123456789"
const VALUES = "123456789"
const UNKNOWN = '.'
const SUGESTION_DELAY = 60000 //ms
let boxes = []
let rows = Array.from(Array(9), x => [])
let columns = Array.from(Array(9), x => [])
let regions = Array.from(Array(9), x => [])
let boxes = []
let rows = Array.from(Array(9), x => [])
let columns = Array.from(Array(9), x => [])
let regions = Array.from(Array(9), x => [])
let areaNames = {
ligne: rows,
colonne: columns,
région: regions,
}
let valueToInsert = ""
let history = []
let accessKeyModifiers = "AccessKey+"
let easyBoxes = []
let insertRadios = []
let easyBoxes = []
let insertRadios = []
function shuffle(iterable) {
array = Array.from(iterable)
@ -33,19 +35,17 @@ window.onload = function() {
for (let box of row.getElementsByTagName('input')) {
let regionId = rowId - rowId % 3 + Math.floor(columnId / 3)
if (!box.disabled) {
box.onfocus = onfocus
box.oninput = oninput
box.onblur = onblur
box.onclick = onclick
box.onfocus = onfocus
box.oninput = oninput
box.onblur = onblur
box.onclick = onclick
box.onmouseenter = onmouseenter
box.onmouseleave = onmouseleave
box.previousValue = ""
box.previousPlaceholder = ""
}
box.oncontextmenu = oncontextmenu
box.rowId = rowId
box.columnId = columnId
box.regionId = regionId
box.rowId = rowId
box.columnId = columnId
box.regionId = regionId
boxes.push(box)
rows[rowId].push(box)
columns[columnId].push(box)
@ -55,50 +55,61 @@ window.onload = function() {
rowId++
}
if (localStorage["sightCheckbox.checked"] == "true") sightCheckbox.checked = true
if (localStorage["highlighterCheckbox.checked"] == "true") highlighterCheckbox.checked = true
loadSavedGame()
if (localStorage["tool"] == "sight") sightCheckbox.checked = true
else if (localStorage["tool"] == "highlighter") highlighterCheckbox.checked = true
colorPickerInput.value = window.getComputedStyle(grid).getPropertyValue("--bs-body-color")
boxes.forEach(box => {
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId]))
box.andNeighbourhood = Array.from(box.neighbourhood)
box.neighbourhood.delete(box)
box.neighbourhood = Array.from(box.neighbourhood)
searchCandidatesOf(box)
})
insertRadios = Array.from(insertRadioGroup.getElementsByTagName("input"))
insertRadios = Array.from(insertRadioGroup.getElementsByTagName("input")).slice(1)
for (label of document.getElementsByTagName("label")) {
label.control.label = label
}
if (/Win/.test(navigator.platform) || /Linux/.test(navigator.platform)) accessKeyModifiers = "Alt+Maj+"
else if (/Mac/.test(navigator.platform)) accessKeyModifiers = "⌃⌥"
let accessKeyModifiers = (/Win/.test(navigator.userAgent) || /Linux/.test(navigator.userAgent)) ? "Alt+Maj+"
: (/Mac/.test(navigator.userAgent)) ? "⌃⌥"
: "AccessKey+"
for (node of document.querySelectorAll("*[accesskey]")) {
shortcut = ` [${node.accessKeyLabel||(accessKeyModifiers+node.accessKey)}]`
if (node.title) node.title += shortcut
else if (node.label) node.label.title += shortcut
}
refreshUI()
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register(`service-worker.php?location=${location.pathname}`)
}
loadGame(history.state)
}
function loadSavedGame() {
const savedGame = localStorage[location.search]
if (savedGame) {
window.onpopstate = (event) => loadGame(event.state)
function loadGame(state) {
if (state) {
boxes.forEach((box, i) => {
if (!box.disabled && savedGame[i] != UNKNOWN) {
box.value = savedGame[i]
box.previousValue = savedGame[i]
if (!box.disabled) {
box.value = state.boxesValues[i]
box.placeholder = state.boxesPlaceholders[i]
}
})
fixGridLink.href = "?" + savedGame
restartLink.classList.remove("disabled")
undoButton.disabled = false
fixGridLink.href = "?" + state.boxesValues.map(value => value || UNKNOWN).join("")
} else {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.placeholder = ""
})
restartLink.classList.add("disabled")
undoButton.disabled = true
fixGridLink.href = ""
}
checkBoxes()
enableRadio()
highlight()
}
function searchCandidatesOf(box) {
@ -108,10 +119,10 @@ function searchCandidatesOf(box) {
switch (box.candidates.size) {
case 0:
box.title = "Aucune possibilité !"
break
break
case 1:
box.title = "Une seule possibilité [Clic-droit]"
break
break
default:
box.title = box.candidates.size + " possibilités [Clic-droit]"
}
@ -120,12 +131,16 @@ function searchCandidatesOf(box) {
function onfocus() {
if (pencilRadio.checked) {
//this.type = "text"
this.value = this.placeholder
this.type = "text"
this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil")
} else {
this.select()
}
if (penColor && inkPenRadio.checked) {
this.style.setProperty("color", penColor)
}
this.style.caretColor = valueToInsert ? "transparent" : "auto"
}
@ -150,77 +165,56 @@ function onclick() {
}
function oninput() {
history.push({
box: this,
value: this.previousValue,
placeholder: this.previousPlaceholder
})
undoButton.disabled = false
saveButton.disabled = false
restartButton.disabled = false
if (pencilRadio.checked) {
this.previousValue = ""
this.previousPlaceholder = this.value
} else {
this.previousValue = this.value
this.previousPlaceholder = this.placeholder
refreshBox(this)
if (inkPenRadio.checked) {
checkBoxes()
enableRadio()
highlight()
fixGridLink.href = "?" + boxes.map(box => box.value || UNKNOWN).join("")
}
saveGame()
}
function refreshBox(box) {
checkBox(box)
refreshUI()
}
function checkBox(box) {
box.andNeighbourhood.forEach(neighbour => {
neighbour.setCustomValidity("")
neighbour.classList.remove("is-invalid")
searchCandidatesOf(neighbour)
if (neighbour.candidates.size == 0) {
neighbour.setCustomValidity("Aucun chiffre possible !")
function checkBoxes() {
boxes.forEach(box => {
box.setCustomValidity("")
box.classList.remove("is-invalid")
box.parentElement.classList.remove("table-danger")
searchCandidatesOf(box)
if (box.candidates.size == 0) {
box.setCustomValidity("Aucun chiffre possible !")
box.classList.add("is-invalid")
}
})
if (box.value) {
for (area of[{
name: "région",
neighbours: regions[box.regionId]
}, {
name: "ligne",
neighbours: rows[box.rowId]
}, {
name: "colonne",
neighbours: columns[box.columnId]
}, ])
for (neighbour of area.neighbours)
if (box != neighbour && box.value == neighbour.value) {
for (neighbour of[box, neighbour]) {
neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${area.name}.`)
for (let [areaName, areas] of Object.entries(areaNames))
for (area of areas)
area.filter(box => box.value).sort((box, neighbour) => {
if(box.value == neighbour.value) {
area.forEach(neighbour => neighbour.parentElement.classList.add("table-danger"))
for (neighbour of [box, neighbour]) {
neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${areaName}.`)
neighbour.classList.add("is-invalid")
}
}
}
return box.value - neighbour.value
})
if (box.form.checkValidity()) { // Correct grid
if (sudokuForm.checkValidity()) { // Correct grid
if (boxes.filter(box => box.value == "").length == 0) {
saveButton.disabled = true
grid.classList.add("table-success")
setTimeout(() => {
if (confirm(`Bravo ! Vous avez résolu la grille. En voulez-vous une autre ?`))
location = "."
}, 400)
} else {
grid.classList.remove("table-success")
}
} else { // Errors on grid
box.form.reportValidity()
grid.classList.remove("table-success")
sudokuForm.reportValidity()
}
}
function refreshUI() {
enableRadio()
highlight()
}
function enableRadio() {
for (radio of insertRadios) {
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(radio.value))) {
@ -229,8 +223,11 @@ function enableRadio() {
} else {
radio.disabled = true
radio.label.title = `Tous les ${radio.value} sont posés.`
if (valueToInsert == radio.value)
if (valueToInsert == radio.value) {
insertRadio0.checked = true
valueToInsert = ""
grid.style.cursor = "text"
}
}
}
}
@ -267,93 +264,69 @@ function onblur() {
if (this.classList.contains("pencil")) {
this.placeholder = this.value
this.value = ""
//this.type = "number"
this.type = "number"
this.classList.remove("pencil")
}
}
function saveGame() {
history.pushState({
boxesValues: boxes.map(box => box.value),
boxesPlaceholders: boxes.map(box => box.placeholder)
}, "")
restartLink.classList.remove("disabled")
undoButton.disabled = false
}
function onmouseenter(event) {
if (sightCheckbox.checked){
box = event.target
box.neighbourhood.concat([box]).forEach(neighbour => {
box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.add("table-active")
})
box.neighbourhood.forEach(neighbour => {
if (valueToInsert && neighbour.value == valueToInsert) {
for (neighbour of[box, neighbour]) {
neighbour.parentElement.classList.add("table-danger")
for (neighbour of [box, neighbour]) {
neighbour.parentElement.classList.add("table-danger", "not-allowed")
}
}
})
}
}
}
function onmouseleave(event) {
if (sightCheckbox.checked){
box = event.target
box.neighbourhood.concat([box]).forEach(neighbour => {
neighbour.parentElement.classList.remove("table-active")
neighbour.parentElement.classList.remove("table-danger")
box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.remove("table-active", "table-danger", "not-allowed")
})
}
}
function insert(radio) {
if (radio.value == valueToInsert) {
valueToInsert = ""
radio.checked = false
if (radio.value && valueToInsert == radio.value) {
radio.blur()
insertRadio0.checked = true
insert(0)
} else {
valueToInsert = radio.value
grid.style.cursor = valueToInsert ? "pointer" : "text"
highlight()
}
grid.style.cursor = valueToInsert ? "copy" : "text"
highlight()
}
function undo() {
if (history.length) {
const previousState = history.pop()
previousState.box.value = previousState.value
previousState.box.placeholder = previousState.placeholder
refreshBox(previousState.box)
if (history.length < 1) {
undoButton.disabled = true
saveButton.disabled = true
}
}
let penColor
function changeColor() {
penColor = colorPickerInput.value
colorPickerLabel.style.color = colorPickerInput.value
}
function restart() {
if (confirm("Effacer toutes les cases ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.previousValue = ""
box.placeholder = ""
box.previousPlaceholder = ""
box.setCustomValidity("")
})
let history = []
undoButton.disabled = true
restartButton.disabled = true
boxes.forEach(searchCandidatesOf)
refreshUI()
}
}
function save() {
let saveGame = boxes.map(box => box.value || UNKNOWN).join("")
localStorage[location.search] = saveGame
fixGridLink.href = "?" + saveGame
saveButton.disabled = true
alert("Partie sauvegardée")
}
window.onbeforeunload = function(event) {
localStorage["sightCheckbox.checked"] = sightCheckbox.checked
localStorage["highlighterCheckbox.checked"] = highlighterCheckbox.checked
if (!saveButton.disabled) {
event.preventDefault()
event.returnValue = "La partie n'est pas sauvegardée. Quitter quand même ?"
location.hash = ""
}
}
@ -380,7 +353,7 @@ function oncontextmenu(event) {
li.onclick = function(e) {
contextMenu.style.display = "none"
valueToInsert = e.target.innerText
grid.style.cursor = "copy"
grid.style.cursor = "pointer"
document.getElementById("insertRadio" + valueToInsert).checked = true
box.onclick()
}
@ -393,7 +366,7 @@ function oncontextmenu(event) {
} else {
li = document.createElement("li")
li.innerText = "Aucune possibilité !"
li.classList.add("error")
li.classList = "list-group-item list-group-item-action disabled"
contextMenu.appendChild(li)
}
contextMenu.style.left = `${event.pageX}px`
@ -412,4 +385,10 @@ document.onkeydown = function(event) {
event.preventDefault()
contextMenu.style.display = "none"
}
}
window.onbeforeunload = function(event) {
saveGame()
if (sightCheckbox.checked) localStorage["tool"] = "sight"
else if (highlighterCheckbox.checked) localStorage["tool"] = "highlighter"
}

View File

@ -2,107 +2,89 @@
<html lang='fr' prefix="og: https://ogp.me/ns#">
<head>
<?php require_once("head.php") ?>
<?php require_once("head.php") ?>
</head>
<body class="text-center">
<header>
<h1 class="display-4 mb-3">Sudoku</h1>
</header>
<div class='d-flex justify-content-between mb-2'>
<div class='btn-group'>
<input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked />
<label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'>
<i class="bi bi-pen-fill"></i>
</label>
<input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' />
<label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'>
<i class="bi bi-pencil-fill"></i>
</label>
<input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' />
<label for='eraserRadio' class='btn btn-primary' title='Effacer une case'>
<i class="bi bi-eraser-fill"></i>
</label>
</div>
<div class='btn-group'>
<input type='checkbox' id='sightCheckbox' class='btn-check' onclick='highlighterCheckbox.checked = false; refreshUI()' />
<label for='sightCheckbox' class='btn btn-primary' title='Surligner la ligne, la colonne et la région de la case survolée'>
<i class="bi bi-plus-circle-fill"></i>
</label>
<input type='checkbox' id='highlighterCheckbox' class='btn-check' onclick='sightCheckbox.checked = false; refreshUI()' />
<label for='highlighterCheckbox' class='btn btn-primary' title='Surligner les lignes, colonnes et régions contenant déjà le chiffre sélectionné'>
<i class="bi bi-magic"></i>
</label>
</div>
<button id="hintButton" type="button" class='btn btn-primary' onclick="showHint()" title="Montrer une case avec une seule possibilité" accesskey="H" disabled="">
<i class="bi bi-lightbulb-fill"></i>
</button>
<button id='restartButton' type='button' class='btn btn-primary' onclick='restart()' disabled title='Recommencer'>
<i class="bi bi-x-circle-fill"></i></i>
</button>
<button id='undoButton' type='button' class='btn btn-primary' onclick='undo()' disabled title='Annuler' accesskey='Z'>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button id='saveButton' type='button' class='btn btn-primary' onclick='save()' disabled title='Sauvegarder' accesskey='S'>
<i class="bi bi-save-fill"></i>
</button>
</div>
<form id='sudokuForm' class='needs-validation' novalidate>
<table id='grid' class='table mb-2'>
<tbody>
<?php
for ($row = 0; $row < 81; $row += 9) {
?>
<tr class="input-group d-inline-block">
<?php
for ($column = 0; $column < 9; $column++) {
$value = $currentGrid[$row+$column];
if ($value == UNKNOWN) {
?>
<td><input type='number' min='1' max='9' step='1' value='' class='form-control'
title='Valeurs possibles [Clic-droit]' /></td>
<?php
} else {
?>
<td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td>
<?php
}
}
?>
</tr>
<?php
}
?>
</tbody>
</table>
</form>
<div class='d-flex mb-2'>
<div id='insertRadioGroup' class='radioGroup btn-group flex-fill'>
<?php
for($value=1; $value<=9; $value++) {
echo " <input type='radio'class='btn-check' id='insertRadio$value' value='$value' name='insertRadioGroup' onclick='insert(this)' accesskey='$value' disabled /><label for='insertRadio$value' class='btn btn-primary' title='Insérer un $value'>$value</label>\n";
}
?>
</div>
</div>
<div class='mb-3'>
<?php
if (isset($warning))
echo(" <strong>⚠️ $warning ⚠️</strong><br/>\n");
else
echo(" Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.\n")
?>
<body>
<nav class="navbar mb-4">
<h1 class="display-4 text-center m-auto">Sudoku</h1>
</nav>
<div class="row g-0">
<main class="col-md-6 order-md-1">
<div class="text-center m-auto" style="width: min-content;">
<div class='d-flex justify-content-between mb-2'>
<div class='btn-group'>
<input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked />
<label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'><i class="ri-ball-pen-fill"></i></label>
<input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' />
<label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'><i class="ri-pencil-fill"></i></label>
<input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' />
<label for='eraserRadio' class='btn btn-primary' title='Effacer une case'><i class="ri-eraser-fill"></i></label>
</div>
<input type="color" class="btn-check" id="colorPickerInput" title="Changer la couleur" oninput="changeColor()"/>
<label id="colorPickerLabel" for="colorPickerInput" class="btn btn-primary" title="Changer de couleur"><i class="ri-palette-fill"></i></label>
<div class='btn-group'>
<input type='checkbox' id='sightCheckbox' class='btn-check' onclick='highlighterCheckbox.checked = false; highlight()' />
<label for='sightCheckbox' class='btn btn-info' title='Surligner la ligne, la colonne et la région de la case survolée'><i class="ri-focus-3-line"></i></label>
<input type='checkbox' id='highlighterCheckbox' class='btn-check' onclick='sightCheckbox.checked = false; highlight()' />
<label for='highlighterCheckbox' class='btn btn-info' title='Surligner les lignes, colonnes et régions contenant déjà le chiffre sélectionné'><i class="ri-mark-pen-fill"></i></label>
</div>
<button id="hintButton" type="button" class='btn btn-info' onclick="showHint()" title="Montrer une case avec une seule possibilité" accesskey="H" disabled=""><i class="ri-lightbulb-line"></i></button>
<a id='restartLink' class='btn btn-primary disabled' href="" title='Recommencer'><i class="ri-restart-line"></i></a>
<button id='undoButton' type='button' class='btn btn-primary' onclick='window.history.back()' disabled title='Annuler' accesskey='Z'><i class="ri-arrow-go-back-fill"></i></button>
</div>
<form id='sudokuForm' class='needs-validation' novalidate>
<table id='grid' class='table mb-2'>
<tbody>
<?php for ($row = 0; $row < 81; $row += 9): ?>
<tr class="input-group d-inline-block w-auto">
<?php for ($column = 0; $column < 9; $column++): $value = $currentGrid[$row+$column]; ?>
<?php if ($value == UNKNOWN): ?>
<td><input type='number' min='1' max='9' step='1' value='' class='form-control' /></td>
<?php else: ?>
<td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td>
<?php endif ?>
<?php endfor?>
</tr>
<?php endfor?>
</tbody>
</table>
</form>
<div class='d-flex mb-4'>
<div id='insertRadioGroup' class='radioGroup btn-group flex-fill'>
<input type='radio' class='btn-check' id='insertRadio0' value='' name='insertRadioGroup' onclick='insert(this)' accesskey='0' checked /><label for='insertRadio0' class='btn btn-primary' title='Clavier'><i class="ri-input-cursor-move"></i></label>
<?php for($value=1; $value<=9; $value++): ?>
<input type='radio' class='btn-check' id='insertRadio<?=$value?>' value='<?=$value?>' name='insertRadioGroup' onclick='insert(this)' accesskey='<?=$value?>' disabled />
<label for='insertRadio<?=$value?>' class='btn btn-primary' title='Insérer un <?=$value?>'><?=$value?></label>
<?php endfor ?>
</div>
</div>
<div class='mb-3'>
<?php if (isset($warning)): ?>
<strong>⚠️ <?=$warning?> ⚠️</strong><br/>
<?php else: ?>
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
<?php endif?>
</div>
</div>
</main>
<aside class="col-md-3 text-center text-md-start">
<div class="d-flex flex-column flex-shrink-0 p-3">
<ul class="nav nav-pills flex-column">
<li><a href="." class="nav-link link-body-emphasis">Nouvelle grille</a></li>
<li><a href="" class="nav-link link-body-emphasis">Lien vers cette grille</a></li>
<li><a href="?---------------------------------------------------------------------------------" class="nav-link link-body-emphasis">Grille vierge</a></li>
<li><a id="fixGridLink" href="" class="nav-link link-body-emphasis">Figer la grille</a></li>
<li><a href="https://git.malingrey.fr/adrien/Sudoku" class="nav-link link-body-emphasis">Code source</a></li>
<li><a href=".." class="nav-link link-body-emphasis">Autres jeux</a></li>
</ul>
</div>
</aside>
</div>
<ul id='contextMenu' class='context-menu modal-content shadow list-group w-auto position-absolute'></ul>
<footer>
<div id='links' class='list-group mb-2'>
<a href='.' class='list-group-item list-group-item-action'>Nouvelle grille</a>
<a href='' class='list-group-item list-group-item-action'>Lien vers cette grille</a>
<a href='?.................................................................................' class='list-group-item list-group-item-action'>Grille
vierge</a>
<a href='' id='fixGridLink' class='list-group-item list-group-item-action'>Figer la grille enregistrée</a>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<script src='sudoku.js' defer></script>
<script>navigator?.serviceWorker.register('service-worker.js')</script>
</body>
</html>

7
thumbnail.php Normal file → Executable file
View File

@ -1,10 +1,9 @@
<?php
require("classes.php");
session_start();
if (isset($_SESSION["currentGrid"]))
$currentGrid = $_SESSION["currentGrid"];
if (isset($_GET["grid"]) && preg_match("/^[1-9-]{81}$/", $_GET["grid"]))
$currentGrid = $_GET["grid"];
else
$currentGrid = ".528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...";
$currentGrid = "-528-3----4-9-1---39-562------73-129---1-64-7---42-3656-13-5---28-6-4---4-5287---";
header ("Content-type: image/png");
if (isset($_GET['size']))
$size = (int) $_GET['size'];

BIN
thumbnail.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB