├── .watchmanconfig
├── index.js
├── public
├── robots.txt
├── icon.png
├── favicon.ico
├── icon-192.png
├── icon-2048.png
├── icon-512.png
├── manifest.json
├── electron.js
├── index.css
└── index.html
├── src
├── op-game
│ ├── index.ts
│ └── Game.tsx
├── op-home
│ ├── index.tsx
│ ├── logo-border-dark.png
│ ├── logo-border-light.png
│ ├── Menu.tsx
│ ├── About.tsx
│ ├── Logo.tsx
│ └── Home.tsx
├── op-intro
│ ├── index.ts
│ └── Intro.tsx
├── op-splash
│ ├── index.ts
│ ├── Splash.tsx
│ └── Splash.native.tsx
├── op-stats
│ ├── index.ts
│ └── Stats.tsx
├── op-message
│ ├── index.ts
│ └── Message.tsx
├── op-success
│ ├── index.ts
│ └── Success.tsx
├── op-tutorial
│ ├── index.tsx
│ ├── Description.tsx
│ └── Tutorial.tsx
├── react-app-env.d.ts
├── op-core
│ ├── index.ts
│ ├── Layout.tsx
│ ├── Main.tsx
│ ├── Router.tsx
│ ├── App.tsx
│ └── store.ts
├── op-native
│ ├── react-native-immersive.ts
│ ├── react-native-keep-awake.ts
│ ├── react-native-bootsplash.ts
│ ├── react-native-sound.native.ts
│ ├── react-native-bootsplash.native.ts
│ ├── react-native-keep-awake.native.ts
│ ├── react-native-immersive.native.ts
│ └── react-native-sound.ts
├── op-board
│ ├── index.ts
│ ├── PointerAwareView.tsx
│ ├── Board.tsx
│ └── Tile.tsx
├── op-config
│ ├── index.ts
│ ├── strings.ts
│ └── constants.ts
├── op-utils
│ ├── delay.ts
│ ├── hapticFeedback.ts
│ ├── useOnMount.ts
│ ├── index.ts
│ ├── useAnimation.ts
│ ├── useHardwareBackButton.ts
│ ├── storage.ts
│ ├── pickPuzzle.ts
│ ├── sound.ts
│ ├── scale.ts
│ └── pickPuzzle.test.ts
├── op-design
│ ├── index.tsx
│ ├── metrics.ts
│ ├── colors.ts
│ ├── fonts.ts
│ └── animations.ts
├── index.native.ts
├── op-common
│ ├── index.tsx
│ ├── AnimatedLetter.tsx
│ ├── Text.tsx
│ ├── BottomNav.tsx
│ ├── Score.tsx
│ ├── Header.tsx
│ └── Button.tsx
├── index.web.ts
├── service-worker.ts
└── serviceWorkerRegistration.ts
├── .gitattributes
├── .github
├── logo.png
├── web-app-badge.png
├── app-store-badge.png
├── play-store-badge.png
├── iphone-screenshot-dark.png
└── iphone-screenshot-light.png
├── .env
├── assets
├── images
│ └── icon.png
├── audio
│ └── buttonpress.wav
└── fonts
│ └── Inter-SemiBold.otf
├── .eslintrc
├── android
├── app
│ ├── debug.keystore
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ ├── colors.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-ldpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── drawable
│ │ │ │ │ ├── bootsplash.xml
│ │ │ │ │ └── ic_launcher_background.xml
│ │ │ │ └── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── ic_launcher-web.png
│ │ │ ├── assets
│ │ │ │ └── fonts
│ │ │ │ │ └── Inter-SemiBold.otf
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── mmazzarolo
│ │ │ │ │ └── ordinarypuzzles
│ │ │ │ │ ├── MainActivity.java
│ │ │ │ │ └── MainApplication.java
│ │ │ └── AndroidManifest.xml
│ │ └── debug
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ └── com
│ │ │ └── mmazzarolo
│ │ │ └── ordinarypuzzles
│ │ │ └── ReactNativeFlipper.java
│ ├── proguard-rules.pro
│ ├── build_defs.bzl
│ ├── BUCK
│ └── build.gradle
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── settings.gradle
├── build.gradle
├── gradle.properties
├── gradlew.bat
└── gradlew
├── ios
├── OrdinaryPuzzles
│ ├── Images.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── ipad_app76x76.png
│ │ │ ├── ipad_app76x76@2x.png
│ │ │ ├── ipad_settings29x29.png
│ │ │ ├── ipad_spotlight40x40.png
│ │ │ ├── iphone_app60x60@2x.png
│ │ │ ├── iphone_app60x60@3x.png
│ │ │ ├── ipad_settings29x29@2x.png
│ │ │ ├── ios_marketing1024x1024.png
│ │ │ ├── ipad_notification20x20.png
│ │ │ ├── ipad_pro_app83.5x83.5@2x.png
│ │ │ ├── ipad_spotlight40x40@2x.png
│ │ │ ├── iphone_settings29x29@2x.png
│ │ │ ├── iphone_settings29x29@3x.png
│ │ │ ├── iphone_spotlight40x40@2x.png
│ │ │ ├── iphone_spotlight40x40@3x.png
│ │ │ ├── ipad_notification20x20@2x.png
│ │ │ ├── iphone_notification20x20@2x.png
│ │ │ ├── iphone_notification20x20@3x.png
│ │ │ └── Contents.json
│ ├── AppDelegate.h
│ ├── main.m
│ ├── Base.lproj
│ │ └── LaunchScreen.xib
│ ├── Info.plist
│ └── AppDelegate.m
├── File.swift
├── OrdinaryPuzzles-Bridging-Header.h
├── OrdinaryPuzzles-tvOS-Bridging-Header.h
├── OrdinaryPuzzles.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── OrdinaryPuzzlesTests
│ ├── Info.plist
│ └── OrdinaryPuzzlesTests.m
├── OrdinaryPuzzles-tvOSTests
│ └── Info.plist
├── Podfile
├── OrdinaryPuzzles-tvOS
│ └── Info.plist
└── OrdinaryPuzzles.xcodeproj
│ └── xcshareddata
│ └── xcschemes
│ ├── OrdinaryPuzzles.xcscheme
│ └── OrdinaryPuzzles-tvOS.xcscheme
├── jest.config.js
├── .buckconfig
├── react-native.config.js
├── metro.config.js
├── .vscode
└── settings.json
├── babel.config.js
├── config-overrides.js
├── tsconfig.json
├── LICENSE.md
├── .gitignore
├── CONTRIBUTING.md
├── README.md
└── package.json
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import "./src/index";
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
--------------------------------------------------------------------------------
/src/op-game/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Game";
2 |
--------------------------------------------------------------------------------
/src/op-home/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Home";
2 |
--------------------------------------------------------------------------------
/src/op-intro/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Intro";
2 |
--------------------------------------------------------------------------------
/src/op-splash/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Splash";
2 |
--------------------------------------------------------------------------------
/src/op-stats/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Stats";
2 |
--------------------------------------------------------------------------------
/src/op-message/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Message";
2 |
--------------------------------------------------------------------------------
/src/op-success/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Success";
2 |
--------------------------------------------------------------------------------
/src/op-tutorial/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Tutorial";
2 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # specific for windows script files
2 | *.bat text eol=crlf
--------------------------------------------------------------------------------
/src/op-core/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./App";
2 | export * from "./store";
3 |
--------------------------------------------------------------------------------
/src/op-native/react-native-immersive.ts:
--------------------------------------------------------------------------------
1 | export const Immersive: any = {};
2 |
--------------------------------------------------------------------------------
/src/op-board/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Board";
2 | export * from "./store";
3 |
--------------------------------------------------------------------------------
/src/op-config/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./constants";
2 | export * from "./strings";
3 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/public/icon.png
--------------------------------------------------------------------------------
/.github/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/.github/logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Ignore CRA warnings on startup caused by misaligned versions of babel
2 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/public/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/public/icon-192.png
--------------------------------------------------------------------------------
/public/icon-2048.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/public/icon-2048.png
--------------------------------------------------------------------------------
/public/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/public/icon-512.png
--------------------------------------------------------------------------------
/src/op-utils/delay.ts:
--------------------------------------------------------------------------------
1 | export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
2 |
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/src/op-native/react-native-keep-awake.ts:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 |
3 | export default Fragment;
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "plugin:prettier/recommended"],
3 | "plugins": ["prettier"]
4 | }
5 |
--------------------------------------------------------------------------------
/.github/web-app-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/.github/web-app-badge.png
--------------------------------------------------------------------------------
/src/op-native/react-native-bootsplash.ts:
--------------------------------------------------------------------------------
1 | const mock = {
2 | hide: () => {},
3 | };
4 |
5 | export default mock;
6 |
--------------------------------------------------------------------------------
/src/op-native/react-native-sound.native.ts:
--------------------------------------------------------------------------------
1 | import Sound from "react-native-sound";
2 |
3 | export default Sound;
4 |
--------------------------------------------------------------------------------
/.github/app-store-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/.github/app-store-badge.png
--------------------------------------------------------------------------------
/.github/play-store-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/.github/play-store-badge.png
--------------------------------------------------------------------------------
/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/debug.keystore
--------------------------------------------------------------------------------
/assets/audio/buttonpress.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/assets/audio/buttonpress.wav
--------------------------------------------------------------------------------
/assets/fonts/Inter-SemiBold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/assets/fonts/Inter-SemiBold.otf
--------------------------------------------------------------------------------
/src/op-home/logo-border-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/src/op-home/logo-border-dark.png
--------------------------------------------------------------------------------
/src/op-home/logo-border-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/src/op-home/logo-border-light.png
--------------------------------------------------------------------------------
/.github/iphone-screenshot-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/.github/iphone-screenshot-dark.png
--------------------------------------------------------------------------------
/.github/iphone-screenshot-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/.github/iphone-screenshot-light.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Ordinary Puzzles
3 |
4 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/op-native/react-native-bootsplash.native.ts:
--------------------------------------------------------------------------------
1 | import RNBootSplash from "react-native-bootsplash";
2 | export default RNBootSplash;
3 |
--------------------------------------------------------------------------------
/src/op-native/react-native-keep-awake.native.ts:
--------------------------------------------------------------------------------
1 | import KeepAwake from "react-native-keep-awake";
2 |
3 | export default KeepAwake;
4 |
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "react-native",
3 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
4 | };
5 |
--------------------------------------------------------------------------------
/src/op-design/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./animations";
2 | export * from "./colors";
3 | export * from "./fonts";
4 | export * from "./metrics";
5 |
--------------------------------------------------------------------------------
/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/react-native.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | project: {
3 | ios: {},
4 | android: {},
5 | },
6 | assets: ["./assets/fonts/"],
7 | };
8 |
--------------------------------------------------------------------------------
/src/op-design/metrics.ts:
--------------------------------------------------------------------------------
1 | export const metrics = {
2 | screenMargin: 16,
3 | webMaxLayoutWidth: 1024,
4 | webBoardMaxLayoutWidth: 800,
5 | };
6 |
--------------------------------------------------------------------------------
/ios/File.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // OrdinaryPuzzles
4 | //
5 | // Created by Matteo Mazzarolo on 11/10/2020.
6 | //
7 |
8 | import Foundation
9 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #84818D`
4 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles-tvOS-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Inter-SemiBold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/assets/fonts/Inter-SemiBold.otf
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/index.native.ts:
--------------------------------------------------------------------------------
1 | import { AppRegistry } from "react-native";
2 | import { App } from "op-core";
3 |
4 | AppRegistry.registerComponent("OrdinaryPuzzles", () => App);
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src/op-config/strings.ts:
--------------------------------------------------------------------------------
1 | export const credits = [
2 | "game design by\nMatteo Mazzarolo",
3 | "puzzles design by\nJuho Snellman",
4 | "font design by\nKostas Bartsokas",
5 | ];
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src/op-native/react-native-immersive.native.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import * as immersive from "react-native-immersive";
3 |
4 | // @ts-ignore
5 | export const Immersive = immersive.Immersive;
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_app76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_app76x76.png
--------------------------------------------------------------------------------
/src/op-common/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./AnimatedLetter";
2 | export * from "./BottomNav";
3 | export * from "./Button";
4 | export * from "./Header";
5 | export * from "./Score";
6 | export * from "./Text";
7 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_app76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_app76x76@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_settings29x29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_settings29x29.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_app60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_app60x60@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_app60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_app60x60@3x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_settings29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_settings29x29@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ios_marketing1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ios_marketing1024x1024.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_notification20x20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_notification20x20.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_pro_app83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_pro_app83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@3x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@3x.png
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'OrdinaryPuzzles'
2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
3 | include ':app'
4 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_notification20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/ipad_notification20x20@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@2x.png
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mmazzarolo/ordinary-puzzles-app/HEAD/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@3x.png
--------------------------------------------------------------------------------
/src/op-native/react-native-sound.ts:
--------------------------------------------------------------------------------
1 | export default class MockWebSound {
2 | constructor(a: any, b: any, c: (a: any) => void) {} // eslint-disable-line
3 | static setCategory(a: string, b: boolean) {}
4 | static MAIN_BUNDLE = "";
5 | }
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/bootsplash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : UIResponder
5 |
6 | @property (nonatomic, strong) UIWindow *window;
7 |
8 | @end
9 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/main.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char * argv[]) {
6 | @autoreleasepool {
7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/op-utils/hapticFeedback.ts:
--------------------------------------------------------------------------------
1 | import ReactNativeHaptic from "react-native-haptic";
2 | import { Platform } from "react-native";
3 |
4 | export const hapticFeedback = {
5 | generate:
6 | Platform.OS === "ios" ? ReactNativeHaptic.generate : () => undefined,
7 | };
8 |
--------------------------------------------------------------------------------
/src/op-utils/useOnMount.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | type EffectCallback = () => void | (() => void);
4 |
5 | export function useOnMount(onMount: EffectCallback) {
6 | // TODO: re-think this
7 | useEffect(onMount, []); // eslint-disable-line
8 | }
9 |
--------------------------------------------------------------------------------
/src/op-utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./delay";
2 | export * from "./hapticFeedback";
3 | export * from "./pickPuzzle";
4 | export * from "./scale";
5 | export * from "./sound";
6 | export * from "./storage";
7 | export * from "./useAnimation";
8 | export * from "./useHardwareBackButton";
9 | export * from "./useOnMount";
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Metro configuration for React Native
3 | * https://github.com/facebook/react-native
4 | *
5 | * @format
6 | */
7 |
8 | module.exports = {
9 | transformer: {
10 | getTransformOptions: async () => ({
11 | transform: {
12 | experimentalImportSupport: false,
13 | inlineRequires: false,
14 | },
15 | }),
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/op-splash/Splash.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { useOnMount } from "op-utils";
3 |
4 | interface SplashProps {
5 | onHide: () => void;
6 | }
7 |
8 | export const Splash: FC = function ({ onHide }) {
9 | // @ts-ignore
10 | document.getElementById("splash").style.display = "none";
11 |
12 | useOnMount(() => {
13 | onHide();
14 | });
15 |
16 | return null;
17 | };
18 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.autoFixOnSave": true,
3 | "editor.formatOnSave": true,
4 | "eslint.enable": true,
5 | "eslint.validate": [
6 | "javascript",
7 | "javascriptreact",
8 | "typescript",
9 | "typescriptreact"
10 | ],
11 | "typescript.preferences.importModuleSpecifier": "auto",
12 | "typescript.tsdk": "node_modules/typescript/lib",
13 | "editor.codeActionsOnSave": {
14 | "source.fixAll.eslint": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["module:metro-react-native-babel-preset"],
3 | plugins: [
4 | [
5 | "module-resolver",
6 | {
7 | root: ["./src"],
8 | extensions: [".ios.js", ".android.js", ".js", ".ts", ".tsx", ".json"],
9 | alias: { "test/*": "./test/" },
10 | },
11 | ],
12 | ],
13 | env: {
14 | production: {
15 | plugins: ["transform-remove-console"],
16 | },
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Ordinary Puzzles",
3 | "name": "Ordinary Puzzles",
4 | "description": "Minimalistic logic puzzle game",
5 | "icons": [
6 | {
7 | "src": "icon-192.png",
8 | "type": "image/png",
9 | "sizes": "192x192",
10 | "purpose": "any maskable"
11 | },
12 | {
13 | "src": "icon-512.png",
14 | "type": "image/png",
15 | "sizes": "512x512",
16 | "purpose": "any maskable"
17 | }
18 | ],
19 | "start_url": ".",
20 | "display": "fullscreen",
21 | "theme_color": "#fff",
22 | "background_color": "#FBFAFF"
23 | }
24 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const { override, addWebpackPlugin } = require("customize-cra");
3 |
4 | // Rename the CRA entry point to avoid confustion between it and the native one.
5 | const paths = require("react-scripts/config/paths");
6 | paths.appIndexJs = `${paths.appSrc}/index.web.ts`;
7 |
8 | module.exports = override(
9 | addWebpackPlugin(
10 | new webpack.DefinePlugin({
11 | "process.env.NODE_ENV": JSON.stringify(
12 | process.env.NODE_ENV || "development"
13 | ),
14 | __DEV__: process.env.NODE_ENV !== "production",
15 | __ELECTRON__: !!process.env.ELECTRON,
16 | })
17 | )
18 | );
19 |
--------------------------------------------------------------------------------
/src/op-core/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { metrics } from "op-design";
2 | import React, { FC } from "react";
3 | import { StyleSheet, Platform, View } from "react-native";
4 |
5 | // Centers the layout horizontally on the web, clamping it to "webMaxLayoutWidth"
6 | export const Layout: FC = function ({ children }) {
7 | return Platform.select({
8 | native: <>{children}>,
9 | default: {children},
10 | });
11 | };
12 |
13 | const styles = StyleSheet.create({
14 | root: {
15 | height: "100%",
16 | width: "100%",
17 | maxWidth: metrics.webMaxLayoutWidth,
18 | alignSelf: "center",
19 | flex: 1,
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/src/op-utils/useAnimation.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from "react";
2 | import { Animated, Easing } from "react-native";
3 |
4 | export const useAnimation = function (initialValue: number = 0) {
5 | const endValue = initialValue === 0 ? 1 : 0;
6 | const animationValueRef = useRef(new Animated.Value(initialValue));
7 |
8 | const setup = (config: Partial = {}) =>
9 | Animated.timing(animationValueRef.current, {
10 | toValue: endValue,
11 | useNativeDriver: true,
12 | easing: Easing.inOut(Easing.quad),
13 | ...config,
14 | });
15 |
16 | return {
17 | value: animationValueRef.current,
18 | setup: setup,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/android/app/build_defs.bzl:
--------------------------------------------------------------------------------
1 | """Helper definitions to glob .aar and .jar targets"""
2 |
3 | def create_aar_targets(aarfiles):
4 | for aarfile in aarfiles:
5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
6 | lib_deps.append(":" + name)
7 | android_prebuilt_aar(
8 | name = name,
9 | aar = aarfile,
10 | )
11 |
12 | def create_jar_targets(jarfiles):
13 | for jarfile in jarfiles:
14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
15 | lib_deps.append(":" + name)
16 | prebuilt_jar(
17 | name = name,
18 | binary_jar = jarfile,
19 | )
20 |
--------------------------------------------------------------------------------
/src/op-utils/useHardwareBackButton.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from "react";
2 | import { NativeEventSubscription, BackHandler } from "react-native";
3 | import { useOnMount } from "./useOnMount";
4 |
5 | export function useHardwareBackButton(onBackButtonPress: () => void) {
6 | const backHandlerRef = useRef();
7 | useOnMount(() => {
8 | backHandlerRef.current = BackHandler.addEventListener(
9 | "hardwareBackPress",
10 | () => {
11 | onBackButtonPress();
12 | return true;
13 | }
14 | );
15 | return () => {
16 | if (backHandlerRef.current) {
17 | backHandlerRef.current.remove();
18 | }
19 | };
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "baseUrl": "src",
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "isolatedModules": true,
9 | "jsx": "react",
10 | "lib": ["esnext", "dom"],
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "noEmit": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "esnext",
19 | "useDefineForClassFields": true
20 | },
21 | "exclude": [
22 | "node_modules",
23 | "babel.config.js",
24 | "metro.config.js",
25 | "jest.config.js"
26 | ],
27 | "include": ["src"]
28 | }
29 |
--------------------------------------------------------------------------------
/src/op-design/colors.ts:
--------------------------------------------------------------------------------
1 | import { Appearance, useColorScheme } from "react-native";
2 | import tinycolor from "tinycolor2";
3 |
4 | const primaryColor = "#171520";
5 | const splashColor = "#84818D";
6 |
7 | const palette = new Array(10).fill(primaryColor).map((color, index) =>
8 | tinycolor(color)
9 | .brighten(index * 10)
10 | .toString()
11 | );
12 | const lightColors = palette;
13 | const darkColors = palette.slice().reverse();
14 |
15 | export const colors = {
16 | primary: Appearance.getColorScheme() === "dark" ? darkColors : lightColors,
17 | splash: splashColor,
18 | };
19 |
20 | export const useColors = function () {
21 | const colorScheme = useColorScheme();
22 | return {
23 | primary: colorScheme === "dark" ? darkColors : lightColors,
24 | splash: splashColor,
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/src/index.web.ts:
--------------------------------------------------------------------------------
1 | /* global __ELECTRON__ */
2 | import { AppRegistry } from "react-native";
3 | import { App } from "op-core";
4 | import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
5 |
6 | AppRegistry.registerComponent("OrdinaryPuzzles", () => App);
7 |
8 | // Load the app only when all the fonts are loaded
9 | Promise.all([
10 | // @ts-ignore
11 | document.fonts.load("12px Averta-Bold"),
12 | // @ts-ignore
13 | document.fonts.load("12px Averta-Semibold"),
14 | // @ts-ignore
15 | document.fonts.load("12px Averta-Regular"),
16 | ]).then((f) => {
17 | AppRegistry.runApplication("OrdinaryPuzzles", {
18 | rootTag: document.getElementById("root"),
19 | });
20 | });
21 |
22 | // Opt-out of the service-worker in Electron
23 | // @ts-ignore
24 | if (!__ELECTRON__) {
25 | serviceWorkerRegistration.register();
26 | }
27 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzlesTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles-tvOSTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/op-common/AnimatedLetter.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { Animated } from "react-native";
3 | import { Text, TextProps } from "op-common";
4 | import { animations } from "op-design";
5 | import { useScale } from "op-utils";
6 |
7 | interface AnimatedLetterProps extends TextProps {
8 | animValue: Animated.Value;
9 | delay: number;
10 | value: string;
11 | secondary?: boolean;
12 | }
13 |
14 | export const AnimatedLetter: FC = function ({
15 | animValue,
16 | delay,
17 | style,
18 | value,
19 | secondary,
20 | }) {
21 | const scale = useScale();
22 | const charAnimatedStyle = animations.fadeSlideBottom(animValue, scale, {
23 | interpolateStart: delay,
24 | });
25 | return (
26 |
31 | {value}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/op-utils/storage.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from "@react-native-community/async-storage";
2 |
3 | const storageItemKeys = ["completedPuzzles", "playedPuzzles"] as const;
4 |
5 | type ElementType> = T extends ReadonlyArray<
6 | infer ElementType
7 | >
8 | ? ElementType
9 | : never;
10 |
11 | type StorageItemKey = ElementType;
12 |
13 | export const clearStorage = async () => {
14 | await Promise.all(storageItemKeys.map((key) => AsyncStorage.removeItem(key)));
15 | };
16 |
17 | export const rehydrateObject = async (key: StorageItemKey) => {
18 | const serializedItem = await AsyncStorage.getItem(key);
19 | const item = serializedItem ? JSON.parse(serializedItem) : undefined;
20 | return item;
21 | };
22 |
23 | export const persistObject = async (key: StorageItemKey, value: Object) => {
24 | const serializedItem = JSON.stringify(value);
25 | await AsyncStorage.setItem(key, serializedItem);
26 | };
27 |
--------------------------------------------------------------------------------
/src/op-core/Main.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from "react";
2 | import { StyleSheet, SafeAreaView } from "react-native";
3 | import { useColors } from "op-design";
4 | import { Splash } from "op-splash";
5 | import { skipSplashScreen } from "op-config";
6 | import { Router } from "./Router";
7 | import { Layout } from "./Layout";
8 |
9 | export const Main: FC = function () {
10 | const colors = useColors();
11 | const [isShowingSplash, setIsShowingSplash] = useState(!skipSplashScreen);
12 | const hideSplash = () => {
13 | setIsShowingSplash(false);
14 | };
15 |
16 | if (isShowingSplash) return ;
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | const styles = StyleSheet.create({
28 | root: {
29 | height: "100%",
30 | width: "100%",
31 | flex: 1,
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/src/op-config/constants.ts:
--------------------------------------------------------------------------------
1 | // Turn "simulateProduction" on to simulate a production environment
2 | const simulateProduction = false;
3 |
4 | // Enable MobX logging (trough the mobx-logger lib)
5 | const _enableMobxLogging = true;
6 |
7 | // Don't show the splash screen
8 | const _skipSplashScreen = false;
9 |
10 | // Clean the local-storage
11 | const _simulateFirstLoad = false;
12 |
13 | // Auto-solve the puzzle after 2000 ms
14 | const _autoSolve = false;
15 |
16 | // Use the Averta font?
17 | const _useAvertaFont = true;
18 |
19 | // To be safe, let's make sure we don't user development settings in production
20 | const isDevelopment = __DEV__ && !simulateProduction;
21 | export const enableMobxLogging = isDevelopment && _enableMobxLogging;
22 | export const skipSplashScreen = isDevelopment && _skipSplashScreen;
23 | export const simulateFirstLoad = isDevelopment && _simulateFirstLoad;
24 | export const autoSolve = isDevelopment && _autoSolve;
25 | export const useAvertaFont = !isDevelopment || _useAvertaFont;
26 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | require_relative '../node_modules/react-native/scripts/react_native_pods'
2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
3 |
4 | platform :ios, '10.0'
5 |
6 | target 'OrdinaryPuzzles' do
7 | config = use_native_modules!
8 |
9 | use_react_native!(:path => config["reactNativePath"])
10 |
11 | target 'OrdinaryPuzzlesTests' do
12 | inherit! :search_paths
13 | # Pods for testing
14 | end
15 |
16 | # Enables Flipper.
17 | #
18 | # Note that if you have use_frameworks! enabled, Flipper will not work and
19 | # you should disable these next few lines.
20 | use_flipper!({ 'Flipper-Folly' => '2.5.3', 'Flipper' => '0.87.0', 'Flipper-RSocket' => '1.3.1' })
21 | post_install do |installer|
22 | flipper_post_install(installer)
23 | end
24 | end
25 |
26 | target 'OrdinaryPuzzles-tvOS' do
27 | # Pods for OrdinaryPuzzles-tvOS
28 |
29 | target 'OrdinaryPuzzles-tvOSTests' do
30 | inherit! :search_paths
31 | # Pods for testing
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/src/op-common/Text.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { TextProps as RNTextProps, StyleSheet, Animated } from "react-native";
3 | import { fonts, useColors } from "op-design";
4 |
5 | export type TextFamily = keyof typeof fonts;
6 |
7 | export type TextWeight = keyof typeof fonts.primary;
8 |
9 | export interface TextProps extends RNTextProps {
10 | family?: TextFamily;
11 | weight?: TextWeight;
12 | secondary?: boolean;
13 | style?: any; // Because on the missing "Animated" typings for the style
14 | }
15 |
16 | export const Text: FC = function ({
17 | children,
18 | family = "primary",
19 | weight = "regular",
20 | secondary = false,
21 | style,
22 | }) {
23 | const colors = useColors();
24 | const font = fonts[family][weight];
25 | const color = secondary ? colors.primary[5] : colors.primary[0];
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | const styles = StyleSheet.create({
34 | text: {},
35 | });
36 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/mmazzarolo/ordinarypuzzles/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.mmazzarolo.ordinarypuzzles;
2 |
3 | import android.os.Bundle;
4 |
5 | import com.facebook.react.ReactActivity;
6 | import com.zoontek.rnbootsplash.RNBootSplash;
7 | import com.rnimmersive.RNImmersiveModule;
8 |
9 | public class MainActivity extends ReactActivity {
10 |
11 | /**
12 | * Returns the name of the main component registered from JavaScript. This is used to schedule
13 | * rendering of the component.
14 | */
15 | @Override
16 | protected String getMainComponentName() {
17 | return "OrdinaryPuzzles";
18 | }
19 |
20 | @Override
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | RNBootSplash.show(R.drawable.bootsplash, MainActivity.this);
24 | }
25 |
26 | @Override
27 | public void onWindowFocusChanged(boolean hasFocus) {
28 | super.onWindowFocusChanged(hasFocus);
29 | if (hasFocus && RNImmersiveModule.getInstance() != null) {
30 | RNImmersiveModule.getInstance().emitImmersiveStateChangeEvent();
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Matteo Mazzarolo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "29.0.2"
6 | minSdkVersion = 16
7 | compileSdkVersion = 29
8 | targetSdkVersion = 29
9 | }
10 | repositories {
11 | google()
12 | jcenter()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle:3.5.3")
16 |
17 | // NOTE: Do not place your application dependencies here; they belong
18 | // in the individual module build.gradle files
19 | }
20 | }
21 |
22 | allprojects {
23 | repositories {
24 | mavenLocal()
25 | maven {
26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
27 | url("$rootDir/../node_modules/react-native/android")
28 | }
29 | maven {
30 | // Android JSC is installed from npm
31 | url("$rootDir/../node_modules/jsc-android/dist")
32 | }
33 |
34 | google()
35 | jcenter()
36 | maven { url 'https://www.jitpack.io' }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/op-common/BottomNav.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { Animated } from "react-native";
3 | import { animations } from "op-design";
4 | import { useScale, ScalingFunc } from "op-utils";
5 | import { defaultButtonTextSize } from "./Button";
6 |
7 | interface BottomNavProps {
8 | animValue: Animated.Value;
9 | }
10 |
11 | export const BottomNav: FC = function ({
12 | animValue,
13 | children,
14 | }) {
15 | const scale = useScale();
16 | const styles = createStyles({ scale });
17 | return (
18 |
21 | {children}
22 |
23 | );
24 | };
25 |
26 | const marginTop = 10;
27 | const marginBottom = 14;
28 |
29 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
30 | root: {
31 | flexDirection: "row",
32 | justifyContent: "space-between",
33 | marginTop: scale(marginTop),
34 | marginBottom: scale(marginBottom),
35 | },
36 | });
37 |
38 | export const getBottomNavHeight = (scale: ScalingFunc): any =>
39 | scale(marginTop) + scale(marginBottom) + scale(defaultButtonTextSize);
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 |
24 | # Android/IntelliJ
25 | #
26 | build/
27 | .idea
28 | .gradle
29 | local.properties
30 | *.iml
31 |
32 | # node.js
33 | #
34 | node_modules/
35 | npm-debug.log
36 | yarn-error.log
37 |
38 | # BUCK
39 | buck-out/
40 | \.buckd/
41 | *.keystore
42 | !debug.keystore
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://docs.fastlane.tools/best-practices/source-control/
50 |
51 | */fastlane/report.xml
52 | */fastlane/Preview.html
53 | */fastlane/screenshots
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
58 | # CocoaPods
59 | /ios/Pods/
60 |
61 | # Fonts
62 | Averta-Bold.*
63 | Averta-Regular.*
64 | Averta-Semibold.*
65 |
66 | # Electron outputs
67 | dist/
--------------------------------------------------------------------------------
/src/op-core/Router.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { observer } from "mobx-react";
3 | import { Home } from "op-home/Home";
4 | import { Game } from "op-game/Game";
5 | import { Intro } from "op-intro/Intro";
6 | import { Tutorial } from "op-tutorial/Tutorial";
7 | import { Message } from "op-message/Message";
8 | import { Success } from "op-success/Success";
9 | import { Stats } from "op-stats/Stats";
10 | import { useCoreStores } from "./store";
11 |
12 | export const Router: FC = observer(function () {
13 | const { puzzle, router } = useCoreStores();
14 | if (router.currentRoute === "home") {
15 | return ;
16 | } else if (router.currentRoute === "intro") {
17 | return ;
18 | } else if (router.currentRoute === "game") {
19 | return ;
20 | } else if (router.currentRoute === "success") {
21 | return ;
22 | } else if (router.currentRoute === "stats") {
23 | return ;
24 | } else if (router.currentRoute === "tutorial") {
25 | if (puzzle.type === "message") {
26 | return ;
27 | } else {
28 | return ;
29 | }
30 | } else {
31 | throw new Error(`Invalid route: ${router.currentRoute}`);
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/op-tutorial/Description.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { Animated, ViewProps } from "react-native";
3 | import { Text } from "op-common";
4 | import { animations } from "op-design";
5 | import { useScale, ScalingFunc } from "op-utils";
6 |
7 | interface DescriptionProps extends ViewProps {
8 | animValue: Animated.Value;
9 | title: string;
10 | message: string;
11 | }
12 |
13 | export const Description: FC = function ({
14 | animValue,
15 | title,
16 | message,
17 | ...otherProps
18 | }) {
19 | const scale = useScale();
20 | const styles = createStyles({ scale });
21 | return (
22 |
26 |
27 | {title}
28 |
29 |
30 | {message}
31 |
32 |
33 | );
34 | };
35 |
36 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
37 | root: {
38 | marginTop: scale(32),
39 | },
40 | title: {
41 | fontSize: scale(24),
42 | marginBottom: scale(4),
43 | },
44 | message: {
45 | fontSize: scale(22),
46 | },
47 | });
48 |
--------------------------------------------------------------------------------
/src/op-design/fonts.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from "react-native";
2 | import { useAvertaFont } from "op-config";
3 |
4 | // Font used across the entire app (menu, title, messages, buttons).
5 | // In production it's the "Averta" font.
6 | // I'm not distributing the Averta font on GitHub.
7 | // If you want to contribute, you can toggle off the "useAvertaFont" config
8 | // variable.
9 | const primaryFont = {
10 | regular: {
11 | fontFamily: useAvertaFont ? "Averta-Regular" : undefined,
12 | fontWeight: "400",
13 | },
14 | semibold: {
15 | fontFamily: useAvertaFont ? "Averta-Semibold" : undefined,
16 | fontWeight: "500",
17 | },
18 | bold: {
19 | fontFamily: useAvertaFont ? "Averta-Bold" : undefined,
20 | fontWeight: Platform.select({
21 | native: "600",
22 | default: "500", // On the web this alligns the Safari and Chrome rendering
23 | }),
24 | },
25 | };
26 |
27 | // Font used for numbers (just for semibold).
28 | // It's the open-source "Inter" font on Android, "San-Francisco" on iOS.
29 | const secondaryFont = {
30 | semibold: {
31 | fontFamily: Platform.OS === "android" ? "Inter-SemiBold" : undefined,
32 | fontWeight: "600",
33 | },
34 | bold: {},
35 | regular: {},
36 | };
37 |
38 | export const fonts = {
39 | primary: primaryFont,
40 | secondary: secondaryFont,
41 | };
42 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
11 |
12 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.54.0
29 |
30 | ORDINARY_PUZZLES_RELEASE_STORE_FILE=ordinary-puzzles-release-key.keystore
31 | ORDINARY_PUZZLES_RELEASE_KEY_ALIAS=ordinary-puzzles-key-alias
--------------------------------------------------------------------------------
/src/op-core/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { StatusBar, Platform, UIManager } from "react-native";
3 | import RNBootSplash from "op-native/react-native-bootsplash";
4 | import { Immersive } from "op-native/react-native-immersive";
5 | import { configure } from "mobx";
6 | import { enableLogging } from "mobx-logger";
7 | import { useOnMount, clearStorage, initializeAudio } from "op-utils";
8 | import { simulateFirstLoad, enableMobxLogging } from "op-config";
9 | import { Main } from "./Main";
10 | import { useCoreStores } from "./store";
11 |
12 | configure({
13 | enforceActions: "always",
14 | });
15 |
16 | if (enableMobxLogging) {
17 | enableLogging({});
18 | }
19 |
20 | if (Platform.OS === "android") {
21 | Immersive.on();
22 | Immersive.setImmersive(true);
23 | Immersive.addImmersiveListener(() => Immersive.on());
24 | if (UIManager.setLayoutAnimationEnabledExperimental) {
25 | UIManager.setLayoutAnimationEnabledExperimental(true);
26 | }
27 | }
28 |
29 | export const App: FC = function () {
30 | const { initializeStore } = useCoreStores();
31 | const initializeApp = async () => {
32 | if (simulateFirstLoad) {
33 | await clearStorage();
34 | }
35 | await initializeStore();
36 | initializeAudio();
37 | if (Platform.OS === "android" || Platform.OS === "ios") {
38 | RNBootSplash.hide();
39 | }
40 | };
41 | useOnMount(() => {
42 | initializeApp();
43 | });
44 | return (
45 | <>
46 |
47 |
48 | >
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/android/app/BUCK:
--------------------------------------------------------------------------------
1 | # To learn about Buck see [Docs](https://buckbuild.com/).
2 | # To run your application with Buck:
3 | # - install Buck
4 | # - `npm start` - to start the packager
5 | # - `cd android`
6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
8 | # - `buck install -r android/app` - compile, install and run application
9 | #
10 |
11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
12 |
13 | lib_deps = []
14 |
15 | create_aar_targets(glob(["libs/*.aar"]))
16 |
17 | create_jar_targets(glob(["libs/*.jar"]))
18 |
19 | android_library(
20 | name = "all-libs",
21 | exported_deps = lib_deps,
22 | )
23 |
24 | android_library(
25 | name = "app-code",
26 | srcs = glob([
27 | "src/main/java/**/*.java",
28 | ]),
29 | deps = [
30 | ":all-libs",
31 | ":build_config",
32 | ":res",
33 | ],
34 | )
35 |
36 | android_build_config(
37 | name = "build_config",
38 | package = "com.ordinarypuzzles",
39 | )
40 |
41 | android_resource(
42 | name = "res",
43 | package = "com.ordinarypuzzles",
44 | res = "src/main/res",
45 | )
46 |
47 | android_binary(
48 | name = "app",
49 | keystore = "//android/keystores:debug",
50 | manifest = "src/main/AndroidManifest.xml",
51 | package_type = "debug",
52 | deps = [
53 | ":app-code",
54 | ],
55 | )
56 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/op-design/animations.ts:
--------------------------------------------------------------------------------
1 | import { Animated } from "react-native";
2 |
3 | interface Config {
4 | interpolateStart?: number;
5 | }
6 |
7 | const createInterpolationRanges = (
8 | from: number,
9 | to: number,
10 | config?: Config
11 | ) => {
12 | if (config && config.interpolateStart) {
13 | return {
14 | inputRange: [0, config.interpolateStart, 1],
15 | outputRange: [from, from, to],
16 | };
17 | }
18 | return {
19 | inputRange: [0, 1],
20 | outputRange: [from, to],
21 | };
22 | };
23 |
24 | const fade = (animValue: Animated.Value, config: Config = {}) => ({
25 | opacity: animValue.interpolate(createInterpolationRanges(0, 1, config)),
26 | });
27 |
28 | const slide = (
29 | animValue: Animated.Value,
30 | from: "top" | "bottom",
31 | config: Config = {},
32 | scale: (n: number) => number
33 | ) => ({
34 | transform: [
35 | {
36 | translateY: animValue.interpolate(
37 | createInterpolationRanges(
38 | from === "top" ? -scale(20) : +scale(20),
39 | 1,
40 | config
41 | )
42 | ),
43 | },
44 | ],
45 | });
46 |
47 | const fadeSlideTop = (
48 | animValue: Animated.Value,
49 | scale: (n: number) => number,
50 | config: Config = {}
51 | ) => ({
52 | ...fade(animValue, config),
53 | ...slide(animValue, "top", config, scale),
54 | });
55 |
56 | const fadeSlideBottom = (
57 | animValue: Animated.Value,
58 | scale: (n: number) => number,
59 | config: Config = {}
60 | ) => ({
61 | ...fade(animValue, config),
62 | ...slide(animValue, "bottom", config, scale),
63 | });
64 |
65 | export const animations = {
66 | fade,
67 | slide,
68 | fadeSlideBottom,
69 | fadeSlideTop,
70 | };
71 |
--------------------------------------------------------------------------------
/src/op-common/Score.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { Animated, ViewStyle } from "react-native";
3 | import { Text } from "op-common";
4 | import { useScale, ScalingFunc } from "op-utils";
5 | import { animations } from "op-design";
6 | import { Button } from "./Button";
7 |
8 | interface ScoreProps {
9 | animValue: Animated.Value;
10 | onPress?: () => void;
11 | score?: string | number;
12 | style?: ViewStyle;
13 | }
14 |
15 | export const Score: FC = function ({
16 | animValue,
17 | onPress,
18 | score,
19 | style,
20 | }) {
21 | const scale = useScale();
22 | const styles = createStyles({ scale });
23 | if (!score) return null;
24 | const buttonHitSlop = {
25 | top: scale(20),
26 | bottom: scale(20),
27 | left: scale(20),
28 | right: scale(20),
29 | };
30 | return (
31 |
32 |
46 |
47 | );
48 | };
49 |
50 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
51 | root: {
52 | position: "absolute",
53 | alignSelf: "flex-end",
54 | },
55 | content: {
56 | alignItems: "center",
57 | },
58 | score: {
59 | fontSize: scale(20),
60 | },
61 | star: {
62 | fontSize: scale(16),
63 | },
64 | });
65 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles-tvOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | NSAppTransportSecurity
26 |
27 | NSExceptionDomains
28 |
29 | localhost
30 |
31 | NSExceptionAllowsInsecureHTTPLoads
32 |
33 |
34 |
35 |
36 | NSLocationWhenInUseUsageDescription
37 |
38 | UILaunchStoryboardName
39 | LaunchScreen
40 | UIRequiredDeviceCapabilities
41 |
42 | armv7
43 |
44 | UISupportedInterfaceOrientations
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationLandscapeLeft
48 | UIInterfaceOrientationLandscapeRight
49 |
50 | UIViewControllerBasedStatusBarAppearance
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/op-common/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { Animated } from "react-native";
3 | import { metrics, animations } from "op-design";
4 | import { Text } from "op-common";
5 | import { observer } from "mobx-react-lite";
6 | import { useScale, ScalingFunc } from "op-utils";
7 |
8 | interface HeaderProps {
9 | fadeAnimValue: Animated.Value;
10 | name: string;
11 | prefix: string;
12 | fontSize?: number;
13 | }
14 |
15 | export const Header: FC = observer(function ({
16 | fadeAnimValue,
17 | name,
18 | prefix,
19 | fontSize,
20 | }) {
21 | const scale = useScale();
22 | const styles = createStyles({ scale });
23 | const textStyle = {
24 | fontSize: fontSize || scale(34),
25 | };
26 | return (
27 |
30 |
31 |
32 | {prefix}{" "}
33 |
34 |
35 | {name}
36 |
37 |
38 |
39 | );
40 | });
41 |
42 | const fontSize = 34;
43 |
44 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
45 | root: {
46 | flexDirection: "column",
47 | marginTop: scale(metrics.screenMargin),
48 | marginBottom: scale(metrics.screenMargin) / 2,
49 | zIndex: 200,
50 | },
51 | identifier: {
52 | flexDirection: "row",
53 | },
54 | text: {
55 | fontSize: scale(fontSize),
56 | },
57 | });
58 |
59 | export const getHeaderHeight = (scale: ScalingFunc) =>
60 | scale(metrics.screenMargin) +
61 | scale(metrics.screenMargin) / 2 +
62 | scale(fontSize);
63 |
--------------------------------------------------------------------------------
/src/op-home/Menu.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { Animated } from "react-native";
3 | import { Button, Text } from "op-common";
4 | import { PuzzleMode } from "op-core";
5 | import { animations, useColors } from "op-design";
6 | import { useScale, ScalingFunc } from "op-utils";
7 |
8 | export interface MenuItem {
9 | label: string;
10 | value: PuzzleMode | "continue";
11 | highlighted?: boolean;
12 | starred?: boolean;
13 | onPress: (value: PuzzleMode | "continue") => void;
14 | }
15 |
16 | interface MenuProps {
17 | animValue: Animated.Value;
18 | disabled?: boolean;
19 | items: MenuItem[];
20 | }
21 |
22 | export const Menu: FC = function ({ animValue, disabled, items }) {
23 | const scale = useScale();
24 | const styles = createStyles({ scale });
25 | const colors = useColors();
26 | const starStyle = {
27 | color: colors.primary[5],
28 | };
29 | return (
30 |
31 | {items.map((item) => (
32 |
47 | ))}
48 |
49 | );
50 | };
51 |
52 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
53 | root: {},
54 | button: {
55 | marginBottom: scale(2),
56 | alignSelf: "flex-start",
57 | },
58 | text: {
59 | fontSize: scale(42),
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Ordinary Puzzles
2 |
3 | PRs are welcome. When submitting a PR, please consider the following:
4 |
5 | - We require tests and will most likely reject a PR if there are no tests.
6 |
7 | - We are using [TypeScript](https://www.typescriptlang.org/index.html) and all development should be done in TypeScript.
8 |
9 | - This project uses [ESLint](https://eslint.org) for linting and [Prettier](https://prettier.io/) for formatting. See more below.
10 |
11 | ## Running Ordinary Puzzles
12 |
13 | 1. Setup the project by running `npm run install`.
14 | 2. Disable the Averta font by setting `_useAvertaFont` to `false` in [`src/op-config/constants.ts`](./src/op-config/constants.ts).
15 | 3. Run `npm run ios` to run the app on iOS, `npm run android` to run the app on Android, or `npm run web` to run the app on the web.
16 |
17 | That's it! 🎉
18 |
19 | ## Testing
20 |
21 | Ordinary Puzzles is being tested with [Jest](https://jestjs.io/docs/en/tutorial-react).
22 | For running the tests, run `npm run test:watch` to test as you develop, or `npm run test` for a single run.
23 |
24 | ## Linting
25 |
26 | This project uses [ESLint](https://eslint.org) with a [simple preset of rules used by Create-React-App](https://github.com/mmazzarolo/eslint-plugin-react-app).
27 | This ESLint setup also includes a standard [Prettier](https://prettier.io/) configuration that handles the code formatting.
28 | The code is automatically formatted before each commit (see the `lint-staged` section in the `package.json` for more details).
29 | We suggest to enable the formatting on save feature of your editor of choice. If you use VSCode it will be already enabled by default while working on this project (see the [`.vscode`](../.vscode) directory included in the project).
30 | To manually invoke the linter you can run `npm run lint`.
31 |
--------------------------------------------------------------------------------
/src/op-common/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { TouchableOpacity, TouchableOpacityProps } from "react-native";
3 | import { hapticFeedback, playSound, ScalingFunc, useScale } from "op-utils";
4 | import { Text, TextFamily, TextWeight } from "./Text";
5 |
6 | export const defaultButtonTextSize = 32;
7 |
8 | interface ButtonProps extends TouchableOpacityProps {
9 | highlighted?: boolean;
10 | label: string;
11 | textColor?: string;
12 | textFamily?: TextFamily;
13 | textSize?: number;
14 | textWeight?: TextWeight;
15 | }
16 |
17 | export const Button: FC = function ({
18 | children,
19 | highlighted = true,
20 | label,
21 | style = {},
22 | textFamily,
23 | textColor,
24 | textSize,
25 | textWeight = "semibold",
26 | ...otherProps
27 | }) {
28 | const scale = useScale();
29 | const styles = createStyles({ scale });
30 | const handlePressIn = () => {
31 | hapticFeedback.generate("impactMedium");
32 | playSound("buttonPress");
33 | };
34 | return (
35 |
40 |
51 | {label.toLowerCase()}
52 |
53 | {children}
54 |
55 | );
56 | };
57 |
58 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
59 | touchable: {
60 | flexDirection: "row",
61 | },
62 | label: {},
63 | labelHighlighted: {},
64 | });
65 |
--------------------------------------------------------------------------------
/public/electron.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow, protocol } = require("electron");
2 | const path = require("path");
3 | const url = require("url");
4 |
5 | let mainWindow;
6 |
7 | const isDev = !!process.env.ELECTRON_START_URL;
8 |
9 | function createWindow() {
10 | mainWindow = new BrowserWindow({
11 | width: 1024,
12 | height: 768,
13 | });
14 |
15 | mainWindow.loadURL(
16 | process.env.ELECTRON_START_URL ||
17 | url.format({
18 | pathname: path.join(__dirname, "../build/index.html"),
19 | protocol: "file:",
20 | slashes: true,
21 | })
22 | );
23 |
24 | if (isDev) {
25 | mainWindow.webContents.openDevTools();
26 | }
27 |
28 | mainWindow.on("closed", () => {
29 | mainWindow = null;
30 | });
31 | }
32 |
33 | // Call when we're ready to create browser windows.
34 | app.on("ready", () => {
35 | // This proxy adjusts the path of the requested files when loading from the
36 | // local production bundle.
37 | protocol.registerHttpProtocol(
38 | "file",
39 | (request, callback) => {
40 | const url = request.url.substr(8);
41 | callback({ path: path.normalize(`${__dirname}/${url}`) });
42 | },
43 | (error) => {
44 | if (error) console.error("Failed to register protocol");
45 | }
46 | );
47 | createWindow();
48 | });
49 |
50 | // Quit when all windows are closed, except on macOS, where it's common
51 | // for applications and their menu bar to stay active until the user quits
52 | // explicitly with Cmd + Q.
53 | app.on("window-all-closed", () => {
54 | if (process.platform !== "darwin") {
55 | app.quit();
56 | }
57 | });
58 |
59 | app.on("activate", () => {
60 | // On macOS it's common to re-create a window in the app when the
61 | // dock icon is clicked and there are no other windows open.
62 | if (mainWindow === null) {
63 | createWindow();
64 | }
65 | });
66 |
--------------------------------------------------------------------------------
/src/op-utils/pickPuzzle.ts:
--------------------------------------------------------------------------------
1 | import last from "lodash/last";
2 | import difference from "lodash/difference";
3 | import takeRight from "lodash/takeRight";
4 |
5 | interface Params {
6 | allPuzzlesLength: number;
7 | playedHistory?: number[];
8 | recentlyPlayedFactor?: number;
9 | completedHistory?: number[];
10 | }
11 |
12 | const contains = (arr: T[], el: T) => arr.indexOf(el) > -1;
13 |
14 | export const pickRandomPuzzle = ({
15 | allPuzzlesLength,
16 | playedHistory = [],
17 | completedHistory = [],
18 | recentlyPlayedFactor = Math.floor(allPuzzlesLength / 2),
19 | }: Params) => {
20 | const hasPlayedAllPuzzles = playedHistory.length >= allPuzzlesLength;
21 | // If there's at least a puzzle that has not been played yet, pick one from
22 | // the next one from the not-played puzzle list
23 | if (!hasPlayedAllPuzzles) {
24 | const lastPlayedPuzzle = last(playedHistory);
25 | const nextPuzzle =
26 | lastPlayedPuzzle === undefined ? 0 : lastPlayedPuzzle + 1;
27 | // Should always be true... but it's better to be safe than sorry
28 | if (!contains(playedHistory, nextPuzzle)) return nextPuzzle;
29 | }
30 | const hasCompletedAllPuzzles =
31 | completedHistory.length >= allPuzzlesLength &&
32 | completedHistory.length >= playedHistory.length;
33 | // If all the puzzles for this mode have already been completed, pick a random
34 | // one, making sure it wasn't played recently
35 | if (hasCompletedAllPuzzles) {
36 | const recentPuzzles = takeRight(playedHistory, recentlyPlayedFactor);
37 | const pickRandomPuzzle = (): number => {
38 | const randomPuzzle = Math.floor(Math.random() * allPuzzlesLength);
39 | const isRecent = contains(recentPuzzles, randomPuzzle);
40 | return !isRecent ? randomPuzzle : pickRandomPuzzle();
41 | };
42 | return pickRandomPuzzle();
43 | }
44 | // If there's at least a puzzle that has not been completed yet, pick it from
45 | // the list
46 | return difference(playedHistory, completedHistory)[0];
47 | };
48 |
--------------------------------------------------------------------------------
/src/op-home/About.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import {
3 | Animated,
4 | ViewStyle,
5 | Image,
6 | useColorScheme,
7 | Platform,
8 | } from "react-native";
9 | import { animations } from "op-design";
10 | import { useScale, ScalingFunc } from "op-utils";
11 | import { Button } from "op-common";
12 | import logoBorderDark from "./logo-border-dark.png";
13 | import logoBorderLight from "./logo-border-light.png";
14 |
15 | interface AboutProps {
16 | animValue: Animated.Value;
17 | style?: ViewStyle;
18 | }
19 |
20 | export const About: FC = function ({ animValue, style }) {
21 | const scale = useScale();
22 | const styles = createStyles({ scale });
23 | const colorScheme = useColorScheme();
24 | const imageSrc = colorScheme === "dark" ? logoBorderDark : logoBorderLight;
25 |
26 | const buttonHitSlop = {
27 | top: scale(20),
28 | bottom: scale(20),
29 | left: scale(20),
30 | right: scale(20),
31 | };
32 |
33 | const handlePress = () => {
34 | if (Platform.OS !== "web") {
35 | console.error("Tried to open about link from a non-web platform");
36 | return;
37 | }
38 | // @ts-ignore
39 | window.open("https://ordinarypuzzles.com", "_blank");
40 | };
41 |
42 | return (
43 |
44 |
54 |
55 | );
56 | };
57 |
58 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
59 | root: {
60 | opacity: 0.9,
61 | flexDirection: "row",
62 | alignItems: "center",
63 | },
64 | button: {},
65 | image: {
66 | marginTop: scale(2),
67 | width: scale(19),
68 | height: scale(19),
69 | marginLeft: scale(4),
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | OrdinaryPuzzles
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 | NSExceptionDomains
32 |
33 | localhost
34 |
35 | NSExceptionAllowsInsecureHTTPLoads
36 |
37 |
38 |
39 |
40 | NSLocationWhenInUseUsageDescription
41 |
42 | UIAppFonts
43 |
44 | Averta-Bold.ttf
45 | Averta-Regular.ttf
46 | Averta-Semibold.ttf
47 |
48 | UILaunchStoryboardName
49 | LaunchScreen
50 | UIRequiredDeviceCapabilities
51 |
52 | armv7
53 |
54 | UIRequiresFullScreen
55 |
56 | UIStatusBarHidden
57 |
58 | UISupportedInterfaceOrientations
59 |
60 | UIInterfaceOrientationPortrait
61 |
62 | UIViewControllerBasedStatusBarAppearance
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/op-utils/sound.ts:
--------------------------------------------------------------------------------
1 | import ReactNativeSound from "op-native/react-native-sound";
2 | import { Platform } from "react-native";
3 |
4 | type SoundEffectId = keyof typeof soundEffects;
5 |
6 | const soundEffects = {
7 | buttonPress: {
8 | path: "buttonpress.wav",
9 | sound: null as any,
10 | volume: Platform.select({ ios: 0.4, android: 0.5 }),
11 | },
12 | };
13 |
14 | // Makes sure we can play audio effects without breaking other backgound app's
15 | // playback
16 | ReactNativeSound.setCategory("Ambient", true);
17 |
18 | const preloadSound = async (id: SoundEffectId) => {
19 | if (Platform.OS === "android") return;
20 | const soundEffect = soundEffects[id];
21 | return new Promise((resolve, reject) => {
22 | const sound: any = new ReactNativeSound(
23 | soundEffect.path,
24 | ReactNativeSound.MAIN_BUNDLE,
25 | (error) => {
26 | if (error) {
27 | console.error(`Failed to preload ${soundEffect.path}`, error);
28 | return reject(error);
29 | } else {
30 | soundEffect.sound = sound;
31 | resolve(sound);
32 | }
33 | }
34 | );
35 | });
36 | };
37 |
38 | export const initializeAudio = async () => {
39 | if (Platform.OS === "android") return;
40 | // Preload sound effects
41 | try {
42 | const soundEffectIds = Object.keys(soundEffects) as SoundEffectId[];
43 | Promise.all(soundEffectIds.map((id) => preloadSound(id)));
44 | } catch (error) {
45 | console.error("Failed to preload sound", error);
46 | }
47 | };
48 |
49 | export const playSound = async (id: SoundEffectId) => {
50 | if (Platform.OS === "android") return;
51 | const soundEffect = soundEffects[id];
52 | return new Promise((resolve, reject) => {
53 | if (soundEffect.sound && soundEffect.sound.play) {
54 | soundEffect.sound.setVolume(soundEffect.volume).play((success: boolean) =>
55 | success
56 | ? // @ts-ignore
57 | resolve()
58 | : reject("Playback failed due to audio decoding errors")
59 | );
60 | }
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzlesTests/OrdinaryPuzzlesTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface OrdinaryPuzzlesTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation OrdinaryPuzzlesTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
38 | if (level >= RCTLogLevelError) {
39 | redboxError = message;
40 | }
41 | });
42 | #endif
43 |
44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 |
48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
50 | return YES;
51 | }
52 | return NO;
53 | }];
54 | }
55 |
56 | #ifdef DEBUG
57 | RCTSetLogFunction(RCTDefaultLogFunction);
58 | #endif
59 |
60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
62 | }
63 |
64 |
65 | @end
66 |
--------------------------------------------------------------------------------
/src/op-home/Logo.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { View, Animated } from "react-native";
3 | import { Text, AnimatedLetter } from "op-common";
4 | import { useScale, ScalingFunc } from "op-utils";
5 |
6 | interface LogoProps {
7 | titleAnimValue: Animated.Value;
8 | dotAnimValue: Animated.Value;
9 | }
10 |
11 | export const Logo: FC = function ({ titleAnimValue, dotAnimValue }) {
12 | const scale = useScale();
13 | const styles = createStyles({ scale });
14 | const title1 = "Ordinary";
15 | const title2 = "Puzzles";
16 | const titleLength = title1.length + title2.length;
17 | const charShowAnimDuration = 1 / titleLength;
18 | const dotAnimStyle = {
19 | opacity: dotAnimValue,
20 | transform: [{ scale: dotAnimValue }],
21 | };
22 | return (
23 |
24 |
25 | {title1.split("").map((char, index) => {
26 | const delay = charShowAnimDuration * index;
27 | return (
28 |
35 | );
36 | })}
37 |
38 |
39 | {title2.split("").map((char, index) => {
40 | const title1AnimDuration = charShowAnimDuration * title1.length;
41 | const delay = title1AnimDuration + charShowAnimDuration * index;
42 | return (
43 |
50 | );
51 | })}
52 |
53 | .
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
61 | root: {},
62 | titleRow: {
63 | flexDirection: "row",
64 | },
65 | titleRow2: {
66 | flexDirection: "row",
67 | },
68 | text: {
69 | fontSize: scale(62),
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/src/op-utils/scale.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PixelRatio,
3 | Dimensions,
4 | Platform,
5 | useWindowDimensions,
6 | } from "react-native";
7 |
8 | export type ScalingFunc = (n: number) => number;
9 |
10 | const isTablet = () => {
11 | const pixelDensity = PixelRatio.get();
12 | const screenDimensions = Dimensions.get("screen");
13 | const adjustedWidth = screenDimensions.width * pixelDensity;
14 | const adjustedHeight = screenDimensions.height * pixelDensity;
15 | if (pixelDensity < 2 && (adjustedWidth >= 1000 || adjustedHeight >= 1000)) {
16 | return true;
17 | } else
18 | return (
19 | pixelDensity === 2 && (adjustedWidth >= 1920 || adjustedHeight >= 1920)
20 | );
21 | };
22 |
23 | export const useScale = (): ScalingFunc => {
24 | if (Platform.OS === "android" || Platform.OS === "ios") {
25 | const windowDimensions = Dimensions.get("window");
26 | const guidelineBaseWidth = isTablet() ? 520 : 350;
27 | return (size: number) =>
28 | (windowDimensions.width / guidelineBaseWidth) * size;
29 | } else {
30 | /* eslint-disable react-hooks/rules-of-hooks */
31 | // Yeah, not the cleanest approach here calling a hook conditionally, but
32 | // we can ensure the order will always be respected because the platform
33 | // cannot change at runtime.
34 | const windowDimensions = useWindowDimensions();
35 | /* eslint-enable react-hooks/rules-of-hooks */
36 | const guidelineBaseWidth = 600;
37 | const shorterWindowDimension =
38 | windowDimensions.width > windowDimensions.height
39 | ? windowDimensions.height
40 | : windowDimensions.width;
41 | return (size: number) =>
42 | (shorterWindowDimension / guidelineBaseWidth) * size;
43 | }
44 | };
45 |
46 | export const scaleTextToFit = (scale: ScalingFunc, text: string) => {
47 | const weight = text
48 | .split("")
49 | .map((char) => {
50 | if (["i", "l", "j", "f", "r", "t"].indexOf(char) >= 0) {
51 | return 0.7;
52 | } else if (["m", "w"].indexOf(char) >= 0) {
53 | return 2;
54 | } else {
55 | return 1;
56 | }
57 | })
58 | .reduce((a, b) => a + b, 0);
59 | if (weight <= 12) {
60 | return scale(48);
61 | } else {
62 | return scale(48 - (weight - 12) * 2.5);
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/AppDelegate.m:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 | #import
5 | #import
6 |
7 | #import "RNBootSplash.h"
8 |
9 | #ifdef FB_SONARKIT_ENABLED
10 | #import
11 | #import
12 | #import
13 | #import
14 | #import
15 | #import
16 | static void InitializeFlipper(UIApplication *application) {
17 | FlipperClient *client = [FlipperClient sharedClient];
18 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
19 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
20 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
21 | [client addPlugin:[FlipperKitReactPlugin new]];
22 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
23 | [client start];
24 | }
25 | #endif
26 |
27 | @implementation AppDelegate
28 |
29 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
30 | {
31 | #ifdef FB_SONARKIT_ENABLED
32 | InitializeFlipper(application);
33 | #endif
34 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
35 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
36 | moduleName:@"OrdinaryPuzzles"
37 | initialProperties:nil];
38 |
39 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
40 |
41 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
42 | UIViewController *rootViewController = [UIViewController new];
43 | rootViewController.view = rootView;
44 | self.window.rootViewController = rootViewController;
45 | [self.window makeKeyAndVisible];
46 | [RNBootSplash show:@"LaunchScreen" inView:rootView];
47 | return YES;
48 | }
49 |
50 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
51 | {
52 | #if DEBUG
53 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
54 | #else
55 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
56 | #endif
57 | }
58 |
59 | @end
60 |
--------------------------------------------------------------------------------
/src/op-board/PointerAwareView.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { View, Platform, ViewProps, GestureResponderEvent } from "react-native";
3 |
4 | const getEventCoordinates = (
5 | event: GestureResponderEvent
6 | ): [number, number] => {
7 | // On Android and iOS, let's use the native location which are relative
8 | // to the target view.
9 | if (Platform.OS === "android" || Platform.OS === "ios") {
10 | return [event.nativeEvent.locationX, event.nativeEvent.locationY];
11 | }
12 |
13 | // On the web, touch events coords are relative to the root (still no idea
14 | // why) so we need to take into account the target view offset.
15 | // @ts-ignore
16 | const touch = event?.targetTouches?.[0] || event?.changedTouches?.[0];
17 | if (touch) {
18 | // @ts-ignore
19 | const targetViewCoords = event?.target?.getBoundingClientRect?.();
20 | event.preventDefault();
21 | return [
22 | touch.clientX - targetViewCoords.x,
23 | touch.clientY - targetViewCoords.y,
24 | ] as [number, number];
25 | }
26 |
27 | // On the web (desktop), we can use layerX and layerY which are relative to
28 | // the target view.
29 | // @ts-ignore
30 | return [event?.nativeEvent?.layerX, event?.nativeEvent?.layerY];
31 | };
32 |
33 | interface PointerAwareViewProps extends ViewProps {
34 | onPointerDown: (coords: [number, number]) => void;
35 | onPointerMove: (coords: [number, number]) => void;
36 | onPointerUp: (coords: [number, number]) => void;
37 | pointerEnabled?: boolean;
38 | }
39 |
40 | export const PointerAwareView: FC = ({
41 | children,
42 | onPointerDown,
43 | onPointerMove,
44 | onPointerUp,
45 | pointerEnabled = false,
46 | style,
47 | ...otherProps
48 | }) => {
49 | const cursorStyle = Platform.select({
50 | native: {},
51 | default: {
52 | cursor: pointerEnabled ? "pointer" : undefined,
53 | },
54 | });
55 | return (
56 | onPointerDown(getEventCoordinates(e))}
59 | onTouchMove={(e) => onPointerMove(getEventCoordinates(e))}
60 | onTouchEnd={(e) => onPointerUp(getEventCoordinates(e))}
61 | // @ts-ignore
62 | onMouseDown={(e) => onPointerDown(getEventCoordinates(e))}
63 | // @ts-ignore
64 | onMouseMove={(e) => onPointerMove(getEventCoordinates(e))}
65 | // @ts-ignore
66 | onMouseUp={(e) => onPointerUp(getEventCoordinates(e))}
67 | // @ts-ignore
68 | onMouseLeave={(e) => onPointerUp(getEventCoordinates(e))}
69 | // @ts-ignore
70 | style={[style, cursorStyle]}
71 | {...otherProps}
72 | >
73 | {children}
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/public/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-background-root: #fbfaff;
3 | --color-spinner: #8f8c96;
4 | }
5 |
6 | @media (prefers-color-scheme: dark) {
7 | :root {
8 | --color-background-root: #161520;
9 | --color-spinner: #93bfec;
10 | }
11 | }
12 |
13 | @font-face {
14 | font-family: "Averta-Bold";
15 | src: url("fonts/Averta-Bold.woff2") format("woff2"),
16 | url("fonts/Averta-Bold.woff") format("woff");
17 | }
18 |
19 | @font-face {
20 | font-family: "Averta-Semibold";
21 | src: url("fonts/Averta-Semibold.woff2") format("woff2"),
22 | url("fonts/Averta-Semibold.woff") format("woff");
23 | }
24 |
25 | @font-face {
26 | font-family: "Averta-Regular";
27 | src: url("fonts/Averta-Regular.woff2") format("woff2"),
28 | url("fonts/Averta-Regular.woff") format("woff");
29 | }
30 |
31 | html,
32 | body,
33 | #root {
34 | margin: 0;
35 | padding: 0;
36 | height: 100%;
37 | width: 100%;
38 | overflow: hidden;
39 | touch-action: none;
40 | -webkit-tap-highlight-color: transparent;
41 | overscroll-behavior-y: contain;
42 | touch-action: none;
43 | overflow-x: hidden;
44 | user-select: text;
45 | -webkit-overflow-scrolling: touch;
46 | position: fixed;
47 | touch-action: manipulation;
48 | display: flex;
49 | flex: 1;
50 | background: var(--color-background-root);
51 | }
52 |
53 | @keyframes fade-in {
54 | 0% {
55 | opacity: 0;
56 | }
57 | 100% {
58 | opacity: 1;
59 | }
60 | }
61 |
62 | #splash {
63 | opacity: 0;
64 | position: absolute;
65 | margin: 0;
66 | padding: 0;
67 | height: 100%;
68 | width: 100%;
69 | top: 0;
70 | left: 0;
71 | right: 0;
72 | bottom: 0;
73 | display: flex;
74 | flex: 1;
75 | background: var(--color-background-root);
76 | justify-content: flex-end;
77 | align-items: flex-end;
78 | animation: fade-in ease 0.5s forwards;
79 | animation-delay: 300ms;
80 | }
81 |
82 | @keyframes splash-loading-rotate {
83 | 100% {
84 | transform: rotate(360deg);
85 | }
86 | }
87 |
88 | @keyframes splash-loading-dash {
89 | 0% {
90 | stroke-dasharray: 1, 150;
91 | stroke-dashoffset: 0;
92 | }
93 | 50% {
94 | stroke-dasharray: 90, 150;
95 | stroke-dashoffset: -35;
96 | }
97 | 100% {
98 | stroke-dasharray: 90, 150;
99 | stroke-dashoffset: -124;
100 | }
101 | }
102 |
103 | #splash-loading {
104 | animation: splash-loading-rotate 2s linear infinite;
105 | z-index: 2;
106 | position: absolute;
107 | bottom: 24px;
108 | right: 24px;
109 | width: 50px;
110 | height: 50px;
111 | }
112 |
113 | #splash-loading-path {
114 | stroke: var(--color-spinner);
115 | stroke-linecap: round;
116 | animation: splash-loading-dash 1.5s ease-in-out infinite;
117 | }
118 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ordinary Puzzles
5 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
23 |
24 |
25 |
29 |
33 |
34 |
35 |
41 |
42 |
47 |
48 |
49 |
50 |
51 |
52 |
58 |
64 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/mmazzarolo/ordinarypuzzles/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.mmazzarolo.ordinarypuzzles;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import com.facebook.react.PackageList;
6 | import com.facebook.react.ReactApplication;
7 | import com.facebook.react.ReactInstanceManager;
8 | import com.facebook.react.ReactNativeHost;
9 | import com.facebook.react.ReactPackage;
10 | import com.facebook.soloader.SoLoader;
11 | import java.lang.reflect.InvocationTargetException;
12 | import java.util.List;
13 |
14 | public class MainApplication extends Application implements ReactApplication {
15 |
16 | private final ReactNativeHost mReactNativeHost =
17 | new ReactNativeHost(this) {
18 | @Override
19 | public boolean getUseDeveloperSupport() {
20 | return BuildConfig.DEBUG;
21 | }
22 |
23 | @Override
24 | protected List getPackages() {
25 | @SuppressWarnings("UnnecessaryLocalVariable")
26 | List packages = new PackageList(this).getPackages();
27 | // Packages that cannot be autolinked yet can be added manually here, for example:
28 | // packages.add(new MyReactNativePackage());
29 | return packages;
30 | }
31 |
32 | @Override
33 | protected String getJSMainModuleName() {
34 | return "index";
35 | }
36 | };
37 |
38 | @Override
39 | public ReactNativeHost getReactNativeHost() {
40 | return mReactNativeHost;
41 | }
42 |
43 | @Override
44 | public void onCreate() {
45 | super.onCreate();
46 | SoLoader.init(this, /* native exopackage */ false);
47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
48 | }
49 |
50 | /**
51 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like
52 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
53 | *
54 | * @param context
55 | * @param reactInstanceManager
56 | */
57 | private static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
58 | if (BuildConfig.DEBUG) {
59 | try {
60 | /*
61 | We use reflection here to pick up the class that initializes Flipper,
62 | since Flipper library is not available in release mode
63 | */
64 | Class> aClass = Class.forName("com.mmazzarolo.ReactNativeFlipper");
65 | aClass
66 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
67 | .invoke(null, context, reactInstanceManager);
68 | } catch (ClassNotFoundException e) {
69 | e.printStackTrace();
70 | } catch (NoSuchMethodException e) {
71 | e.printStackTrace();
72 | } catch (IllegalAccessException e) {
73 | e.printStackTrace();
74 | } catch (InvocationTargetException e) {
75 | e.printStackTrace();
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "filename": "ios_marketing1024x1024.png",
5 | "idiom": "ios-marketing",
6 | "size": "1024x1024",
7 | "scale": "1x"
8 | },
9 | {
10 | "filename": "iphone_notification20x20@2x.png",
11 | "idiom": "iphone",
12 | "size": "20x20",
13 | "scale": "2x"
14 | },
15 | {
16 | "filename": "iphone_notification20x20@3x.png",
17 | "idiom": "iphone",
18 | "size": "20x20",
19 | "scale": "3x"
20 | },
21 | {
22 | "filename": "iphone_settings29x29@2x.png",
23 | "idiom": "iphone",
24 | "size": "29x29",
25 | "scale": "2x"
26 | },
27 | {
28 | "filename": "iphone_settings29x29@3x.png",
29 | "idiom": "iphone",
30 | "size": "29x29",
31 | "scale": "3x"
32 | },
33 | {
34 | "filename": "iphone_spotlight40x40@2x.png",
35 | "idiom": "iphone",
36 | "size": "40x40",
37 | "scale": "2x"
38 | },
39 | {
40 | "filename": "iphone_spotlight40x40@3x.png",
41 | "idiom": "iphone",
42 | "size": "40x40",
43 | "scale": "3x"
44 | },
45 | {
46 | "filename": "iphone_app60x60@2x.png",
47 | "idiom": "iphone",
48 | "size": "60x60",
49 | "scale": "2x"
50 | },
51 | {
52 | "filename": "iphone_app60x60@3x.png",
53 | "idiom": "iphone",
54 | "size": "60x60",
55 | "scale": "3x"
56 | },
57 | {
58 | "filename": "ipad_notification20x20.png",
59 | "idiom": "ipad",
60 | "size": "20x20",
61 | "scale": "1x"
62 | },
63 | {
64 | "filename": "ipad_notification20x20@2x.png",
65 | "idiom": "ipad",
66 | "size": "20x20",
67 | "scale": "2x"
68 | },
69 | {
70 | "filename": "ipad_settings29x29.png",
71 | "idiom": "ipad",
72 | "size": "29x29",
73 | "scale": "1x"
74 | },
75 | {
76 | "filename": "ipad_settings29x29@2x.png",
77 | "idiom": "ipad",
78 | "size": "29x29",
79 | "scale": "2x"
80 | },
81 | {
82 | "filename": "ipad_spotlight40x40.png",
83 | "idiom": "ipad",
84 | "size": "40x40",
85 | "scale": "1x"
86 | },
87 | {
88 | "filename": "ipad_spotlight40x40@2x.png",
89 | "idiom": "ipad",
90 | "size": "40x40",
91 | "scale": "2x"
92 | },
93 | {
94 | "filename": "ipad_app76x76.png",
95 | "idiom": "ipad",
96 | "size": "76x76",
97 | "scale": "1x"
98 | },
99 | {
100 | "filename": "ipad_app76x76@2x.png",
101 | "idiom": "ipad",
102 | "size": "76x76",
103 | "scale": "2x"
104 | },
105 | {
106 | "filename": "ipad_pro_app83.5x83.5@2x.png",
107 | "idiom": "ipad",
108 | "size": "83.5x83.5",
109 | "scale": "2x"
110 | }
111 | ],
112 | "info": {
113 | "version": 1,
114 | "author": "xcode"
115 | }
116 | }
--------------------------------------------------------------------------------
/src/op-stats/Stats.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useRef } from "react";
2 | import { Animated } from "react-native";
3 | import { observer } from "mobx-react";
4 | import { useCoreStores } from "op-core";
5 | import { BottomNav, Button, Text, getBottomNavHeight } from "op-common";
6 | import { metrics, animations } from "op-design";
7 | import {
8 | useAnimation,
9 | useOnMount,
10 | useScale,
11 | useHardwareBackButton,
12 | ScalingFunc,
13 | } from "op-utils";
14 |
15 | export const Stats: FC = observer(function () {
16 | const scale = useScale();
17 | const styles = createStyles({ scale });
18 | const { stats, router } = useCoreStores();
19 | const interactionsDisabledRef = useRef(false);
20 |
21 | // Routing setup
22 | const navigateToHome = () => router.changeRoute("home");
23 | useHardwareBackButton(navigateToHome);
24 |
25 | // Animations setup
26 | const fadeRootInAnimDuration = 400;
27 | const fadeRootOutAnimDuration = 200;
28 | const fadeRootAnim = useAnimation(0);
29 | const fadeRootIn = () =>
30 | fadeRootAnim.setup({ duration: fadeRootInAnimDuration });
31 | const fadeRootOut = () =>
32 | fadeRootAnim.setup({ duration: fadeRootOutAnimDuration, toValue: 0 });
33 |
34 | useOnMount(() => {
35 | fadeRootIn().start();
36 | });
37 |
38 | // Callback handlers
39 | const handleMenuPress = () => {
40 | if (interactionsDisabledRef.current) return;
41 | interactionsDisabledRef.current = true;
42 | fadeRootOut().start(() => {
43 | navigateToHome();
44 | });
45 | };
46 |
47 | return (
48 |
49 |
52 |
53 | Statistics
54 |
55 |
56 | small: {stats.completedPuzzles["small"].length}/99
57 |
58 |
59 | medium: {stats.completedPuzzles["medium"].length}/99
60 |
61 |
62 | large: {stats.completedPuzzles["large"].length}/99
63 |
64 |
65 | score: {stats.score}
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | });
74 |
75 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
76 | root: {
77 | flex: 1,
78 | marginHorizontal: metrics.screenMargin,
79 | },
80 | middle: {
81 | flex: 1,
82 | marginTop: getBottomNavHeight,
83 | justifyContent: "center",
84 | },
85 | title: {
86 | fontSize: scale(48),
87 | },
88 | score: {
89 | fontSize: scale(36),
90 | marginTop: scale(36),
91 | },
92 | progress: {
93 | fontSize: scale(36),
94 | marginTop: scale(14),
95 | },
96 | });
97 |
--------------------------------------------------------------------------------
/src/op-intro/Intro.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useRef } from "react";
2 | import { StyleSheet, Animated, TouchableWithoutFeedback } from "react-native";
3 | import { animations, metrics } from "op-design";
4 | import { AnimatedLetter } from "op-common";
5 | import {
6 | useAnimation,
7 | scaleTextToFit,
8 | delay,
9 | useOnMount,
10 | useScale,
11 | } from "op-utils";
12 | import { useCoreStores } from "op-core";
13 |
14 | const asyncAnimationStart = (anim: Animated.CompositeAnimation) =>
15 | new Promise((resolve) => anim.start(resolve));
16 |
17 | export const Intro: FC = function () {
18 | const { puzzle, router } = useCoreStores();
19 | const skippingEnabledRef = useRef(true);
20 | const hasSkippedRef = useRef(false);
21 |
22 | const scale = useScale();
23 |
24 | // Routing setup
25 | const handleComplete = () => router.changeRoute("game");
26 |
27 | // Animation setup
28 | const showAnimDuration = 400;
29 | const animDelay = 1000;
30 | const hideAnimDuration = 400;
31 | const showAnim = useAnimation();
32 | const hideAnim = useAnimation(1);
33 | const animate = async () => {
34 | await asyncAnimationStart(showAnim.setup({ duration: showAnimDuration }));
35 | await delay(animDelay);
36 | skippingEnabledRef.current = false;
37 | if (!hasSkippedRef.current) {
38 | await asyncAnimationStart(hideAnim.setup({ duration: hideAnimDuration }));
39 | handleComplete();
40 | }
41 | };
42 | useOnMount(() => {
43 | animate();
44 | });
45 |
46 | // If the user touches the screen, end the animation early
47 | const handlePress = () => {
48 | if (!skippingEnabledRef.current || hasSkippedRef.current) return;
49 | hasSkippedRef.current = true;
50 | hideAnim.setup({ duration: hideAnimDuration }).start(handleComplete);
51 | };
52 |
53 | // Split the title into multiple charaters to animate them asynchronously
54 | const fitFontSize = scaleTextToFit(scale, `${puzzle.prefix} ${puzzle.name}`);
55 | const digitList = puzzle.prefix.split("");
56 | const letterList = puzzle.name.split("");
57 | const chars = digitList
58 | .map((digit) => ({ type: "digit", value: digit }))
59 | .concat({ type: "space", value: " " })
60 | .concat(letterList.map((letter) => ({ type: "letter", value: letter })));
61 |
62 | return (
63 |
64 |
67 | {chars.map((char, index) => {
68 | const delay = (1 / chars.length) * index;
69 | return (
70 |
78 | );
79 | })}
80 |
81 |
82 | );
83 | };
84 |
85 | const styles = StyleSheet.create({
86 | root: {
87 | marginHorizontal: metrics.screenMargin,
88 | flex: 1,
89 | alignItems: "center",
90 | flexDirection: "row",
91 | },
92 | letter: {},
93 | });
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Ordinary Puzzles: A minimalistic puzzle game built with React-Native.
6 |
7 |
8 |
9 |
15 |
16 |
17 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | ## Overview
48 |
49 | Ordinary Puzzles is a minimalistic puzzle game build with [React Native](https://facebook.github.io/react-native/), [React Native for Web](https://github.com/necolas/react-native-web), [TypeScript](https://www.typescriptlang.org/) and [MobX](https://mobx.js.org/README.html).
50 | Ordinary Puzzles is free, has no ads, and no in-app purchases.
51 | You can download it on the [Google Play Store](https://play.google.com/store/apps/details?id=com.mmazzarolo.ordinarypuzzles), on the [Apple App Store](https://apps.apple.com/us/app/ordinary-puzzles/id1489599807), or play it directly [on the web](https://ordinarypuzzles.com/play) as a Progressive Web App.
52 |
53 | ## Some Background
54 |
55 | Ordinary Puzzles is a game inspired by [Juho Snellman](https://www.snellman.net/)'s [Linjat](https://linjat.snellman.net/#fp).
56 | I discovered Linjat a while ago on Hacker News. I liked the concept of its Picross-like puzzles, so after a quick chat with Juho I decided to port it to a mobile game.
57 |
58 | The font used across the entire app is [Averta](https://www.myfonts.com/fonts/intelligent-foundry/averta/), by [Kostas Bartsokas](http://www.kostasbartsokas.com/).
59 |
60 | ## Contributing
61 |
62 | See [CONTRIBUTING.md](./CONTRIBUTING.md).
63 | Pull requests are welcome. File an issue for ideas, conversation or feedback.
64 |
--------------------------------------------------------------------------------
/src/op-message/Message.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { View, Animated } from "react-native";
3 | import { Text, Button, BottomNav } from "op-common";
4 | import {
5 | useAnimation,
6 | useOnMount,
7 | useScale,
8 | useHardwareBackButton,
9 | ScalingFunc,
10 | } from "op-utils";
11 | import { animations } from "op-design";
12 | import { useCoreStores } from "op-core";
13 |
14 | export const Message: FC = function () {
15 | const scale = useScale();
16 | const styles = createStyles({ scale });
17 | const { router, puzzle, stats } = useCoreStores();
18 |
19 | // Routing setup
20 | const navigateToHome = () => router.changeRoute("home");
21 | useHardwareBackButton(navigateToHome);
22 |
23 | // Animations setup
24 | const fadeInTitleAnimDuration = 200;
25 | const fadeInMessageAnimDuration = 200;
26 | const fadeInBottomAnimDuration = 200;
27 | const fadeInStaggerDuration = 200;
28 | const fadeOutAnimDuration = 200;
29 | const fadeTitleAnim = useAnimation();
30 | const fadeMessageAnim = useAnimation();
31 | const fadeBottomAnim = useAnimation();
32 |
33 | const fadeIn = () =>
34 | Animated.stagger(fadeInStaggerDuration, [
35 | fadeTitleAnim.setup({ duration: fadeInTitleAnimDuration }),
36 | fadeMessageAnim.setup({ duration: fadeInMessageAnimDuration }),
37 | fadeBottomAnim.setup({ duration: fadeInBottomAnimDuration }),
38 | ]);
39 |
40 | const fadeOut = () =>
41 | Animated.parallel([
42 | fadeTitleAnim.setup({ duration: fadeOutAnimDuration, toValue: 0 }),
43 | fadeMessageAnim.setup({ duration: fadeOutAnimDuration, toValue: 0 }),
44 | fadeBottomAnim.setup({ duration: fadeOutAnimDuration, toValue: 0 }),
45 | ]);
46 |
47 | useOnMount(() => {
48 | fadeIn().start();
49 | });
50 |
51 | // Callback handlers
52 | const handleButtonPress = () => {
53 | fadeOut().start(() => {
54 | stats.updateCompletedPuzzles("tutorial", 0);
55 | navigateToHome();
56 | });
57 | };
58 |
59 | return (
60 |
61 |
62 |
69 | {puzzle.tutorialTitle}
70 |
71 |
78 | {puzzle.tutorialMessage}
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
89 | root: {
90 | margin: scale(20),
91 | flex: 1,
92 | },
93 | top: {
94 | flexDirection: "column",
95 | flex: 1,
96 | justifyContent: "center",
97 | },
98 | title: {
99 | fontSize: scale(42),
100 | },
101 | message: {
102 | fontSize: scale(26),
103 | },
104 | });
105 |
--------------------------------------------------------------------------------
/src/op-utils/pickPuzzle.test.ts:
--------------------------------------------------------------------------------
1 | import times from "lodash/times";
2 | import { pickRandomPuzzle } from "./pickPuzzle";
3 |
4 | describe("pickRandomPuzzle", () => {
5 | const allPuzzlesLength = 6;
6 | describe("with 6 available puzzles", () => {
7 | describe("with no puzzles played yet", () => {
8 | it("should return the puzzle with index 0", () => {
9 | expect(
10 | pickRandomPuzzle({
11 | allPuzzlesLength: allPuzzlesLength,
12 | playedHistory: [],
13 | })
14 | ).toBe(0);
15 | });
16 | });
17 |
18 | describe("with 1 puzzle played", () => {
19 | it("should return the second puzzle", () => {
20 | expect(
21 | pickRandomPuzzle({
22 | allPuzzlesLength: allPuzzlesLength,
23 | playedHistory: [0],
24 | })
25 | ).toBe(1);
26 | });
27 | });
28 |
29 | describe("with 2 puzzle played", () => {
30 | it("should return the third puzzle", () => {
31 | expect(
32 | pickRandomPuzzle({
33 | allPuzzlesLength: allPuzzlesLength,
34 | playedHistory: [0, 1],
35 | })
36 | ).toBe(2);
37 | });
38 | });
39 |
40 | describe("with 3 puzzle played", () => {
41 | it("should return the fourth puzzle", () => {
42 | expect(
43 | pickRandomPuzzle({
44 | allPuzzlesLength: allPuzzlesLength,
45 | playedHistory: [0, 1, 2],
46 | })
47 | ).toBe(3);
48 | });
49 | });
50 |
51 | describe("with 4 puzzle played", () => {
52 | it("should return the fifth puzzle", () => {
53 | expect(
54 | pickRandomPuzzle({
55 | allPuzzlesLength: allPuzzlesLength,
56 | playedHistory: [0, 1, 2, 3],
57 | })
58 | ).toBe(4);
59 | });
60 | });
61 |
62 | describe("with 5 puzzle played", () => {
63 | it("should return the sixth puzzle", () => {
64 | expect(
65 | pickRandomPuzzle({
66 | allPuzzlesLength: allPuzzlesLength,
67 | playedHistory: [0, 1, 2, 3, 4],
68 | })
69 | ).toBe(5);
70 | });
71 | });
72 |
73 | describe("with all puzzles played", () => {
74 | describe("with all puzzles completed", () => {
75 | const randomTestSuite = times(10);
76 | it.each(randomTestSuite)(
77 | "should return a random non-recently played puzzle",
78 | () => {
79 | const randomPuzzle = pickRandomPuzzle({
80 | allPuzzlesLength: allPuzzlesLength,
81 | playedHistory: [0, 1, 2, 3, 4, 5],
82 | completedHistory: [0, 1, 2, 3, 4, 5],
83 | });
84 | expect([0, 1, 2]).toContain(randomPuzzle);
85 | }
86 | );
87 | });
88 |
89 | describe("with at least a non-completed puzzle", () => {
90 | it("should return a non-completed puzzle", () => {
91 | const pickedPuzzle = pickRandomPuzzle({
92 | allPuzzlesLength: allPuzzlesLength,
93 | playedHistory: [0, 1, 2, 3, 4, 5],
94 | completedHistory: [0, 1, 4, 5],
95 | });
96 | expect(pickedPuzzle).toBe(2);
97 | });
98 | });
99 | });
100 | });
101 | });
102 |
--------------------------------------------------------------------------------
/src/service-worker.ts:
--------------------------------------------------------------------------------
1 | ///
2 | /* eslint-disable no-restricted-globals */
3 |
4 | // This service worker can be customized!
5 | // See https://developers.google.com/web/tools/workbox/modules
6 | // for the list of available Workbox modules, or add any other
7 | // code you'd like.
8 | // You can also remove this file if you'd prefer not to use a
9 | // service worker, and the Workbox build step will be skipped.
10 |
11 | import { clientsClaim } from "workbox-core";
12 | import { ExpirationPlugin } from "workbox-expiration";
13 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
14 | import { registerRoute } from "workbox-routing";
15 | import { StaleWhileRevalidate } from "workbox-strategies";
16 |
17 | declare const self: ServiceWorkerGlobalScope;
18 |
19 | clientsClaim();
20 |
21 | // Precache all of the assets generated by your build process.
22 | // Their URLs are injected into the manifest variable below.
23 | // This variable must be present somewhere in your service worker file,
24 | // even if you decide not to use precaching. See https://cra.link/PWA
25 | precacheAndRoute(self.__WB_MANIFEST);
26 |
27 | // Set up App Shell-style routing, so that all navigation requests
28 | // are fulfilled with your index.html shell. Learn more at
29 | // https://developers.google.com/web/fundamentals/architecture/app-shell
30 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
31 | registerRoute(
32 | // Return false to exempt requests from being fulfilled by index.html.
33 | ({ request, url }: { request: Request; url: URL }) => {
34 | // If this isn't a navigation, skip.
35 | if (request.mode !== "navigate") {
36 | return false;
37 | }
38 |
39 | // If this is a URL that starts with /_, skip.
40 | if (url.pathname.startsWith("/_")) {
41 | return false;
42 | }
43 |
44 | // If this looks like a URL for a resource, because it contains
45 | // a file extension, skip.
46 | if (url.pathname.match(fileExtensionRegexp)) {
47 | return false;
48 | }
49 |
50 | // Return true to signal that we want to use the handler.
51 | return true;
52 | },
53 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html")
54 | );
55 |
56 | // An example runtime caching route for requests that aren't handled by the
57 | // precache, in this case same-origin .png requests like those from in public/
58 | registerRoute(
59 | // Add in any other file extensions or routing criteria as needed.
60 | ({ url }) =>
61 | url.origin === self.location.origin &&
62 | (url.pathname.endsWith(".png") ||
63 | url.pathname.endsWith(".css") ||
64 | url.pathname.endsWith(".woff") ||
65 | url.pathname.endsWith(".woff2")),
66 | // Customize this strategy as needed, e.g., by changing to CacheFirst.
67 | new StaleWhileRevalidate({
68 | cacheName: "public",
69 | plugins: [
70 | // Ensure that once this runtime cache reaches a maximum size the
71 | // least-recently used images are removed.
72 | new ExpirationPlugin({ maxEntries: 50 }),
73 | ],
74 | })
75 | );
76 |
77 | // This allows the web app to trigger skipWaiting via
78 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
79 | self.addEventListener("message", (event) => {
80 | if (event.data && event.data.type === "SKIP_WAITING") {
81 | self.skipWaiting();
82 | }
83 | });
84 |
85 | // Any other custom service worker logic can go here.
86 |
--------------------------------------------------------------------------------
/android/app/src/debug/java/com/mmazzarolo/ordinarypuzzles/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.rndiffapp;
8 | import android.content.Context;
9 | import com.facebook.flipper.android.AndroidFlipperClient;
10 | import com.facebook.flipper.android.utils.FlipperUtils;
11 | import com.facebook.flipper.core.FlipperClient;
12 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
13 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
14 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
15 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
16 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
17 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
18 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
19 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
21 | import com.facebook.react.ReactInstanceManager;
22 | import com.facebook.react.bridge.ReactContext;
23 | import com.facebook.react.modules.network.NetworkingModule;
24 | import okhttp3.OkHttpClient;
25 | public class ReactNativeFlipper {
26 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
27 | if (FlipperUtils.shouldEnableFlipper(context)) {
28 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
29 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
30 | client.addPlugin(new ReactFlipperPlugin());
31 | client.addPlugin(new DatabasesFlipperPlugin(context));
32 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
33 | client.addPlugin(CrashReporterPlugin.getInstance());
34 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
35 | NetworkingModule.setCustomClientBuilder(
36 | new NetworkingModule.CustomClientBuilder() {
37 | @Override
38 | public void apply(OkHttpClient.Builder builder) {
39 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
40 | }
41 | });
42 | client.addPlugin(networkFlipperPlugin);
43 | client.start();
44 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
45 | // Hence we run if after all native modules have been initialized
46 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
47 | if (reactContext == null) {
48 | reactInstanceManager.addReactInstanceEventListener(
49 | new ReactInstanceManager.ReactInstanceEventListener() {
50 | @Override
51 | public void onReactContextInitialized(ReactContext reactContext) {
52 | reactInstanceManager.removeReactInstanceEventListener(this);
53 | reactContext.runOnNativeModulesQueueThread(
54 | new Runnable() {
55 | @Override
56 | public void run() {
57 | client.addPlugin(new FrescoFlipperPlugin());
58 | }
59 | });
60 | }
61 | });
62 | } else {
63 | client.addPlugin(new FrescoFlipperPlugin());
64 | }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto init
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto init
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :init
68 | @rem Get command-line arguments, handling Windows variants
69 |
70 | if not "%OS%" == "Windows_NT" goto win9xME_args
71 |
72 | :win9xME_args
73 | @rem Slurp the command line arguments.
74 | set CMD_LINE_ARGS=
75 | set _SKIP=2
76 |
77 | :win9xME_args_slurp
78 | if "x%~1" == "x" goto execute
79 |
80 | set CMD_LINE_ARGS=%*
81 |
82 | :execute
83 | @rem Setup the command line
84 |
85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
86 |
87 | @rem Execute Gradle
88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
89 |
90 | :end
91 | @rem End local scope for the variables with windows NT shell
92 | if "%ERRORLEVEL%"=="0" goto mainEnd
93 |
94 | :fail
95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
96 | rem the _cmd.exe /c_ return code!
97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
98 | exit /b 1
99 |
100 | :mainEnd
101 | if "%OS%"=="Windows_NT" endlocal
102 |
103 | :omega
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles.xcodeproj/xcshareddata/xcschemes/OrdinaryPuzzles.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/op-splash/Splash.native.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useRef } from "react";
2 | import { Animated, TouchableWithoutFeedback } from "react-native";
3 | import { Text } from "op-common";
4 | import {
5 | useAnimation,
6 | useOnMount,
7 | useScale,
8 | delay,
9 | ScalingFunc,
10 | } from "op-utils";
11 | import { animations, metrics, colors } from "op-design";
12 | import { credits } from "op-config";
13 |
14 | const asyncAnimationStart = (anim: Animated.CompositeAnimation) =>
15 | new Promise((resolve) => anim.start(resolve));
16 |
17 | interface SplashProps {
18 | onHide: () => void;
19 | }
20 |
21 | export const Splash: FC = function ({ onHide }) {
22 | const scale = useScale();
23 | const styles = createStyles({ scale });
24 | const skippingEnabledRef = useRef(true);
25 | const hasSkippedRef = useRef(false);
26 |
27 | // Animations setup
28 | const fadeCreditsAnimDuration = 200;
29 | const fadeCreditsStaggerDuration = 100;
30 | const backgroundColorAnimDuration = 200;
31 | // We can use hooks in loops in this case because "credits" is a constant
32 | // eslint-disable-next-line react-hooks/rules-of-hooks
33 | const fadeCreditsAnims = credits.map(() => useAnimation());
34 | const backgroundColorAnim = useAnimation();
35 | const splashDuration = 2000;
36 | const showAnim = Animated.stagger(
37 | fadeCreditsStaggerDuration,
38 | fadeCreditsAnims.map((anim) =>
39 | anim.setup({ duration: fadeCreditsAnimDuration })
40 | )
41 | );
42 |
43 | const hideAnim = Animated.sequence([
44 | Animated.stagger(
45 | fadeCreditsStaggerDuration,
46 | fadeCreditsAnims.map((anim) =>
47 | anim.setup({ duration: fadeCreditsAnimDuration, toValue: 0 })
48 | )
49 | ),
50 | backgroundColorAnim.setup({
51 | duration: backgroundColorAnimDuration,
52 | useNativeDriver: false,
53 | }),
54 | ]);
55 |
56 | const animate = async () => {
57 | await asyncAnimationStart(showAnim);
58 | await delay(splashDuration);
59 | skippingEnabledRef.current = false;
60 | if (!hasSkippedRef.current) {
61 | await asyncAnimationStart(hideAnim);
62 | onHide();
63 | }
64 | };
65 |
66 | useOnMount(() => {
67 | animate();
68 | });
69 |
70 | const rootAnimStyle = {
71 | backgroundColor: backgroundColorAnim.value.interpolate({
72 | inputRange: [0, 1],
73 | outputRange: [colors.splash, colors.primary[9]],
74 | }),
75 | };
76 |
77 | // If the user touches the screen, end the animation early
78 | const handlePress = () => {
79 | if (!skippingEnabledRef.current || hasSkippedRef.current) return;
80 | hasSkippedRef.current = true;
81 | hideAnim.start(onHide);
82 | };
83 |
84 | return (
85 |
86 |
87 | {credits.map((credit, index) => (
88 |
96 | {credit}
97 |
98 | ))}
99 |
100 |
101 | );
102 | };
103 |
104 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
105 | root: {
106 | padding: metrics.screenMargin,
107 | flex: 1,
108 | flexDirection: "column",
109 | justifyContent: "center",
110 | backgroundColor: colors.splash,
111 | },
112 | credit: {
113 | color: "white",
114 | fontSize: scale(28),
115 | marginVertical: scale(12),
116 | },
117 | });
118 |
--------------------------------------------------------------------------------
/ios/OrdinaryPuzzles.xcodeproj/xcshareddata/xcschemes/OrdinaryPuzzles-tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/op-success/Success.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useRef } from "react";
2 | import { View, Animated, Platform, ViewStyle } from "react-native";
3 | import { observer } from "mobx-react";
4 | import KeepAwake from "op-native/react-native-keep-awake";
5 | import { useCoreStores } from "op-core";
6 | import { BottomNav, Button, Header, Score, Text } from "op-common";
7 | import { metrics, animations } from "op-design";
8 | import {
9 | useAnimation,
10 | useOnMount,
11 | useScale,
12 | scaleTextToFit,
13 | ScalingFunc,
14 | } from "op-utils";
15 |
16 | export const Success: FC = observer(function () {
17 | const scale = useScale();
18 | const styles = createStyles({ scale });
19 | const { puzzle, router } = useCoreStores();
20 | const interactionsDisabledRef = useRef(false);
21 |
22 | // Routing setup
23 | const startNewGame = () => router.changeRoute("intro");
24 | const navigateToHome = () => router.changeRoute("home");
25 |
26 | // Animations setup
27 | // https://github.com/facebook/react-native/issues/27146
28 | const maxOpacity = Platform.OS === "android" ? 0.99 : 1;
29 | const fadeInterfaceInAnimDuration = 400;
30 | const fadeInterfaceInStaggerDuration = 200;
31 | const fadeRootOutDuration = 200;
32 | const fadeTitleAnim = useAnimation();
33 | const fadeSubtilteAnim = useAnimation();
34 | const fadeBottomNavAnim = useAnimation();
35 | const fadeScoreAnim = useAnimation();
36 | const fadeRootAnim = useAnimation(maxOpacity);
37 | const fadeInterfaceIn = () =>
38 | Animated.stagger(fadeInterfaceInStaggerDuration, [
39 | fadeTitleAnim.setup({ duration: fadeInterfaceInAnimDuration }),
40 | fadeSubtilteAnim.setup({ duration: fadeInterfaceInAnimDuration }),
41 | fadeBottomNavAnim.setup({ duration: fadeInterfaceInAnimDuration }),
42 | fadeScoreAnim.setup({ duration: fadeInterfaceInAnimDuration }),
43 | ]);
44 | const fadeRootOut = () =>
45 | fadeRootAnim.setup({ duration: fadeRootOutDuration });
46 |
47 | useOnMount(() => {
48 | fadeInterfaceIn().start();
49 | });
50 |
51 | // Callback handlers
52 | const handleMenuPress = () => {
53 | if (interactionsDisabledRef.current) return;
54 | interactionsDisabledRef.current = true;
55 | fadeRootOut().start(() => {
56 | navigateToHome();
57 | });
58 | };
59 | const handleNewPuzzlePress = () => {
60 | if (interactionsDisabledRef.current) return;
61 | interactionsDisabledRef.current = true;
62 | fadeRootOut().start(startNewGame);
63 | };
64 |
65 | const fitFontSize = scaleTextToFit(scale, `${puzzle.prefix} ${puzzle.name}`);
66 |
67 | const scoreStyle: ViewStyle = {
68 | top: metrics.screenMargin,
69 | };
70 |
71 | return (
72 |
73 |
74 | {puzzle.increasesScore && (
75 |
80 | )}
81 |
82 |
88 |
95 | Completed
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | });
105 |
106 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
107 | root: {
108 | flex: 1,
109 | marginHorizontal: metrics.screenMargin,
110 | },
111 | middle: {
112 | flex: 1,
113 | justifyContent: "center",
114 | },
115 | textCompleted: {
116 | fontSize: scale(36),
117 | },
118 | textScoreValue: {
119 | fontSize: scale(32),
120 | },
121 | completedWrapper: {
122 | flexDirection: "row",
123 | },
124 | });
125 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "OrdinaryPuzzles",
3 | "version": "0.0.1",
4 | "private": true,
5 | "author": "Mazzarolo Matteo",
6 | "description": "Minimalistic puzzle game inspired by Picross and Sudoku.",
7 | "main": "./public/electron.js",
8 | "scripts": {
9 | "android:dev": "react-native run-android",
10 | "ios:dev": "react-native run-ios",
11 | "web:dev": "PUBLIC_URL='/play' react-app-rewired start",
12 | "web:build": "PUBLIC_URL='/play' NODE_ENV=production react-app-rewired build",
13 | "electron:dev": "run-p --print-label electron:dev:*",
14 | "electron:dev:cra": "PUBLIC_URL='./' BROWSER=none ELECTRON=true react-app-rewired start",
15 | "electron:dev:start": "wait-on tcp:3000 && ELECTRON_START_URL=http://localhost:3000 electron .",
16 | "electron:build": "run-s --print-label electron:build:*",
17 | "electron:build:cra": "PUBLIC_URL='./' NODE_ENV=production ELECTRON=true react-app-rewired build",
18 | "electron:build:package": "electron-builder build --mac -c.extraMetadata.main=build/electron.js --publish never",
19 | "start": "react-native start",
20 | "test": "jest",
21 | "test:watch": "jest --watch",
22 | "lint": "eslint 'src/' --ext .js,.jsx,.ts,.tsx",
23 | "lint:fix": "yarn lint --fix",
24 | "typecheck": "tsc",
25 | "xcode": "open ios/OrdinaryPuzzles.xcworkspace",
26 | "studio": "open -a /Applications/Android\\ Studio.app ./android/",
27 | "postinstall": "electron-builder install-app-deps"
28 | },
29 | "dependencies": {
30 | "@react-native-community/async-storage": "^1.12.1",
31 | "lodash": "^4.17.20",
32 | "mobx": "^6.0.1",
33 | "mobx-logger": "^0.7.1",
34 | "mobx-react": "^7.0.0",
35 | "react": "16.13.1",
36 | "react-dom": "^17.0.1",
37 | "react-native": "0.63.3",
38 | "react-native-bootsplash": "^1.0.3",
39 | "react-native-haptic": "oblador/react-native-haptic#package-cleanup",
40 | "react-native-immersive": "^2.0.0",
41 | "react-native-keep-awake": "corbt/react-native-keep-awake#146c2db",
42 | "react-native-sound": "^0.11.0",
43 | "react-native-web": "^0.14.4",
44 | "tinycolor2": "^1.4.2"
45 | },
46 | "devDependencies": {
47 | "@babel/core": "^7.8.4",
48 | "@babel/plugin-proposal-class-properties": "^7.8.4",
49 | "@babel/runtime": "^7.8.4",
50 | "@types/jest": "^26.0.14",
51 | "@types/lodash": "^4.14.162",
52 | "@types/react": "^16.9.34",
53 | "@types/react-native": "^0.63.25",
54 | "@types/react-test-renderer": "^16.9.3",
55 | "@types/tinycolor2": "^1.4.2",
56 | "babel-jest": "^24.9.0",
57 | "babel-plugin-module-resolver": "^4.0.0",
58 | "babel-plugin-transform-remove-console": "^6.9.4",
59 | "cross-env": "^7.0.3",
60 | "customize-cra": "^1.0.0",
61 | "electron": "^13.1.7",
62 | "electron-builder": "^22.11.7",
63 | "eslint": "^7.11.0",
64 | "eslint-config-prettier": "^6.12.0",
65 | "eslint-plugin-prettier": "^3.1.4",
66 | "husky": "^4.3.0",
67 | "jest": "^26.5.2",
68 | "lint-staged": "^10.4.0",
69 | "metro-react-native-babel-preset": "^0.59.0",
70 | "npm-run-all": "^4.1.5",
71 | "prettier": "^2.1.2",
72 | "react-app-rewired": "^2.1.6",
73 | "react-scripts": "^4.0.0",
74 | "react-test-renderer": "16.13.1",
75 | "typescript": "^4.0.3",
76 | "wait-on": "^6.0.0"
77 | },
78 | "jest": {
79 | "preset": "react-native"
80 | },
81 | "husky": {
82 | "hooks": {
83 | "pre-commit": "lint-staged",
84 | "pre-push": "npm run typecheck && npm run lint && npm run test"
85 | }
86 | },
87 | "lint-staged": {
88 | "*.{js,jsx,ts,tsx}": [
89 | "eslint --fix"
90 | ],
91 | "*.md": [
92 | "prettier --write"
93 | ]
94 | },
95 | "build": {
96 | "appId": "com.mmazzarolo.ordinarypuzzles",
97 | "files": [
98 | "**/*",
99 | "build/**/*",
100 | "node_modules/**/*"
101 | ],
102 | "directories": {
103 | "buildResources": "public"
104 | },
105 | "mac": {
106 | "target": "dmg",
107 | "category": "public.app-category.puzzle-games"
108 | },
109 | "win": {
110 | "target": "nsis"
111 | },
112 | "linux": {
113 | "target": "deb",
114 | "category": "Games"
115 | }
116 | },
117 | "browserslist": {
118 | "production": [
119 | ">0.2%",
120 | "not dead",
121 | "not op_mini all"
122 | ],
123 | "development": [
124 | "last 1 chrome version",
125 | "last 1 firefox version",
126 | "last 1 safari version"
127 | ]
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/op-tutorial/Tutorial.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useRef, useState } from "react";
2 | import {
3 | View,
4 | LayoutChangeEvent,
5 | LayoutRectangle,
6 | Platform,
7 | Dimensions,
8 | useWindowDimensions,
9 | } from "react-native";
10 | import { observer } from "mobx-react";
11 | import KeepAwake from "op-native/react-native-keep-awake";
12 | import { Board } from "op-board";
13 | import { useBoardStores } from "op-board";
14 | import { useCoreStores } from "op-core";
15 | import { metrics } from "op-design";
16 | import { BottomNav, Button, getBottomNavHeight } from "op-common";
17 | import {
18 | useAnimation,
19 | useOnMount,
20 | useHardwareBackButton,
21 | useScale,
22 | ScalingFunc,
23 | } from "op-utils";
24 | import { Description } from "./Description";
25 | import { clamp } from "lodash";
26 |
27 | export const Tutorial: FC = observer(function () {
28 | const scale = useScale();
29 | const styles = createStyles({ scale });
30 | const { board } = useBoardStores();
31 | const { puzzle, router } = useCoreStores();
32 |
33 | // Screen width/height setup
34 | const windowDimensions = useWindowDimensions();
35 | const screenWidth = Platform.select({
36 | native: Dimensions.get("screen").width,
37 | default: clamp(windowDimensions.width, metrics.webBoardMaxLayoutWidth),
38 | });
39 | const screenHeight = Platform.select({
40 | native: Dimensions.get("screen").height,
41 | default: windowDimensions.height,
42 | });
43 |
44 | // Routing setup
45 | const navigateToHome = () => router.changeRoute("home");
46 | const goToNextTutorial = () => puzzle.nextPuzzle();
47 | useHardwareBackButton(navigateToHome);
48 |
49 | // Disable interactions while animating
50 | const isUnmountingRef = useRef(false);
51 |
52 | // Fade UI animation
53 | const fadeInterfaceInAnimDuration = 200;
54 | const fadeInterfaceOutAnimDuration = 200;
55 | const fadeInterfaceAnim = useAnimation();
56 | useOnMount(() => {
57 | fadeInterfaceAnim.setup({ duration: fadeInterfaceInAnimDuration }).start();
58 | return () => {
59 | board.destroy();
60 | };
61 | });
62 |
63 | // Wait for every HUD pieces to be mounted so that we can measure their height
64 | // and make sure we can fit the board
65 | const [
66 | descriptionLayout,
67 | setDescriptionLayout,
68 | ] = useState(null);
69 | const handleDescriptionLayout = (event: LayoutChangeEvent) => {
70 | if (descriptionLayout?.height !== event.nativeEvent.layout.height) {
71 | setDescriptionLayout(event.nativeEvent.layout);
72 | }
73 | };
74 | const availableHorizontalSpace = screenWidth - metrics.screenMargin * 2;
75 | const availableVerticalSpace = descriptionLayout
76 | ? screenHeight -
77 | descriptionLayout.height -
78 | getBottomNavHeight(scale) -
79 | metrics.screenMargin * 8 // Additional vertical padding
80 | : undefined;
81 |
82 | // Callback handlers
83 | const handleMenuPress = () => {
84 | if (!isUnmountingRef.current) {
85 | navigateToHome();
86 | }
87 | };
88 | const handleBoardClearedAnimStart = () => {
89 | isUnmountingRef.current = true;
90 | fadeInterfaceAnim
91 | .setup({ duration: fadeInterfaceOutAnimDuration, toValue: 0 })
92 | .start();
93 | };
94 | const handleBoardClearedAnimEnd = () => {
95 | goToNextTutorial();
96 | };
97 |
98 | return (
99 |
100 |
101 |
107 |
108 | {puzzle.data && availableVerticalSpace && (
109 |
116 | )}
117 |
118 | {board.isInitialized && (
119 |
120 |
121 |
122 | )}
123 |
124 | );
125 | });
126 |
127 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
128 | root: {
129 | flex: 1,
130 | marginHorizontal: metrics.screenMargin,
131 | },
132 | boardWrapper: {
133 | flex: 1,
134 | justifyContent: "center",
135 | alignItems: "center",
136 | },
137 | });
138 |
--------------------------------------------------------------------------------
/src/op-game/Game.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useRef } from "react";
2 | import {
3 | View,
4 | Animated,
5 | Platform,
6 | Dimensions,
7 | useWindowDimensions,
8 | } from "react-native";
9 | import { observer } from "mobx-react";
10 | import KeepAwake from "op-native/react-native-keep-awake";
11 | import { Board } from "op-board";
12 | import { useCoreStores } from "op-core";
13 | import { useBoardStores } from "op-board";
14 | import {
15 | BottomNav,
16 | getBottomNavHeight,
17 | Button,
18 | Header,
19 | getHeaderHeight,
20 | } from "op-common";
21 | import { metrics, animations } from "op-design";
22 | import {
23 | useAnimation,
24 | useOnMount,
25 | useHardwareBackButton,
26 | useScale,
27 | ScalingFunc,
28 | } from "op-utils";
29 | import { clamp } from "lodash";
30 |
31 | export const Game: FC = observer(function () {
32 | const { puzzle, router } = useCoreStores();
33 | const { board } = useBoardStores();
34 | const interactionsDisabledRef = useRef(false);
35 |
36 | const scale = useScale();
37 | const styles = createStyles({ scale });
38 |
39 | // Screen width/height setup
40 | const windowDimensions = useWindowDimensions();
41 | const screenWidth = Platform.select({
42 | native: Dimensions.get("screen").width,
43 | default: clamp(windowDimensions.width, metrics.webBoardMaxLayoutWidth),
44 | });
45 | const screenHeight = Platform.select({
46 | native: Dimensions.get("screen").height,
47 | default: windowDimensions.height,
48 | });
49 |
50 | // Routing setup
51 | const navigateToHome = () => router.changeRoute("home");
52 | const navigateToSuccess = () => router.changeRoute("success");
53 | useHardwareBackButton(navigateToHome);
54 |
55 | // Animations setup
56 | // https://github.com/facebook/react-native/issues/27146
57 | const maxOpacity = Platform.OS === "android" ? 0.99 : 1;
58 | const fadeInterfaceInAnimDuration = 400;
59 | const fadeInterfaceOutAnimDuration = 200;
60 | const fadeRootOutDuration = 200;
61 | const fadeInterfaceAnim = useAnimation();
62 | const fadeRootAnim = useAnimation(maxOpacity);
63 | const fadeInterfaceIn = () =>
64 | fadeInterfaceAnim.setup({ duration: fadeInterfaceInAnimDuration });
65 | const fadeInterfaceOut = () =>
66 | fadeInterfaceAnim.setup({
67 | duration: fadeInterfaceOutAnimDuration,
68 | toValue: 0,
69 | });
70 | const fadeRootOut = () =>
71 | fadeRootAnim.setup({ duration: fadeRootOutDuration });
72 |
73 | useOnMount(() => {
74 | fadeInterfaceIn().start();
75 | });
76 |
77 | // Callback handlers
78 | const handleMenuPress = () => {
79 | if (interactionsDisabledRef.current) return;
80 | interactionsDisabledRef.current = true;
81 | fadeRootOut().start(navigateToHome);
82 | };
83 | const handleResetPress = () => {
84 | if (interactionsDisabledRef.current) return;
85 | board.reset();
86 | };
87 | const handleBoardClearedAnimStart = () => {
88 | interactionsDisabledRef.current = true;
89 | puzzle.onPuzzleCompleted();
90 | };
91 | const handleBoardClearedAnimEnd = () => {
92 | board.destroy();
93 | fadeInterfaceOut().start(navigateToSuccess);
94 | };
95 |
96 | // Calculate the available space for the board
97 | const availableHorizontalSpace = screenWidth - metrics.screenMargin * 2;
98 | const availableVerticalSpace =
99 | screenHeight -
100 | metrics.screenMargin * 2 -
101 | getBottomNavHeight(scale) -
102 | getHeaderHeight(scale) -
103 | metrics.screenMargin * 4; // Additional vertical padding
104 |
105 | return (
106 |
107 |
108 |
113 |
114 | {puzzle.data && (
115 |
121 | )}
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | });
130 |
131 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
132 | root: {
133 | marginHorizontal: metrics.screenMargin,
134 | flex: 1,
135 | },
136 | boardWrapper: {
137 | flex: 1,
138 | justifyContent: "center",
139 | alignItems: "center",
140 | },
141 | });
142 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/src/op-home/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from "react";
2 | import { View, Animated, Platform, ViewStyle } from "react-native";
3 | import { observer } from "mobx-react";
4 | import { useCoreStores, PuzzleMode } from "op-core";
5 | import { useBoardStores } from "op-board";
6 | import { metrics, animations } from "op-design";
7 | import { useScale, useAnimation, useOnMount, ScalingFunc } from "op-utils";
8 | import { Score } from "op-common";
9 | import { Logo } from "./Logo";
10 | import { Menu, MenuItem } from "./Menu";
11 | import { About } from "./About";
12 |
13 | export const Home: FC = observer(function () {
14 | const scale = useScale();
15 | const styles = createStyles({ scale });
16 |
17 | // Initialization
18 | const { router, stats } = useCoreStores();
19 | const { board } = useBoardStores();
20 |
21 | // Animations setup
22 | // https://github.com/facebook/react-native/issues/27146
23 | const maxOpacity = Platform.OS === "android" ? 0.99 : 1;
24 | const animateInSequence = !router.hasLoadedHomeOnce;
25 | const titleAnimDuration = 600;
26 | const menuAnimDuration = 600;
27 | const dotAnimDuration = 200;
28 | const rootFadeInAnimDuration = 200;
29 | const rootFadeOutAnimDuration = 200;
30 | const [isMenuDisabled, setIsMenuDisabled] = useState(true);
31 | const titleAnim = useAnimation(animateInSequence ? 0 : 1);
32 | const menuAnim = useAnimation(animateInSequence ? 0 : 1);
33 | const dotAnim = useAnimation(animateInSequence ? 0 : 1);
34 | const rootFadeAnim = useAnimation(animateInSequence ? maxOpacity : 0);
35 | useOnMount(() => {
36 | if (router.hasLoadedHomeOnce) {
37 | rootFadeAnim.setup({ duration: rootFadeInAnimDuration }).start(() => {
38 | setIsMenuDisabled(false);
39 | });
40 | } else {
41 | Animated.sequence([
42 | titleAnim.setup({ duration: titleAnimDuration }),
43 | dotAnim.setup({ duration: dotAnimDuration }),
44 | ]).start(() => {
45 | setIsMenuDisabled(false);
46 | menuAnim.setup({ duration: menuAnimDuration }).start();
47 | });
48 | }
49 | });
50 |
51 | // Menu items setup
52 | const canContinue = board.isInitialized;
53 | const handleItemPress = (value: PuzzleMode | "continue") => {
54 | if (isMenuDisabled) return;
55 | setIsMenuDisabled(true);
56 | rootFadeAnim
57 | .setup({ duration: rootFadeOutAnimDuration, toValue: 0 })
58 | .start(() => {
59 | if (value === "tutorial") {
60 | router.changeRoute(value);
61 | } else {
62 | router.changeRoute("intro", value);
63 | }
64 | });
65 | };
66 | const menuItems: MenuItem[] = [
67 | {
68 | label: "tutorial",
69 | value: "tutorial",
70 | onPress: handleItemPress,
71 | highlighted: !canContinue,
72 | starred: !stats.tutorialCompleted && !canContinue,
73 | },
74 | {
75 | label: "small",
76 | value: "small",
77 | onPress: handleItemPress,
78 | highlighted: !canContinue,
79 | },
80 | {
81 | label: "medium",
82 | value: "medium",
83 | onPress: handleItemPress,
84 | highlighted: !canContinue,
85 | },
86 | {
87 | label: "large",
88 | value: "large",
89 | onPress: handleItemPress,
90 | highlighted: !canContinue,
91 | },
92 | ];
93 | if (canContinue) {
94 | menuItems.unshift({
95 | label: "continue",
96 | value: "continue",
97 | onPress: handleItemPress,
98 | highlighted: true,
99 | });
100 | }
101 |
102 | // Score setup
103 | const scoreStyle: ViewStyle = {
104 | top: metrics.screenMargin,
105 | right: metrics.screenMargin,
106 | };
107 | const handleScorePress = () => {
108 | if (isMenuDisabled) return;
109 | setIsMenuDisabled(true);
110 | rootFadeAnim
111 | .setup({ duration: rootFadeOutAnimDuration, toValue: 0 })
112 | .start(() => {
113 | router.changeRoute("stats");
114 | });
115 | };
116 |
117 | return (
118 |
119 |
120 |
121 |
122 |
123 |
128 |
129 |
135 | {Platform.OS === "web" && (
136 |
137 | )}
138 |
139 | );
140 | });
141 |
142 | const createStyles = ({ scale }: { scale: ScalingFunc }): any => ({
143 | root: {
144 | flex: 1,
145 | paddingVertical: metrics.screenMargin * 2,
146 | paddingHorizontal: metrics.screenMargin,
147 | },
148 | top: {
149 | flex: 1,
150 | justifyContent: "center",
151 | },
152 | bottom: {
153 | flex: 1,
154 | flexDirection: "column",
155 | justifyContent: "flex-end",
156 | },
157 | about: {
158 | position: "absolute",
159 | alignItems: "flex-end",
160 | bottom: metrics.screenMargin * 2 + scale(5),
161 | right: metrics.screenMargin,
162 | },
163 | });
164 |
--------------------------------------------------------------------------------
/src/op-board/Board.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect } from "react";
2 | import { View, StyleSheet, Animated, Platform } from "react-native";
3 | import { reaction } from "mobx";
4 | import { observer } from "mobx-react";
5 | import { animations, useColors } from "op-design";
6 | import { useAnimation, useOnMount, hapticFeedback } from "op-utils";
7 | import { useCoreStores } from "op-core";
8 | import { autoSolve } from "op-config";
9 | import { useBoardStores } from "./store";
10 | import { Tile } from "./Tile";
11 | import { PointerAwareView } from "./PointerAwareView";
12 |
13 | const isAndroid = Platform.OS === "android";
14 |
15 | interface BoardProps {
16 | availableHorizontalSpace: number;
17 | availableVerticalSpace: number;
18 | onClearedAnimEnd?: () => void;
19 | onClearedAnimStart?: () => void;
20 | }
21 |
22 | export const Board: FC = observer(function ({
23 | availableHorizontalSpace,
24 | availableVerticalSpace,
25 | onClearedAnimEnd = () => undefined,
26 | onClearedAnimStart = () => undefined,
27 | }) {
28 | const { board, interactions } = useBoardStores();
29 | const { puzzle } = useCoreStores();
30 | const colors = useColors();
31 |
32 | // Animations setup
33 | const fadeInAnimDuration = 400;
34 | const fadeOutAnimDuration = 200;
35 | const successAnimDuration = 200;
36 | const successAnimDelay = 400;
37 | const fadeAnim = useAnimation();
38 | const successAnim = useAnimation();
39 | const animateSuccess = () => {
40 | onClearedAnimStart();
41 | Animated.sequence([
42 | successAnim.setup({
43 | duration: successAnimDuration,
44 | useNativeDriver: false,
45 | }),
46 | Animated.delay(successAnimDelay),
47 | fadeAnim.setup({ duration: fadeOutAnimDuration, toValue: 0 }),
48 | ]).start(onClearedAnimEnd);
49 | };
50 |
51 | useOnMount(() => {
52 | fadeAnim.setup({ duration: fadeInAnimDuration }).start();
53 | });
54 |
55 | // Auto-solve the puzzle in development mode if needed
56 | if (autoSolve) {
57 | useOnMount(() => {
58 | setTimeout(animateSuccess, 2000);
59 | });
60 | }
61 |
62 | // - Trigger an light haptic feedback when a cell is pressed
63 | // - Trigger the success animation when the board is cleared
64 | useEffect(() => {
65 | if (!puzzle.data) {
66 | throw new Error('Board » missing "puzzle.data"');
67 | }
68 | const isContinuing = puzzle.id === board.puzzleId;
69 | if (!isContinuing) {
70 | board.initialize(puzzle.id, puzzle.data);
71 | }
72 | const disposeHapticReaction = reaction(
73 | () => interactions.hoveredCell,
74 | () => hapticFeedback.generate("impactLight")
75 | );
76 | const disposeClearedReaction = reaction(
77 | () => board.cleared,
78 | (didSucceed) => {
79 | if (didSucceed) animateSuccess();
80 | }
81 | );
82 | return () => {
83 | disposeHapticReaction();
84 | disposeClearedReaction();
85 | };
86 | // eslint-disable-next-line react-hooks/exhaustive-deps
87 | }, [puzzle.id]);
88 |
89 | // Don't show the board unless initializes, otherwise the cells might not
90 | // work correctly
91 | if (!board.isInitialized) return null;
92 |
93 | // Scale the board given the vertical and horizontal contraints defined
94 | // in the pros
95 | const doesBoardFitVertically =
96 | (availableHorizontalSpace / board.colsCount) * board.rowsCount <
97 | availableVerticalSpace;
98 | const tileSize = doesBoardFitVertically
99 | ? Math.floor(availableHorizontalSpace / board.colsCount)
100 | : Math.floor(availableVerticalSpace / board.rowsCount);
101 |
102 | // On Android transitioning the opacity of the root view causes a janky
103 | // animation. As a workaround we place an absolutely positioned layer on top
104 | // of the UI and transition its opacity instead.
105 | const androidOpacityStyle = {
106 | backgroundColor: colors.primary[9],
107 | opacity: fadeAnim.value.interpolate({
108 | inputRange: [0, 1],
109 | outputRange: [1, 0],
110 | }),
111 | };
112 |
113 | return (
114 | interactions.onGridPointerDown(coords)}
116 | onPointerMove={(coords) => interactions.onGridPointerMove(coords)}
117 | onPointerUp={(coords) => interactions.onGridPointerUp(coords)}
118 | onLayout={(e) => interactions.enableInteraction(e)}
119 | pointerEnabled={!board.cleared}
120 | style={styles.root}
121 | >
122 |
123 | {board.grid.map((cellsRow, rowIndex) => (
124 |
125 | {cellsRow.map((cell) => (
126 |
132 | ))}
133 |
134 | ))}
135 |
136 | {isAndroid && (
137 |
141 | )}
142 |
143 | );
144 | });
145 |
146 | const styles = StyleSheet.create({
147 | root: {},
148 | row: {
149 | flexDirection: "row",
150 | },
151 | androidOpacity: {
152 | position: "absolute",
153 | top: -5,
154 | left: -5,
155 | right: -5,
156 | bottom: -5,
157 | },
158 | });
159 |
--------------------------------------------------------------------------------
/src/serviceWorkerRegistration.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://cra.link/PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
32 | if (publicUrl.origin !== window.location.origin) {
33 | // Our service worker won't work if PUBLIC_URL is on a different origin
34 | // from what our page is served on. This might happen if a CDN is used to
35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
36 | return;
37 | }
38 |
39 | window.addEventListener("load", () => {
40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
41 |
42 | if (isLocalhost) {
43 | // This is running on localhost. Let's check if a service worker still exists or not.
44 | checkValidServiceWorker(swUrl, config);
45 |
46 | // Add some additional logging to localhost, pointing developers to the
47 | // service worker/PWA documentation.
48 | navigator.serviceWorker.ready.then(() => {
49 | console.log(
50 | "This web app is being served cache-first by a service " +
51 | "worker. To learn more, visit https://cra.link/PWA"
52 | );
53 | });
54 | } else {
55 | // Is not localhost. Just register service worker
56 | registerValidSW(swUrl, config);
57 | }
58 | });
59 | }
60 | }
61 |
62 | function registerValidSW(swUrl: string, config?: Config) {
63 | navigator.serviceWorker
64 | .register(swUrl)
65 | .then((registration) => {
66 | registration.onupdatefound = () => {
67 | const installingWorker = registration.installing;
68 | if (installingWorker == null) {
69 | return;
70 | }
71 | installingWorker.onstatechange = () => {
72 | if (installingWorker.state === "installed") {
73 | if (navigator.serviceWorker.controller) {
74 | // At this point, the updated precached content has been fetched,
75 | // but the previous service worker will still serve the older
76 | // content until all client tabs are closed.
77 | console.log(
78 | "New content is available and will be used when all " +
79 | "tabs for this page are closed. See https://cra.link/PWA."
80 | );
81 |
82 | // Execute callback
83 | if (config && config.onUpdate) {
84 | config.onUpdate(registration);
85 | }
86 | } else {
87 | // At this point, everything has been precached.
88 | // It's the perfect time to display a
89 | // "Content is cached for offline use." message.
90 | console.log("Content is cached for offline use.");
91 |
92 | // Execute callback
93 | if (config && config.onSuccess) {
94 | config.onSuccess(registration);
95 | }
96 | }
97 | }
98 | };
99 | };
100 | })
101 | .catch((error) => {
102 | console.error("Error during service worker registration:", error);
103 | });
104 | }
105 |
106 | function checkValidServiceWorker(swUrl: string, config?: Config) {
107 | // Check if the service worker can be found. If it can't reload the page.
108 | fetch(swUrl, {
109 | headers: { "Service-Worker": "script" },
110 | })
111 | .then((response) => {
112 | // Ensure service worker exists, and that we really are getting a JS file.
113 | const contentType = response.headers.get("content-type");
114 | if (
115 | response.status === 404 ||
116 | (contentType != null && contentType.indexOf("javascript") === -1)
117 | ) {
118 | // No service worker found. Probably a different app. Reload the page.
119 | navigator.serviceWorker.ready.then((registration) => {
120 | registration.unregister().then(() => {
121 | window.location.reload();
122 | });
123 | });
124 | } else {
125 | // Service worker found. Proceed as normal.
126 | registerValidSW(swUrl, config);
127 | }
128 | })
129 | .catch(() => {
130 | console.log(
131 | "No internet connection found. App is running in offline mode."
132 | );
133 | });
134 | }
135 |
136 | export function unregister() {
137 | if ("serviceWorker" in navigator) {
138 | navigator.serviceWorker.ready
139 | .then((registration) => {
140 | registration.unregister();
141 | })
142 | .catch((error) => {
143 | console.error(error.message);
144 | });
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/op-board/Tile.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import {
3 | StyleSheet,
4 | View,
5 | TextStyle,
6 | ViewStyle,
7 | Animated,
8 | Platform,
9 | } from "react-native";
10 | import { observer } from "mobx-react";
11 | import { useColors } from "op-design";
12 | import { Text } from "op-common";
13 | import { Cell } from "./store";
14 |
15 | interface Props {
16 | cell: Cell;
17 | size: number;
18 | successAnimValue: Animated.Value;
19 | }
20 |
21 | export const calculateFontSize = (size: number) => size * 0.6;
22 |
23 | export const calculateBorderWidth = (size: number) => Math.floor(size / 14);
24 |
25 | const adjustBorderForOrientation = (cell: Cell, style: ViewStyle) => {
26 | if (cell.orientation === "horizontal-left") {
27 | style.borderRightWidth = 0;
28 | }
29 | if (cell.orientation === "horizontal-middle") {
30 | style.borderRightWidth = 0;
31 | style.borderLeftWidth = 0;
32 | }
33 | if (cell.orientation === "horizontal-right") {
34 | style.borderLeftWidth = 0;
35 | }
36 | if (cell.orientation === "vertical-top") {
37 | style.borderBottomWidth = 0;
38 | }
39 | if (cell.orientation === "vertical-middle") {
40 | style.borderBottomWidth = 0;
41 | style.borderTopWidth = 0;
42 | }
43 | if (cell.orientation === "vertical-bottom") {
44 | style.borderTopWidth = 0;
45 | }
46 | };
47 |
48 | export const Tile: FC = observer(function (props) {
49 | const colors = useColors();
50 | const { cell, size, successAnimValue } = props;
51 |
52 | // Border width
53 | const borderWidth = calculateBorderWidth(size);
54 |
55 | // Tile size
56 | let tileStyle: ViewStyle = {};
57 | tileStyle.width = size;
58 | tileStyle.height = size;
59 | if (cell.col === 0) {
60 | tileStyle.width = size + borderWidth;
61 | }
62 | if (cell.row === 0) {
63 | tileStyle.height = size + borderWidth;
64 | }
65 |
66 | // Tile border style
67 | tileStyle.borderColor = colors.primary[7];
68 | tileStyle.borderRightWidth = borderWidth;
69 | tileStyle.borderBottomWidth = borderWidth;
70 | if (cell.col === 0) {
71 | tileStyle.borderLeftWidth = borderWidth;
72 | }
73 | if (cell.row === 0) {
74 | tileStyle.borderTopWidth = borderWidth;
75 | }
76 | adjustBorderForOrientation(cell, tileStyle);
77 | // @ts-ignore
78 | tileStyle.borderColor = successAnimValue.interpolate({
79 | inputRange: [0, 1],
80 | outputRange: [colors.primary[7], colors.primary[9]],
81 | });
82 |
83 | // Tile background color
84 | tileStyle.backgroundColor = "transparent";
85 | if (cell.completed) {
86 | tileStyle.backgroundColor = colors.primary[3];
87 | } else if (cell.orientation !== "none") {
88 | tileStyle.backgroundColor = colors.primary[5];
89 | }
90 |
91 | // Content color (shouldn't be needed, but declaring it here fixes some a
92 | // couple of white gaps on Android)
93 | const contentStyle: ViewStyle = {};
94 | if (cell.completed) {
95 | contentStyle.backgroundColor = colors.primary[3];
96 | } else if (cell.orientation !== "none") {
97 | contentStyle.backgroundColor = colors.primary[5];
98 | }
99 |
100 | // Content border
101 | if (cell.orientation !== "none") {
102 | contentStyle.borderColor = colors.primary[1];
103 | contentStyle.borderTopWidth = borderWidth;
104 | contentStyle.borderRightWidth = borderWidth;
105 | contentStyle.borderBottomWidth = borderWidth;
106 | contentStyle.borderLeftWidth = borderWidth;
107 | }
108 | adjustBorderForOrientation(cell, contentStyle);
109 |
110 | // Content position
111 | contentStyle.top = -borderWidth;
112 | contentStyle.left = -borderWidth;
113 | contentStyle.right = -borderWidth;
114 | contentStyle.bottom = -borderWidth;
115 |
116 | // Text style
117 | const textStyle: TextStyle = {};
118 | if (cell.value === "." && cell.orientation === "none") {
119 | textStyle.color = colors.primary[0];
120 | } else {
121 | textStyle.color = "#fff";
122 | }
123 | textStyle.fontSize = calculateFontSize(size);
124 | if (Platform.OS === "android" || Platform.OS === "ios") {
125 | textStyle.paddingRight = borderWidth / 2;
126 | textStyle.paddingBottom = borderWidth;
127 | if (cell.col === 0) {
128 | textStyle.paddingRight = 0;
129 | }
130 | if (cell.row === 0) {
131 | textStyle.paddingBottom = 0;
132 | }
133 | } else {
134 | // For some reasons on the web the text centering glitches a bit when a cell
135 | // is extended... I suppose there might be some differences on who the cell
136 | // border is kept into account for the centering calculation.
137 | if (
138 | cell.orientation === "horizontal-middle" ||
139 | cell.orientation === "horizontal-left"
140 | ) {
141 | textStyle.paddingRight = borderWidth;
142 | }
143 | if (
144 | cell.orientation === "vertical-middle" ||
145 | cell.orientation === "vertical-top"
146 | ) {
147 | textStyle.paddingBottom = borderWidth;
148 | }
149 | }
150 |
151 | if (Platform.OS !== "android" && Platform.OS !== "ios") {
152 | // @ts-ignore
153 | textStyle.userSelect = "none";
154 | }
155 |
156 | // Hover style
157 | const hoverStyle: ViewStyle = {};
158 | hoverStyle.backgroundColor = colors.primary[9];
159 |
160 | // Symbol
161 | const symbol = cell.value === "." ? "•" : cell.value;
162 |
163 | return (
164 |
165 |
166 |
171 | {symbol}
172 |
173 | {cell.highlighted && }
174 |
175 | );
176 | });
177 |
178 | const styles = StyleSheet.create({
179 | root: {
180 | position: "relative",
181 | justifyContent: "center",
182 | alignItems: "center",
183 | },
184 | text: {
185 | position: "absolute",
186 | textAlign: "center",
187 | justifyContent: "center",
188 | alignItems: "center",
189 | },
190 | content: {
191 | position: "absolute",
192 | justifyContent: "center",
193 | alignItems: "center",
194 | },
195 | hover: {
196 | position: "absolute",
197 | opacity: 0.5,
198 | top: 0,
199 | left: 0,
200 | right: 0,
201 | bottom: 0,
202 | },
203 | });
204 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=`expr $i + 1`
158 | done
159 | case $i in
160 | 0) set -- ;;
161 | 1) set -- "$args0" ;;
162 | 2) set -- "$args0" "$args1" ;;
163 | 3) set -- "$args0" "$args1" "$args2" ;;
164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=`save "$@"`
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | exec "$JAVACMD" "$@"
--------------------------------------------------------------------------------
/src/op-core/store.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 | import {
3 | observable,
4 | action,
5 | computed,
6 | toJS,
7 | runInAction,
8 | makeObservable,
9 | } from "mobx";
10 | import { rehydrateObject, persistObject, pickRandomPuzzle } from "op-utils";
11 | import uniq from "lodash/uniq";
12 | import puzzles from "./puzzles.json";
13 |
14 | export type Route =
15 | | "home"
16 | | "game"
17 | | "intro"
18 | | "tutorial"
19 | | "success"
20 | | "stats";
21 | export type PuzzleMode = "tutorial" | "small" | "medium" | "large";
22 |
23 | const sum = (a: number, b: number) => a + b;
24 |
25 | class RouterStore {
26 | root: RootStore;
27 |
28 | currentRoute: Route;
29 | routesHistory = observable.array();
30 |
31 | constructor(rootStore: RootStore) {
32 | this.root = rootStore;
33 | this.currentRoute = "home";
34 | this.routesHistory.replace(["home"]);
35 |
36 | makeObservable(this, {
37 | currentRoute: observable,
38 | routesHistory: observable,
39 | hasLoadedHomeOnce: computed,
40 | changeRoute: action,
41 | });
42 | }
43 |
44 | get hasLoadedHomeOnce() {
45 | return (
46 | this.routesHistory.length > 1 &&
47 | this.routesHistory.filter((x) => x === "home").length > 1
48 | );
49 | }
50 |
51 | changeRoute(route: Route, puzzleMode?: PuzzleMode | "continue") {
52 | switch (route) {
53 | case "intro": {
54 | if (puzzleMode !== "continue") {
55 | this.root.puzzle.setRandomPuzzle(puzzleMode);
56 | }
57 | break;
58 | }
59 | case "tutorial": {
60 | this.root.puzzle.setPuzzle("tutorial", 0);
61 | break;
62 | }
63 | case "home":
64 | case "game":
65 | case "success":
66 | case "stats": {
67 | break;
68 | }
69 | default: {
70 | throw new Error(`"RouterStore.changeRoute » Invalid route ${route}`);
71 | }
72 | }
73 | this.currentRoute = route;
74 | this.routesHistory.push(route);
75 | }
76 | }
77 |
78 | class PuzzleStore {
79 | root: RootStore;
80 |
81 | mode?: PuzzleMode = undefined;
82 | index?: number = undefined;
83 | increasesScore: boolean = false;
84 |
85 | constructor(rootStore: RootStore) {
86 | this.root = rootStore;
87 | this.increasesScore = false;
88 |
89 | makeObservable(this, {
90 | mode: observable,
91 | index: observable,
92 | increasesScore: observable,
93 | current: computed,
94 | name: computed,
95 | prefix: computed,
96 | id: computed,
97 | data: computed,
98 | type: computed,
99 | score: computed,
100 | isTutorialEnd: computed,
101 | tutorialTitle: computed,
102 | tutorialMessage: computed,
103 | setPuzzle: action,
104 | setRandomPuzzle: action,
105 | nextPuzzle: action,
106 | onPuzzleCompleted: action,
107 | reset: action,
108 | });
109 | }
110 |
111 | get current() {
112 | if (this.mode && this.index !== undefined) {
113 | return puzzles[this.mode][this.index];
114 | } else {
115 | return undefined;
116 | }
117 | }
118 |
119 | get name() {
120 | return this.current?.name || "";
121 | }
122 |
123 | get prefix() {
124 | const modePrefix = {
125 | tutorial: "xs",
126 | small: "sm",
127 | medium: "md",
128 | large: "lg",
129 | };
130 | return this.mode ? modePrefix[this.mode] : "ko";
131 | }
132 |
133 | get id() {
134 | return this.name;
135 | }
136 |
137 | get data() {
138 | return this.current?.data;
139 | }
140 |
141 | get type() {
142 | // @ts-ignore
143 | return this.current?.type || "puzzle";
144 | }
145 |
146 | get score() {
147 | return this.current?.score || 0;
148 | }
149 |
150 | /* ===================
151 | * TUTORIAL
152 | * =================== */
153 | get isTutorialEnd() {
154 | return (
155 | this.mode === "tutorial" && this.index === puzzles.tutorial.length - 1
156 | );
157 | }
158 |
159 | get tutorialTitle() {
160 | // @ts-ignore
161 | return this.current?.title || "";
162 | }
163 |
164 | get tutorialMessage() {
165 | // @ts-ignore
166 | return this.current?.message || "";
167 | }
168 |
169 | /* ===================
170 | * GENERIC ACTIONS
171 | * =================== */
172 | setPuzzle(mode: PuzzleMode = this.mode || "small", index: number) {
173 | this.mode = mode;
174 | this.index = index;
175 | this.root.stats.updatePlayedPuzzles(mode, index);
176 | this.increasesScore =
177 | this.root.stats.completedPuzzles[this.mode]?.indexOf(this.index) === -1;
178 | }
179 |
180 | setRandomPuzzle(mode: PuzzleMode = this.mode || "small") {
181 | const randomPuzzleIndex = pickRandomPuzzle({
182 | allPuzzlesLength: puzzles[mode].length,
183 | playedHistory: this.root.stats.playedPuzzles[mode],
184 | completedHistory: this.root.stats.completedPuzzles[mode],
185 | });
186 | this.setPuzzle(mode, randomPuzzleIndex);
187 | }
188 |
189 | nextPuzzle() {
190 | if (this.index !== undefined) this.index = this.index + 1;
191 | }
192 |
193 | onPuzzleCompleted() {
194 | this.root.stats.updateCompletedPuzzles(this.mode, this.index);
195 | }
196 |
197 | reset() {
198 | this.mode = undefined;
199 | this.index = undefined;
200 | }
201 | }
202 |
203 | const emptyPuzzleHistory: Record = {
204 | tutorial: [],
205 | small: [],
206 | medium: [],
207 | large: [],
208 | };
209 |
210 | class StatsStore {
211 | root: RootStore;
212 |
213 | initialized: boolean;
214 | playedPuzzles: Record;
215 | completedPuzzles: Record;
216 |
217 | constructor(rootStore: RootStore) {
218 | this.root = rootStore;
219 | this.initialized = false;
220 | this.playedPuzzles = emptyPuzzleHistory;
221 | this.completedPuzzles = emptyPuzzleHistory;
222 |
223 | makeObservable(this, {
224 | initialized: observable,
225 | playedPuzzles: observable,
226 | completedPuzzles: observable,
227 | initializeStore: action,
228 | score: computed,
229 | tutorialCompleted: computed,
230 | updateCompletedPuzzles: action,
231 | updatePlayedPuzzles: action,
232 | });
233 | }
234 |
235 | async initializeStore() {
236 | const playedPuzzles = await rehydrateObject("playedPuzzles");
237 | runInAction(() => {
238 | this.playedPuzzles = playedPuzzles || emptyPuzzleHistory;
239 | });
240 | const completedPuzzles = await rehydrateObject("completedPuzzles");
241 | runInAction(() => {
242 | this.completedPuzzles = completedPuzzles || emptyPuzzleHistory;
243 | });
244 | runInAction(() => {
245 | this.initialized = true;
246 | });
247 | }
248 |
249 | get score() {
250 | const _score = (Object.keys(this.completedPuzzles) as PuzzleMode[])
251 | .map((mode) => {
252 | return this.completedPuzzles[mode]
253 | .map((index) => {
254 | const puzzleScore = puzzles[mode]?.[index]?.score || 0;
255 | return puzzleScore;
256 | })
257 | .reduce(sum, 0);
258 | })
259 | .reduce(sum, 0);
260 |
261 | return _score;
262 | }
263 |
264 | get tutorialCompleted() {
265 | return this.completedPuzzles["tutorial"].length > 0;
266 | }
267 |
268 | updateCompletedPuzzles(mode?: PuzzleMode, index?: number) {
269 | if (mode && index !== undefined) {
270 | this.completedPuzzles[mode] = uniq(
271 | this.completedPuzzles[mode] || []
272 | ).filter((x) => x !== index);
273 | this.completedPuzzles[mode].push(index);
274 | persistObject("completedPuzzles", toJS(this.completedPuzzles));
275 | }
276 | }
277 |
278 | updatePlayedPuzzles(mode?: PuzzleMode, index?: number) {
279 | if (mode && index !== undefined) {
280 | this.playedPuzzles[mode] = uniq(this.playedPuzzles[mode] || []).filter(
281 | (x) => x !== index
282 | );
283 | this.playedPuzzles[mode].push(index);
284 | persistObject("playedPuzzles", toJS(this.playedPuzzles));
285 | }
286 | }
287 | }
288 |
289 | class RootStore {
290 | puzzle: PuzzleStore;
291 | router: RouterStore;
292 | stats: StatsStore;
293 |
294 | constructor() {
295 | this.puzzle = new PuzzleStore(this);
296 | this.router = new RouterStore(this);
297 | this.stats = new StatsStore(this);
298 | }
299 |
300 | async initializeStore() {
301 | await rootStore.stats.initializeStore();
302 | }
303 | }
304 |
305 | const rootStore = new RootStore();
306 |
307 | export const storesContext = createContext({
308 | initializeStore: rootStore.initializeStore,
309 | puzzle: rootStore.puzzle,
310 | router: rootStore.router,
311 | stats: rootStore.stats,
312 | });
313 |
314 | export const useCoreStores = () => useContext(storesContext);
315 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 |
3 | import com.android.build.OutputFile
4 |
5 | /**
6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
7 | * and bundleReleaseJsAndAssets).
8 | * These basically call `react-native bundle` with the correct arguments during the Android build
9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
10 | * bundle directly from the development server. Below you can see all the possible configurations
11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the
12 | * `apply from: "../../node_modules/react-native/react.gradle"` line.
13 | *
14 | * project.ext.react = [
15 | * // the name of the generated asset file containing your JS bundle
16 | * bundleAssetName: "index.android.bundle",
17 | *
18 | * // the entry file for bundle generation. If none specified and
19 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is
20 | * // default. Can be overridden with ENTRY_FILE environment variable.
21 | * entryFile: "index.android.js",
22 | *
23 | * // https://reactnative.dev/docs/performance#enable-the-ram-format
24 | * bundleCommand: "ram-bundle",
25 | *
26 | * // whether to bundle JS and assets in debug mode
27 | * bundleInDebug: false,
28 | *
29 | * // whether to bundle JS and assets in release mode
30 | * bundleInRelease: true,
31 | *
32 | * // whether to bundle JS and assets in another build variant (if configured).
33 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
34 | * // The configuration property can be in the following formats
35 | * // 'bundleIn${productFlavor}${buildType}'
36 | * // 'bundleIn${buildType}'
37 | * // bundleInFreeDebug: true,
38 | * // bundleInPaidRelease: true,
39 | * // bundleInBeta: true,
40 | *
41 | * // whether to disable dev mode in custom build variants (by default only disabled in release)
42 | * // for example: to disable dev mode in the staging build type (if configured)
43 | * devDisabledInStaging: true,
44 | * // The configuration property can be in the following formats
45 | * // 'devDisabledIn${productFlavor}${buildType}'
46 | * // 'devDisabledIn${buildType}'
47 | *
48 | * // the root of your project, i.e. where "package.json" lives
49 | * root: "../../",
50 | *
51 | * // where to put the JS bundle asset in debug mode
52 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
53 | *
54 | * // where to put the JS bundle asset in release mode
55 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release",
56 | *
57 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
58 | * // require('./image.png')), in debug mode
59 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
60 | *
61 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
62 | * // require('./image.png')), in release mode
63 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
64 | *
65 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means
66 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
67 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle
68 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
69 | * // for example, you might want to remove it from here.
70 | * inputExcludes: ["android/**", "ios/**"],
71 | *
72 | * // override which node gets called and with what additional arguments
73 | * nodeExecutableAndArgs: ["node"],
74 | *
75 | * // supply additional arguments to the packager
76 | * extraPackagerArgs: []
77 | * ]
78 | */
79 |
80 | project.ext.react = [
81 | enableHermes: false, // clean and rebuild if changing
82 | ]
83 |
84 | apply from: "../../node_modules/react-native/react.gradle"
85 |
86 | /**
87 | * Retrieve the Keystore password from the MacOS keychain
88 | */
89 | def getPassword(String currentUser, String keyChain) {
90 | def stdout = new ByteArrayOutputStream()
91 | def stderr = new ByteArrayOutputStream()
92 | exec {
93 | commandLine 'security', '-q', 'find-generic-password', '-a', currentUser, '-s', keyChain, '-w'
94 | standardOutput = stdout
95 | errorOutput = stderr
96 | ignoreExitValue true
97 | }
98 | //noinspection GroovyAssignabilityCheck
99 | stdout.toString().trim()
100 | }
101 |
102 | /**
103 | * Set this to true to create two separate APKs instead of one:
104 | * - An APK that only works on ARM devices
105 | * - An APK that only works on x86 devices
106 | * The advantage is the size of the APK is reduced by about 4MB.
107 | * Upload all the APKs to the Play Store and people will download
108 | * the correct one based on the CPU architecture of their device.
109 | */
110 | def enableSeparateBuildPerCPUArchitecture = true
111 |
112 | /**
113 | * Run Proguard to shrink the Java bytecode in release builds.
114 | */
115 | def enableProguardInReleaseBuilds = true
116 |
117 | /**
118 | * The preferred build flavor of JavaScriptCore.
119 | *
120 | * For example, to use the international variant, you can use:
121 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
122 | *
123 | * The international variant includes ICU i18n library and necessary data
124 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
125 | * give correct results when using with locales other than en-US. Note that
126 | * this variant is about 6MiB larger per architecture than default.
127 | */
128 | def jscFlavor = 'org.webkit:android-jsc:+'
129 |
130 | /**
131 | * Whether to enable the Hermes VM.
132 | *
133 | * This should be set on project.ext.react and mirrored here. If it is not set
134 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
135 | * and the benefits of using Hermes will therefore be sharply reduced.
136 | */
137 | def enableHermes = project.ext.react.get("enableHermes", false);
138 |
139 | android {
140 | compileSdkVersion rootProject.ext.compileSdkVersion
141 |
142 | compileOptions {
143 | sourceCompatibility JavaVersion.VERSION_1_8
144 | targetCompatibility JavaVersion.VERSION_1_8
145 | }
146 |
147 | defaultConfig {
148 | applicationId "com.mmazzarolo.ordinarypuzzles"
149 | minSdkVersion rootProject.ext.minSdkVersion
150 | targetSdkVersion rootProject.ext.targetSdkVersion
151 | versionCode 7
152 | versionName "1.2"
153 | }
154 | splits {
155 | abi {
156 | reset()
157 | enable enableSeparateBuildPerCPUArchitecture
158 | universalApk false // If true, also generate a universal APK
159 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
160 | }
161 | }
162 | signingConfigs {
163 | debug {
164 | storeFile file('debug.keystore')
165 | storePassword 'android'
166 | keyAlias 'androiddebugkey'
167 | keyPassword 'android'
168 | }
169 | release {
170 | if (project.hasProperty('ORDINARY_PUZZLES_RELEASE_STORE_FILE')) {
171 | storeFile file(ORDINARY_PUZZLES_RELEASE_STORE_FILE)
172 | storePassword getPassword("mmazzarolo", "OrdinaryPuzzles Android Keystore")
173 | keyAlias ORDINARY_PUZZLES_RELEASE_KEY_ALIAS
174 | keyPassword getPassword("mmazzarolo", "OrdinaryPuzzles Android Keystore")
175 | }
176 | }
177 | }
178 | buildTypes {
179 | debug {
180 | signingConfig signingConfigs.debug
181 | }
182 | release {
183 | // Caution! In production, you need to generate your own keystore file.
184 | // see https://reactnative.dev/docs/signed-apk-android.
185 | signingConfig signingConfigs.release
186 | minifyEnabled enableProguardInReleaseBuilds
187 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
188 | }
189 | }
190 | // applicationVariants are e.g. debug, release
191 | applicationVariants.all { variant ->
192 | variant.outputs.each { output ->
193 | // For each separate APK per architecture, set a unique version code as described here:
194 | // https://developer.android.com/studio/build/configure-apk-splits.html
195 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
196 | def abi = output.getFilter(OutputFile.ABI)
197 | if (abi != null) { // null for the universal-debug, universal-release variants
198 | output.versionCodeOverride =
199 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
200 | }
201 |
202 | }
203 | }
204 | }
205 |
206 | dependencies {
207 | implementation fileTree(dir: "libs", include: ["*.jar"])
208 | //noinspection GradleDynamicVersion
209 | implementation "com.facebook.react:react-native:+" // From node_modules
210 |
211 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
212 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
213 | exclude group:'com.facebook.fbjni'
214 | }
215 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
216 | exclude group:'com.facebook.flipper'
217 | exclude group:'com.squareup.okhttp3', module:'okhttp'
218 | }
219 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
220 | exclude group:'com.facebook.flipper'
221 | }
222 |
223 | if (enableHermes) {
224 | def hermesPath = "../../node_modules/hermes-engine/android/";
225 | debugImplementation files(hermesPath + "hermes-debug.aar")
226 | releaseImplementation files(hermesPath + "hermes-release.aar")
227 | } else {
228 | implementation jscFlavor
229 | }
230 | }
231 |
232 | // Run this once to be able to run the application with BUCK
233 | // puts all compile dependencies into folder libs for BUCK to use
234 | task copyDownloadableDepsToLibs(type: Copy) {
235 | from configurations.compile
236 | into 'libs'
237 | }
238 |
239 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
240 |
--------------------------------------------------------------------------------