├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── johnnysbug │ │ │ │ └── flutterdle │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── allowed_guesses.txt ├── answers.txt └── stats.json ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── app_theme.dart ├── domain.dart ├── game.dart ├── helpers │ └── tile_builder.dart ├── main.dart ├── services │ ├── context_service.dart │ ├── keyboard_service.dart │ ├── matching_service.dart │ ├── settings_service.dart │ ├── stats_service.dart │ ├── version_service.dart │ └── word_service.dart └── widgets │ ├── board.dart │ ├── countdown.dart │ ├── how_to.dart │ ├── keyboard.dart │ ├── settings.dart │ └── stats.dart ├── license.txt ├── privacy.md ├── pubspec.yaml ├── test └── services │ └── matching_service_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter repo-specific 26 | /bin/cache/ 27 | /bin/internal/bootstrap.bat 28 | /bin/internal/bootstrap.sh 29 | /bin/mingit/ 30 | /dev/benchmarks/mega_gallery/ 31 | /dev/bots/.recipe_deps 32 | /dev/bots/android_tools/ 33 | /dev/devicelab/ABresults*.json 34 | /dev/docs/doc/ 35 | /dev/docs/flutter.docs.zip 36 | /dev/docs/lib/ 37 | /dev/docs/pubspec.yaml 38 | /dev/integration_tests/**/xcuserdata 39 | /dev/integration_tests/**/Pods 40 | /packages/flutter/coverage/ 41 | version 42 | analysis_benchmark.json 43 | 44 | # packages file containing multi-root paths 45 | .packages.generated 46 | 47 | # Flutter/Dart/Pub related 48 | **/doc/api/ 49 | .dart_tool/ 50 | .flutter-plugins 51 | .flutter-plugins-dependencies 52 | **/generated_plugin_registrant.dart 53 | .packages 54 | .pub-cache/ 55 | .pub/ 56 | build/ 57 | flutter_*.png 58 | linked_*.ds 59 | unlinked.ds 60 | unlinked_spec.ds 61 | 62 | # Android related 63 | **/android/**/gradle-wrapper.jar 64 | .gradle/ 65 | **/android/captures/ 66 | **/android/gradlew 67 | **/android/gradlew.bat 68 | **/android/local.properties 69 | **/android/**/GeneratedPluginRegistrant.java 70 | **/android/key.properties 71 | *.jks 72 | 73 | # iOS/XCode related 74 | **/ios/**/*.mode1v3 75 | **/ios/**/*.mode2v3 76 | **/ios/**/*.moved-aside 77 | **/ios/**/*.pbxuser 78 | **/ios/**/*.perspectivev3 79 | **/ios/**/*sync/ 80 | **/ios/**/.sconsign.dblite 81 | **/ios/**/.tags* 82 | **/ios/**/.vagrant/ 83 | **/ios/**/DerivedData/ 84 | **/ios/**/Icon? 85 | **/ios/**/Pods/ 86 | **/ios/**/.symlinks/ 87 | **/ios/**/profile 88 | **/ios/**/xcuserdata 89 | **/ios/.generated/ 90 | **/ios/Flutter/.last_build_id 91 | **/ios/Flutter/App.framework 92 | **/ios/Flutter/Flutter.framework 93 | **/ios/Flutter/Flutter.podspec 94 | **/ios/Flutter/Generated.xcconfig 95 | **/ios/Flutter/ephemeral 96 | **/ios/Flutter/app.flx 97 | **/ios/Flutter/app.zip 98 | **/ios/Flutter/flutter_assets/ 99 | **/ios/Flutter/flutter_export_environment.sh 100 | **/ios/ServiceDefinitions.json 101 | **/ios/Runner/GeneratedPluginRegistrant.* 102 | 103 | Runner* 104 | 105 | # macOS 106 | **/macos/Flutter/GeneratedPluginRegistrant.swift 107 | **/macos/Flutter/ephemeral 108 | 109 | # Coverage 110 | coverage/ 111 | 112 | # Symbols 113 | app.*.symbols 114 | 115 | # Exceptions to above rules. 116 | !**/ios/**/default.mode1v3 117 | !**/ios/**/default.mode2v3 118 | !**/ios/**/default.pbxuser 119 | !**/ios/**/default.perspectivev3 120 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 121 | !/dev/ci/**/Gemfile.lock 122 | android/app/release/app-release.aab 123 | android/app/local.properties 124 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7e9793dee1b85a243edd0e06cb1658e98b077561 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutterdle 2 | 3 | I wanted to challenge myself after writing a [version of Flutterdle using F#](https://github.com/johnnysbug/fsharp-command-line-wordle), so I decided to try Flutter. This was a fun project, since Flutter allows for rapid feedback cycles during UI development. 4 | 5 | You should be able to clone this repo and run it yourself, given you have setup your Flutter development environment. Here's a link to Flutter's [online documentation](https://flutter.dev/docs) for getting started. 6 | 7 | Now on the [Apple AppStore](https://apps.apple.com/us/app/flutterdle/id1619710555) and [Google Play Store](https://play.google.com/store/apps/details?id=com.johnnysbug.flutterdle)! 8 | 9 | I tried to implement the majority of features seen in the actual [game](https://www.nytimes.com/games/wordle/index.html). Here's a list of features: 10 | 11 | - displays shake animation when a word doesn't exist in the word list or is shorter than five characters 12 | - flips the letter tiles in a staggered manner when a guess is accepted 13 | - animates winning word tiles 14 | - gives feeback phrases when winning the game 15 | - dark/light theme settings 16 | - rules screen 17 | - generates same word as original each day 18 | - updates keyboard to show used letters with appropriate colors 19 | - tracks game stats and persists them to the device (tested on iOS and Android, but may work in Chrome as well) 20 | - saves game context during play 21 | - shows a count down timer until next word generates on stats screen 22 | - includes hard mode 23 | - includes high constrast setting 24 | - includes accessibility semantics 25 | 26 |

27 | 28 | 29 | 30 |

31 | --- 32 | 33 | Here's few resources to get you started if this is your first time using Flutter: 34 | 35 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 36 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 37 | 38 | For help getting started with Flutter, view our 39 | [online documentation](https://flutter.dev/docs), which offers tutorials, 40 | samples, guidance on mobile development, and a full API reference. 41 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '5' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if (keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | compileSdkVersion flutter.compileSdkVersion 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = '1.8' 44 | } 45 | 46 | sourceSets { 47 | main.java.srcDirs += 'src/main/kotlin' 48 | } 49 | 50 | defaultConfig { 51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 52 | applicationId "com.johnnysbug.flutterdle" 53 | minSdkVersion localProperties.getProperty('flutter.minSdkVersion').toInteger() 54 | targetSdkVersion flutter.targetSdkVersion 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | multiDexEnabled true 58 | } 59 | 60 | signingConfigs { 61 | release { 62 | keyAlias keystoreProperties['keyAlias'] 63 | keyPassword keystoreProperties['keyPassword'] 64 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 65 | storePassword keystoreProperties['storePassword'] 66 | } 67 | } 68 | 69 | buildTypes { 70 | release { 71 | signingConfig signingConfigs.release 72 | } 73 | } 74 | } 75 | 76 | flutter { 77 | source '../..' 78 | } 79 | 80 | dependencies { 81 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 82 | implementation 'androidx.multidex:multidex:2.0.1' 83 | } 84 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/johnnysbug/flutterdle/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.johnnysbug.flutterdle 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.3' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "won": 0, 3 | "lost": 0, 4 | "streak": { 5 | "current": 0, 6 | "max": 0 7 | }, 8 | "guessDistribution": [ 0, 0, 0, 0, 0, 0 ], 9 | "lastGuess": 0, 10 | "lastBoard": "" 11 | } -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "20.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "filename" : "40.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "20x20" 80 | }, 81 | { 82 | "filename" : "29.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "29x29" 86 | }, 87 | { 88 | "filename" : "58.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "29x29" 92 | }, 93 | { 94 | "filename" : "40.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "40x40" 98 | }, 99 | { 100 | "filename" : "80.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "40x40" 104 | }, 105 | { 106 | "filename" : "50.png", 107 | "idiom" : "ipad", 108 | "scale" : "1x", 109 | "size" : "50x50" 110 | }, 111 | { 112 | "filename" : "100.png", 113 | "idiom" : "ipad", 114 | "scale" : "2x", 115 | "size" : "50x50" 116 | }, 117 | { 118 | "filename" : "72.png", 119 | "idiom" : "ipad", 120 | "scale" : "1x", 121 | "size" : "72x72" 122 | }, 123 | { 124 | "filename" : "144.png", 125 | "idiom" : "ipad", 126 | "scale" : "2x", 127 | "size" : "72x72" 128 | }, 129 | { 130 | "filename" : "76.png", 131 | "idiom" : "ipad", 132 | "scale" : "1x", 133 | "size" : "76x76" 134 | }, 135 | { 136 | "filename" : "152.png", 137 | "idiom" : "ipad", 138 | "scale" : "2x", 139 | "size" : "76x76" 140 | }, 141 | { 142 | "filename" : "167.png", 143 | "idiom" : "ipad", 144 | "scale" : "2x", 145 | "size" : "83.5x83.5" 146 | }, 147 | { 148 | "filename" : "1024.png", 149 | "idiom" : "ios-marketing", 150 | "scale" : "1x", 151 | "size" : "1024x1024" 152 | }, 153 | { 154 | "filename" : "48.png", 155 | "idiom" : "watch", 156 | "role" : "notificationCenter", 157 | "scale" : "2x", 158 | "size" : "24x24", 159 | "subtype" : "38mm" 160 | }, 161 | { 162 | "filename" : "55.png", 163 | "idiom" : "watch", 164 | "role" : "notificationCenter", 165 | "scale" : "2x", 166 | "size" : "27.5x27.5", 167 | "subtype" : "42mm" 168 | }, 169 | { 170 | "filename" : "58.png", 171 | "idiom" : "watch", 172 | "role" : "companionSettings", 173 | "scale" : "2x", 174 | "size" : "29x29" 175 | }, 176 | { 177 | "filename" : "87.png", 178 | "idiom" : "watch", 179 | "role" : "companionSettings", 180 | "scale" : "3x", 181 | "size" : "29x29" 182 | }, 183 | { 184 | "filename" : "66.png", 185 | "idiom" : "watch", 186 | "role" : "notificationCenter", 187 | "scale" : "2x", 188 | "size" : "33x33", 189 | "subtype" : "45mm" 190 | }, 191 | { 192 | "filename" : "80.png", 193 | "idiom" : "watch", 194 | "role" : "appLauncher", 195 | "scale" : "2x", 196 | "size" : "40x40", 197 | "subtype" : "38mm" 198 | }, 199 | { 200 | "filename" : "88.png", 201 | "idiom" : "watch", 202 | "role" : "appLauncher", 203 | "scale" : "2x", 204 | "size" : "44x44", 205 | "subtype" : "40mm" 206 | }, 207 | { 208 | "filename" : "92.png", 209 | "idiom" : "watch", 210 | "role" : "appLauncher", 211 | "scale" : "2x", 212 | "size" : "46x46", 213 | "subtype" : "41mm" 214 | }, 215 | { 216 | "filename" : "100.png", 217 | "idiom" : "watch", 218 | "role" : "appLauncher", 219 | "scale" : "2x", 220 | "size" : "50x50", 221 | "subtype" : "44mm" 222 | }, 223 | { 224 | "filename" : "102.png", 225 | "idiom" : "watch", 226 | "role" : "appLauncher", 227 | "scale" : "2x", 228 | "size" : "51x51", 229 | "subtype" : "45mm" 230 | }, 231 | { 232 | "filename" : "172.png", 233 | "idiom" : "watch", 234 | "role" : "quickLook", 235 | "scale" : "2x", 236 | "size" : "86x86", 237 | "subtype" : "38mm" 238 | }, 239 | { 240 | "filename" : "196.png", 241 | "idiom" : "watch", 242 | "role" : "quickLook", 243 | "scale" : "2x", 244 | "size" : "98x98", 245 | "subtype" : "42mm" 246 | }, 247 | { 248 | "filename" : "216.png", 249 | "idiom" : "watch", 250 | "role" : "quickLook", 251 | "scale" : "2x", 252 | "size" : "108x108", 253 | "subtype" : "44mm" 254 | }, 255 | { 256 | "filename" : "234.png", 257 | "idiom" : "watch", 258 | "role" : "quickLook", 259 | "scale" : "2x", 260 | "size" : "117x117", 261 | "subtype" : "45mm" 262 | }, 263 | { 264 | "filename" : "1024.png", 265 | "idiom" : "watch-marketing", 266 | "scale" : "1x", 267 | "size" : "1024x1024" 268 | }, 269 | { 270 | "filename" : "16.png", 271 | "idiom" : "mac", 272 | "scale" : "1x", 273 | "size" : "16x16" 274 | }, 275 | { 276 | "filename" : "32.png", 277 | "idiom" : "mac", 278 | "scale" : "2x", 279 | "size" : "16x16" 280 | }, 281 | { 282 | "filename" : "32.png", 283 | "idiom" : "mac", 284 | "scale" : "1x", 285 | "size" : "32x32" 286 | }, 287 | { 288 | "filename" : "64.png", 289 | "idiom" : "mac", 290 | "scale" : "2x", 291 | "size" : "32x32" 292 | }, 293 | { 294 | "filename" : "128.png", 295 | "idiom" : "mac", 296 | "scale" : "1x", 297 | "size" : "128x128" 298 | }, 299 | { 300 | "filename" : "256.png", 301 | "idiom" : "mac", 302 | "scale" : "2x", 303 | "size" : "128x128" 304 | }, 305 | { 306 | "filename" : "256.png", 307 | "idiom" : "mac", 308 | "scale" : "1x", 309 | "size" : "256x256" 310 | }, 311 | { 312 | "filename" : "512.png", 313 | "idiom" : "mac", 314 | "scale" : "2x", 315 | "size" : "256x256" 316 | }, 317 | { 318 | "filename" : "512.png", 319 | "idiom" : "mac", 320 | "scale" : "1x", 321 | "size" : "512x512" 322 | }, 323 | { 324 | "filename" : "1024.png", 325 | "idiom" : "mac", 326 | "scale" : "2x", 327 | "size" : "512x512" 328 | } 329 | ], 330 | "info" : { 331 | "author" : "xcode", 332 | "version" : 1 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1024-2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "1024-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "1024.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutterdle 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutterdle 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationPortraitUpsideDown 35 | 36 | UISupportedInterfaceOrientations~ipad 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationPortraitUpsideDown 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UIViewControllerBasedStatusBarAppearance 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppTheme { 4 | AppTheme._(); 5 | 6 | static final ThemeData lightTheme = ThemeData( 7 | scaffoldBackgroundColor: Colors.white, 8 | appBarTheme: const AppBarTheme( 9 | color: Colors.white, 10 | titleTextStyle: TextStyle( 11 | color: Colors.black, 12 | fontSize: 32, 13 | fontWeight: FontWeight.bold 14 | ) , 15 | iconTheme: IconThemeData( 16 | color: Colors.black, 17 | ), 18 | ), 19 | colorScheme: const ColorScheme.light( 20 | primary: Colors.white, 21 | onPrimary: Colors.black, 22 | primaryContainer: Colors.white38, 23 | secondary: Colors.black, 24 | onSecondary: Colors.white 25 | ), 26 | iconTheme: const IconThemeData( 27 | color: Colors.white54, 28 | ), 29 | textTheme: Typography.blackMountainView, 30 | textButtonTheme: TextButtonThemeData( 31 | style: ButtonStyle( 32 | foregroundColor: MaterialStateProperty.all(Colors.black), 33 | textStyle: MaterialStateProperty.all(const TextStyle(color: Colors.black)) 34 | ) 35 | ), 36 | cardColor: Colors.white 37 | ); 38 | 39 | static final ThemeData darkTheme = ThemeData( 40 | scaffoldBackgroundColor: Colors.black, 41 | appBarTheme: const AppBarTheme( 42 | color: Colors.black, 43 | titleTextStyle: TextStyle( 44 | color: Colors.white, 45 | fontSize: 32, 46 | fontWeight: FontWeight.bold 47 | ) , 48 | iconTheme: IconThemeData( 49 | color: Colors.white, 50 | ), 51 | ), 52 | colorScheme: const ColorScheme.dark( 53 | primary: Colors.black, 54 | onPrimary: Colors.white, 55 | primaryContainer: Colors.black38, 56 | secondary: Colors.white, 57 | onSecondary: Colors.black 58 | ), 59 | iconTheme: const IconThemeData( 60 | color: Colors.white54, 61 | ), 62 | textTheme: Typography.whiteMountainView, 63 | textButtonTheme: TextButtonThemeData( 64 | style: ButtonStyle( 65 | foregroundColor: MaterialStateProperty.all(Colors.white), 66 | textStyle: MaterialStateProperty.all(const TextStyle(color: Colors.white)) 67 | ) 68 | ), 69 | cardColor: Colors.black 70 | ); 71 | } -------------------------------------------------------------------------------- /lib/domain.dart: -------------------------------------------------------------------------------- 1 | enum GameColor { tbd, absent, present, correct } 2 | 3 | enum Dialog { none, help, stats, settings } 4 | 5 | enum TurnResult { unset, successful, unsuccessful, partial } 6 | enum KeyboardLayout { qwerty, dvorak } 7 | 8 | class Settings { 9 | bool isDarkMode; 10 | bool isHardMode; 11 | bool isHighContrast; 12 | KeyboardLayout keyboardLayout; 13 | 14 | Settings(this.isDarkMode, this.isHardMode, this.isHighContrast, this.keyboardLayout); 15 | 16 | Settings.fromJson(Map json) 17 | : isDarkMode = json['isDarkMode'], 18 | isHardMode = json['isHardMode'], 19 | isHighContrast = json['isHighConstrat'] ?? false, 20 | keyboardLayout = 21 | KeyboardLayout.values.byName(json['keyboardLayout'] ?? KeyboardLayout.qwerty.name); 22 | 23 | Map toJson() { 24 | final Map data = {}; 25 | data['isDarkMode'] = isDarkMode; 26 | data['isHardMode'] = isHardMode; 27 | data['isHighContrast'] = isHighContrast; 28 | data['keyboardLayout'] = keyboardLayout.name; 29 | 30 | return data; 31 | } 32 | } 33 | 34 | class Letter { 35 | int index; 36 | String value; 37 | GameColor color; 38 | bool isKey; 39 | 40 | static const empty = ''; 41 | 42 | Letter( 43 | {this.index = 0, this.value = Letter.empty, this.color = GameColor.tbd, this.isKey = false}); 44 | 45 | String get semanticsLabel { 46 | if (isKey) { 47 | return value.length > 1 48 | ? value == 'ENTER' 49 | ? 'Enter.' 50 | : 'Backspace.' 51 | : '${value.toUpperCase()}. key ${_match()}.'; 52 | } 53 | return value == Letter.empty 54 | ? '' 55 | : color == GameColor.tbd 56 | ? '${value.toUpperCase()}.' 57 | : '${value.toUpperCase()}. ${_match()}'; 58 | } 59 | 60 | set semanticsLabel(String value) {} 61 | 62 | String _match() { 63 | switch (color) { 64 | case GameColor.tbd: 65 | return 'is T.B.D.'; 66 | case GameColor.absent: 67 | return 'is absent.'; 68 | case GameColor.present: 69 | return 'is present.'; 70 | case GameColor.correct: 71 | return 'is correct.'; 72 | } 73 | } 74 | 75 | static String upgradeEnum(String oldName) { 76 | switch (oldName) { 77 | case 'unset': 78 | return GameColor.tbd.name; 79 | case 'none': 80 | return GameColor.absent.name; 81 | case 'partial': 82 | return GameColor.present.name; 83 | case 'exact': 84 | return GameColor.correct.name; 85 | default: 86 | return oldName; 87 | } 88 | } 89 | 90 | Letter.fromJson(Map json) 91 | : index = json['index'], 92 | value = json['value'], 93 | color = GameColor.values.byName(upgradeEnum(json['color'])), 94 | isKey = json['isKey'] ?? false; 95 | 96 | Map toJson() { 97 | final Map data = {}; 98 | data['index'] = index; 99 | data['value'] = value; 100 | data['color'] = color.name; 101 | data['isKey'] = isKey; 102 | return data; 103 | } 104 | } 105 | 106 | class Board { 107 | List tiles; 108 | 109 | Board(this.tiles); 110 | 111 | factory Board.fromJson(Map json) { 112 | var tiles = []; 113 | json['tiles'].forEach((tile) { 114 | tiles.add(Letter.fromJson(tile)); 115 | }); 116 | return Board(tiles); 117 | } 118 | 119 | Map toJson() { 120 | final Map data = {}; 121 | data['tiles'] = tiles; 122 | return data; 123 | } 124 | } 125 | 126 | class Context { 127 | Board board; 128 | List> keys; 129 | String answer; 130 | String guess; 131 | List attempt; 132 | TurnResult turnResult; 133 | int remainingTries; 134 | String message; 135 | int currentIndex; 136 | DateTime? lastPlayed; 137 | 138 | Context(this.board, this.keys, this.answer, this.guess, this.attempt, this.turnResult, 139 | this.remainingTries, this.message, this.currentIndex, this.lastPlayed); 140 | 141 | factory Context.fromJson(Map json) { 142 | var isPrevious = json['board'] is List; 143 | Board board; 144 | if (isPrevious) { 145 | var tiles = []; 146 | json['board'].forEach((letter) { 147 | tiles.add(Letter.fromJson(letter)); 148 | }); 149 | board = Board(tiles); 150 | } else { 151 | board = Board.fromJson(json['board']); 152 | } 153 | var keys = >[]; 154 | if (json['keys'] != null) { 155 | json['keys'].forEach((row) { 156 | var rowKeys = []; 157 | row.forEach((key) { 158 | var letter = Letter.fromJson(key); 159 | letter.isKey = true; 160 | rowKeys.add(letter); 161 | }); 162 | keys.add(rowKeys); 163 | }); 164 | } 165 | String answer = json['answer']; 166 | String guess = json['guess']; 167 | var attempt = []; 168 | json['attempt'].forEach((letter) { 169 | attempt.add(Letter.fromJson(letter)); 170 | }); 171 | TurnResult turnResult = TurnResult.values.byName(json['turnResult']); 172 | int remainingTries = json['remainingTries']; 173 | String message = json['message']; 174 | int currentIndex = json['currentIndex']; 175 | DateTime? lastPlayed = json['lastPlayed'] != null ? DateTime.parse(json['lastPlayed']) : null; 176 | 177 | return Context(board, keys, answer, guess, attempt, turnResult, remainingTries, message, 178 | currentIndex, lastPlayed); 179 | } 180 | 181 | Map toJson() { 182 | final Map data = {}; 183 | data['board'] = board; 184 | data['keys'] = keys; 185 | data['answer'] = answer; 186 | data['guess'] = guess; 187 | data['attempt'] = attempt; 188 | data['turnResult'] = turnResult.name; 189 | data['remainingTries'] = remainingTries; 190 | data['message'] = message; 191 | data['currentIndex'] = currentIndex; 192 | data['lastPlayed'] = (lastPlayed ?? DateTime.now()).toIso8601String(); 193 | return data; 194 | } 195 | } 196 | 197 | class Stats { 198 | int won; 199 | int lost; 200 | Streak streak; 201 | List guessDistribution; 202 | int lastGuess; 203 | String lastBoard; 204 | int gameNumber; 205 | 206 | Stats(this.won, this.lost, this.streak, this.guessDistribution, this.lastGuess, this.lastBoard, 207 | this.gameNumber); 208 | 209 | Stats.fromJson(Map json) 210 | : won = json['won'], 211 | lost = json['lost'], 212 | streak = json['streak'] = Streak.fromJson(json['streak']), 213 | guessDistribution = json['guessDistribution'].cast(), 214 | lastGuess = json['lastGuess'], 215 | lastBoard = json['lastBoard'] ?? '', 216 | gameNumber = json['gameNumber'] ?? 0; 217 | 218 | int get played => guessDistribution.reduce((value, g) => value + g) + lost; 219 | 220 | int get percentWon => played == 0 ? 0 : (won / played * 100).round(); 221 | 222 | Map toJson() { 223 | final Map data = {}; 224 | data['won'] = won; 225 | data['lost'] = lost; 226 | data['streak'] = streak.toJson(); 227 | data['guessDistribution'] = guessDistribution; 228 | data['lastGuess'] = lastGuess; 229 | data['lastBoard'] = lastBoard; 230 | data['gameNumber'] = gameNumber; 231 | return data; 232 | } 233 | } 234 | 235 | class Streak { 236 | int current; 237 | int max; 238 | 239 | Streak(this.current, this.max); 240 | 241 | Streak.fromJson(Map json) 242 | : current = json['current'], 243 | max = json['max']; 244 | 245 | Map toJson() { 246 | final Map data = {}; 247 | data['current'] = current; 248 | data['max'] = max; 249 | return data; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /lib/game.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/semantics.dart'; 3 | import 'package:flutter_animator/flutter_animator.dart'; 4 | import 'package:flutterdle/domain.dart'; 5 | import 'package:flutterdle/services/context_service.dart'; 6 | import 'package:flutterdle/services/keyboard_service.dart'; 7 | import 'package:flutterdle/services/matching_service.dart'; 8 | import 'package:flutterdle/services/settings_service.dart'; 9 | import 'package:flutterdle/services/stats_service.dart'; 10 | import 'package:flutterdle/services/version_service.dart'; 11 | import 'package:flutterdle/services/word_service.dart'; 12 | import 'package:package_info_plus/package_info_plus.dart'; 13 | 14 | class Flutterdle { 15 | static const boardSize = 30; 16 | static const rowLength = 5; 17 | static const totalTries = 6; 18 | static const cellMargin = 8; 19 | 20 | static final StatsService _statsService = StatsService(); 21 | 22 | final _baseDate = DateTime(2021, DateTime.june, 19); 23 | 24 | late Context _context; 25 | late Stats _stats; 26 | late Settings _settings; 27 | late PackageInfo _packageInfo; 28 | late final List> _shakeKeys = []; 29 | late final List> _bounceKeys = []; 30 | 31 | final _wordService = WordService(); 32 | 33 | bool isEvaluating = false; 34 | 35 | Stats get stats => _stats; 36 | Settings get settings => _settings; 37 | PackageInfo get packageInfo => _packageInfo; 38 | int get gameNumber => DateTime.now().difference(_baseDate).inDays; 39 | 40 | void updateBoard(List attempt) { 41 | for (var i = 0; i < attempt.length; i++) { 42 | var offset = i + ((totalTries - _context.remainingTries) * rowLength); 43 | _context.board.tiles[offset] = attempt[i]; 44 | } 45 | } 46 | 47 | List> _updateKeys(List> keys, List guess) { 48 | for (var i = 0; i < guess.length; i++) { 49 | for (var x = 0; x < keys.length; x++) { 50 | for (var y = 0; y < keys[x].length; y++) { 51 | if (keys[x][y].value == guess[i].value && keys[x][y].color.index < guess[i].color.index) { 52 | keys[x][y] = Letter(value: guess[i].value, color: guess[i].color, isKey: true); 53 | } 54 | } 55 | } 56 | } 57 | return keys; 58 | } 59 | 60 | bool _isToday(DateTime? dateTime) { 61 | if (dateTime == null) { 62 | return false; 63 | } 64 | var today = DateTime.now(); 65 | return dateTime.year == today.year && 66 | dateTime.month == today.month && 67 | dateTime.day == today.day; 68 | } 69 | 70 | Future init() async { 71 | for (var i = 0; i < totalTries; i++) { 72 | _shakeKeys.add(GlobalKey()); 73 | } 74 | for (var i = 0; i < boardSize; i++) { 75 | _bounceKeys.add(GlobalKey()); 76 | } 77 | 78 | var context = await ContextService().loadContext(); 79 | _stats = await _statsService.loadStats(); 80 | _settings = await SettingsService().load(); 81 | _packageInfo = await VersionService().loadVersion(); 82 | 83 | await _wordService.init(); 84 | if (context == null) { 85 | _initContext(); 86 | } else { 87 | if (_isToday(context.lastPlayed)) { 88 | _context = context; 89 | } else { 90 | _initContext(); 91 | } 92 | } 93 | return true; 94 | } 95 | 96 | void _initContext() { 97 | var board = Board(List.filled(boardSize, Letter(), growable: false)); 98 | _context = Context(board, KeyboardService.init(keyboardLayout: _settings.keyboardLayout).keys, 99 | '', '', [], TurnResult.unset, totalTries, 'Good Luck!', 0, DateTime.now()); 100 | SemanticsService.announce(_context.message, TextDirection.ltr); 101 | _context.answer = _wordService.getWordOfTheDay(_baseDate); 102 | } 103 | 104 | bool didWin(List attempt) => 105 | attempt.isNotEmpty && attempt.every((l) => l.color == GameColor.correct); 106 | 107 | String _winningMessage(int remainingTries) { 108 | switch (remainingTries) { 109 | case 5: 110 | return 'Amazing!'; 111 | case 4: 112 | return 'Fantastic!'; 113 | case 3: 114 | return 'Great!'; 115 | case 2: 116 | return 'Not bad'; 117 | case 1: 118 | return 'Cutting it close!'; 119 | default: 120 | return 'Phew!'; 121 | } 122 | } 123 | 124 | String _getTileBlock(GameColor color) { 125 | switch (color) { 126 | case GameColor.tbd: 127 | case GameColor.absent: 128 | return '⬛️'; 129 | case GameColor.present: 130 | return _settings.isHighContrast ? '🟧' : '🟨'; 131 | case GameColor.correct: 132 | return _settings.isHighContrast ? '🟦' : '🟩'; 133 | } 134 | } 135 | 136 | String _getShareableBoard(int index) { 137 | String board = ''; 138 | int endingIndex = ((totalTries - _context.remainingTries) * rowLength) + rowLength; 139 | for (int i = 0; i < endingIndex; i++) { 140 | if (i > 0 && i % rowLength == 0) { 141 | board += '\n'; 142 | } 143 | board += _getTileBlock(_context.board.tiles[i].color); 144 | } 145 | return board; 146 | } 147 | 148 | Future _updateStats(bool won, int remainingTries) async { 149 | return await _statsService.updateStats( 150 | _stats, won, (remainingTries - totalTries).abs(), _getShareableBoard, gameNumber); 151 | } 152 | 153 | void evaluateTurn(String letter) { 154 | isEvaluating = true; 155 | _context.turnResult = TurnResult.partial; 156 | if (KeyboardService.isEnter(letter)) { 157 | if (!_wordService.isLongEnough(_context.guess)) { 158 | _context.message = 'Not enough letters'; 159 | SemanticsService.announce(_context.message, TextDirection.ltr); 160 | _context.turnResult = TurnResult.unsuccessful; 161 | } else if (_wordService.isValidGuess(_context.guess)) { 162 | _context.attempt = 163 | MatchingService.matches(_context.guess.toLowerCase(), _context.answer).toList(); 164 | if (_settings.isHardMode) { 165 | var unusedLetter = _checkHardMode(); 166 | if (unusedLetter.isNotEmpty) { 167 | _context.message = 'Guess must contain $unusedLetter'; 168 | SemanticsService.announce(_context.message, TextDirection.ltr); 169 | _context.turnResult = TurnResult.unsuccessful; 170 | } else { 171 | SemanticsService.announce( 172 | 'Your guess ${_context.guess} was accepted', TextDirection.ltr); 173 | _context.turnResult = TurnResult.successful; 174 | } 175 | } else { 176 | SemanticsService.announce('Your guess ${_context.guess} was accepted', TextDirection.ltr); 177 | _context.turnResult = TurnResult.successful; 178 | } 179 | } else { 180 | _context.message = 'Not in Word list'; 181 | SemanticsService.announce('${_context.guess} was ${_context.message}', TextDirection.ltr); 182 | _context.turnResult = TurnResult.unsuccessful; 183 | } 184 | } else if (KeyboardService.isBackspace(letter)) { 185 | if (_context.guess.isNotEmpty) { 186 | var letterToRemove = _context.guess.characters.last; 187 | var guess = _context.guess.substring(0, _context.guess.length - 1).padRight(rowLength); 188 | var buffer = guess.split('').map((l) => Letter(value: l)); 189 | updateBoard(buffer.toList()); 190 | _context.guess = guess.replaceAll(' ', ''); 191 | _context.currentIndex -= 1; 192 | SemanticsService.announce('$letterToRemove removed', TextDirection.ltr); 193 | } 194 | } else { 195 | if (_context.guess.length < rowLength) { 196 | _context.guess = _context.guess + letter; 197 | _context.board.tiles[_context.currentIndex++] = Letter(value: letter); 198 | SemanticsService.announce('$letter added to board', TextDirection.ltr); 199 | } 200 | } 201 | } 202 | 203 | Context get context => _context; 204 | List> get shakeKeys => _shakeKeys; 205 | List> get bounceKeys => _bounceKeys; 206 | 207 | Future updateAfterSuccessfulGuess() async { 208 | if (_context.turnResult == TurnResult.successful) { 209 | _context.keys = _updateKeys(_context.keys, _context.attempt); 210 | var won = didWin(_context.attempt); 211 | if (won || _context.remainingTries == 1) { 212 | _stats = await _updateStats(won, _context.remainingTries); 213 | } 214 | var remaining = _context.remainingTries - 1; 215 | _context.guess = ''; 216 | _context.attempt = []; 217 | _context.remainingTries = won ? 0 : remaining; 218 | _context.message = won 219 | ? _winningMessage(remaining) 220 | : remaining == 0 221 | ? _context.answer 222 | : ''; 223 | if (_context.message.isNotEmpty) { 224 | SemanticsService.announce(_context.message, TextDirection.ltr); 225 | } 226 | persist(); 227 | } 228 | } 229 | 230 | void persist() { 231 | Future.delayed(Duration.zero, () async { 232 | await ContextService().saveContext(_context); 233 | }); 234 | } 235 | 236 | String _checkHardMode() { 237 | var previousMatches = _context.board.tiles 238 | .where((l) => l.color == GameColor.correct || l.color == GameColor.present) 239 | .map((l) => l.value); 240 | var currentMatches = _context.attempt 241 | .where((l) => l.color == GameColor.correct || l.color == GameColor.present) 242 | .map((l) => l.value); 243 | 244 | if (previousMatches.isNotEmpty) { 245 | for (var i = 0; i < previousMatches.length; i++) { 246 | if (!currentMatches.contains(previousMatches.elementAt(i))) { 247 | return previousMatches.elementAt(i); 248 | } 249 | } 250 | } 251 | return ''; 252 | } 253 | 254 | void updateKeyboardLayout() { 255 | var keys = KeyboardService.init(keyboardLayout: _settings.keyboardLayout).keys; 256 | for (var row in _context.keys) { 257 | for (var key in row) { 258 | keyLoop: 259 | for (var x = 0; x < keys.length; x++) { 260 | for (var y = 0; y < keys[x].length; y++) { 261 | if (keys[x][y].value == key.value) { 262 | keys[x][y].color = key.color; 263 | break keyLoop; 264 | } 265 | } 266 | } 267 | } 268 | } 269 | context.keys = keys; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /lib/helpers/tile_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutterdle/domain.dart'; 3 | 4 | class TileBuilder { 5 | 6 | static Color _toColor(GameColor color, Settings settings) { 7 | switch (color) { 8 | case GameColor.correct: 9 | return settings.isHighContrast ? Colors.orange : Colors.green; 10 | case GameColor.present: 11 | return settings.isHighContrast ? Colors.blue : const Color.fromARGB(255, 207, 187, 98); 12 | case GameColor.absent: 13 | return const Color.fromARGB(255, 90, 87, 87); 14 | case GameColor.tbd: 15 | return Colors.transparent; 16 | } 17 | } 18 | 19 | static Widget build(Letter letter, Settings settings) { 20 | return Padding( 21 | key: ValueKey(letter.color == GameColor.tbd), 22 | padding: const EdgeInsets.all(2.0), 23 | child: AspectRatio( 24 | aspectRatio: 1, 25 | child: Container( 26 | decoration: BoxDecoration( 27 | border: Border.all( 28 | width: 2, 29 | color: Colors.grey.shade800, 30 | ), 31 | borderRadius: const BorderRadius.all(Radius.circular(8)), 32 | color: _toColor(letter.color, settings)), 33 | child: FittedBox( 34 | fit: BoxFit.contain, 35 | child: Semantics( 36 | label: letter.semanticsLabel, 37 | child: ExcludeSemantics( 38 | excluding: true, 39 | child: Text( 40 | letter.value, 41 | textAlign: TextAlign.center, 42 | style: TextStyle( 43 | color: (letter.color != GameColor.tbd) ? Colors.white : null, 44 | fontWeight: FontWeight.bold 45 | ), 46 | ), 47 | ), 48 | )), 49 | ), 50 | ), 51 | ); 52 | } 53 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/semantics.dart'; 5 | import 'package:flutterdle/app_theme.dart'; 6 | import 'package:flutterdle/domain.dart'; 7 | import 'package:flutterdle/domain.dart' as domain; 8 | import 'package:flutterdle/game.dart'; 9 | import 'package:flutterdle/widgets/board.dart'; 10 | import 'package:flutterdle/widgets/how_to.dart'; 11 | import 'package:flutterdle/widgets/keyboard.dart'; 12 | import 'package:flutterdle/widgets/settings.dart'; 13 | import 'package:flutterdle/widgets/stats.dart'; 14 | 15 | void main() { 16 | runApp(const MyApp()); 17 | } 18 | 19 | class MyApp extends StatefulWidget { 20 | const MyApp({Key? key}) : super(key: key); 21 | 22 | @override 23 | _MyAppState createState() => _MyAppState(); 24 | } 25 | 26 | class _MyAppState extends State { 27 | final StreamController _streamController = StreamController.broadcast(); 28 | ThemeMode _appTheme = ThemeMode.dark; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | 34 | _streamController.stream.listen((settings) { 35 | setState(() { 36 | _appTheme = settings.isDarkMode ? ThemeMode.dark : ThemeMode.light; 37 | }); 38 | }); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return MaterialApp( 44 | title: 'Flutterdle', 45 | debugShowCheckedModeBanner: false, 46 | showSemanticsDebugger: false, 47 | theme: AppTheme.lightTheme, 48 | themeMode: _appTheme, 49 | darkTheme: AppTheme.darkTheme, 50 | home: MyHomePage(_appTheme, _streamController, title: 'Flutterdle'), 51 | ); 52 | } 53 | } 54 | 55 | class MyHomePage extends StatefulWidget { 56 | const MyHomePage(this.themeMode, this.streamController, {Key? key, required this.title}) 57 | : super(key: key); 58 | final ThemeMode themeMode; 59 | final StreamController streamController; 60 | final String title; 61 | 62 | @override 63 | State createState() => _MyHomePageState(); 64 | } 65 | 66 | class _MyHomePageState extends State { 67 | final Flutterdle _game = Flutterdle(); 68 | Future _initialized = Future.value(false); 69 | 70 | domain.Dialog _currentDialog = domain.Dialog.none; 71 | 72 | @override 73 | void initState() { 74 | super.initState(); 75 | 76 | widget.streamController.stream.listen((settings) { 77 | if (_game.settings.isDarkMode != settings.isDarkMode) { 78 | _game.settings.isDarkMode = settings.isDarkMode; 79 | _game.persist(); 80 | } else { 81 | _game.updateKeyboardLayout(); 82 | _game.persist(); 83 | } 84 | }); 85 | 86 | _initialized = _game.init().then((value) { 87 | widget.streamController.add(_game.settings); 88 | return value; 89 | }); 90 | } 91 | 92 | void _onKeyPressed(String val) { 93 | if (_game.context.remainingTries == 0 || _game.isEvaluating) { 94 | return; 95 | } 96 | setState(() { 97 | _game.evaluateTurn(val); 98 | if (_game.context.turnResult == TurnResult.unsuccessful) { 99 | var index = (_game.context.remainingTries - Flutterdle.totalTries).abs(); 100 | _game.shakeKeys[index].currentState?.forward(); 101 | _game.isEvaluating = false; 102 | } else if (_game.context.turnResult == TurnResult.successful) { 103 | for (var i = 0; i < _game.context.attempt.length; i++) { 104 | var offset = 105 | i + ((Flutterdle.totalTries - _game.context.remainingTries) * Flutterdle.rowLength); 106 | Timer(Duration(milliseconds: (i * 200)), () { 107 | setState(() { 108 | _game.context.board.tiles[offset] = _game.context.attempt[i]; 109 | }); 110 | }); 111 | } 112 | var didWin = _game.didWin(_game.context.attempt); 113 | var delay = didWin ? 4 : 2; 114 | if (didWin) { 115 | Timer(const Duration(seconds: 2), () { 116 | for (var i = 0; i < _game.context.attempt.length; i++) { 117 | var offset = i + 118 | ((Flutterdle.totalTries - _game.context.remainingTries) * Flutterdle.rowLength); 119 | Timer(Duration(milliseconds: (i * 200)), () { 120 | setState(() { 121 | _game.bounceKeys[offset].currentState?.forward(); 122 | }); 123 | }); 124 | } 125 | }); 126 | } 127 | Timer(Duration(seconds: delay), () { 128 | setState(() { 129 | _game.updateAfterSuccessfulGuess().then((_) => setState(() {})); 130 | _resetMessage(); 131 | _game.isEvaluating = false; 132 | }); 133 | }); 134 | } else { 135 | _game.isEvaluating = false; 136 | } 137 | }); 138 | _resetMessage(); 139 | } 140 | 141 | void _newGame() { 142 | setState(() { 143 | _game.init(); 144 | }); 145 | _resetMessage(); 146 | } 147 | 148 | void _resetMessage() { 149 | if (_game.context.message.isNotEmpty) { 150 | var duration = const Duration(seconds: 2); 151 | Timer(duration, (() { 152 | setState(() { 153 | _game.context.message = ''; 154 | }); 155 | if (_game.context.remainingTries == 0) { 156 | Timer(const Duration(milliseconds: 500), (() { 157 | _setDialog(domain.Dialog.stats); 158 | })); 159 | } 160 | })); 161 | } 162 | } 163 | 164 | @override 165 | Widget build(BuildContext context) { 166 | return FutureBuilder( 167 | future: _initialized, 168 | builder: (BuildContext context, AsyncSnapshot snapshot) { 169 | List children = []; 170 | if (snapshot.hasData) { 171 | _resetMessage(); 172 | children = [ 173 | Scaffold( 174 | appBar: AppBar( 175 | leading: Padding( 176 | padding: const EdgeInsets.only(left: 16, right: 20.0), 177 | child: GestureDetector( 178 | onTap: () => {_setDialog(domain.Dialog.help)}, 179 | child: Semantics( 180 | label: 'tap to open Help', 181 | child: const Icon( 182 | Icons.help_outline, 183 | size: 26.0, 184 | ), 185 | ), 186 | )), 187 | title: Text(widget.title), 188 | centerTitle: true, 189 | actions: [ 190 | Padding( 191 | padding: const EdgeInsets.only(left: 16, right: 16.0), 192 | child: GestureDetector( 193 | onTap: () => {_setDialog(domain.Dialog.stats)}, 194 | child: Semantics( 195 | label: 'tap to open Stats', 196 | child: const Icon( 197 | Icons.leaderboard, 198 | size: 26.0, 199 | ), 200 | ), 201 | )), 202 | Padding( 203 | padding: const EdgeInsets.only(right: 20.0), 204 | child: GestureDetector( 205 | onTap: () => {_setDialog(domain.Dialog.settings)}, 206 | child: Semantics( 207 | label: 'tap to open Settings', 208 | child: const Icon( 209 | Icons.settings, 210 | size: 26.0, 211 | ), 212 | ), 213 | )), 214 | ], 215 | ), 216 | body: LayoutBuilder( 217 | builder: (context, boxConstraints) { 218 | return Stack(children: [ 219 | SizedBox.expand( 220 | child: FittedBox( 221 | fit: BoxFit.contain, 222 | child: SizedBox( 223 | width: 400, 224 | height: 670, 225 | child: Stack(children: [ 226 | Positioned( 227 | top: 25, 228 | left: 25, 229 | child: BoardWidget(_game, Flutterdle.rowLength, 230 | _game.shakeKeys, _game.bounceKeys, _game.settings)), 231 | Positioned( 232 | top: 470, 233 | left: 0, 234 | child: Keyboard( 235 | _game.context.keys, _game.settings, _onKeyPressed)), 236 | if (_currentDialog == domain.Dialog.stats) ...[ 237 | Positioned( 238 | top: 50, 239 | left: 0, 240 | child: StatsWidget( 241 | _game.stats, _game.settings, _setDialog, _newGame)) 242 | ], 243 | if (_currentDialog == domain.Dialog.settings) ...[ 244 | Positioned( 245 | top: 50, 246 | left: 0, 247 | child: SettingsWidget( 248 | _setDialog, widget.streamController, _game.settings, _game.packageInfo)) 249 | ] 250 | ])))), 251 | ]); 252 | } 253 | )), 254 | if (_currentDialog == domain.Dialog.help) ...[ 255 | SafeArea(child: HowTo(_setDialog, _game.settings)) 256 | ] 257 | ]; 258 | } else { 259 | children = [ 260 | Center( 261 | child: CircularProgressIndicator( 262 | color: Theme.of(context).colorScheme.secondary, 263 | ), 264 | ) 265 | ]; 266 | } 267 | return Stack(children: children); 268 | }); 269 | } 270 | 271 | _setDialog(domain.Dialog dialog, {bool show = true}) { 272 | setState(() { 273 | _currentDialog = show ? dialog : domain.Dialog.none; 274 | SemanticsService.announce( 275 | '${show ? 'Showing' : 'Closing'} ${_currentDialog.name}', TextDirection.ltr); 276 | }); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /lib/services/context_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutterdle/domain.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | class ContextService { 8 | Future loadContext() async { 9 | final directory = await getApplicationDocumentsDirectory(); 10 | final exists = await File("${directory.path}/context.json").exists(); 11 | final String? jsonString; 12 | if (exists) { 13 | jsonString = await File("${directory.path}/context.json").readAsString(); 14 | return Context.fromJson(json.decode(jsonString)); 15 | } else { 16 | return null; 17 | } 18 | } 19 | 20 | Future saveContext(Context context) async { 21 | final directory = await getApplicationDocumentsDirectory(); 22 | await File("${directory.path}/context.json").writeAsString(json.encode(context.toJson())); 23 | } 24 | } -------------------------------------------------------------------------------- /lib/services/keyboard_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterdle/domain.dart'; 2 | 3 | class KeyboardService { 4 | final List> _keys; 5 | 6 | List> get keys => _keys; 7 | 8 | KeyboardService._(this._keys); 9 | 10 | static Letter _toLetter(String letter) { 11 | return Letter(value: letter, isKey: true); 12 | } 13 | 14 | static KeyboardService init({ KeyboardLayout keyboardLayout = KeyboardLayout.qwerty }) { 15 | return KeyboardService._(keyboardLayout == KeyboardLayout.qwerty 16 | ? >[ 17 | ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'].map((l) => _toLetter(l)).toList(), 18 | ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'].map((l) => _toLetter(l)).toList(), 19 | ['ENTER', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', 'BACK'].map((l) => _toLetter(l)).toList() 20 | ] 21 | : >[ 22 | ['ENTER', 'P', 'Y', 'F', 'G', 'C', 'R', 'L', 'BACK'].map((l) => _toLetter(l)).toList(), 23 | ['A', 'O', 'E', 'U', 'I', 'D', 'H', 'T', 'N', 'S'].map((l) => _toLetter(l)).toList(), 24 | ['Q', 'J', 'K', 'X', 'B', 'M', 'W', 'V', 'Z'].map((l) => _toLetter(l)).toList() 25 | ]); 26 | } 27 | 28 | static bool isEnter(String letter) => letter == 'ENTER'; 29 | static bool isBackspace(String letter) => letter == 'BACK'; 30 | } 31 | -------------------------------------------------------------------------------- /lib/services/matching_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterdle/domain.dart'; 2 | 3 | class MatchingService { 4 | static Iterable _convert(String word) { 5 | return word.split('').asMap().entries.map((e) => Letter(index: e.key, value: e.value, color: GameColor.absent)); 6 | } 7 | 8 | static Iterable matches(String guess, String answer) { 9 | var cg = _convert(guess); 10 | var ca = _convert(answer); 11 | 12 | var greens = cg 13 | .where((l) => guess[l.index] == answer[l.index]) 14 | .map((l) => Letter(index: l.index, value: l.value, color: GameColor.correct)); 15 | 16 | var glg = cg.where((g) => !greens.any((l) => l.index == g.index)); 17 | var alg = ca.where((a) => !greens.any((l) => l.index == a.index)); 18 | var results = _notGreens(glg, alg, greens).toList(); 19 | results.sort((a, b) => a.index.compareTo(b.index)); 20 | return results.map((l) => Letter(index: l.index, value: l.value.toUpperCase(), color: l.color)).toList(); 21 | } 22 | 23 | static Iterable _notGreens( 24 | Iterable guess, Iterable answer, Iterable results) { 25 | var guessList = guess.toList(); 26 | var answerList = answer.toList(); 27 | var resultList = results.toList(); 28 | 29 | if (guessList.isEmpty) { 30 | return resultList; 31 | } else { 32 | var letter = guessList.removeAt(0); 33 | var i = answerList.indexWhere((l) => l.value == letter.value); 34 | if (i != -1) { 35 | answerList.removeAt(i); 36 | letter.color = GameColor.present; 37 | resultList.add(letter); 38 | } else { 39 | resultList.add(letter); 40 | } 41 | return _notGreens(guessList, answerList, resultList); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/services/settings_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutterdle/domain.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | class SettingsService { 8 | Future load() async { 9 | final directory = await getApplicationDocumentsDirectory(); 10 | final exists = await File("${directory.path}/settings.json").exists(); 11 | final jsonString = exists ? await File("${directory.path}/settings.json").readAsString() : ''; 12 | 13 | if (jsonString.isEmpty) { 14 | return Settings(true, false, false, KeyboardLayout.qwerty); 15 | } 16 | final map = json.decode(jsonString); 17 | return Settings.fromJson(map); 18 | } 19 | 20 | Future save(Settings settings) async { 21 | final directory = await getApplicationDocumentsDirectory(); 22 | await File("${directory.path}/settings.json").writeAsString(json.encode(settings.toJson())); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/services/stats_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutterdle/domain.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | 9 | class StatsService { 10 | Future _readAsset(String fileName) async { 11 | WidgetsFlutterBinding.ensureInitialized(); 12 | return await rootBundle.loadString(fileName); 13 | } 14 | 15 | Future loadStats() async { 16 | final directory = await getApplicationDocumentsDirectory(); 17 | final exists = await File("${directory.path}/stats.json").exists(); 18 | final jsonString = exists 19 | ? await File("${directory.path}/stats.json").readAsString() 20 | : await _readAsset('assets/stats.json'); 21 | 22 | final map = json.decode(jsonString); 23 | return Stats.fromJson(map); 24 | } 25 | 26 | Future updateStats( 27 | Stats stats, bool won, int index, String Function(int n) getSharable, int gameNumber) async { 28 | if (won) { 29 | stats.guessDistribution[index] += 1; 30 | stats.lastGuess = index + 1; 31 | stats.won += 1; 32 | stats.streak.current += 1; 33 | if (stats.streak.current > stats.streak.max) { 34 | stats.streak.max = stats.streak.current; 35 | } 36 | } else { 37 | stats.lost += 1; 38 | stats.streak.current = 0; 39 | stats.lastGuess = -1; 40 | } 41 | stats.lastBoard = getSharable(index); 42 | stats.gameNumber = gameNumber; 43 | 44 | await saveStats(stats); 45 | return stats; 46 | } 47 | 48 | Future saveStats(Stats stats) async { 49 | final directory = await getApplicationDocumentsDirectory(); 50 | await File("${directory.path}/stats.json").writeAsString(json.encode(stats.toJson())); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/services/version_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:package_info_plus/package_info_plus.dart'; 2 | 3 | class VersionService { 4 | Future loadVersion() async => await PackageInfo.fromPlatform(); 5 | } -------------------------------------------------------------------------------- /lib/services/word_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart' show rootBundle; 2 | import 'package:flutter/widgets.dart' show WidgetsFlutterBinding; 3 | import 'dart:convert'; 4 | import 'dart:core'; 5 | 6 | class WordService { 7 | late List _answers; 8 | late List _guesses; 9 | 10 | Future> _readFile(String fileName) async { 11 | WidgetsFlutterBinding.ensureInitialized(); 12 | final text = await rootBundle.loadString(fileName); 13 | var ls = const LineSplitter(); 14 | return ls.convert(text); 15 | } 16 | 17 | List get answers => _answers; 18 | List get guesses => _guesses; 19 | Iterable get _combined => _answers.followedBy(_guesses); 20 | 21 | Future init() async { 22 | _answers = await _readFile('assets/answers.txt'); 23 | _guesses = await _readFile('assets/allowed_guesses.txt'); 24 | } 25 | 26 | String getWordOfTheDay(DateTime baseDate) { 27 | final today = DateTime.now(); 28 | final todayDate = DateTime(today.year, today.month, today.day); 29 | final millisecondsDiff = (todayDate.difference(baseDate).inMilliseconds); 30 | final daysDiff = (millisecondsDiff / 864e5).round(); 31 | final index = daysDiff % _answers.length; 32 | return answers[index]; 33 | } 34 | 35 | bool isValidGuess(String guess) { 36 | return _combined.contains(guess.toLowerCase()); 37 | } 38 | 39 | bool isLongEnough(String guess) { 40 | return guess.length == 5; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/widgets/board.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_animator/flutter_animator.dart'; 5 | import 'package:flutterdle/domain.dart'; 6 | import 'package:flutterdle/game.dart'; 7 | import 'package:flutterdle/helpers/tile_builder.dart'; 8 | 9 | class BoardWidget extends StatelessWidget { 10 | final List> _shakeKeys; 11 | final List> _bounceKeys; 12 | final Settings _settings; 13 | 14 | final Flutterdle _game; 15 | final int _rowLength; 16 | 17 | const BoardWidget(this._game, this._rowLength, this._shakeKeys, this._bounceKeys, this._settings, 18 | {Key? key}) 19 | : super(key: key); 20 | 21 | List _buildRows() { 22 | final rows = []; 23 | var tiles = _game.context.board.tiles; 24 | 25 | var i = 0; 26 | for (var x = 0; x < tiles.length / _rowLength; x++) { 27 | final cells = []; 28 | for (var y = 0; y < _rowLength; y++) { 29 | cells.add(Flexible( 30 | child: Bounce( 31 | key: _bounceKeys[i], 32 | preferences: const AnimationPreferences(autoPlay: AnimationPlayStates.None), 33 | child: _buildFlipAnimation(tiles[i]), 34 | ), 35 | )); 36 | i++; 37 | } 38 | var startAt = x * Flutterdle.rowLength; 39 | var endAt = startAt + (Flutterdle.rowLength - 1); 40 | rows.add(Shake( 41 | key: _shakeKeys[x], 42 | preferences: const AnimationPreferences( 43 | magnitude: 0.7, 44 | duration: Duration(milliseconds: 700), 45 | autoPlay: AnimationPlayStates.None), 46 | child: Semantics( 47 | label: tiles.getRange(startAt, endAt).any((t) => t.color == GameColor.tbd) 48 | ? '' 49 | : _convertNumber(x + 1), 50 | child: Flex( 51 | children: cells, 52 | direction: Axis.horizontal, 53 | ), 54 | ))); 55 | } 56 | return rows; 57 | } 58 | 59 | String _convertNumber(int x) { 60 | switch (x) { 61 | case 1: 62 | return 'First guess'; 63 | case 2: 64 | return 'Second guess'; 65 | case 3: 66 | return 'Third guess'; 67 | case 4: 68 | return 'Fourth guess'; 69 | case 5: 70 | return 'Fifth guess'; 71 | case 6: 72 | return 'Sixth guess'; 73 | default: 74 | return 'However you got here, that is just between you and me. The guy who wrote this code. Have a great day.'; 75 | } 76 | } 77 | 78 | Widget _buildFlipAnimation(Letter letter) { 79 | return AnimatedSwitcher( 80 | duration: const Duration(milliseconds: 800), 81 | transitionBuilder: _transitionBuilder, 82 | layoutBuilder: (widget, list) => 83 | Stack(children: widget != null ? [widget, ...list] : [...list]), 84 | child: TileBuilder.build(letter, _settings), 85 | switchInCurve: Curves.easeInBack, 86 | switchOutCurve: Curves.easeInBack.flipped, 87 | ); 88 | } 89 | 90 | Widget _transitionBuilder(Widget widget, Animation animation) { 91 | final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation); 92 | return AnimatedBuilder( 93 | animation: rotateAnim, 94 | child: widget, 95 | builder: (context, widget) { 96 | var tilt = ((animation.value - 0.5).abs() - 0.5) * 0.003; 97 | tilt *= -1.0; 98 | final value = min(rotateAnim.value, pi / 2); 99 | return Transform( 100 | transform: (Matrix4.rotationX(value)..setEntry(3, 1, tilt)), 101 | child: widget, 102 | alignment: Alignment.center, 103 | ); 104 | }, 105 | ); 106 | } 107 | 108 | @override 109 | Widget build(BuildContext context) { 110 | return Semantics( 111 | label: "game number ${_game.gameNumber}", 112 | child: SizedBox( 113 | width: 350, 114 | height: 420, 115 | child: Stack(alignment: Alignment.center, children: [ 116 | Column(children: _buildRows()), 117 | if (_game.context.message.isNotEmpty) ...[ 118 | Container( 119 | decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary), 120 | child: FittedBox( 121 | fit: BoxFit.contain, 122 | child: Padding( 123 | padding: const EdgeInsets.all(12.0), 124 | child: Text( 125 | _game.context.message, 126 | textAlign: TextAlign.center, 127 | style: TextStyle( 128 | color: Theme.of(context).colorScheme.onSecondary, 129 | fontWeight: FontWeight.bold, 130 | fontSize: 20), 131 | ), 132 | )), 133 | ) 134 | ] 135 | ])), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/widgets/countdown.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class CountdownWidget extends StatefulWidget { 6 | final Function newGame; 7 | 8 | const CountdownWidget(this.newGame, {Key? key}) : super(key: key); 9 | 10 | @override 11 | State createState() => _CountdownState(); 12 | } 13 | 14 | class _CountdownState extends State { 15 | Timer? _countdownTimer; 16 | Duration? _durationUntilTomorrow; 17 | 18 | @override 19 | void initState() { 20 | _resetTimer(); 21 | super.initState(); 22 | } 23 | 24 | void _resetTimer() { 25 | final tomorrow = DateTime.now().add(const Duration(days: 1)); 26 | final midnight = DateTime(tomorrow.year, tomorrow.month, tomorrow.day); 27 | final secondsUntilMidnight = midnight.difference(DateTime.now()).inSeconds; 28 | 29 | _durationUntilTomorrow = Duration(seconds: secondsUntilMidnight); 30 | _countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) => _setCountDown()); 31 | } 32 | 33 | void _setCountDown() { 34 | const reduceSecondsBy = 1; 35 | 36 | if (mounted) { 37 | setState(() { 38 | final seconds = _durationUntilTomorrow!.inSeconds - reduceSecondsBy; 39 | if (seconds < 0) { 40 | _countdownTimer!.cancel(); 41 | _resetTimer(); 42 | widget.newGame(); 43 | } else { 44 | _durationUntilTomorrow = Duration(seconds: seconds); 45 | } 46 | }); 47 | } 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | String padDigits(int n) => n.toString().padLeft(2, '0'); 53 | final hours = padDigits(_durationUntilTomorrow!.inHours.remainder(24)); 54 | final minutes = padDigits(_durationUntilTomorrow!.inMinutes.remainder(60)); 55 | final seconds = padDigits(_durationUntilTomorrow!.inSeconds.remainder(60)); 56 | return Semantics( 57 | label: 'Next game available in $hours hours, $minutes minutes, and $seconds seconds', 58 | child: ExcludeSemantics( 59 | excluding: true, 60 | child: Text( 61 | '$hours:$minutes:$seconds', 62 | style: const TextStyle( 63 | fontSize: 34, 64 | ), 65 | ), 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/widgets/how_to.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutterdle/domain.dart'; 3 | import 'package:flutterdle/domain.dart' as domain; 4 | import 'package:flutterdle/helpers/tile_builder.dart'; 5 | 6 | class HowTo extends StatelessWidget { 7 | final void Function(domain.Dialog dialog, {bool show}) close; 8 | 9 | const HowTo(this.close, this._settings, {Key? key}) : super(key: key); 10 | 11 | final Settings _settings; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Material( 16 | shadowColor: Colors.black12, 17 | child: BlockSemantics( 18 | blocking: true, 19 | child: FittedBox( 20 | fit: BoxFit.contain, 21 | child: SizedBox( 22 | width: 500, 23 | height: 840, 24 | child: Stack(children: [ 25 | Positioned( 26 | top: 40, 27 | left: 0, 28 | child: SizedBox( 29 | width: 500, 30 | height: 800, 31 | child: Column(children: [ 32 | Row( 33 | children: [ 34 | const Spacer(), 35 | const Padding( 36 | padding: EdgeInsets.only(left: 48), 37 | child: Text( 38 | "HOW TO PLAY", 39 | textAlign: TextAlign.center, 40 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 41 | ), 42 | ), 43 | const Spacer(), 44 | TextButton( 45 | onPressed: () => close(domain.Dialog.help, show: false), 46 | child: Semantics( 47 | label: 'tap to close help', 48 | child: const ExcludeSemantics( 49 | excluding: true, 50 | child: Text("X", style: TextStyle(fontSize: 20))), 51 | )) 52 | ], 53 | ), 54 | Row( 55 | children: const [ 56 | Padding( 57 | padding: EdgeInsets.all(16), 58 | child: Text.rich( 59 | TextSpan( 60 | text: 'Guess the ', 61 | children: [ 62 | TextSpan( 63 | text: 'FLUTTERDLE', 64 | style: TextStyle(fontWeight: FontWeight.bold)), 65 | TextSpan(text: ' in six tries.') 66 | ], 67 | ), 68 | textScaleFactor: 1.25), 69 | ), 70 | ], 71 | ), 72 | const Padding( 73 | padding: EdgeInsets.only(left: 16, right: 16, bottom: 16), 74 | child: Text.rich(TextSpan( 75 | text: 76 | 'Each guess must be a valid five-letter word. Hit the enter button to submit.')), 77 | ), 78 | const Padding( 79 | padding: EdgeInsets.only(left: 16, right: 16, bottom: 8), 80 | child: Text.rich( 81 | TextSpan( 82 | text: 83 | 'After each guess, the color of the tiles will change to show how close your guess was to the word.'), 84 | textScaleFactor: 1.25), 85 | ), 86 | Padding( 87 | padding: const EdgeInsets.only(left: 16, right: 16), 88 | child: Divider(color: Colors.grey.shade800), 89 | ), 90 | Row( 91 | children: const [ 92 | Padding( 93 | padding: EdgeInsets.all(16), 94 | child: Text.rich( 95 | TextSpan( 96 | text: "Examples", 97 | style: TextStyle( 98 | fontSize: 18, 99 | )), 100 | textScaleFactor: 1.25)), 101 | ], 102 | ), 103 | Row( 104 | children: [ 105 | Semantics( 106 | label: 'Example word weary with W as an exact match', 107 | child: Container( 108 | padding: const EdgeInsets.only(left: 16, right: 16), 109 | alignment: Alignment.centerLeft, 110 | width: 300, 111 | height: 80, 112 | child: Flex( 113 | mainAxisAlignment: MainAxisAlignment.start, 114 | direction: Axis.horizontal, 115 | children: [ 116 | Flexible( 117 | child: TileBuilder.build( 118 | Letter(value: 'W', color: GameColor.correct), 119 | _settings)), 120 | Flexible( 121 | child: TileBuilder.build(Letter(value: 'E'), _settings)), 122 | Flexible( 123 | child: TileBuilder.build(Letter(value: 'A'), _settings)), 124 | Flexible( 125 | child: TileBuilder.build(Letter(value: 'R'), _settings)), 126 | Flexible( 127 | child: TileBuilder.build(Letter(value: 'Y'), _settings)), 128 | ], 129 | ), 130 | ), 131 | ), 132 | ], 133 | ), 134 | Row( 135 | children: const [ 136 | Padding( 137 | padding: EdgeInsets.all(16), 138 | child: Text.rich( 139 | TextSpan( 140 | text: 'The letter ', 141 | children: [ 142 | TextSpan( 143 | text: 'W', 144 | style: TextStyle(fontWeight: FontWeight.bold)), 145 | TextSpan(text: ' is in the word and in the correct spot.') 146 | ], 147 | ), 148 | textScaleFactor: 1.25), 149 | ), 150 | ], 151 | ), 152 | Row( 153 | children: [ 154 | Semantics( 155 | label: 'Example word pills with I as a partial match', 156 | child: Container( 157 | padding: const EdgeInsets.only(left: 16, right: 16), 158 | alignment: Alignment.centerLeft, 159 | width: 300, 160 | height: 80, 161 | child: Flex( 162 | mainAxisAlignment: MainAxisAlignment.start, 163 | direction: Axis.horizontal, 164 | children: [ 165 | Flexible( 166 | child: TileBuilder.build(Letter(value: 'P'), _settings)), 167 | Flexible( 168 | child: TileBuilder.build( 169 | Letter(value: 'I', color: GameColor.present), 170 | _settings)), 171 | Flexible( 172 | child: TileBuilder.build(Letter(value: 'L'), _settings)), 173 | Flexible( 174 | child: TileBuilder.build(Letter(value: 'L'), _settings)), 175 | Flexible( 176 | child: TileBuilder.build(Letter(value: 'S'), _settings)), 177 | ], 178 | ), 179 | ), 180 | ), 181 | ], 182 | ), 183 | Row( 184 | children: const [ 185 | Padding( 186 | padding: EdgeInsets.all(16), 187 | child: Text.rich( 188 | TextSpan( 189 | text: 'The letter ', 190 | children: [ 191 | TextSpan( 192 | text: 'I', 193 | style: TextStyle(fontWeight: FontWeight.bold)), 194 | TextSpan(text: ' is in the word but in the wrong spot.') 195 | ], 196 | ), 197 | textScaleFactor: 1.25), 198 | ), 199 | ], 200 | ), 201 | Row( 202 | children: [ 203 | Semantics( 204 | label: 'Example word vague with U not matching', 205 | child: Container( 206 | padding: const EdgeInsets.only(left: 16, right: 16), 207 | alignment: Alignment.centerLeft, 208 | width: 300, 209 | height: 80, 210 | child: Flex( 211 | mainAxisAlignment: MainAxisAlignment.start, 212 | direction: Axis.horizontal, 213 | children: [ 214 | Flexible( 215 | child: TileBuilder.build(Letter(value: 'V'), _settings)), 216 | Flexible( 217 | child: TileBuilder.build(Letter(value: 'A'), _settings)), 218 | Flexible( 219 | child: TileBuilder.build(Letter(value: 'G'), _settings)), 220 | Flexible( 221 | child: TileBuilder.build( 222 | Letter(value: 'U', color: GameColor.absent), 223 | _settings)), 224 | Flexible( 225 | child: TileBuilder.build(Letter(value: 'E'), _settings)), 226 | ], 227 | ), 228 | ), 229 | ), 230 | ], 231 | ), 232 | Row( 233 | children: const [ 234 | Padding( 235 | padding: EdgeInsets.all(16), 236 | child: Text.rich( 237 | TextSpan( 238 | text: 'The letter ', 239 | children: [ 240 | TextSpan( 241 | text: 'U', 242 | style: TextStyle(fontWeight: FontWeight.bold)), 243 | TextSpan(text: ' is not in the word in any spot.') 244 | ], 245 | ), 246 | textScaleFactor: 1.25), 247 | ), 248 | ], 249 | ), 250 | Padding( 251 | padding: const EdgeInsets.only(left: 16, right: 16), 252 | child: Divider(color: Colors.grey.shade800), 253 | ), 254 | Row( 255 | children: const [ 256 | Padding( 257 | padding: EdgeInsets.all(16), 258 | child: Text.rich( 259 | TextSpan( 260 | text: 'A new ', 261 | children: [ 262 | TextSpan( 263 | text: 'FLUTTERDLE', 264 | style: TextStyle(fontWeight: FontWeight.bold)), 265 | TextSpan(text: ' is available each day.') 266 | ], 267 | ), 268 | textScaleFactor: 1.25), 269 | ), 270 | ], 271 | ), 272 | ]), 273 | )) 274 | ]))), 275 | ), 276 | ); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /lib/widgets/keyboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutterdle/domain.dart'; 3 | 4 | class Keyboard extends StatelessWidget { 5 | final List> _keys; 6 | final ValueSetter _onKeyPressed; 7 | final Settings _settings; 8 | 9 | const Keyboard(this._keys, this._settings, this._onKeyPressed, {Key? key}) : super(key: key); 10 | 11 | Color _toColor(GameColor color) { 12 | switch (color) { 13 | case GameColor.correct: 14 | return _settings.isHighContrast ? Colors.orange : Colors.green; 15 | case GameColor.present: 16 | return _settings.isHighContrast ? Colors.blue : const Color.fromARGB(255, 207, 187, 98); 17 | case GameColor.absent: 18 | return const Color.fromARGB(255, 90, 87, 87); 19 | case GameColor.tbd: 20 | return const Color.fromARGB(255, 151, 151, 151); 21 | } 22 | } 23 | 24 | Widget _buildCell(Letter letter) { 25 | return Semantics( 26 | label: letter.semanticsLabel, 27 | keyboardKey: true, 28 | child: GestureDetector( 29 | onTap: () { 30 | _onKeyPressed.call(letter.value); 31 | }, 32 | child: SizedBox( 33 | width: letter.value.length > 1 ? 60 : 40, 34 | height: 58, 35 | child: Padding( 36 | padding: const EdgeInsets.all(2.0), 37 | child: Container( 38 | alignment: Alignment.center, 39 | decoration: BoxDecoration( 40 | border: Border.all( 41 | width: 1, 42 | color: Colors.grey.shade800, 43 | ), 44 | borderRadius: const BorderRadius.all(Radius.circular(4)), 45 | color: _toColor(letter.color)), 46 | child: ExcludeSemantics( 47 | excluding: true, 48 | child: Text( 49 | letter.value, 50 | textAlign: TextAlign.center, 51 | style: TextStyle( 52 | fontSize: letter.value.length > 1 ? 10 : 18, 53 | color: letter.color != GameColor.tbd ? Colors.white : null), 54 | ), 55 | ), 56 | ), 57 | ), 58 | ), 59 | ), 60 | ); 61 | } 62 | 63 | List _buildKeys() { 64 | final rows = []; 65 | 66 | for (var x = 0; x < _keys.length; x++) { 67 | final cells = []; 68 | for (var y = 0; y < _keys[x].length; y++) { 69 | cells.add(_buildCell(_keys[x][y])); 70 | } 71 | rows.add(Row(children: cells, mainAxisAlignment: MainAxisAlignment.center)); 72 | } 73 | return rows; 74 | } 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return SizedBox(width: 400, height: 200, child: Column(children: _buildKeys())); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/widgets/settings.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutterdle/domain.dart'; 5 | import 'package:flutterdle/domain.dart' as domain; 6 | import 'package:flutterdle/services/settings_service.dart'; 7 | import 'package:package_info_plus/package_info_plus.dart'; 8 | 9 | class SettingsWidget extends StatefulWidget { 10 | final void Function(domain.Dialog, {bool show}) close; 11 | final StreamController streamController; 12 | final Settings _settings; 13 | final PackageInfo _packageInfo; 14 | 15 | const SettingsWidget(this.close, this.streamController, this._settings, this._packageInfo, {Key? key}) 16 | : super(key: key); 17 | 18 | @override 19 | State createState() => _SettingsState(); 20 | } 21 | 22 | class _SettingsState extends State { 23 | @override 24 | Widget build(BuildContext context) { 25 | var keyboardOptions = KeyboardLayout.values.map((k) => DropdownMenuItem( 26 | child: Text(k.name), 27 | value: k, 28 | )).toList(); 29 | return Material( 30 | shadowColor: Colors.black12, 31 | child: BlockSemantics( 32 | blocking: true, 33 | child: FittedBox( 34 | fit: BoxFit.contain, 35 | child: SizedBox( 36 | width: 500, 37 | height: 500, 38 | child: Stack(children: [ 39 | Positioned( 40 | top: 0, 41 | left: 0, 42 | child: SizedBox( 43 | width: 410, 44 | height: 500, 45 | child: Column(children: [ 46 | Row( 47 | children: [ 48 | const Spacer(), 49 | TextButton( 50 | onPressed: () => 51 | widget.close(domain.Dialog.settings, show: false), 52 | child: Semantics( 53 | label: 'Close settings.', 54 | child: const ExcludeSemantics( 55 | excluding: true, 56 | child: Text('X', style: TextStyle(fontSize: 20))), 57 | )) 58 | ], 59 | ), 60 | const Padding( 61 | padding: EdgeInsets.only(bottom: 16.0), 62 | child: Center( 63 | child: Text( 64 | "SETTINGS", 65 | style: TextStyle( 66 | fontSize: 18, 67 | ), 68 | )), 69 | ), 70 | Expanded( 71 | child: ListView( 72 | children: ListTile.divideTiles( 73 | context: context, 74 | tiles: [ 75 | SwitchListTile( 76 | subtitle: const Text( 77 | "Any revealed hints must be used in subsequent guesses"), 78 | title: const Text("Hard Mode", 79 | style: TextStyle( 80 | fontSize: 18, 81 | )), 82 | value: widget._settings.isHardMode, 83 | onChanged: (bool value) { 84 | widget._settings.isHardMode = value; 85 | SettingsService().save(widget._settings); 86 | widget.streamController.add(widget._settings); 87 | setState(() { 88 | widget._settings.isHardMode = value; 89 | }); 90 | }, 91 | ), 92 | SwitchListTile( 93 | title: const Text("Dark Mode", 94 | style: TextStyle( 95 | fontSize: 18, 96 | )), 97 | value: widget._settings.isDarkMode, 98 | onChanged: (bool value) { 99 | widget._settings.isDarkMode = value; 100 | SettingsService().save(widget._settings); 101 | widget.streamController.add(widget._settings); 102 | setState( 103 | () { 104 | widget._settings.isDarkMode = value; 105 | }, 106 | ); 107 | }, 108 | ), 109 | SwitchListTile( 110 | subtitle: const Text("For improved color vision"), 111 | title: const Text("High Contrast Mode", 112 | style: TextStyle( 113 | fontSize: 18, 114 | )), 115 | value: widget._settings.isHighContrast, 116 | onChanged: (bool value) { 117 | widget._settings.isHighContrast = value; 118 | SettingsService().save(widget._settings); 119 | widget.streamController.add(widget._settings); 120 | setState(() { 121 | widget._settings.isHighContrast = value; 122 | }); 123 | }, 124 | ), 125 | ListTile( 126 | title: const Text( 127 | 'Keyboard Layout', 128 | style: TextStyle(fontSize: 18), 129 | ), 130 | trailing: Padding( 131 | padding: const EdgeInsets.only(right: 16.0), 132 | child: DropdownButton( 133 | value: widget._settings.keyboardLayout, 134 | items: keyboardOptions, 135 | onChanged: (KeyboardLayout? layout) { 136 | widget.streamController.add(widget._settings); 137 | widget._settings.keyboardLayout = layout!; 138 | SettingsService().save(widget._settings); 139 | setState(() { 140 | }); 141 | }, 142 | ), 143 | ), 144 | ), 145 | ], 146 | ).toList()), 147 | ), 148 | Padding( 149 | padding: const EdgeInsets.all(16.0), 150 | child: Text('Version ${widget._packageInfo.version} - Build ${widget._packageInfo.buildNumber}'), 151 | ) 152 | ]))) 153 | ]))), 154 | )); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/widgets/stats.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutterdle/domain.dart'; 4 | import 'package:flutterdle/domain.dart' as domain; 5 | import 'package:flutterdle/services/stats_service.dart'; 6 | import 'package:flutterdle/widgets/countdown.dart'; 7 | import 'package:share_plus/share_plus.dart'; 8 | 9 | class StatsWidget extends StatefulWidget { 10 | const StatsWidget(this._stats, this._settings, this._close, this._newGame, {Key? key}) 11 | : super(key: key); 12 | 13 | final Stats _stats; 14 | final Settings _settings; 15 | final void Function(domain.Dialog, {bool show}) _close; 16 | final Function _newGame; 17 | 18 | Stats get stats => _stats; 19 | Settings get settings => _settings; 20 | 21 | Function(domain.Dialog, {bool show}) get close => _close; 22 | Function get newGame => _newGame; 23 | 24 | @override 25 | State createState() => _StatsState(); 26 | } 27 | 28 | class _StatsState extends State { 29 | int _getFlex(int number, int total) => 30 | total == 0 ? 0 : (number / (number + (total - number)) * 10).ceil(); 31 | 32 | Future get _stats => Future.microtask(() => StatsService().loadStats()); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return FutureBuilder( 37 | future: _stats, 38 | builder: (BuildContext context, AsyncSnapshot snapshot) { 39 | return Material( 40 | shadowColor: Colors.black12, 41 | child: BlockSemantics( 42 | blocking: true, 43 | child: FittedBox( 44 | fit: BoxFit.contain, 45 | child: SizedBox( 46 | width: 500, 47 | height: 520, 48 | child: Stack(children: [ 49 | Positioned( 50 | top: 0, 51 | left: 0, 52 | child: SizedBox( 53 | width: 410, 54 | height: 520, 55 | child: Column(children: [ 56 | Row( 57 | children: [ 58 | const Spacer(), 59 | TextButton( 60 | onPressed: () => 61 | widget.close(domain.Dialog.stats, show: false), 62 | child: Semantics( 63 | label: 'tap to close Stats', 64 | child: const ExcludeSemantics( 65 | excluding: true, 66 | child: Text("X", style: TextStyle(fontSize: 20))))) 67 | ], 68 | ), 69 | const Center( 70 | child: Text( 71 | "STATISTICS", 72 | style: TextStyle( 73 | fontSize: 18, 74 | ), 75 | )), 76 | Padding( 77 | padding: const EdgeInsets.fromLTRB(10, 20, 10, 5), 78 | child: Row( 79 | crossAxisAlignment: CrossAxisAlignment.start, 80 | mainAxisAlignment: MainAxisAlignment.center, 81 | children: [ 82 | Padding( 83 | padding: const EdgeInsets.only(left: 4, right: 4), 84 | child: Column( 85 | children: [ 86 | Semantics( 87 | label: 'Games played is ${widget.stats.played}', 88 | child: ExcludeSemantics( 89 | excluding: true, 90 | child: Text( 91 | widget.stats.played.toString(), 92 | style: const TextStyle( 93 | fontSize: 36, 94 | ), 95 | ), 96 | ), 97 | ), 98 | const ExcludeSemantics( 99 | child: Text( 100 | "Played", 101 | style: TextStyle( 102 | fontSize: 12, 103 | ), 104 | ), 105 | ) 106 | ], 107 | ), 108 | ), 109 | Padding( 110 | padding: const EdgeInsets.only(left: 4, right: 4), 111 | child: Column( 112 | children: [ 113 | Semantics( 114 | label: 'Win percentage is ${widget.stats.percentWon}', 115 | child: ExcludeSemantics( 116 | excluding: true, 117 | child: Text( 118 | widget.stats.percentWon.toString(), 119 | style: const TextStyle( 120 | fontSize: 36, 121 | ), 122 | ), 123 | ), 124 | ), 125 | const ExcludeSemantics( 126 | child: Text( 127 | "Win %", 128 | style: TextStyle( 129 | fontSize: 12, 130 | ), 131 | ), 132 | ) 133 | ], 134 | ), 135 | ), 136 | Padding( 137 | padding: const EdgeInsets.only(left: 4, right: 4), 138 | child: Column( 139 | children: [ 140 | Semantics( 141 | label: 142 | 'Current streak is ${widget.stats.streak.current}', 143 | child: ExcludeSemantics( 144 | excluding: true, 145 | child: Text( 146 | widget.stats.streak.current.toString(), 147 | style: const TextStyle( 148 | fontSize: 36, 149 | ), 150 | ), 151 | ), 152 | ), 153 | ExcludeSemantics( 154 | child: Column(children: const [ 155 | Center( 156 | child: Text( 157 | "Current", 158 | style: TextStyle( 159 | fontSize: 12, 160 | ), 161 | )), 162 | Center( 163 | child: Text( 164 | "Streak", 165 | style: TextStyle( 166 | fontSize: 12, 167 | ), 168 | )) 169 | ]), 170 | ), 171 | ], 172 | ), 173 | ), 174 | Padding( 175 | padding: const EdgeInsets.only(left: 4, right: 4), 176 | child: Column( 177 | children: [ 178 | Semantics( 179 | label: 'Max Streak is ${widget.stats.streak.max}', 180 | child: ExcludeSemantics( 181 | excluding: true, 182 | child: Text( 183 | widget.stats.streak.max.toString(), 184 | style: const TextStyle( 185 | fontSize: 36, 186 | ), 187 | ), 188 | ), 189 | ), 190 | ExcludeSemantics( 191 | child: Column(children: const [ 192 | Center( 193 | child: Text( 194 | "Max", 195 | style: TextStyle( 196 | fontSize: 12, 197 | ), 198 | )), 199 | Center( 200 | child: Text( 201 | "Streak", 202 | style: TextStyle( 203 | fontSize: 12, 204 | ), 205 | )) 206 | ]), 207 | ), 208 | ], 209 | ), 210 | ) 211 | ], 212 | ), 213 | ), 214 | const Padding( 215 | padding: EdgeInsets.only(top: 20), 216 | child: Center( 217 | child: Text( 218 | "GUESS DISTRIBUTION", 219 | style: TextStyle( 220 | fontSize: 18, 221 | ), 222 | )), 223 | ), 224 | Padding( 225 | padding: const EdgeInsets.fromLTRB(40, 10, 40, 10), 226 | child: _guessDistribution(), 227 | ), 228 | Padding( 229 | padding: const EdgeInsets.all(20.0), 230 | child: IntrinsicHeight( 231 | child: Row( 232 | children: [ 233 | Expanded( 234 | child: Column( 235 | children: [ 236 | const Center( 237 | child: Text( 238 | "NEXT FLUTTERDLE", 239 | style: TextStyle( 240 | fontSize: 16, 241 | ), 242 | )), 243 | CountdownWidget(widget.newGame), 244 | ], 245 | ), 246 | ), 247 | VerticalDivider( 248 | color: Theme.of(context).colorScheme.secondary, 249 | width: 2, 250 | indent: 2, 251 | endIndent: 2, 252 | thickness: 2, 253 | ), 254 | Expanded( 255 | child: Padding( 256 | padding: const EdgeInsets.only(right: 16, left: 16), 257 | child: Semantics( 258 | label: 'Share button', 259 | child: ElevatedButton( 260 | style: ElevatedButton.styleFrom( 261 | primary: widget.settings.isHighContrast 262 | ? Colors.orange 263 | : Colors.green), 264 | onPressed: () { 265 | var guesses = widget.stats.lastGuess == -1 266 | ? 'X' 267 | : widget.stats.lastGuess; 268 | Share.share( 269 | 'Flutterdle ${widget.stats.gameNumber} $guesses/6\n${widget.stats.lastBoard}', 270 | subject: 'Flutterdle $guesses/6'); 271 | }, 272 | child: Row( 273 | mainAxisSize: MainAxisSize.min, 274 | children: const [ 275 | Text('SHARE', 276 | style: TextStyle( 277 | fontWeight: FontWeight.bold, 278 | color: Colors.white, 279 | fontSize: 18, 280 | )), 281 | SizedBox( 282 | width: 5, 283 | ), 284 | Icon( 285 | Icons.share, 286 | color: Colors.white, 287 | size: 24.0, 288 | ), 289 | ], 290 | ), 291 | ), 292 | ), 293 | ), 294 | ), 295 | ], 296 | ), 297 | ), 298 | ) 299 | ]), 300 | )) 301 | ]))), 302 | ), 303 | ); 304 | }); 305 | } 306 | 307 | Widget _guessDistribution() { 308 | if (widget.stats.played == 0) { 309 | return const Center( 310 | child: Text( 311 | "No Data", 312 | style: TextStyle(fontSize: 20), 313 | )); 314 | } 315 | 316 | var maxGuess = widget.stats.guessDistribution.reduce(max); 317 | var children = []; 318 | 319 | for (var i = 0; i < widget.stats.guessDistribution.length; i++) { 320 | children.add(_statRow(i + 1, widget.stats.guessDistribution[i], maxGuess, 321 | isCurrent: (i + 1) == widget.stats.lastGuess)); 322 | } 323 | return Column(children: children); 324 | } 325 | 326 | Widget _statRow(int rowNumber, int completed, int total, {bool isCurrent = false}) { 327 | return Semantics( 328 | label: 329 | 'Guess $rowNumber has ${isCurrent ? 'most recently ' : ''}won $completed time${completed == 1 ? '' : 's'}', 330 | child: Padding( 331 | padding: const EdgeInsets.all(3), 332 | child: Row( 333 | children: [ 334 | Padding( 335 | padding: const EdgeInsets.only(right: 3.0), 336 | child: ExcludeSemantics( 337 | excluding: true, 338 | child: Text( 339 | rowNumber.toString(), 340 | style: const TextStyle( 341 | fontSize: 16, 342 | ), 343 | ), 344 | ), 345 | ), 346 | Expanded( 347 | flex: _getFlex(completed, total), 348 | child: Container( 349 | color: isCurrent 350 | ? widget.settings.isHighContrast 351 | ? Colors.orange 352 | : Colors.green 353 | : const Color.fromARGB(255, 90, 87, 87), 354 | child: Padding( 355 | padding: const EdgeInsets.only(left: 4, right: 4), 356 | child: ExcludeSemantics( 357 | excluding: true, 358 | child: Text( 359 | completed.toString(), 360 | textAlign: TextAlign.end, 361 | style: const TextStyle( 362 | color: Colors.white, 363 | fontWeight: FontWeight.bold, 364 | fontSize: 16, 365 | ), 366 | ), 367 | ), 368 | )), 369 | ), 370 | Expanded( 371 | flex: _getFlex(total - completed, total), 372 | child: Container(), 373 | ), 374 | ], 375 | ), 376 | ), 377 | ); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jonathan Moosekian 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 | -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 |

Privacy Policy for Flutterdle

2 | 3 |

At Flutterdle, accessible from https://github.com/johnnysbug/flutter_wordle, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by Flutterdle and how we use it.

4 | 5 |

If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us.

6 | 7 |

This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in Flutterdle. This policy is not applicable to any information collected offline or via channels other than this website. Our Privacy Policy was created with the help of the Free Privacy Policy Generator.

8 | 9 |

Consent

10 | 11 |

By using our website, you hereby consent to our Privacy Policy and agree to its terms.

12 | 13 |

Information we collect

14 | 15 |

The personal information that you are asked to provide, and the reasons why you are asked to provide it, will be made clear to you at the point we ask you to provide your personal information.

16 |

If you contact us directly, we may receive additional information about you such as your name, email address, phone number, the contents of the message and/or attachments you may send us, and any other information you may choose to provide.

17 |

When you register for an Account, we may ask for your contact information, including items such as name, company name, address, email address, and telephone number.

18 | 19 |

How we use your information

20 | 21 |

We use the information we collect in various ways, including to:

22 | 23 |
    24 |
  • Provide, operate, and maintain our website
  • 25 |
  • Improve, personalize, and expand our website
  • 26 |
  • Understand and analyze how you use our website
  • 27 |
  • Develop new products, services, features, and functionality
  • 28 |
  • Communicate with you, either directly or through one of our partners, including for customer service, to provide you with updates and other information relating to the website, and for marketing and promotional purposes
  • 29 |
  • Send you emails
  • 30 |
  • Find and prevent fraud
  • 31 |
32 | 33 |

Log Files

34 | 35 |

Flutterdle follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.

36 | 37 | 38 | 39 | 40 |

Advertising Partners Privacy Policies

41 | 42 |

You may consult this list to find the Privacy Policy for each of the advertising partners of Flutterdle.

43 | 44 |

Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on Flutterdle, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.

45 | 46 |

Note that Flutterdle has no access to or control over these cookies that are used by third-party advertisers.

47 | 48 |

Third Party Privacy Policies

49 | 50 |

Flutterdle's Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options.

51 | 52 |

You can choose to disable cookies through your individual browser options. To know more detailed information about cookie management with specific web browsers, it can be found at the browsers' respective websites.

53 | 54 |

CCPA Privacy Rights (Do Not Sell My Personal Information)

55 | 56 |

Under the CCPA, among other rights, California consumers have the right to:

57 |

Request that a business that collects a consumer's personal data disclose the categories and specific pieces of personal data that a business has collected about consumers.

58 |

Request that a business delete any personal data about the consumer that a business has collected.

59 |

Request that a business that sells a consumer's personal data, not sell the consumer's personal data.

60 |

If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us.

61 | 62 |

GDPR Data Protection Rights

63 | 64 |

We would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:

65 |

The right to access – You have the right to request copies of your personal data. We may charge you a small fee for this service.

66 |

The right to rectification – You have the right to request that we correct any information you believe is inaccurate. You also have the right to request that we complete the information you believe is incomplete.

67 |

The right to erasure – You have the right to request that we erase your personal data, under certain conditions.

68 |

The right to restrict processing – You have the right to request that we restrict the processing of your personal data, under certain conditions.

69 |

The right to object to processing – You have the right to object to our processing of your personal data, under certain conditions.

70 |

The right to data portability – You have the right to request that we transfer the data that we have collected to another organization, or directly to you, under certain conditions.

71 |

If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us.

72 | 73 |

Children's Information

74 | 75 |

Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.

76 | 77 |

Flutterdle does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.

78 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutterdle 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.7+1 19 | 20 | environment: 21 | sdk: ">=2.16.1 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | flutter_animator: ^3.2.1 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.2 37 | test: ^1.19.5 38 | tuple: ^2.0.0 39 | share_plus: ^4.0.2 40 | path_provider: ^2.0.9 41 | package_info_plus: ^1.0.6 42 | 43 | dev_dependencies: 44 | flutter_test: 45 | sdk: flutter 46 | 47 | # The "flutter_lints" package below contains a set of recommended lints to 48 | # encourage good coding practices. The lint set provided by the package is 49 | # activated in the `analysis_options.yaml` file located at the root of your 50 | # package. See that file for information about deactivating specific lint 51 | # rules and activating additional ones. 52 | flutter_lints: ^1.0.0 53 | 54 | # For information on the generic Dart part of this file, see the 55 | # following page: https://dart.dev/tools/pub/pubspec 56 | 57 | # The following section is specific to Flutter. 58 | flutter: 59 | uses-material-design: true 60 | assets: 61 | - assets/answers.txt 62 | - assets/allowed_guesses.txt 63 | - assets/stats.json 64 | # The following line ensures that the Material Icons font is 65 | # included with your application, so that you can use the icons in 66 | # the material Icons class. 67 | # uses-material-design: true 68 | 69 | # An image asset can refer to one or more resolution-specific "variants", see 70 | # https://flutter.dev/assets-and-images/#resolution-aware. 71 | 72 | # For details regarding adding assets from package dependencies, see 73 | # https://flutter.dev/assets-and-images/#from-packages 74 | 75 | # To add custom fonts to your application, add a fonts section here, 76 | # in this "flutter" section. Each entry in this list should have a 77 | # "family" key with the font family name, and a "fonts" key with a 78 | # list giving the asset and other descriptors for the font. For 79 | # example: 80 | # fonts: 81 | # - family: Schyler 82 | # fonts: 83 | # - asset: fonts/Schyler-Regular.ttf 84 | # - asset: fonts/Schyler-Italic.ttf 85 | # style: italic 86 | # - family: Trajan Pro 87 | # fonts: 88 | # - asset: fonts/TrajanPro.ttf 89 | # - asset: fonts/TrajanPro_Bold.ttf 90 | # weight: 700 91 | # 92 | # For details regarding fonts from package dependencies, 93 | # see https://flutter.dev/custom-fonts/#from-packages 94 | -------------------------------------------------------------------------------- /test/services/matching_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterdle/domain.dart'; 2 | import 'package:test/test.dart'; 3 | import 'package:flutterdle/services/matching_service.dart'; 4 | import 'package:tuple/tuple.dart'; 5 | 6 | bool compareMatches(List first, List second) { 7 | for (var i = 0; i < 5; i++) { 8 | if (first[i].index != second[i].index || 9 | first[i].value != second[i].value || 10 | first[i].color != second[i].color) { 11 | return false; 12 | } 13 | } 14 | return true; 15 | } 16 | 17 | void main() { 18 | var testData = >>[ 19 | Tuple3('STRAW', 'STRAW', [ 20 | Letter(index: 0, value: 'S', color: GameColor.correct), 21 | Letter(index: 1, value: 'T', color: GameColor.correct), 22 | Letter(index: 2, value: 'R', color: GameColor.correct), 23 | Letter(index: 3, value: 'A', color: GameColor.correct), 24 | Letter(index: 4, value: 'W', color: GameColor.correct) 25 | ]), 26 | Tuple3('STRAW', 'TRAIN', [ 27 | Letter(index: 0, value: 'S', color: GameColor.absent), 28 | Letter(index: 1, value: 'T', color: GameColor.present), 29 | Letter(index: 2, value: 'R', color: GameColor.present), 30 | Letter(index: 3, value: 'A', color: GameColor.present), 31 | Letter(index: 4, value: 'W', color: GameColor.absent) 32 | ]), 33 | Tuple3('STRAW', 'MULCH', [ 34 | Letter(index: 0, value: 'S', color: GameColor.absent), 35 | Letter(index: 1, value: 'T', color: GameColor.absent), 36 | Letter(index: 2, value: 'R', color: GameColor.absent), 37 | Letter(index: 3, value: 'A', color: GameColor.absent), 38 | Letter(index: 4, value: 'W', color: GameColor.absent) 39 | ]), 40 | Tuple3('CLASS', 'SMART', [ 41 | Letter(index: 0, value: 'C', color: GameColor.absent), 42 | Letter(index: 1, value: 'L', color: GameColor.absent), 43 | Letter(index: 2, value: 'A', color: GameColor.correct), 44 | Letter(index: 3, value: 'S', color: GameColor.present), 45 | Letter(index: 4, value: 'S', color: GameColor.absent) 46 | ]), 47 | Tuple3('SMART', 'CLASS', [ 48 | Letter(index: 0, value: 'S', color: GameColor.present), 49 | Letter(index: 1, value: 'M', color: GameColor.absent), 50 | Letter(index: 2, value: 'A', color: GameColor.correct), 51 | Letter(index: 3, value: 'R', color: GameColor.absent), 52 | Letter(index: 4, value: 'T', color: GameColor.absent) 53 | ]), 54 | Tuple3('QUICK', 'VIVID', [ 55 | Letter(index: 0, value: 'Q', color: GameColor.absent), 56 | Letter(index: 1, value: 'U', color: GameColor.absent), 57 | Letter(index: 2, value: 'I', color: GameColor.present), 58 | Letter(index: 3, value: 'C', color: GameColor.absent), 59 | Letter(index: 4, value: 'K', color: GameColor.absent) 60 | ]), 61 | Tuple3('SILLY', 'LILLY', [ 62 | Letter(index: 0, value: 'S', color: GameColor.absent), 63 | Letter(index: 1, value: 'I', color: GameColor.correct), 64 | Letter(index: 2, value: 'L', color: GameColor.correct), 65 | Letter(index: 3, value: 'L', color: GameColor.correct), 66 | Letter(index: 4, value: 'Y', color: GameColor.correct) 67 | ]), 68 | Tuple3('LILLY', 'SILLY', [ 69 | Letter(index: 0, value: 'L', color: GameColor.absent), 70 | Letter(index: 1, value: 'I', color: GameColor.correct), 71 | Letter(index: 2, value: 'L', color: GameColor.correct), 72 | Letter(index: 3, value: 'L', color: GameColor.correct), 73 | Letter(index: 4, value: 'Y', color: GameColor.correct) 74 | ]), 75 | Tuple3('BUDDY', 'ADDED', [ 76 | Letter(index: 0, value: 'B', color: GameColor.absent), 77 | Letter(index: 1, value: 'U', color: GameColor.absent), 78 | Letter(index: 2, value: 'D', color: GameColor.correct), 79 | Letter(index: 3, value: 'D', color: GameColor.present), 80 | Letter(index: 4, value: 'Y', color: GameColor.absent) 81 | ]), 82 | Tuple3('ADDED', 'BUDDY', [ 83 | Letter(index: 0, value: 'A', color: GameColor.absent), 84 | Letter(index: 1, value: 'D', color: GameColor.present), 85 | Letter(index: 2, value: 'D', color: GameColor.correct), 86 | Letter(index: 3, value: 'E', color: GameColor.absent), 87 | Letter(index: 4, value: 'D', color: GameColor.absent) 88 | ]), 89 | Tuple3('ABATE', 'EAGER', [ 90 | Letter(index: 0, value: 'A', color: GameColor.present), 91 | Letter(index: 1, value: 'B', color: GameColor.absent), 92 | Letter(index: 2, value: 'A', color: GameColor.absent), 93 | Letter(index: 3, value: 'T', color: GameColor.absent), 94 | Letter(index: 4, value: 'E', color: GameColor.present) 95 | ]), 96 | Tuple3('EAGER', 'ABATE', [ 97 | Letter(index: 0, value: 'E', color: GameColor.present), 98 | Letter(index: 1, value: 'A', color: GameColor.present), 99 | Letter(index: 2, value: 'G', color: GameColor.absent), 100 | Letter(index: 3, value: 'E', color: GameColor.absent), 101 | Letter(index: 4, value: 'R', color: GameColor.absent) 102 | ]), 103 | ]; 104 | 105 | for (var t in testData) { 106 | test( 107 | 'MatchingService should return correct results for ${t.item1} and ${t.item2}', 108 | () { 109 | var actualResult = MatchingService.matches(t.item1, t.item2).toList(); 110 | expect(compareMatches(actualResult, t.item3), true); 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutterdle 33 | 34 | 35 | 36 | 39 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutterdle", 3 | "short_name": "flutterdle", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(flutterdle LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "flutterdle") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | 54 | # === Installation === 55 | # Support files are copied into place next to the executable, so that it can 56 | # run in place. This is done instead of making a separate bundle (as on Linux) 57 | # so that building and running from within Visual Studio will work. 58 | set(BUILD_BUNDLE_DIR "$") 59 | # Make the "install" step default, as it's required to run. 60 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 61 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 62 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 63 | endif() 64 | 65 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 66 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 67 | 68 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 69 | COMPONENT Runtime) 70 | 71 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 72 | COMPONENT Runtime) 73 | 74 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 75 | COMPONENT Runtime) 76 | 77 | if(PLUGIN_BUNDLED_LIBRARIES) 78 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 79 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 80 | COMPONENT Runtime) 81 | endif() 82 | 83 | # Fully re-copy the assets directory on each build to avoid having stale files 84 | # from a previous install. 85 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 86 | install(CODE " 87 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 88 | " COMPONENT Runtime) 89 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 90 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 91 | 92 | # Install the AOT library on non-Debug builds only. 93 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 94 | CONFIGURATIONS Profile;Release 95 | COMPONENT Runtime) 96 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void RegisterPlugins(flutter::PluginRegistry* registry) { 12 | UrlLauncherWindowsRegisterWithRegistrar( 13 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 14 | } 15 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_windows 7 | ) 8 | 9 | set(PLUGIN_BUNDLED_LIBRARIES) 10 | 11 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 12 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 13 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 14 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 15 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 16 | endforeach(plugin) 17 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "utils.cpp" 8 | "win32_window.cpp" 9 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 10 | "Runner.rc" 11 | "runner.exe.manifest" 12 | ) 13 | apply_standard_settings(${BINARY_NAME}) 14 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 15 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 16 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 17 | add_dependencies(${BINARY_NAME} flutter_assemble) 18 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "flutter_wordle" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "flutter_wordle" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "flutter_wordle.exe" "\0" 98 | VALUE "ProductName", "flutter_wordle" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.CreateAndShow(L"flutterdle", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnysbug/flutterdle/7d5cee3969867605beea21ecf5183c9075162748/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | namespace { 8 | 9 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 10 | 11 | // The number of Win32Window objects that currently exist. 12 | static int g_active_window_count = 0; 13 | 14 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 15 | 16 | // Scale helper to convert logical scaler values to physical using passed in 17 | // scale factor 18 | int Scale(int source, double scale_factor) { 19 | return static_cast(source * scale_factor); 20 | } 21 | 22 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 23 | // This API is only needed for PerMonitor V1 awareness mode. 24 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 25 | HMODULE user32_module = LoadLibraryA("User32.dll"); 26 | if (!user32_module) { 27 | return; 28 | } 29 | auto enable_non_client_dpi_scaling = 30 | reinterpret_cast( 31 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 32 | if (enable_non_client_dpi_scaling != nullptr) { 33 | enable_non_client_dpi_scaling(hwnd); 34 | FreeLibrary(user32_module); 35 | } 36 | } 37 | 38 | } // namespace 39 | 40 | // Manages the Win32Window's window class registration. 41 | class WindowClassRegistrar { 42 | public: 43 | ~WindowClassRegistrar() = default; 44 | 45 | // Returns the singleton registar instance. 46 | static WindowClassRegistrar* GetInstance() { 47 | if (!instance_) { 48 | instance_ = new WindowClassRegistrar(); 49 | } 50 | return instance_; 51 | } 52 | 53 | // Returns the name of the window class, registering the class if it hasn't 54 | // previously been registered. 55 | const wchar_t* GetWindowClass(); 56 | 57 | // Unregisters the window class. Should only be called if there are no 58 | // instances of the window. 59 | void UnregisterWindowClass(); 60 | 61 | private: 62 | WindowClassRegistrar() = default; 63 | 64 | static WindowClassRegistrar* instance_; 65 | 66 | bool class_registered_ = false; 67 | }; 68 | 69 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 70 | 71 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 72 | if (!class_registered_) { 73 | WNDCLASS window_class{}; 74 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 75 | window_class.lpszClassName = kWindowClassName; 76 | window_class.style = CS_HREDRAW | CS_VREDRAW; 77 | window_class.cbClsExtra = 0; 78 | window_class.cbWndExtra = 0; 79 | window_class.hInstance = GetModuleHandle(nullptr); 80 | window_class.hIcon = 81 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 82 | window_class.hbrBackground = 0; 83 | window_class.lpszMenuName = nullptr; 84 | window_class.lpfnWndProc = Win32Window::WndProc; 85 | RegisterClass(&window_class); 86 | class_registered_ = true; 87 | } 88 | return kWindowClassName; 89 | } 90 | 91 | void WindowClassRegistrar::UnregisterWindowClass() { 92 | UnregisterClass(kWindowClassName, nullptr); 93 | class_registered_ = false; 94 | } 95 | 96 | Win32Window::Win32Window() { 97 | ++g_active_window_count; 98 | } 99 | 100 | Win32Window::~Win32Window() { 101 | --g_active_window_count; 102 | Destroy(); 103 | } 104 | 105 | bool Win32Window::CreateAndShow(const std::wstring& title, 106 | const Point& origin, 107 | const Size& size) { 108 | Destroy(); 109 | 110 | const wchar_t* window_class = 111 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 112 | 113 | const POINT target_point = {static_cast(origin.x), 114 | static_cast(origin.y)}; 115 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 116 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 117 | double scale_factor = dpi / 96.0; 118 | 119 | HWND window = CreateWindow( 120 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 121 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 122 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 123 | nullptr, nullptr, GetModuleHandle(nullptr), this); 124 | 125 | if (!window) { 126 | return false; 127 | } 128 | 129 | return OnCreate(); 130 | } 131 | 132 | // static 133 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 134 | UINT const message, 135 | WPARAM const wparam, 136 | LPARAM const lparam) noexcept { 137 | if (message == WM_NCCREATE) { 138 | auto window_struct = reinterpret_cast(lparam); 139 | SetWindowLongPtr(window, GWLP_USERDATA, 140 | reinterpret_cast(window_struct->lpCreateParams)); 141 | 142 | auto that = static_cast(window_struct->lpCreateParams); 143 | EnableFullDpiSupportIfAvailable(window); 144 | that->window_handle_ = window; 145 | } else if (Win32Window* that = GetThisFromHandle(window)) { 146 | return that->MessageHandler(window, message, wparam, lparam); 147 | } 148 | 149 | return DefWindowProc(window, message, wparam, lparam); 150 | } 151 | 152 | LRESULT 153 | Win32Window::MessageHandler(HWND hwnd, 154 | UINT const message, 155 | WPARAM const wparam, 156 | LPARAM const lparam) noexcept { 157 | switch (message) { 158 | case WM_DESTROY: 159 | window_handle_ = nullptr; 160 | Destroy(); 161 | if (quit_on_close_) { 162 | PostQuitMessage(0); 163 | } 164 | return 0; 165 | 166 | case WM_DPICHANGED: { 167 | auto newRectSize = reinterpret_cast(lparam); 168 | LONG newWidth = newRectSize->right - newRectSize->left; 169 | LONG newHeight = newRectSize->bottom - newRectSize->top; 170 | 171 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 172 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 173 | 174 | return 0; 175 | } 176 | case WM_SIZE: { 177 | RECT rect = GetClientArea(); 178 | if (child_content_ != nullptr) { 179 | // Size and position the child window. 180 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 181 | rect.bottom - rect.top, TRUE); 182 | } 183 | return 0; 184 | } 185 | 186 | case WM_ACTIVATE: 187 | if (child_content_ != nullptr) { 188 | SetFocus(child_content_); 189 | } 190 | return 0; 191 | } 192 | 193 | return DefWindowProc(window_handle_, message, wparam, lparam); 194 | } 195 | 196 | void Win32Window::Destroy() { 197 | OnDestroy(); 198 | 199 | if (window_handle_) { 200 | DestroyWindow(window_handle_); 201 | window_handle_ = nullptr; 202 | } 203 | if (g_active_window_count == 0) { 204 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 205 | } 206 | } 207 | 208 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 209 | return reinterpret_cast( 210 | GetWindowLongPtr(window, GWLP_USERDATA)); 211 | } 212 | 213 | void Win32Window::SetChildContent(HWND content) { 214 | child_content_ = content; 215 | SetParent(content, window_handle_); 216 | RECT frame = GetClientArea(); 217 | 218 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 219 | frame.bottom - frame.top, true); 220 | 221 | SetFocus(child_content_); 222 | } 223 | 224 | RECT Win32Window::GetClientArea() { 225 | RECT frame; 226 | GetClientRect(window_handle_, &frame); 227 | return frame; 228 | } 229 | 230 | HWND Win32Window::GetHandle() { 231 | return window_handle_; 232 | } 233 | 234 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 235 | quit_on_close_ = quit_on_close; 236 | } 237 | 238 | bool Win32Window::OnCreate() { 239 | // No-op; provided for subclasses. 240 | return true; 241 | } 242 | 243 | void Win32Window::OnDestroy() { 244 | // No-op; provided for subclasses. 245 | } 246 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | --------------------------------------------------------------------------------