4 Commits

Author SHA1 Message Date
b25d3f4015 1.15.0
All checks were successful
Deploy to Production / deploy (push) Successful in 25s
2026-02-19 06:25:16 +01:00
43629d72a4 feat(hints): visualize completed rows and columns 2026-02-19 06:24:56 +01:00
4138c99e20 (release): prepare android build config and signing
All checks were successful
Deploy to Production / deploy (push) Successful in 7s
2026-02-19 05:22:13 +01:00
588a131d68 Refactor docker-compose to match standard NPM setup (remove internal network)
All checks were successful
Deploy to Production / deploy (push) Successful in 3s
2026-02-16 13:45:16 +01:00
16 changed files with 167 additions and 23 deletions

5
android/.gitignore vendored
View File

@@ -54,8 +54,9 @@ captures/
# Keystore files # Keystore files
# Uncomment the following lines if you do not want to check your keystore files in. # Uncomment the following lines if you do not want to check your keystore files in.
#*.jks *.jks
#*.keystore *.keystore
keystore.properties
# External native build folder generated in Android Studio 2.2 and later # External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild .externalNativeBuild

View File

@@ -3,12 +3,27 @@ apply plugin: 'com.android.application'
android { android {
namespace = "pl.nonograms.app" namespace = "pl.nonograms.app"
compileSdk = rootProject.ext.compileSdkVersion compileSdk = rootProject.ext.compileSdkVersion
signingConfigs {
release {
def keystorePropertiesFile = rootProject.file("keystore.properties")
if (keystorePropertiesFile.exists()) {
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
storeFile = file(keystoreProperties['storeFile'])
storePassword = keystoreProperties['storePassword']
keyAlias = keystoreProperties['keyAlias']
keyPassword = keystoreProperties['keyPassword']
}
}
}
defaultConfig { defaultConfig {
applicationId "pl.nonograms.app" applicationId "pl.nonograms.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1144
versionName "1.0" versionName "1.14.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -18,8 +33,9 @@ android {
} }
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
} }

View File

@@ -30,6 +30,7 @@
</application> </application>
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest> </manifest>

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.13.0' classpath 'com.android.tools.build:gradle:9.0.1'
classpath 'com.google.gms:google-services:4.4.4' classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View File

@@ -20,3 +20,13 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@@ -82,7 +82,7 @@ define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict';
*/ */
workbox.precacheAndRoute([{ workbox.precacheAndRoute([{
"url": "index.html", "url": "index.html",
"revision": "0.kkc80cp3p5o" "revision": "0.n1n8rjsg38"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@@ -1,14 +1,17 @@
version: '3.8' version: '3.8'
services: services:
app: nonograms:
container_name: nonograms-app container_name: nonograms
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
ports: expose:
- "8081:80" - "80"
restart: unless-stopped restart: unless-stopped
# Uncomment the following lines if you want to mount the configuration locally for development/testing networks:
# volumes: - npm_public
# - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
npm_public:
external: true

View File

@@ -24,6 +24,8 @@
<string>$(CURRENT_PROJECT_VERSION)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>This app requires camera access to import nonograms from photos.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "vue-nonograms-solid", "name": "vue-nonograms-solid",
"version": "1.14.4", "version": "1.15.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "vue-nonograms-solid", "name": "vue-nonograms-solid",
"version": "1.14.4", "version": "1.15.0",
"dependencies": { "dependencies": {
"@capacitor/android": "^8.1.0", "@capacitor/android": "^8.1.0",
"@capacitor/cli": "^8.1.0", "@capacitor/cli": "^8.1.0",

View File

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

View File

@@ -247,10 +247,10 @@ watch(() => store.size, async () => {
<div class="corner-spacer"></div> <div class="corner-spacer"></div>
<!-- Column Hints --> <!-- Column Hints -->
<Hints :hints="colHints" orientation="col" :size="gridCols" :activeIndex="activeCol" /> <Hints :hints="colHints" orientation="col" :size="gridCols" :activeIndex="activeCol" :completedLines="store.completedCols" />
<!-- Row Hints --> <!-- Row Hints -->
<Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="gridRows" :activeIndex="activeRow" /> <Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="gridRows" :activeIndex="activeRow" :completedLines="store.completedRows" />
<!-- Grid --> <!-- Grid -->
<div <div

View File

@@ -16,6 +16,10 @@ defineProps({
activeIndex: { activeIndex: {
type: Number, type: Number,
default: null default: null
},
completedLines: {
type: Array,
default: () => []
} }
}); });
</script> </script>
@@ -34,6 +38,7 @@ defineProps({
class="hint-group" class="hint-group"
:class="{ :class="{
'is-active': index === activeIndex, 'is-active': index === activeIndex,
'is-completed': completedLines[index],
'guide-right': orientation === 'col' && (index + 1) % 5 === 0 && index !== size - 1, 'guide-right': orientation === 'col' && (index + 1) % 5 === 0 && index !== size - 1,
'guide-bottom': orientation === 'row' && (index + 1) % 5 === 0 && index !== size - 1 'guide-bottom': orientation === 'row' && (index + 1) % 5 === 0 && index !== size - 1
}" }"
@@ -81,6 +86,15 @@ defineProps({
height: 100%; height: 100%;
} }
.hint-group.is-completed {
opacity: 0.5;
background-color: var(--hint-bg);
}
.hint-group.is-completed .hint-num {
color: var(--text-muted);
}
.col .hint-group { .col .hint-group {
flex-direction: column; flex-direction: column;
padding: 4px 2px; padding: 4px 2px;

View File

@@ -56,6 +56,31 @@ export const usePuzzleStore = defineStore('puzzle', () => {
return Math.min(100, (filledCorrectly.value / totalCellsToFill.value) * 100); return Math.min(100, (filledCorrectly.value / totalCellsToFill.value) * 100);
}); });
const completedRows = computed(() => {
if (!solution.value.length || !playerGrid.value.length) return [];
const rows = solution.value.length;
return Array(rows).fill().map((_, r) => {
const targetHints = calculateLineHints(solution.value[r]);
const playerLine = playerGrid.value[r];
return validateLine(playerLine, targetHints);
});
});
const completedCols = computed(() => {
if (!solution.value.length || !playerGrid.value.length) return [];
const rows = solution.value.length;
const cols = solution.value[0].length;
return Array(cols).fill().map((_, c) => {
const col = [];
for (let r = 0; r < rows; r++) {
col.push(solution.value[r][c]);
}
const targetHints = calculateLineHints(col);
const playerLine = playerGrid.value.map(row => row[c]);
return validateLine(playerLine, targetHints);
});
});
// Actions // Actions
function initGame(levelId = 'easy') { function initGame(levelId = 'easy') {
stopTimer(); stopTimer();
@@ -408,7 +433,9 @@ export const usePuzzleStore = defineStore('puzzle', () => {
currentDensity, currentDensity,
markGuideUsed, markGuideUsed,
startInteraction, startInteraction,
endInteraction endInteraction,
completedRows,
completedCols
}; };
}); });

View File

@@ -0,0 +1,70 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { usePuzzleStore } from './puzzle';
describe('Puzzle Store - Completion Logic', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('should correctly identify completed rows and columns', () => {
const store = usePuzzleStore();
// Setup a simple 2x2 puzzle
// Solution:
// 1 0
// 0 1
// Row Hints: [1], [1]
// Col Hints: [1], [1]
store.solution = [
[1, 0],
[0, 1]
];
store.playerGrid = [
[0, 0],
[0, 0]
];
// Initially nothing is completed
expect(store.completedRows).toEqual([false, false]);
expect(store.completedCols).toEqual([false, false]);
// Fill first row correctly: 1 0
store.playerGrid[0][0] = 1;
store.playerGrid[0][1] = 0;
expect(store.completedRows).toEqual([true, false]);
// Fill second row incorrectly (too many filled): 1 1
store.playerGrid[1][0] = 1;
store.playerGrid[1][1] = 1;
// Row 2 hint is [1], user has [2]. Should be false.
expect(store.completedRows).toEqual([true, false]);
// Fix second row: 0 1
store.playerGrid[1][0] = 0;
store.playerGrid[1][1] = 1;
expect(store.completedRows).toEqual([true, true]);
// Check columns
// Col 1: 1, 0 -> Hint [1]. Matches.
// Col 2: 0, 1 -> Hint [1]. Matches.
expect(store.completedCols).toEqual([true, true]);
});
it('should mark row as completed if constraints are met even if wrong position', () => {
const store = usePuzzleStore();
// Solution: 1 0 0 (Hint 1)
store.solution = [[1, 0, 0]];
store.playerGrid = [[0, 0, 0]];
// User puts 0 0 1 (Hint 1)
store.playerGrid[0] = [0, 0, 1];
// Should be marked as completed because it satisfies the hint "1"
expect(store.completedRows).toEqual([true]);
});
});

View File

@@ -39,6 +39,6 @@ describe('Large Grid Solver', () => {
console.log('Result:', result); console.log('Result:', result);
expect(result.percentSolved).toBeGreaterThan(0); expect(result.percentSolved).toBeGreaterThan(0);
expect(result.difficulty).toBeDefined(); expect(result.difficultyScore).toBeDefined();
}); });
}); });