Compare commits
6 Commits
df336eeb8a
...
v1.15.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
b25d3f4015
|
|||
|
43629d72a4
|
|||
|
4138c99e20
|
|||
| 588a131d68 | |||
| 000ef8f715 | |||
| c39c22691e |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -14,9 +14,3 @@ dist-ssr/
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
# Security keys and certificates
|
|
||||||
*.keystore
|
|
||||||
*.jks
|
|
||||||
*.p12
|
|
||||||
*.mobileprovision
|
|
||||||
|
|||||||
5
android/.gitignore
vendored
5
android/.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"), {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.14.3",
|
"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.3",
|
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.14.3",
|
"version": "1.15.0",
|
||||||
"homepage": "https://nonograms.7u.pl/",
|
"homepage": "https://nonograms.7u.pl/",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ const triggerVibration = () => {
|
|||||||
const getShareData = () => ({
|
const getShareData = () => ({
|
||||||
grid: store.playerGrid,
|
grid: store.playerGrid,
|
||||||
size: store.size,
|
size: store.size,
|
||||||
|
rows: store.playerGrid?.length || store.size,
|
||||||
|
cols: store.playerGrid?.[0]?.length || store.size,
|
||||||
currentDensity: store.currentDensity,
|
currentDensity: store.currentDensity,
|
||||||
guideUsageCount: store.guideUsageCount,
|
guideUsageCount: store.guideUsageCount,
|
||||||
hasUsedBoost: store.hasUsedBoost,
|
hasUsedBoost: store.hasUsedBoost,
|
||||||
@@ -71,9 +73,11 @@ const shareTo = async (target) => {
|
|||||||
try {
|
try {
|
||||||
// Try native share first if available (supports images)
|
// Try native share first if available (supports images)
|
||||||
if (navigator.share && navigator.canShare) {
|
if (navigator.share && navigator.canShare) {
|
||||||
const blob = await createShareBlob(getShareData(), t, formattedTime.value);
|
const data = getShareData();
|
||||||
|
const blob = await createShareBlob(data, t, formattedTime.value);
|
||||||
if (blob) {
|
if (blob) {
|
||||||
const file = new File([blob], `nonogram-${store.size}x${store.size}.png`, { type: 'image/png' });
|
const dims = (data.cols && data.rows) ? `${data.cols}x${data.rows}` : `${store.size}x${store.size}`;
|
||||||
|
const file = new File([blob], `nonogram-${dims}.png`, { type: 'image/png' });
|
||||||
if (navigator.canShare({ files: [file] })) {
|
if (navigator.canShare({ files: [file] })) {
|
||||||
await navigator.share({
|
await navigator.share({
|
||||||
files: [file],
|
files: [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
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
70
src/stores/puzzle_completion.test.js
Normal file
70
src/stores/puzzle_completion.test.js
Normal 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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
import { calculateDifficulty } from '@/utils/puzzleUtils';
|
import { calculateDifficulty } from '@/utils/puzzleUtils';
|
||||||
|
|
||||||
export function buildShareCanvas(data, t, formattedTime) {
|
export function buildShareCanvas(data, t, formattedTime) {
|
||||||
const { grid, size, currentDensity, guideUsageCount, hasUsedBoost } = data;
|
const { grid, size, rows, cols, currentDensity, guideUsageCount, hasUsedBoost } = data;
|
||||||
if (!grid || !grid.length) return null;
|
if (!grid || !grid.length) return null;
|
||||||
|
|
||||||
|
// Backward compatibility if rows/cols not provided
|
||||||
|
const numRows = rows || size;
|
||||||
|
const numCols = cols || size;
|
||||||
|
|
||||||
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
||||||
const maxBoard = 640;
|
const maxBoard = 640;
|
||||||
const cellSize = Math.max(8, Math.floor(maxBoard / size));
|
// Calculate cell size based on the largest dimension to fit within maxBoard
|
||||||
const boardSize = cellSize * size;
|
const maxDim = Math.max(numRows, numCols);
|
||||||
|
const cellSize = Math.max(8, Math.floor(maxBoard / maxDim));
|
||||||
|
|
||||||
|
const boardWidth = cellSize * numCols;
|
||||||
|
const boardHeight = cellSize * numRows;
|
||||||
|
|
||||||
const padding = 28;
|
const padding = 28;
|
||||||
const headerHeight = 64;
|
const headerHeight = 64;
|
||||||
const footerHeight = 28;
|
const footerHeight = 28;
|
||||||
const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40;
|
const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40;
|
||||||
const width = boardSize + padding * 2;
|
|
||||||
const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight;
|
const width = boardWidth + padding * 2;
|
||||||
|
const height = boardHeight + padding * 2 + headerHeight + footerHeight + infoHeight;
|
||||||
|
|
||||||
const scale = window.devicePixelRatio || 1;
|
const scale = window.devicePixelRatio || 1;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = width * scale;
|
canvas.width = width * scale;
|
||||||
@@ -38,7 +49,8 @@ export function buildShareCanvas(data, t, formattedTime) {
|
|||||||
|
|
||||||
// Difficulty & Density Info
|
// Difficulty & Density Info
|
||||||
const densityPercent = Math.round(currentDensity * 100);
|
const densityPercent = Math.round(currentDensity * 100);
|
||||||
const { level: difficultyKey } = calculateDifficulty(currentDensity, size);
|
// Calculate difficulty using the max dimension (size) as it relates to complexity
|
||||||
|
const { level: difficultyKey } = calculateDifficulty(currentDensity, maxDim);
|
||||||
let diffColor = '#33ff33';
|
let diffColor = '#33ff33';
|
||||||
if (difficultyKey === 'extreme') diffColor = '#ff3333';
|
if (difficultyKey === 'extreme') diffColor = '#ff3333';
|
||||||
else if (difficultyKey === 'hardest') diffColor = '#ff9933';
|
else if (difficultyKey === 'hardest') diffColor = '#ff9933';
|
||||||
@@ -56,26 +68,34 @@ export function buildShareCanvas(data, t, formattedTime) {
|
|||||||
const gridX = padding;
|
const gridX = padding;
|
||||||
const gridY = padding + headerHeight;
|
const gridY = padding + headerHeight;
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
|
||||||
ctx.fillRect(gridX, gridY, boardSize, boardSize);
|
ctx.fillRect(gridX, gridY, boardWidth, boardHeight);
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
for (let i = 0; i <= size; i++) {
|
|
||||||
|
// Vertical lines
|
||||||
|
for (let i = 0; i <= numCols; i++) {
|
||||||
const x = gridX + i * cellSize;
|
const x = gridX + i * cellSize;
|
||||||
const y = gridY + i * cellSize;
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x, gridY);
|
ctx.moveTo(x, gridY);
|
||||||
ctx.lineTo(x, gridY + boardSize);
|
ctx.lineTo(x, gridY + boardHeight);
|
||||||
ctx.stroke();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(gridX, y);
|
|
||||||
ctx.lineTo(gridX + boardSize, y);
|
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal lines
|
||||||
|
for (let i = 0; i <= numRows; i++) {
|
||||||
|
const y = gridY + i * cellSize;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(gridX, y);
|
||||||
|
ctx.lineTo(gridX + boardWidth, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
ctx.fillStyle = '#00f2fe';
|
ctx.fillStyle = '#00f2fe';
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
||||||
ctx.lineWidth = Math.max(1.5, Math.floor(cellSize * 0.12));
|
ctx.lineWidth = Math.max(1.5, Math.floor(cellSize * 0.12));
|
||||||
for (let r = 0; r < size; r++) {
|
|
||||||
for (let c = 0; c < size; c++) {
|
for (let r = 0; r < numRows; r++) {
|
||||||
|
for (let c = 0; c < numCols; c++) {
|
||||||
const state = grid[r]?.[c];
|
const state = grid[r]?.[c];
|
||||||
if (state === 1) {
|
if (state === 1) {
|
||||||
const x = gridX + c * cellSize + 1;
|
const x = gridX + c * cellSize + 1;
|
||||||
@@ -107,7 +127,7 @@ export function buildShareCanvas(data, t, formattedTime) {
|
|||||||
ctx.fillStyle = '#ff4d4d';
|
ctx.fillStyle = '#ff4d4d';
|
||||||
ctx.font = '600 14px "Segoe UI", sans-serif';
|
ctx.font = '600 14px "Segoe UI", sans-serif';
|
||||||
|
|
||||||
const totalCells = size * size;
|
const totalCells = numRows * numCols;
|
||||||
const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100));
|
const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100));
|
||||||
const guideText = t('win.usedGuide', { count: guideUsageCount, percent });
|
const guideText = t('win.usedGuide', { count: guideUsageCount, percent });
|
||||||
|
|
||||||
@@ -129,19 +149,27 @@ export function buildShareCanvas(data, t, formattedTime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildShareSVG(data, t, formattedTime) {
|
export function buildShareSVG(data, t, formattedTime) {
|
||||||
const { grid, size, currentDensity, guideUsageCount, hasUsedBoost } = data;
|
const { grid, size, rows, cols, currentDensity, guideUsageCount, hasUsedBoost } = data;
|
||||||
if (!grid || !grid.length) return null;
|
if (!grid || !grid.length) return null;
|
||||||
|
|
||||||
|
// Backward compatibility
|
||||||
|
const numRows = rows || size;
|
||||||
|
const numCols = cols || size;
|
||||||
|
|
||||||
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
||||||
const maxBoard = 640;
|
const maxBoard = 640;
|
||||||
const cellSize = Math.max(8, Math.floor(maxBoard / size));
|
const maxDim = Math.max(numRows, numCols);
|
||||||
const boardSize = cellSize * size;
|
const cellSize = Math.max(8, Math.floor(maxBoard / maxDim));
|
||||||
|
|
||||||
|
const boardWidth = cellSize * numCols;
|
||||||
|
const boardHeight = cellSize * numRows;
|
||||||
|
|
||||||
const padding = 28;
|
const padding = 28;
|
||||||
const headerHeight = 64;
|
const headerHeight = 64;
|
||||||
const footerHeight = 28;
|
const footerHeight = 28;
|
||||||
const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40;
|
const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40;
|
||||||
const width = boardSize + padding * 2;
|
const width = boardWidth + padding * 2;
|
||||||
const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight;
|
const height = boardHeight + padding * 2 + headerHeight + footerHeight + infoHeight;
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
const bgGradientStart = '#1b2a4a';
|
const bgGradientStart = '#1b2a4a';
|
||||||
@@ -156,7 +184,7 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
|
|
||||||
// Difficulty Logic
|
// Difficulty Logic
|
||||||
const densityPercent = Math.round(currentDensity * 100);
|
const densityPercent = Math.round(currentDensity * 100);
|
||||||
const { level: difficultyKey } = calculateDifficulty(currentDensity, size);
|
const { level: difficultyKey } = calculateDifficulty(currentDensity, maxDim);
|
||||||
|
|
||||||
let diffColor = '#33ff33';
|
let diffColor = '#33ff33';
|
||||||
if (difficultyKey === 'extreme') diffColor = '#ff3333';
|
if (difficultyKey === 'extreme') diffColor = '#ff3333';
|
||||||
@@ -194,16 +222,19 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
const gridY = padding + headerHeight;
|
const gridY = padding + headerHeight;
|
||||||
|
|
||||||
// Grid Background
|
// Grid Background
|
||||||
svgContent += `<rect x="${gridX}" y="${gridY}" width="${boardSize}" height="${boardSize}" fill="${gridColor}"/>`;
|
svgContent += `<rect x="${gridX}" y="${gridY}" width="${boardWidth}" height="${boardHeight}" fill="${gridColor}"/>`;
|
||||||
|
|
||||||
// Grid Lines
|
// Grid Lines
|
||||||
let gridLines = '';
|
let gridLines = '';
|
||||||
for (let i = 0; i <= size; i++) {
|
// Vertical
|
||||||
|
for (let i = 0; i <= numCols; i++) {
|
||||||
const pos = i * cellSize;
|
const pos = i * cellSize;
|
||||||
// Vertical
|
gridLines += `<line x1="${gridX + pos}" y1="${gridY}" x2="${gridX + pos}" y2="${gridY + boardHeight}" stroke="${gridLineColor}" stroke-width="1"/>`;
|
||||||
gridLines += `<line x1="${gridX + pos}" y1="${gridY}" x2="${gridX + pos}" y2="${gridY + boardSize}" stroke="${gridLineColor}" stroke-width="1"/>`;
|
}
|
||||||
// Horizontal
|
// Horizontal
|
||||||
gridLines += `<line x1="${gridX}" y1="${gridY + pos}" x2="${gridX + boardSize}" y2="${gridY + pos}" stroke="${gridLineColor}" stroke-width="1"/>`;
|
for (let i = 0; i <= numRows; i++) {
|
||||||
|
const pos = i * cellSize;
|
||||||
|
gridLines += `<line x1="${gridX}" y1="${gridY + pos}" x2="${gridX + boardWidth}" y2="${gridY + pos}" stroke="${gridLineColor}" stroke-width="1"/>`;
|
||||||
}
|
}
|
||||||
svgContent += gridLines;
|
svgContent += gridLines;
|
||||||
|
|
||||||
@@ -211,8 +242,8 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
let cells = '';
|
let cells = '';
|
||||||
const lineWidth = Math.max(1.5, Math.floor(cellSize * 0.12));
|
const lineWidth = Math.max(1.5, Math.floor(cellSize * 0.12));
|
||||||
|
|
||||||
for (let r = 0; r < size; r++) {
|
for (let r = 0; r < numRows; r++) {
|
||||||
for (let c = 0; c < size; c++) {
|
for (let c = 0; c < numCols; c++) {
|
||||||
const state = grid[r]?.[c];
|
const state = grid[r]?.[c];
|
||||||
const cx = gridX + c * cellSize;
|
const cx = gridX + c * cellSize;
|
||||||
const cy = gridY + r * cellSize;
|
const cy = gridY + r * cellSize;
|
||||||
@@ -238,7 +269,7 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (guideUsageCount > 0) {
|
if (guideUsageCount > 0) {
|
||||||
const totalCells = size * size;
|
const totalCells = numRows * numCols;
|
||||||
const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100));
|
const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100));
|
||||||
const guideText = t('win.usedGuide', { count: guideUsageCount, percent });
|
const guideText = t('win.usedGuide', { count: guideUsageCount, percent });
|
||||||
svgContent += `<text x="${padding}" y="${infoY}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ff4d4d">⚠️ ${guideText}</text>`;
|
svgContent += `<text x="${padding}" y="${infoY}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ff4d4d">⚠️ ${guideText}</text>`;
|
||||||
@@ -276,7 +307,9 @@ export const downloadShareSVG = (data, t, formattedTime) => {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `nonogram-${data.size}x${data.size}.svg`;
|
// Use cols x rows if available, else size x size
|
||||||
|
const dims = (data.cols && data.rows) ? `${data.cols}x${data.rows}` : `${data.size}x${data.size}`;
|
||||||
|
link.download = `nonogram-${dims}.svg`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
@@ -289,7 +322,8 @@ export const downloadShareImage = async (data, t, formattedTime) => {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `nonogram-${data.size}x${data.size}.png`;
|
const dims = (data.cols && data.rows) ? `${data.cols}x${data.rows}` : `${data.size}x${data.size}`;
|
||||||
|
link.download = `nonogram-${dims}.png`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
|
|||||||
Reference in New Issue
Block a user