Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ testem.log
._*
.AppleDouble
.LSOverride
Icon?
Thumbs.db

# neutralino
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The original open-source Mahjong Solitaire game powering many Mahjong experience

🖼️ **Massive visual customization** - 8 image backgrounds, 375 pattern backgrounds, light/dark mode, 14 color themes

🏆 **3 difficulty levels** - from relaxed casual play to expert-level challenge
🏆 **Difficulty levels** - from relaxed casual play to expert-level challenge

💾 **Auto-save** - your game state and best times are saved locally in your browser, never to the cloud

Expand Down Expand Up @@ -111,7 +111,7 @@ Most modern phones use `arm64`. Try these APK variants in order:

## 🙏 Acknowledgements

Mah's art is built on open-source creative work. See the credits for [artwork](src/assets/svg/README.md), [backgrounds](src/assets/img/README.md), [sounds](src/assets/sounds/README.md), and [fonts](src/fonts/README.md).
Mah's art is built on open-source creative work. See the credits for [artwork](src/assets/svg/README.md), [backgrounds](src/assets/img/README.md), [sounds](src/assets/sounds/README.md), [icons](src/app/components/icons/README.md) and [fonts](src/fonts/README.md).

---

Expand Down
2 changes: 0 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@
"src/assets"
],
"styles": [
"src/fonts/fontello/css/mah.css",
"src/fonts/editor/css/editor.css",
"src/fonts/kulim-park/css/kulim-park.css",
"src/styles.scss"
],
Expand Down
2 changes: 1 addition & 1 deletion resources/images/svg-to-png.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ async function main() {
const sample = nameGrid.flat().filter(Boolean).slice(0, 5).join(", ");
console.log(`[t_preview] Detected layout ${cols}x${rows}; sample names: ${sample}`);
} else {
console.log("[t_preview] Not found or unparsable used fallback grid.");
console.log("[t_preview] Not found or unparsable - used fallback grid.");
}
}
}
Expand Down
55 changes: 43 additions & 12 deletions src/app/components/choose-layout/choose-layout.component.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
<app-layout-list (startEvent)="onStart($event)" [layouts]="this.layoutService.layouts.items" />
<div class="choose-buttons">
<div class="generator">
<label for="board-generator-select">{{ 'BOARD_GENERATOR' | translate }}:</label>
<select id="board-generator-select" (change)="buildMode = $any($event.target).value">
<label class="info-label" (click)="activeInfo.set('generator')">{{ 'BOARD_GENERATOR' | translate }}
<app-icon-info />
</label>
<select id="board-generator-select" (change)="buildMode.set($any($event.target).value)">
@for (m of buildModes; track m) {
<option [value]="m.id" [selected]="buildMode === m.id">{{ m.id | translate }} [{{ m.id + '_DESC' | translate }}]</option>
<option [value]="m.id" [selected]="buildMode() === m.id">{{ m.id | translate }}</option>
}
</select>
</div>
<div class="mode">
<label for="game-mode-select">{{ 'GAME_MODE' | translate }}:</label>
<select id="game-mode-select" (change)="this.gameMode.set($any($event.target).value)">
<label class="info-label" (click)="activeInfo.set('mode')">{{ 'GAME_MODE' | translate }}
<app-icon-info />
</label>
<select id="game-mode-select" (change)="onGameModeChange($any($event.target).value)">
@for (m of gameModes; track m) {
<option [value]="m.id" [selected]="gameMode() === m.id">{{ m.id | translate }}
@if (m.features.length > 0) {
[@for (f of m.features; track f; let last = $last) {
{{ f.title | translate }}{{ !last ? ', ' : '' }}
}]
}
</option>
<option [value]="m.id" [selected]="gameMode() === m.id">{{ m.id | translate }}</option>
}
</select>
</div>
Expand All @@ -27,3 +25,36 @@
</div>
</div>

@if (activeInfo()) {
<div class="info-overlay" (click)="activeInfo.set(null)">
<div class="info-popup" (click)="$event.stopPropagation()">
<a role="button" class="close" (click)="activeInfo.set(null)"><app-icon-close /></a>
@if (activeInfo() === 'generator') {
<h2>{{ 'BOARD_GENERATOR' | translate }}</h2>
@for (m of buildModes; track m) {
<div class="info-item">
<strong>{{ m.id | translate }}</strong>
<span>{{ m.id + '_DESC' | translate }}</span>
</div>
}
}
@if (activeInfo() === 'mode') {
<h2>{{ 'GAME_MODE' | translate }}</h2>
@for (m of gameModes; track m) {
<div class="info-item">
<strong>{{ m.id | translate }}</strong>
@if (m.features.length > 0) {
<ul>
@for (f of m.features; track f) {
<li>{{ f.title + '_LONG' | translate }}</li>
}
</ul>
} @else {
<span>{{ 'GAME_MODE_NO_HELPERS' | translate }}</span>
}
</div>
}
}
</div>
</div>
}
97 changes: 84 additions & 13 deletions src/app/components/choose-layout/choose-layout.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;

.choose-buttons {
padding-top: 10px;
Expand All @@ -25,25 +26,18 @@
flex: 1;

label {
display: block;
margin-bottom: 2px;
padding: 1px 5px 5px;
display: flex;
align-items: center;
user-select: none;
gap: 4px;
}

select {
width: 100%;
}
}

.mode {
max-width: 30%;
}

@include mixins.respond-to-height(medium-down) {
.mode {
max-width: 50%;
}
}

@include mixins.respond-to-height(small-down) {
padding-top: 4px;
}
Expand All @@ -65,7 +59,7 @@
align-items: unset;

label {
font-size: 0.7em;
font-size: 0.9em;
}

.mode {
Expand All @@ -77,5 +71,82 @@
}
}
}

label.info-label {
cursor: pointer;
transition: color 0.15s;

&:hover {
color: var(--text-highlight-color);
}
}

.info-overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 40%);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;

.info-popup {
position: relative;
background: var(--dialog-background-color);
border: 1px solid var(--dialog-border-color);
border-radius: 12px;
padding: 1.2em 1.5em 1em;
max-width: 90%;
max-height: 90%;
overflow-y: auto;
box-shadow: var(--overlay-popup-shadow);
color: var(--main-content-text-color);

h2 {
margin: 0 0 0.8em;
font-size: 1em;
color: var(--dialog-headline-color);
text-align: center;
}

.close {
position: absolute;
top: 0.5em;
right: 0.6em;
cursor: pointer;
color: var(--close-color);
line-height: 1;

&:hover {
color: var(--close-color-hover);
}
}

.info-item {
padding: 0.5em 0;
border-bottom: 1px solid var(--dialog-border-color);

&:last-child {
border-bottom: none;
padding-bottom: 0;
}

strong {
display: block;
margin-bottom: 0.2em;
}

span, ul {
color: var(--main-content-text-color-muted);
font-size: 0.85em;
}

ul {
margin: 0;
padding-left: 1.2em;
}
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ describe('ChooseLayoutComponent', () => {

it('should initialize with default values', () => {
expect(component.gameMode()).toBe(GAME_MODE_EASY);
expect(component.buildMode).toBe(MODE_SOLVABLE);
expect(component.buildModes).toHaveLength(2);
expect(component.buildMode()).toBe(MODE_SOLVABLE);
expect(component.buildModes).toHaveLength(4);
expect(component.gameModes).toHaveLength(3);
});

Expand Down Expand Up @@ -111,7 +111,7 @@ describe('ChooseLayoutComponent', () => {
fixture.detectChanges();

// Assert
expect(component.buildMode).toBe(MODE_RANDOM);
expect(component.buildMode()).toBe(MODE_RANDOM);
});

it('should update gameMode when select is changed', () => {
Expand Down
18 changes: 13 additions & 5 deletions src/app/components/choose-layout/choose-layout.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Component, inject, model, output } from '@angular/core';
import { type BUILD_MODE_ID, BuilderModes, MODE_SOLVABLE } from '../../model/builder';
import { Component, inject, model, output, signal } from '@angular/core';
import { type BUILD_MODE_ID, BuilderModes, MODE_SOLVABLE, solvableModeForGameMode } from '../../model/builder';
import type { Layout } from '../../model/types';
import { LayoutService } from '../../service/layout.service';
import { LocalstorageService } from '../../service/localstorage.service';
import { type GAME_MODE_ID, GameModes } from '../../model/consts';
import { TranslatePipe } from '@ngx-translate/core';
import { LayoutListComponent } from '../layout-list/layout-list.component';
import { IconInfoComponent } from '../icons/icon-info.component';
import { IconCloseComponent } from '../icons/icon-close.component';

export interface StartEvent {
layout: Layout;
Expand All @@ -17,20 +19,26 @@ export interface StartEvent {
selector: 'app-choose-layout',
templateUrl: './choose-layout.component.html',
styleUrls: ['./choose-layout.component.scss'],
imports: [LayoutListComponent, TranslatePipe]
imports: [LayoutListComponent, TranslatePipe, IconInfoComponent, IconCloseComponent]
})
export class ChooseLayoutComponent {
readonly startEvent = output<StartEvent>();
readonly gameMode = model.required<GAME_MODE_ID>();
buildMode: BUILD_MODE_ID = MODE_SOLVABLE;
readonly buildMode = model<BUILD_MODE_ID>(MODE_SOLVABLE);
buildModes = BuilderModes;
gameModes = GameModes;
activeInfo = signal<'generator' | 'mode' | null>(null);
layoutService = inject(LayoutService);
storage = inject(LocalstorageService);

onGameModeChange(mode: GAME_MODE_ID): void {
this.gameMode.set(mode);
this.buildMode.set(solvableModeForGameMode(mode));
}

onStart(layout: Layout): void {
if (layout) {
this.startEvent.emit({ layout, buildMode: this.buildMode, gameMode: this.gameMode() });
this.startEvent.emit({ layout, buildMode: this.buildMode(), gameMode: this.gameMode() });
this.storage.storeLastPlayed(layout.id);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/dialog/dialog.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<div class="overlay-popup" (click)="$event.stopPropagation();">
<div class="dialog-header">
@if (title()) {
<h1><i class="icon-logo"></i>{{ title() }}</h1>
<h1><app-icon-logo />{{ title() }}</h1>
}
@if (!noCloseButton()) {
<a role="button" class="close" (click)="toggle()" title="{{ 'CLOSE' | translate }}"><i class="icon-cancel-circled2"></i></a>
<a role="button" class="close" (click)="toggle()" title="{{ 'CLOSE' | translate }}"><app-icon-close /></a>
}
</div>
<ng-content></ng-content>
Expand Down
4 changes: 3 additions & 1 deletion src/app/components/dialog/dialog.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Component, input, model, output } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import { IconCloseComponent } from '../icons/icon-close.component';
import { IconLogoComponent } from '../icons/icon-logo.component';

@Component({
selector: 'app-dialog',
templateUrl: './dialog.component.html',
styleUrls: ['./dialog.component.scss'],
imports: [TranslatePipe]
imports: [TranslatePipe, IconLogoComponent, IconCloseComponent]
})
export class DialogComponent {
readonly title = input<string>();
Expand Down
Loading