20 Commits

Author SHA1 Message Date
bae67fc1ec chore: bump version to 1.11.0 2026-02-12 17:05:06 +01:00
47426d529a ui: unify app icons with new nonogram-style N logo 2026-02-12 17:03:47 +01:00
71edc3103d chore: bump version to 1.10.0 2026-02-12 16:56:53 +01:00
30c1faeae4 fix: make app link clickable in generated SVG 2026-02-12 16:52:36 +01:00
b90809fca1 style: dodaj margines po prawej stronie planszy w wersji desktop 2026-02-12 16:43:21 +01:00
322182245c docs: add .gpg to gitignore 2026-02-12 15:54:13 +01:00
324b761d37 1.9.14 2026-02-12 15:08:45 +01:00
4dab0e2c63 chore: homepage w package.json; __APP_HOMEPAGE__ w Vite; użycie w shareUtils 2026-02-12 15:00:00 +01:00
b3e08b53fc Update README.md 2026-02-12 13:33:02 +00:00
3ce15ed794 1.9.13 2026-02-12 14:28:11 +01:00
bd310d8305 ui: linie pomocnicze co 5 w opisach wierszy i kolumn 2026-02-12 14:27:51 +01:00
a22897e19e 1.9.12 2026-02-12 14:21:05 +01:00
6ec3e66e9c i18n: uzupełnij etykiety language.*; skrypty tłumaczeń 2026-02-12 14:21:05 +01:00
defde986a4 docs: usuń sekcję linku z README 2026-02-12 14:19:44 +01:00
bb3752b8ae 1.9.11 2026-02-12 14:08:40 +01:00
be04f333b0 feat: tłumaczenia symulacji w wielu językach; prędkości x32/x64; README EN; MIT license 2026-02-12 14:08:30 +01:00
fc25246594 1.9.10 2026-02-12 13:58:44 +01:00
d7e104c17a fix: priorytet ESC i zatrzymanie propagacji w modalach 2026-02-12 13:58:28 +01:00
c197445f35 1.9.9 2026-02-12 13:51:28 +01:00
57ae54d716 feat: ESC zamyka wszystkie modale (Custom Game, Simulation, Win) 2026-02-12 13:51:10 +01:00
22 changed files with 13515 additions and 129 deletions

8
.gitignore vendored
View File

@@ -1,7 +1,3 @@
.gpg/
node_modules
dist
.DS_Store
.vscode
.idea
*.log
dev-dist

23
LICENSE Normal file
View File

@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2026 gkucmierz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
See README.md for project description.

View File

@@ -1,3 +1,11 @@
# Nonograms
Link do aplikacji: https://nonograms.7u.pl
## Description
Nonograms is a modern, fast, and accessible logic puzzle game (also known as Picross or Griddlers). Solve pixel-art puzzles by marking cells according to numeric clues for rows and columns. The app features:
- Clean UX with keyboard and touch support
- Multiple languages and PWA support (installable on desktop and mobile)
- Difficulty simulation and guide to learn solving strategies
- Shareable puzzles and persistent progress
Play online at https://nonograms.7u.pl or install as a PWA for an app-like experience.

28
check_i18n.cjs Normal file
View File

@@ -0,0 +1,28 @@
const fs = require('fs');
const fileContent = fs.readFileSync('src/composables/useI18n.js', 'utf8');
const match = fileContent.match(/const messages = ({[\s\S]*?});/);
if (!match) {
console.error('Could not find messages object');
process.exit(1);
}
const messagesStr = match[1];
const messages = eval(`(${messagesStr})`);
const enKeys = Object.keys(messages.en);
const languages = Object.keys(messages);
const missing = {};
languages.forEach(lang => {
if (lang === 'en') return;
const langKeys = Object.keys(messages[lang]);
const missingKeys = enKeys.filter(k => !langKeys.includes(k));
if (missingKeys.length > 0) {
missing[lang] = missingKeys;
}
});
console.log(JSON.stringify(missing, null, 2));

92
dev-dist/sw.js Normal file
View File

@@ -0,0 +1,92 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict';
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "index.html",
"revision": "0.ohmkvc7m8mo"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
}));

3377
dev-dist/workbox-7a5e81cd.js Normal file

File diff suppressed because it is too large Load Diff

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "vue-nonograms-solid",
"version": "1.9.8",
"version": "1.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vue-nonograms-solid",
"version": "1.9.8",
"version": "1.11.0",
"dependencies": {
"fireworks-js": "^2.10.8",
"flag-icons": "^7.5.0",

View File

@@ -1,6 +1,7 @@
{
"name": "vue-nonograms-solid",
"version": "1.9.8",
"version": "1.11.0",
"homepage": "https://nonograms.7u.pl/",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -8,17 +8,37 @@
<stop offset="0" stop-color="#00f2fe"/>
<stop offset="1" stop-color="#4facfe"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<rect width="192" height="192" rx="28" fill="url(#bg)"/>
<rect x="28" y="28" width="136" height="136" rx="14" fill="rgba(0,0,0,0.35)"/>
<g fill="url(#cell)">
<rect x="48" y="48" width="20" height="20" rx="4"/>
<rect x="76" y="48" width="20" height="20" rx="4"/>
<rect x="104" y="48" width="20" height="20" rx="4"/>
<rect x="48" y="76" width="20" height="20" rx="4"/>
<rect x="104" y="76" width="20" height="20" rx="4"/>
<rect x="48" y="104" width="20" height="20" rx="4"/>
<rect x="76" y="104" width="20" height="20" rx="4"/>
<rect x="104" y="104" width="20" height="20" rx="4"/>
<!-- Main Background -->
<rect width="192" height="192" rx="42" fill="url(#bg)"/>
<!-- Console Screen Background -->
<rect x="26" y="26" width="140" height="140" rx="16" fill="rgba(0,10,30,0.5)" stroke="rgba(0,242,254,0.2)" stroke-width="1.5"/>
<!-- Letter N built from Nonogram cells -->
<g fill="url(#cell)" filter="url(#glow)">
<!-- Left Column -->
<rect x="38" y="38" width="20" height="20" rx="4"/>
<rect x="38" y="62" width="20" height="20" rx="4"/>
<rect x="38" y="86" width="20" height="20" rx="4"/>
<rect x="38" y="110" width="20" height="20" rx="4"/>
<rect x="38" y="134" width="20" height="20" rx="4"/>
<!-- Diagonal -->
<rect x="62" y="62" width="20" height="20" rx="4"/>
<rect x="86" y="86" width="20" height="20" rx="4"/>
<rect x="110" y="110" width="20" height="20" rx="4"/>
<!-- Right Column -->
<rect x="134" y="38" width="20" height="20" rx="4"/>
<rect x="134" y="62" width="20" height="20" rx="4"/>
<rect x="134" y="86" width="20" height="20" rx="4"/>
<rect x="134" y="110" width="20" height="20" rx="4"/>
<rect x="134" y="134" width="20" height="20" rx="4"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 192 192">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#43C6AC"/>
@@ -8,17 +8,37 @@
<stop offset="0" stop-color="#00f2fe"/>
<stop offset="1" stop-color="#4facfe"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<rect width="512" height="512" rx="80" fill="url(#bg)"/>
<rect x="74" y="74" width="364" height="364" rx="40" fill="rgba(0,0,0,0.35)"/>
<g fill="url(#cell)">
<rect x="138" y="138" width="54" height="54" rx="10"/>
<rect x="214" y="138" width="54" height="54" rx="10"/>
<rect x="290" y="138" width="54" height="54" rx="10"/>
<rect x="138" y="214" width="54" height="54" rx="10"/>
<rect x="290" y="214" width="54" height="54" rx="10"/>
<rect x="138" y="290" width="54" height="54" rx="10"/>
<rect x="214" y="290" width="54" height="54" rx="10"/>
<rect x="290" y="290" width="54" height="54" rx="10"/>
<!-- Main Background -->
<rect width="192" height="192" rx="42" fill="url(#bg)"/>
<!-- Console Screen Background -->
<rect x="26" y="26" width="140" height="140" rx="16" fill="rgba(0,10,30,0.5)" stroke="rgba(0,242,254,0.2)" stroke-width="1.5"/>
<!-- Letter N built from Nonogram cells -->
<g fill="url(#cell)" filter="url(#glow)">
<!-- Left Column -->
<rect x="38" y="38" width="20" height="20" rx="4"/>
<rect x="38" y="62" width="20" height="20" rx="4"/>
<rect x="38" y="86" width="20" height="20" rx="4"/>
<rect x="38" y="110" width="20" height="20" rx="4"/>
<rect x="38" y="134" width="20" height="20" rx="4"/>
<!-- Diagonal -->
<rect x="62" y="62" width="20" height="20" rx="4"/>
<rect x="86" y="86" width="20" height="20" rx="4"/>
<rect x="110" y="110" width="20" height="20" rx="4"/>
<!-- Right Column -->
<rect x="134" y="38" width="20" height="20" rx="4"/>
<rect x="134" y="62" width="20" height="20" rx="4"/>
<rect x="134" y="86" width="20" height="20" rx="4"/>
<rect x="134" y="110" width="20" height="20" rx="4"/>
<rect x="134" y="134" width="20" height="20" rx="4"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,49 @@
const fs = require('fs');
const path = 'src/composables/useI18n.js';
let content = fs.readFileSync(path, 'utf8');
const messagesMatch = content.match(/const messages = ({[\s\S]*?});/);
if (!messagesMatch) {
console.error('Could not find messages object');
process.exit(1);
}
const messages = eval(`(${messagesMatch[1]})`);
const en = messages.en || {};
const allLangKeys = Object.keys(en).filter((k) => k.startsWith('language.'));
function injectMissingLanguageLabels(localeCode) {
const blockStartRegex = new RegExp(`\\s{2}['\"]?${localeCode}['\"]?\\s*:\\s*\\{`);
const startIndex = content.search(blockStartRegex);
if (startIndex === -1) return;
const braceStart = content.indexOf('{', startIndex);
let i = braceStart + 1;
let depth = 1;
while (i < content.length && depth > 0) {
if (content[i] === '{') depth++;
else if (content[i] === '}') depth--;
i++;
}
const block = content.slice(braceStart + 1, i - 1);
let updated = block;
let addedAny = false;
allLangKeys.forEach((key) => {
const keyRegex = new RegExp(`(['\"])${key}\\1\\s*:\\s*(['\"]).*?\\2`);
if (!keyRegex.test(updated)) {
const value = en[key];
updated = updated.trim().endsWith(',') ? updated + `\n '${key}': '${value}'` : updated + `,\n '${key}': '${value}'`;
addedAny = true;
}
});
if (addedAny) {
content = content.slice(0, braceStart + 1) + updated + content.slice(i - 1);
}
}
Object.keys(messages).forEach((locale) => {
if (locale === 'en') return;
injectMissingLanguageLabels(locale);
});
fs.writeFileSync(path, content);
console.log('Filled missing language.* labels for all locales.');

View File

@@ -0,0 +1,514 @@
const fs = require('fs');
const path = 'src/composables/useI18n.js';
let content = fs.readFileSync(path, 'utf8');
const keys = {
'custom.showMap': {
es: 'Mostrar mapa de dificultad',
fr: 'Afficher la carte de difficulté',
de: 'Schwierigkeitskarte anzeigen',
it: 'Mostra mappa difficoltà',
pt: 'Mostrar mapa de dificuldade',
'pt-br': 'Mostrar mapa de dificuldade',
ru: 'Показать карту сложности',
zh: '显示难度地图',
ja: '難易度マップを表示',
ko: '난이도 맵 표시',
tr: 'Zorluk haritasını göster',
uk: 'Показати карту складності',
cs: 'Zobrazit mapu obtížnosti',
sk: 'Zobraziť mapu náročnosti',
hu: 'Nehézségi térkép megjelenítése',
ro: 'Arată harta dificultății',
bg: 'Покажи картата на трудността',
el: 'Εμφάνιση χάρτη δυσκολίας',
sr: 'Прикажи карту тежине',
hr: 'Prikaži kartu težine',
sl: 'Prikaži zemljevid težavnosti',
lt: 'Rodyti sudėtingumo žemėlapį',
lv: 'Rādīt grūtības karti',
et: 'Näita raskusaste kaarti',
nl: 'Moeilijkheidskaart weergeven',
sv: 'Visa svårighetskarta',
da: 'Vis sværhedsgradskort',
fi: 'Näytä vaikeuskartta',
no: 'Vis vanskelighetskart',
ar: 'إظهار خريطة الصعوبة',
hi: 'कठिनाई मानचित्र दिखाएँ',
bn: 'কঠিনতার মানচিত্র দেখান'
},
'custom.hideMap': {
es: 'Ocultar mapa de dificultad',
fr: 'Masquer la carte de difficulté',
de: 'Schwierigkeitskarte ausblenden',
it: 'Nascondi mappa difficoltà',
pt: 'Ocultar mapa de dificuldade',
'pt-br': 'Ocultar mapa de dificuldade',
ru: 'Скрыть карту сложности',
zh: '隐藏难度地图',
ja: '難易度マップを非表示',
ko: '난이도 맵 숨기기',
tr: 'Zorluk haritasını gizle',
uk: 'Приховати карту складності',
cs: 'Skrýt mapu obtížnosti',
sk: 'Skryť mapu náročnosti',
hu: 'Nehézségi térkép elrejtése',
ro: 'Ascunde harta dificultății',
bg: 'Скрий картата на трудността',
el: 'Απόκρυψη χάρτη δυσκολίας',
sr: 'Сакриј карту тежине',
hr: 'Sakrij kartu težine',
sl: 'Skrij zemljevid težavnosti',
lt: 'Slėpti sudėtingumo žemėlapį',
lv: 'Paslēpt grūtības karti',
et: 'Peida raskusaste kaart',
nl: 'Moeilijkheidskaart verbergen',
sv: 'Dölj svårighetskarta',
da: 'Skjul sværhedsgradskort',
fi: 'Piilota vaikeuskartta',
no: 'Skjul vanskelighetskart',
ar: 'إخفاء خريطة الصعوبة',
hi: 'कठिनाई मानचित्र छुपाएँ',
bn: 'কঠিনতার মানচিত্র লুকান'
},
'simulation.title': {
es: 'Simulación de Dificultad',
fr: 'Simulation de difficulté',
de: 'Schwierigkeitssimulation',
it: 'Simulazione della difficoltà',
pt: 'Simulação de Dificuldade',
'pt-br': 'Simulação de Dificuldade',
ru: 'Симуляция сложности',
zh: '难度模拟',
ja: '難易度シミュレーション',
ko: '난이도 시뮬레이션',
tr: 'Zorluk simülasyonu',
uk: 'Симуляція складності',
cs: 'Simulace obtížnosti',
sk: 'Simulácia náročnosti',
hu: 'Nehézség szimuláció',
ro: 'Simulare de dificultate',
bg: 'Симулиране на трудност',
el: 'Προσομοίωση δυσκολίας',
sr: 'Симулација тежине',
hr: 'Simulacija težine',
sl: 'Simulacija težavnosti',
lt: 'Sudėtingumo simuliacija',
lv: 'Grūtības simulācija',
et: 'Raskusastme simulatsioon',
nl: 'Moeilijkheidssimulatie',
sv: 'Svårighetssimulering',
da: 'Sværhedsgradssimulering',
fi: 'Vaikeussimulointi',
no: 'Vanskelighetssimulering',
ar: 'محاكاة الصعوبة',
hi: 'कठिनाई सिमुलेशन',
bn: 'কঠিনতা সিমুলেশন'
},
'simulation.status.ready': {
es: 'Listo',
fr: 'Prêt',
de: 'Bereit',
it: 'Pronto',
pt: 'Pronto',
'pt-br': 'Pronto',
ru: 'Готово',
zh: '就绪',
ja: '準備完了',
ko: '준비됨',
tr: 'Hazır',
uk: 'Готово',
cs: 'Připraveno',
sk: 'Pripravené',
hu: 'Kész',
ro: 'Gata',
bg: 'Готово',
el: 'Έτοιμο',
sr: 'Спремно',
hr: 'Spremno',
sl: 'Pripravljeno',
lt: 'Paruošta',
lv: 'Gatavs',
et: 'Valmis',
nl: 'Gereed',
sv: 'Klar',
da: 'Klar',
fi: 'Valmis',
no: 'Klar',
ar: 'جاهز',
hi: 'तैयार',
bn: 'প্রস্তুত'
},
'simulation.status.stopped': {
es: 'Detenido',
fr: 'Arrêté',
de: 'Gestoppt',
it: 'Arrestato',
pt: 'Parado',
'pt-br': 'Parado',
ru: 'Остановлено',
zh: '已停止',
ja: '停止',
ko: '중지됨',
tr: 'Durduruldu',
uk: 'Зупинено',
cs: 'Zastaveno',
sk: 'Zastavené',
hu: 'Leállítva',
ro: 'Oprit',
bg: 'Спряно',
el: 'Διακοπή',
sr: 'Заустављено',
hr: 'Zaustavljeno',
sl: 'Ustavljeno',
lt: 'Sustabdyta',
lv: 'Apturēts',
et: 'Peatatud',
nl: 'Gestopt',
sv: 'Stoppad',
da: 'Stoppet',
fi: 'Pysäytetty',
no: 'Stoppet',
ar: 'متوقف',
hi: 'रोका गया',
bn: 'বন্ধ'
},
'simulation.status.completed': {
es: 'Completado',
fr: 'Terminé',
de: 'Abgeschlossen',
it: 'Completato',
pt: 'Concluído',
'pt-br': 'Concluído',
ru: 'Завершено',
zh: '已完成',
ja: '完了',
ko: '완료됨',
tr: 'Tamamlandı',
uk: 'Завершено',
cs: 'Dokončeno',
sk: 'Dokončené',
hu: 'Befejezve',
ro: 'Finalizat',
bg: 'Завършено',
el: 'Ολοκληρώθηκε',
sr: 'Завршено',
hr: 'Dovršeno',
sl: 'Dokončano',
lt: 'Baigta',
lv: 'Pabeigts',
et: 'Lõpetatud',
nl: 'Voltooid',
sv: 'Slutförd',
da: 'Fuldført',
fi: 'Valmis',
no: 'Fullført',
ar: 'مكتمل',
hi: 'पूर्ण',
bn: 'সম্পন্ন'
},
'simulation.status.simulating': {
es: 'Simulando {size}x{size} @ {density}%',
fr: 'Simulation de {size}x{size} à {density}%',
de: 'Simuliere {size}x{size} @ {density}%',
it: 'Simulazione {size}x{size} @ {density}%',
pt: 'Simulando {size}x{size} @ {density}%',
'pt-br': 'Simulando {size}x{size} @ {density}%',
ru: 'Симуляция {size}x{size} @ {density}%',
zh: '正在模拟 {size}x{size} @ {density}%',
ja: '{size}x{size} @ {density}% をシミュレーション中',
ko: '{size}x{size} @ {density}% 시뮬레이션 중',
tr: '{size}x{size} @ {density}% simüle ediliyor',
uk: 'Симулювання {size}x{size} @ {density}%',
cs: 'Simulace {size}x{size} @ {density}%',
sk: 'Simulácia {size}x{size} @ {density}%',
hu: 'Szimulálás {size}x{size} @ {density}%',
ro: 'Simulare {size}x{size} @ {density}%',
bg: 'Симулиране {size}x{size} @ {density}%',
el: 'Προσομοίωση {size}x{size} @ {density}%',
sr: 'Симулирање {size}x{size} @ {density}%',
hr: 'Simulacija {size}x{size} @ {density}%',
sl: 'Simulacija {size}x{size} @ {density}%',
lt: 'Simuliuojama {size}x{size} @ {density}%',
lv: 'Simulācija {size}x{size} @ {density}%',
et: 'Simuleerimine {size}x{size} @ {density}%',
nl: 'Simuleren {size}x{size} @ {density}%',
sv: 'Simulerar {size}x{size} @ {density}%',
da: 'Simulerer {size}x{size} @ {density}%',
fi: 'Simulointi {size}x{size} @ {density}%',
no: 'Simulerer {size}x{size} @ {density}%',
ar: 'محاكاة {size}x{size} @ {density}%',
hi: '{size}x{size} @ {density}% का सिमुलेशन',
bn: '{size}x{size} @ {density}% সিমুলেট করা হচ্ছে'
},
'simulation.start': {
es: 'Iniciar simulación',
fr: 'Démarrer la simulation',
de: 'Simulation starten',
it: 'Avvia simulazione',
pt: 'Iniciar simulação',
'pt-br': 'Iniciar simulação',
ru: 'Начать симуляцию',
zh: '开始模拟',
ja: 'シミュレーション開始',
ko: '시뮬레이션 시작',
tr: 'Simülasyonu başlat',
uk: 'Почати симуляцію',
cs: 'Spustit simulaci',
sk: 'Spustiť simuláciu',
hu: 'Szimuláció indítása',
ro: 'Pornește simularea',
bg: 'Стартирай симулация',
el: 'Έναρξη προσομοίωσης',
sr: 'Покрени симулацију',
hr: 'Pokreni simulaciju',
sl: 'Zaženi simulacijo',
lt: 'Pradėti simuliaciją',
lv: 'Sākt simulāciju',
et: 'Alusta simulatsiooni',
nl: 'Simulatie starten',
sv: 'Starta simulering',
da: 'Start simulering',
fi: 'Aloita simulointi',
no: 'Start simulering',
ar: 'بدء المحاكاة',
hi: 'सिमुलेशन शुरू करें',
bn: 'সিমুলেশন শুরু'
},
'simulation.stop': {
es: 'Detener',
fr: 'Arrêter',
de: 'Stoppen',
it: 'Stop',
pt: 'Parar',
'pt-br': 'Parar',
ru: 'Стоп',
zh: '停止',
ja: '停止',
ko: '중지',
tr: 'Durdur',
uk: 'Зупинити',
cs: 'Zastavit',
sk: 'Zastaviť',
hu: 'Leállítás',
ro: 'Oprește',
bg: 'Спри',
el: 'Διακοπή',
sr: 'Заустави',
hr: 'Zaustavi',
sl: 'Ustavi',
lt: 'Stabdyti',
lv: 'Apturēt',
et: 'Peata',
nl: 'Stoppen',
sv: 'Stoppa',
da: 'Stop',
fi: 'Pysäytä',
no: 'Stopp',
ar: 'إيقاف',
hi: 'रोकें',
bn: 'বন্ধ করুন'
},
'simulation.table.size': {
es: 'Tamaño',
fr: 'Taille',
de: 'Größe',
it: 'Dimensione',
pt: 'Tamanho',
'pt-br': 'Tamanho',
ru: 'Размер',
zh: '大小',
ja: 'サイズ',
ko: '크기',
tr: 'Boyut',
uk: 'Розмір',
cs: 'Velikost',
sk: 'Veľkosť',
hu: 'Méret',
ro: 'Dimensiune',
bg: 'Размер',
el: 'Μέγεθος',
sr: 'Величина',
hr: 'Veličina',
sl: 'Velikost',
lt: 'Dydis',
lv: 'Izmērs',
et: 'Suurus',
nl: 'Grootte',
sv: 'Storlek',
da: 'Størrelse',
fi: 'Koko',
no: 'Størrelse',
ar: 'الحجم',
hi: 'आकार',
bn: 'আকার'
},
'simulation.table.density': {
es: 'Densidad',
fr: 'Densité',
de: 'Dichte',
it: 'Densità',
pt: 'Densidade',
'pt-br': 'Densidade',
ru: 'Плотность',
zh: '密度',
ja: '密度',
ko: '밀도',
tr: 'Yoğunluk',
uk: 'Щільність',
cs: 'Hustota',
sk: 'Hustota',
hu: 'Sűrűség',
ro: 'Densitate',
bg: 'Плътност',
el: 'Πυκνότητα',
sr: 'Густина',
hr: 'Gustoća',
sl: 'Gostota',
lt: 'Tankis',
lv: 'Blīvums',
et: 'Tihedus',
nl: 'Dichtheid',
sv: 'Densitet',
da: 'Densitet',
fi: 'Tiheys',
no: 'Tetthet',
ar: 'الكثافة',
hi: 'घनत्व',
bn: 'ঘনত্ব'
},
'simulation.table.solved': {
es: 'Resuelto (Lógica)',
fr: 'Résolu (Logique)',
de: 'Gelöst (Logik)',
it: 'Risolto (Logica)',
pt: 'Resolvido (Lógica)',
'pt-br': 'Resolvido (Lógica)',
ru: 'Решено (Логика)',
zh: '已解(逻辑)',
ja: '解決(ロジック)',
ko: '해결됨(논리)',
tr: 'Çözüldü (Mantık)',
uk: 'Розв’язано (Логіка)',
cs: 'Vyřešeno (Logika)',
sk: 'Vyriešené (Logika)',
hu: 'Megoldva (Logika)',
ro: 'Rezolvat (Logică)',
bg: 'Решено (Логика)',
el: 'Επιλύθηκε (Λογική)',
sr: 'Решено (Логика)',
hr: 'Riješeno (Logika)',
sl: 'Rešeno (Logika)',
lt: 'Išspręsta (Logika)',
lv: 'Atrisināts (Loģika)',
et: 'Lahendatud (Loogika)',
nl: 'Opgelost (Logica)',
sv: 'Löst (Logik)',
da: 'Løst (Logik)',
fi: 'Ratkaistu (Logiikka)',
no: 'Løst (Logikk)',
ar: 'تم الحل (منطق)',
hi: 'हल (तर्क)',
bn: 'সমাধান (লজিক)'
},
'simulation.empty': {
es: 'Pulsa Iniciar para ejecutar la simulación Monte Carlo',
fr: 'Appuyez sur Démarrer pour lancer la simulation Monte Carlo',
de: 'Drücke Start, um die Monte-Carlo-Simulation zu starten',
it: 'Premi Avvia per eseguire la simulazione Monte Carlo',
pt: 'Pressione Iniciar para executar a simulação de Monte Carlo',
'pt-br': 'Pressione Iniciar para executar a simulação de Monte Carlo',
ru: 'Нажмите «Старт», чтобы запустить моделирование Монте‑Карло',
zh: '点击开始运行蒙特卡罗模拟',
ja: 'Monte Carlo シミュレーションを実行するには開始を押してください',
ko: 'Monte Carlo 시뮬레이션을 실행하려면 시작을 누르세요',
tr: 'Monte Carlo simülasyonunu çalıştırmak için Başlata basın',
uk: 'Натисніть «Почати», щоб запустити симуляцію Монте‑Карло',
cs: 'Stiskněte Start pro spuštění simulace Monte Carlo',
sk: 'Stlačte Štart pre spustenie simulácie Monte Carlo',
hu: 'Nyomd meg a Startot a Monte Carlo szimulációhoz',
ro: 'Apasă Start pentru a rula simularea Monte Carlo',
bg: 'Натисни Старт, за да стартираш симулация Монте Карло',
el: 'Πατήστε Έναρξη για να τρέξετε προσομοίωση Monte Carlo',
sr: 'Притисни Старт да покренеш Монте Карло симулацију',
hr: 'Pritisni Start za pokretanje Monte Carlo simulacije',
sl: 'Pritisnite Start za zagon simulacije Monte Carlo',
lt: 'Paspauskite Start, kad paleistumėte Monte Karlo simuliaciją',
lv: 'Nospiediet Start, lai palaistu Monte Carlo simulāciju',
et: 'Vajuta Start, et käivitada Monte Carlo simulatsioon',
nl: 'Druk op Start om de Monte Carlo-simulatie te starten',
sv: 'Tryck Start för att köra Monte Carlo-simuleringen',
da: 'Tryk Start for at køre Monte Carlo-simuleringen',
fi: 'Paina Käynnistä aloittaaksesi Monte Carlo -simulaation',
no: 'Trykk Start for å kjøre Monte Carlo-simuleringen',
ar: 'اضغط ابدأ لتشغيل محاكاة مونتِ كارلو',
hi: 'मोंटे कार्लो सिमुलेशन चलाने के लिए स्टार्ट दबाएँ',
bn: 'মোন্টে কার্লো সিমুলেশন চালাতে স্টার্ট চাপুন'
},
'custom.simulationHelp': {
es: '¿Cómo se calcula?',
fr: 'Comment est-ce calculé ?',
de: 'Wie wird das berechnet?',
it: 'Come viene calcolato?',
pt: 'Como isso é calculado?',
'pt-br': 'Como isso é calculado?',
ru: 'Как это рассчитывается?',
zh: '这是如何计算的?',
ja: 'これはどのように計算されますか?',
ko: '이것은 어떻게 계산됩니까?',
tr: 'Bu nasıl hesaplanıyor?',
uk: 'Як це обчислюється?',
cs: 'Jak se to počítá?',
sk: 'Ako sa to počíta?',
hu: 'Hogyan számoljuk?',
ro: 'Cum este calculat?',
bg: 'Как се изчислява?',
el: 'Πώς υπολογίζεται;',
sr: 'Како се израчунава?',
hr: 'Kako se izračunava?',
sl: 'Kako je izračunano?',
lt: 'Kaip tai apskaičiuojama?',
lv: 'Kā tas tiek aprēķināts?',
et: 'Kuidas see arvutatakse?',
nl: 'Hoe wordt dit berekend?',
sv: 'Hur beräknas detta?',
da: 'Hvordan beregnes dette?',
fi: 'Miten tämä lasketaan?',
no: 'Hvordan beregnes dette?',
ar: 'كيف يتم احتساب ذلك؟',
hi: 'यह कैसे गणना किया जाता है?',
bn: 'এটি কীভাবে গণনা করা হয়?'
}
};
function replaceInLanguage(lang, key, value) {
const langStart = new RegExp(`\\s{2}['\"]?${lang}['\"]?\\s*:\\s*\\{`);
const index = content.search(langStart);
if (index === -1) return;
const start = content.indexOf('{', index);
let depth = 1;
let i = start + 1;
while (i < content.length && depth > 0) {
if (content[i] === '{') depth++;
else if (content[i] === '}') depth--;
i++;
}
const block = content.slice(start + 1, i - 1);
let newBlock;
const keyRegex = new RegExp(`(['\"])${key}\\1\\s*:\\s*(['\"]).*?\\2`);
if (keyRegex.test(block)) {
newBlock = block.replace(keyRegex, `'${key}': '${value}'`);
} else {
newBlock = block.trim().endsWith(',') ? block + `\n '${key}': '${value}'` : block + `,\n '${key}': '${value}'`;
}
content = content.slice(0, start + 1) + newBlock + content.slice(i - 1);
}
Object.entries(keys).forEach(([key, translations]) => {
Object.entries(translations).forEach(([lang, value]) => {
replaceInLanguage(lang, key, value);
});
});
fs.writeFileSync(path, content);
console.log('Translations updated.');

View File

@@ -28,6 +28,21 @@ const appVersion = __APP_VERSION__;
let displayModeMedia = null;
let prefersColorSchemeMedia = null;
const onKeyDownGlobal = (e) => {
if (e.key !== 'Escape') return;
if (showSimulation.value) {
showSimulation.value = false;
return;
}
if (showCustomModal.value) {
showCustomModal.value = false;
return;
}
if (store.isGameWon) {
store.closeWinModal();
}
};
const installLabel = computed(() => {
return isCoarsePointer.value ? t('pwa.installMobile') : t('pwa.installDesktop');
});
@@ -114,6 +129,7 @@ onMounted(() => {
} else if (displayModeMedia?.addListener) {
displayModeMedia.addListener(updateStandalone);
}
window.addEventListener('keydown', onKeyDownGlobal);
}
});
@@ -131,6 +147,7 @@ onUnmounted(() => {
} else if (displayModeMedia?.removeListener) {
displayModeMedia.removeListener(updateStandalone);
}
window.removeEventListener('keydown', onKeyDownGlobal);
});
</script>

BIN
src/assets/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -272,6 +272,7 @@ watch(() => store.size, async () => {
min-width: 100%;
margin: 0 auto; /* Center the wrapper safely */
align-items: flex-start; /* Prevent cropping when centered */
padding-right: 40px;
}
.game-container {

View File

@@ -32,7 +32,11 @@ defineProps({
v-for="(group, index) in hints"
:key="index"
class="hint-group"
:class="{ 'is-active': index === activeIndex }"
:class="{
'is-active': index === activeIndex,
'guide-right': orientation === 'col' && (index + 1) % 5 === 0 && index !== size - 1,
'guide-bottom': orientation === 'row' && (index + 1) % 5 === 0 && index !== size - 1
}"
>
<span
v-for="(num, idx) in group"
@@ -111,4 +115,12 @@ defineProps({
border-color: rgba(79, 172, 254, 0.8);
box-shadow: 0 0 12px rgba(79, 172, 254, 0.35);
}
/* Guide lines every 5 */
.hint-group.guide-right {
border-right: 2px solid rgba(0, 242, 255, 0.5);
}
.hint-group.guide-bottom {
border-bottom: 2px solid rgba(0, 242, 255, 0.5);
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { generateRandomGrid, calculateHints } from '@/utils/puzzleUtils';
import { solvePuzzle } from '@/utils/solver';
import { useI18n } from '@/composables/useI18n';
@@ -21,6 +21,22 @@ const simulationSpeed = ref(1); // 1 = Normal, 2 = Fast (less render updates)
let stopRequested = false;
const onKeyDown = (e) => {
if (e.key === 'Escape') {
e.stopImmediatePropagation?.();
e.preventDefault?.();
emit('close');
}
};
onMounted(() => {
window.addEventListener('keydown', onKeyDown);
});
onUnmounted(() => {
window.removeEventListener('keydown', onKeyDown);
});
const displayStatus = computed(() => {
if (!currentStatus.value) return t('simulation.status.ready');
return currentStatus.value;

View File

@@ -28,6 +28,8 @@ const handleClose = () => {
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
e.stopImmediatePropagation?.();
e.preventDefault?.();
handleClose();
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@ export function useSolver() {
const isPlaying = ref(false);
const isProcessing = ref(false);
const speedIndex = ref(0);
const speeds = [1000, 500, 250, 125, 62];
const speedLabels = ['x1', 'x2', 'x4', 'x8', 'x16'];
const speeds = [1000, 500, 250, 125, 62, 31, 16];
const speedLabels = ['x1', 'x2', 'x4', 'x8', 'x16', 'x32', 'x64'];
const statusText = ref(t('guide.waiting'));
let intervalId = null;

View File

@@ -4,7 +4,7 @@ export function buildShareCanvas(data, t, formattedTime) {
const { grid, size, currentDensity, guideUsageCount } = data;
if (!grid || !grid.length) return null;
const appUrl = 'https://nonograms.7u.pl/';
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
const maxBoard = 640;
const cellSize = Math.max(8, Math.floor(maxBoard / size));
const boardSize = cellSize * size;
@@ -119,7 +119,7 @@ export function buildShareSVG(data, t, formattedTime) {
const { grid, size, currentDensity, guideUsageCount } = data;
if (!grid || !grid.length) return null;
const appUrl = 'https://nonograms.7u.pl/';
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
const maxBoard = 640;
const cellSize = Math.max(8, Math.floor(maxBoard / size));
const boardSize = cellSize * size;
@@ -227,7 +227,11 @@ export function buildShareSVG(data, t, formattedTime) {
}
// URL
svgContent += `<text x="${padding}" y="${height - padding + 6}" font-family="Segoe UI, sans-serif" font-weight="500" font-size="14" fill="${urlColor}">${appUrl}</text>`;
svgContent += `
<a href="${appUrl}" target="_blank">
<text x="${padding}" y="${height - padding + 6}" font-family="Segoe UI, sans-serif" font-weight="500" font-size="14" fill="${urlColor}" style="text-decoration: underline; cursor: pointer;">${appUrl}</text>
</a>
`;
svgContent += '</svg>';
return svgContent;

View File

@@ -5,7 +5,8 @@ import path from 'path'
export default defineConfig({
define: {
'__APP_VERSION__': JSON.stringify(process.env.npm_package_version)
'__APP_VERSION__': JSON.stringify(process.env.npm_package_version),
'__APP_HOMEPAGE__': JSON.stringify(process.env.npm_package_homepage)
},
plugins: [
vue(),