27 Commits

Author SHA1 Message Date
Rene Kievits
14ad880f79 fix error with browser view and levels 2025-11-02 19:53:10 +01:00
Rene Kievits
5f1b9ba12e finish v3 2025-11-02 19:00:17 +01:00
Rene Kievits
16da0f04ac possible to exclude levels and change how questions are served 2025-11-02 17:07:23 +01:00
Rene Kievits
e9f115a32a more cleanup and small fixes, new sound effects 2025-11-01 07:50:21 +01:00
Rene Kievits
d5ff5eb12f some cleanup and some fixes 2025-11-01 06:38:14 +01:00
732408997d Merge pull request 'themeing' (#7) from themeing into master
Reviewed-on: #7
2025-10-31 17:19:39 +01:00
Rene Kievits
de3501c3e4 themes and some refractoring 2025-10-31 17:18:33 +01:00
Rene Kievits
4eb488e28c themes 2025-10-31 13:40:14 +01:00
Rene Kievits
ad61292263 working on themes 2025-10-31 07:39:32 +01:00
4a6fce37b2 Merge pull request 'change a bunch of stuff, seperate tracking for progress, updated custom srs layout' (#6) from seperate_leassons_custom into master
Reviewed-on: #6
2025-10-31 07:17:46 +01:00
Rene Kievits
d8edfa1686 change a bunch of stuff, seperate tracking for progress, updated custom srs layout 2025-10-31 07:16:44 +01:00
Rene Kievits
cafec12888 fix .env loading 2025-10-30 18:02:06 +01:00
Rene Kievits
78fc69aeb4 fix .env on dev 2025-10-30 17:48:12 +01:00
45d52dbc84 Merge pull request 'custom_srs' (#5) from custom_srs into master
Reviewed-on: #5
2025-10-30 17:42:46 +01:00
Rene Kievits
d8a5c27fb3 finish custom srs for now 2025-10-30 17:41:53 +01:00
Rene Kievits
ee4fd7ffc1 add a shit ton of feature for the custom srs 2025-10-30 03:44:04 +01:00
Rene Kievits
b58a4020e1 add custom srs 2025-10-30 02:00:29 +01:00
Rene Kievits
fe5ac30294 quick fix 2025-10-29 02:56:56 +01:00
Rene Kievits
6785bb9133 some build stuff and quick fix 2025-10-29 02:47:41 +01:00
Rene Kievits
d2070cd6b8 small readme change 2025-10-28 22:26:20 +01:00
Rene Kievits
e034ecd763 remove prints 2025-10-28 21:43:21 +01:00
Rene Kievits
9c1a3f8cc2 remove tests, tests are for losers 2025-10-28 21:40:49 +01:00
Rene Kievits
a57140fb2e update app name and remove unnecessary files 2025-10-28 21:12:07 +01:00
580d453339 Merge pull request 'add new listening comprehension mode' (#4) from listening_comprehension_mode into master
Reviewed-on: Crylia/wanikani-kanji-srs#4
2025-10-28 20:33:31 +01:00
Rene Kievits
ff219a8291 add new listening comprehension mode 2025-10-28 20:32:52 +01:00
4448b02b81 Merge pull request 'add pages and level grouping for the browser' (#3) from brower_pages into master
Reviewed-on: Crylia/wanikani-kanji-srs#3
2025-10-28 20:14:24 +01:00
Rene Kievits
bef4519ab5 add pages and level grouping for the browser 2025-10-28 20:13:46 +01:00
143 changed files with 4311 additions and 5412 deletions

6
.gitignore vendored
View File

@@ -43,3 +43,9 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
*.jks
gradle.properties
# Environment variables
.env

108
README.md
View File

@@ -1,74 +1,60 @@
# WaniKani Kanji SRS
# Hirameki SRS
A **Spaced Repetition System (SRS) app for learning Japanese kanji** using WaniKanis API.
Test your **kanji meanings, readings, and recognition skills** on your mobile device.
A simple and effective **Spaced Repetition System (SRS) app** for learning Japanese kanji and vocabulary from your WaniKani account. This app is built with Flutter and uses the official WaniKani API to sync your unlocked items.
---
## 📱 Features
## Features
- Quiz modes:
- **Kanji → English**: Guess the meaning of the kanji.
- **English → Kanji**: Match the meaning back to the kanji.
- **Reading**: Practice **onyomi** and **kunyomi** readings.
- **Multiple-choice answers** with dynamic distractors.
- **Score tracking** for correct and attempted answers.
- Clean and responsive **Flutter UI** with animated kanji cards.
- Fully **offline-capable** once deck is fetched and cached.
- **Customizable themes** (dark mode by default).
- Settings page to manage your **WaniKani API key**.
This app is designed to be a lightweight and focused tool for reinforcing your WaniKani lessons on the go.
- **Kanji & Vocabulary Quizzes**: Separate quiz sessions for both Kanji and Vocabulary items.
- **Multiple Quiz Modes**: Test your knowledge in different ways:
- **Kanji/Vocab → English**: Guess the English meaning.
- **English → Kanji/Vocab**: Recall the Japanese characters from the English meaning.
- **Kanji Reading**: Practice the **On'yomi** and **Kun'yomi** for kanji.
- **Vocabulary Listening**: A special mode to test your listening comprehension by playing the audio and having you choose the meaning.
- **SRS-Based Learning**: Questions are prioritized based on their SRS level, helping you focus on items that need the most practice.
- **Browse Your Deck**: A dedicated screen to browse all your unlocked Kanji and Vocabulary.
- **Paginated by Level**: Items in the browse screen are grouped by their WaniKani level and organized into swipeable pages.
- **Quick Item Details**: Tap on a Kanji in the browse screen to see its readings, meanings, and level in a quick-view popup.
- **Offline Access**: Once your decks are downloaded, you can quiz yourself anywhere, anytime.
- **Simple & Clean UI**: A dark-themed, minimalist interface that keeps you focused on learning.
---
## 🛠️ Tech Stack
## Getting Started
- **Flutter** for cross-platform mobile development (Android/iOS)
- **Provider** for state management
- **Shared Preferences** for storing API key and settings
- **Dart** for core logic
- **WaniKani API** for fetching kanji data
Getting the app up and running is simple.
### 1. Installation
Clone the repository and install the dependencies:
```bash
git clone https://github.com/crylia/hirameki-srs.git
cd hirameki-srs
flutter pub get
```
### 2. WaniKani API Key
To use the app, you need a WaniKani API key. You can generate one from your WaniKani account settings page under "API Tokens".
The app will prompt you to enter your API key on first launch. If you need to change it later, you can do so from the settings screen.
### 3. Run the App
Connect a device or start an emulator and run the app:
```bash
flutter run
```
---
## ⚡ Installation
## How to Use the App
1. Clone the repository:
`git clone https://git.crylia.de/Crylia/wanikani-kanji-srs.git`
`cd wanikani_srs`
2. Install dependencies:
`flutter pub get`
3. Run on a device/emulator:
`flutter run`
4. Set your **WaniKani API key** in the apps settings to fetch your deck.
---
## 🔧 Usage
1. Open the app and enter your **WaniKani API key**.
2. Select a quiz mode:
- Kanji → English
- English → Kanji
- Reading
3. Tap on the multiple-choice options to answer.
4. Your **score** is displayed at the bottom.
5. Progress is updated automatically.
---
## 📦 Build APK
To generate a release APK:
`flutter build apk --release`
- **Start Screen**: From the main screen, you can jump into a "Kanji Quiz", "Vocabulary Quiz", or "Browse Items".
- **Quiz Screen**: Choose your desired quiz mode at the top. The app will start presenting questions based on your SRS progress.
- **Browse Screen**: Switch between Kanji and Vocabulary using the tabs at the top. Swipe left or right to navigate between levels. Use the scrollable navigator at the bottom to jump to a specific level quickly.

View File

@@ -1,12 +1,12 @@
plugins {
id("com.android.application")
id("kotlin-android")
id("org.jetbrains.kotlin.android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.untitled1"
namespace = "com.crylia.hirameki"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -20,17 +20,31 @@ android {
}
defaultConfig {
applicationId = "com.crylia.wanikani_kanji_srs"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
applicationId = "com.crylia.hirameki_srs"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
create("release") {
storeFile = file("hirameki-release-key.jks")
storePassword = project.findProperty("KEYSTORE_PASSWORD")?.toString()
keyAlias = project.findProperty("KEY_ALIAS")?.toString()
keyPassword = project.findProperty("KEY_PASSWORD")?.toString()
}
}
buildTypes {
release {
getByName("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}

View File

@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="WaniKani Kanji Srs"
android:label="Hirameki SRS"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -42,5 +42,8 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
</manifest>

View File

@@ -1,4 +1,4 @@
package com.example.untitled1
package com.crylia.hirameki
import io.flutter.embedding.android.FlutterActivity

View File

@@ -1,3 +1,11 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
// Ensure these match your Flutter Gradle plugin requirements
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
allprojects {
repositories {
google()
@@ -5,18 +13,17 @@ allprojects {
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
// Optional: custom build directory (keep this if you really need a shared build folder)
val newBuildDir = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.set(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
val newSubprojectBuildDir = newBuildDir.dir(name)
layout.buildDirectory.set(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {

Binary file not shown.

BIN
assets/sfx/correct.wav Normal file

Binary file not shown.

BIN
assets/sfx/incorrect.wav Normal file

Binary file not shown.

34
ios/.gitignore vendored
View File

@@ -1,34 +0,0 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -1 +0,0 @@
#include "Generated.xcconfig"

View File

@@ -1 +0,0 @@
#include "Generated.xcconfig"

View File

@@ -1,616 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -1,13 +0,0 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -1,122 +0,0 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,23 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

View File

@@ -1,5 +0,0 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
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.

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Untitled1</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>untitled1</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -1 +0,0 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -1,12 +0,0 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -1,14 +1,33 @@
import 'package:flutter/material.dart';
import 'package:hirameki_srs/src/models/theme_model.dart';
import 'package:hirameki_srs/src/services/vocab_deck_repository.dart';
import 'package:provider/provider.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'src/services/deck_repository.dart';
import 'src/screens/start_screen.dart';
import 'src/services/tts_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await dotenv.load(fileName: ".env");
} catch (_) {}
runApp(
Provider<DeckRepository>(
create: (_) => DeckRepository(),
MultiProvider(
providers: [
Provider<DeckRepository>(create: (_) => DeckRepository()),
Provider<VocabDeckRepository>(create: (_) => VocabDeckRepository()),
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel()),
Provider<TtsService>(
create: (_) {
final ttsService = TtsService();
ttsService.initTts();
return ttsService;
},
dispose: (_, ttsService) => ttsService.dispose(),
),
],
child: const WkApp(),
),
);
@@ -19,11 +38,15 @@ class WkApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WaniKani SRS',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(useMaterial3: true),
home: const StartScreen(),
return Consumer<ThemeModel>(
builder: (context, themeModel, child) {
return MaterialApp(
title: 'Hirameki SRS',
debugShowCheckedModeBanner: false,
theme: themeModel.currentTheme,
home: const StartScreen(),
);
},
);
}
}

View File

@@ -1,14 +1,24 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/subject.dart';
import '../models/kanji_item.dart';
import '../models/vocabulary_item.dart';
class WkClient {
final String apiKey;
final Map<String, String> headers;
final String base = 'https://api.wanikani.com/v2';
WkClient(this.apiKey) : headers = {'Authorization': 'Bearer $apiKey', 'Wanikani-Revision': '20170710', 'Accept': 'application/json'};
WkClient(this.apiKey)
: headers = {
'Authorization': 'Bearer $apiKey',
'Wanikani-Revision': '20170710',
'Accept': 'application/json',
};
Future<List<Map<String, dynamic>>> fetchAllAssignments({List<String>? subjectTypes}) async {
Future<List<Map<String, dynamic>>> fetchAllAssignments({
List<String>? subjectTypes,
}) async {
final out = <Map<String, dynamic>>[];
String url = '$base/assignments?page=1';
if (subjectTypes != null && subjectTypes.isNotEmpty) {
@@ -30,13 +40,15 @@ class WkClient {
return out;
}
Future<List<Map<String, dynamic>>> fetchAllSubjects({List<String>? types}) async {
Future<List<Map<String, dynamic>>> fetchAllSubjects({
List<String>? types,
}) async {
final out = <Map<String, dynamic>>[];
String url = '$base/subjects';
if (types != null && types.isNotEmpty) {
url += '?types=${types.join(',')}';
}
while (url.isNotEmpty) {
final resp = await http.get(Uri.parse(url), headers: headers);
if (resp.statusCode != 200) throw Exception('API ${resp.statusCode}');
@@ -56,7 +68,10 @@ class WkClient {
final out = <Map<String, dynamic>>[];
const batch = 100;
for (var i = 0; i < ids.length; i += batch) {
final chunk = ids.sublist(i, i + batch > ids.length ? ids.length : i + batch);
final chunk = ids.sublist(
i,
i + batch > ids.length ? ids.length : i + batch,
);
String url = '$base/subjects?ids=${chunk.join(',')}&page=1';
while (true) {
final resp = await http.get(Uri.parse(url), headers: headers);
@@ -73,4 +88,14 @@ class WkClient {
}
return out;
}
static Subject createSubjectFromMap(Map<String, dynamic> map) {
final String object = map['object'];
if (object == 'kanji') {
return KanjiItem.fromSubject(map);
} else if (object == 'vocabulary') {
return VocabularyItem.fromSubject(map);
}
throw Exception('Unknown subject type: $object');
}
}

View File

@@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'screens/home_screen.dart';
import 'services/deck_repository.dart';
class WkApp extends StatelessWidget {
const WkApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<DeckRepository>(create: (_) => DeckRepository()),
],
child: MaterialApp(
title: 'WaniKani SRS',
theme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
home: const HomeScreen(),
),
);
}
}

View File

@@ -0,0 +1,108 @@
class CustomKanjiItem {
final String characters;
final String meaning;
final String? kanji;
final bool useInterval;
SrsData srsData;
CustomKanjiItem({
required this.characters,
required this.meaning,
this.kanji,
this.useInterval = false,
SrsData? srsData,
}) : srsData = srsData ?? SrsData();
factory CustomKanjiItem.fromJson(Map<String, dynamic> json) {
SrsData srsData;
if (json['srsData'] != null) {
srsData = SrsData.fromJson(json['srsData']);
if (json['nextReview'] != null) {
final oldNextReview = DateTime.parse(json['nextReview'] as String);
srsData.japaneseToEnglishNextReview ??= oldNextReview;
srsData.englishToJapaneseNextReview ??= oldNextReview;
srsData.listeningComprehensionNextReview ??= oldNextReview;
}
} else {
DateTime? nextReview = json['nextReview'] != null
? DateTime.parse(json['nextReview'] as String)
: null;
srsData = SrsData(
japaneseToEnglish: json['srsLevel'] as int? ?? 0,
japaneseToEnglishNextReview: nextReview,
englishToJapanese: json['srsLevel'] as int? ?? 0,
englishToJapaneseNextReview: nextReview,
listeningComprehension: json['srsLevel'] as int? ?? 0,
listeningComprehensionNextReview: nextReview,
);
}
return CustomKanjiItem(
characters: json['characters'] as String,
meaning: json['meaning'] as String,
kanji: json['kanji'] as String?,
useInterval: json['useInterval'] as bool? ?? false,
srsData: srsData,
);
}
Map<String, dynamic> toJson() {
return {
'characters': characters,
'meaning': meaning,
'kanji': kanji,
'useInterval': useInterval,
'srsData': srsData.toJson(),
};
}
}
class SrsData {
int japaneseToEnglish;
DateTime? japaneseToEnglishNextReview;
int englishToJapanese;
DateTime? englishToJapaneseNextReview;
int listeningComprehension;
DateTime? listeningComprehensionNextReview;
SrsData({
this.japaneseToEnglish = 0,
this.japaneseToEnglishNextReview,
this.englishToJapanese = 0,
this.englishToJapaneseNextReview,
this.listeningComprehension = 0,
this.listeningComprehensionNextReview,
});
factory SrsData.fromJson(Map<String, dynamic> json) {
return SrsData(
japaneseToEnglish: json['japaneseToEnglish'] as int? ?? 0,
japaneseToEnglishNextReview: json['japaneseToEnglishNextReview'] != null
? DateTime.parse(json['japaneseToEnglishNextReview'] as String)
: null,
englishToJapanese: json['englishToJapanese'] as int? ?? 0,
englishToJapaneseNextReview: json['englishToJapaneseNextReview'] != null
? DateTime.parse(json['englishToJapaneseNextReview'] as String)
: null,
listeningComprehension: json['listeningComprehension'] as int? ?? 0,
listeningComprehensionNextReview:
json['listeningComprehensionNextReview'] != null
? DateTime.parse(json['listeningComprehensionNextReview'] as String)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'japaneseToEnglish': japaneseToEnglish,
'japaneseToEnglishNextReview': japaneseToEnglishNextReview
?.toIso8601String(),
'englishToJapanese': englishToJapanese,
'englishToJapaneseNextReview': englishToJapaneseNextReview
?.toIso8601String(),
'listeningComprehension': listeningComprehension,
'listeningComprehensionNextReview': listeningComprehensionNextReview
?.toIso8601String(),
};
}
}

View File

@@ -1,51 +1,24 @@
enum QuizMode { kanjiToEnglish, englishToKanji, reading }
import 'subject.dart';
class SrsItem {
final int kanjiId;
final QuizMode quizMode;
final String? readingType; // 'onyomi' or 'kunyomi'
int srsStage;
DateTime lastAsked;
SrsItem({
required this.kanjiId,
required this.quizMode,
this.readingType,
this.srsStage = 0,
DateTime? lastAsked,
}) : lastAsked = lastAsked ?? DateTime.now();
}
class KanjiItem {
final int id;
final String characters;
final List<String> meanings;
class KanjiItem extends Subject {
final List<String> onyomi;
final List<String> kunyomi;
final Map<String, SrsItem> srsItems = {};
KanjiItem({
required this.id,
required this.characters,
required this.meanings,
required super.id,
required super.level,
required super.characters,
required super.meanings,
required this.onyomi,
required this.kunyomi,
});
factory KanjiItem.fromSubject(Map<String, dynamic> subj) {
final int id = subj['id'] as int;
final data = subj['data'] as Map<String, dynamic>;
final String characters = (data['characters'] ?? '') as String;
final List<String> meanings = <String>[];
final commonFields = Subject.parseCommonFields(subj);
final data = commonFields['data'] as Map<String, dynamic>;
final List<String> onyomi = <String>[];
final List<String> kunyomi = <String>[];
if (data['meanings'] != null) {
for (final m in data['meanings'] as List) {
meanings.add((m['meaning'] as String).toLowerCase());
}
}
if (data['readings'] != null) {
for (final r in data['readings'] as List) {
final typ = r['type'] as String? ?? '';
@@ -59,9 +32,10 @@ class KanjiItem {
}
return KanjiItem(
id: id,
characters: characters,
meanings: meanings,
id: commonFields['id'] as int,
level: commonFields['level'] as int,
characters: commonFields['characters'] as String,
meanings: commonFields['meanings'] as List<String>,
onyomi: onyomi,
kunyomi: kunyomi,
);
@@ -79,85 +53,3 @@ String _katakanaToHiragana(String input) {
}
return buf.toString();
}
enum VocabQuizMode { vocabToEnglish, englishToVocab }
class VocabSrsItem {
final int vocabId;
final VocabQuizMode quizMode;
int srsStage;
DateTime lastAsked;
VocabSrsItem({
required this.vocabId,
required this.quizMode,
this.srsStage = 0,
DateTime? lastAsked,
}) : lastAsked = lastAsked ?? DateTime.now();
}
class PronunciationAudio {
final String url;
final String gender;
PronunciationAudio({required this.url, required this.gender});
}
class VocabularyItem {
final int id;
final String characters;
final List<String> meanings;
final List<String> readings;
final List<PronunciationAudio> pronunciationAudios;
final Map<String, VocabSrsItem> srsItems = {};
VocabularyItem(
{required this.id,
required this.characters,
required this.meanings,
required this.readings,
required this.pronunciationAudios});
factory VocabularyItem.fromSubject(Map<String, dynamic> subj) {
final int id = subj['id'] as int;
final data = subj['data'] as Map<String, dynamic>;
final String characters = (data['characters'] ?? '') as String;
final List<String> meanings = <String>[];
final List<String> readings = <String>[];
final List<PronunciationAudio> pronunciationAudios = <PronunciationAudio>[];
if (data['meanings'] != null) {
for (final m in data['meanings'] as List) {
meanings.add((m['meaning'] as String).toLowerCase());
}
}
if (data['readings'] != null) {
for (final r in data['readings'] as List) {
readings.add(r['reading'] as String);
}
}
if (data['pronunciation_audios'] != null) {
for (final audio in data['pronunciation_audios'] as List) {
final url = audio['url'] as String?;
final metadata = audio['metadata'] as Map<String, dynamic>?;
final gender = metadata?['gender'] as String?;
if (url != null && gender != null) {
pronunciationAudios.add(PronunciationAudio(
url: url,
gender: gender,
));
}
}
}
return VocabularyItem(
id: id,
characters: characters,
meanings: meanings,
readings: readings,
pronunciationAudios: pronunciationAudios);
}
}

View File

@@ -0,0 +1,19 @@
enum QuizMode { kanjiToEnglish, englishToKanji, reading, vocabToEnglish, englishToVocab, audioToEnglish }
class SrsItem {
final int subjectId;
final QuizMode quizMode;
final String? readingType;
int srsStage;
DateTime lastAsked;
bool disabled;
SrsItem({
required this.subjectId,
required this.quizMode,
this.readingType,
this.srsStage = 0,
DateTime? lastAsked,
this.disabled = false,
}) : lastAsked = lastAsked ?? DateTime.now();
}

View File

@@ -0,0 +1,38 @@
import 'srs_item.dart';
abstract class Subject {
final int id;
final int level;
final String characters;
final List<String> meanings;
final Map<String, SrsItem> srsItems = {};
Subject({
required this.id,
required this.level,
required this.characters,
required this.meanings,
});
static Map<String, dynamic> parseCommonFields(Map<String, dynamic> subj) {
final int id = subj['id'] as int;
final data = subj['data'] as Map<String, dynamic>;
final int level = data['level'] as int;
final String characters = (data['characters'] ?? '') as String;
final List<String> meanings = <String>[];
if (data['meanings'] != null) {
for (final m in data['meanings'] as List) {
meanings.add((m['meaning'] as String).toLowerCase());
}
}
return {
'id': id,
'level': level,
'characters': characters,
'meanings': meanings,
'data': data,
};
}
}

View File

@@ -0,0 +1,15 @@
import 'kanji_item.dart';
import 'vocabulary_item.dart';
import 'subject.dart';
class SubjectFactory {
static Subject fromMap(Map<String, dynamic> map) {
final String object = map['object'];
if (object == 'kanji') {
return KanjiItem.fromSubject(map);
} else if (object == 'vocabulary') {
return VocabularyItem.fromSubject(map);
}
throw Exception('Unknown subject type: $object');
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:hirameki_srs/src/themes.dart';
class ThemeModel extends ChangeNotifier {
ThemeData _currentTheme = Themes.dark;
ThemeData get currentTheme => _currentTheme;
void setTheme(ThemeData theme) {
_currentTheme = theme;
notifyListeners();
}
}

View File

@@ -0,0 +1,56 @@
import 'subject.dart';
class PronunciationAudio {
final String url;
final String gender;
PronunciationAudio({required this.url, required this.gender});
}
class VocabularyItem extends Subject {
final List<String> readings;
final List<PronunciationAudio> pronunciationAudios;
VocabularyItem({
required super.id,
required super.level,
required super.characters,
required super.meanings,
required this.readings,
required this.pronunciationAudios,
});
factory VocabularyItem.fromSubject(Map<String, dynamic> subj) {
final commonFields = Subject.parseCommonFields(subj);
final data = commonFields['data'] as Map<String, dynamic>;
final List<String> readings = <String>[];
final List<PronunciationAudio> pronunciationAudios = <PronunciationAudio>[];
if (data['readings'] != null) {
for (final r in data['readings'] as List) {
readings.add(r['reading'] as String);
}
}
if (data['pronunciation_audios'] != null) {
for (final audio in data['pronunciation_audios'] as List) {
final url = audio['url'] as String?;
final metadata = audio['metadata'] as Map<String, dynamic>?;
final gender = metadata?['gender'] as String?;
if (url != null && gender != null) {
pronunciationAudios.add(PronunciationAudio(url: url, gender: gender));
}
}
}
return VocabularyItem(
id: commonFields['id'] as int,
level: commonFields['level'] as int,
characters: commonFields['characters'] as String,
meanings: commonFields['meanings'] as List<String>,
readings: readings,
pronunciationAudios: pronunciationAudios,
);
}
}

View File

@@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
import 'package:kana_kit/kana_kit.dart';
import '../models/custom_kanji_item.dart';
import '../services/custom_deck_repository.dart';
class AddCardScreen extends StatefulWidget {
const AddCardScreen({super.key});
@override
State<AddCardScreen> createState() => _AddCardScreenState();
}
class _AddCardScreenState extends State<AddCardScreen> {
final _formKey = GlobalKey<FormState>();
final _japaneseController = TextEditingController();
final _englishController = TextEditingController();
final _kanjiController = TextEditingController();
final _kanaKit = const KanaKit();
final _deckRepository = CustomDeckRepository();
bool _useInterval = false;
late FocusNode _japaneseFocusNode;
@override
void initState() {
super.initState();
_japaneseController.addListener(_convertToKana);
_japaneseFocusNode = FocusNode();
_japaneseFocusNode.addListener(_onJapaneseFocusChange);
}
@override
void dispose() {
_japaneseController.removeListener(_convertToKana);
_japaneseController.dispose();
_englishController.dispose();
_kanjiController.dispose();
_japaneseFocusNode.removeListener(_onJapaneseFocusChange);
_japaneseFocusNode.dispose();
super.dispose();
}
void _convertToKana() {
final text = _japaneseController.text;
final selection = _japaneseController.selection;
final offset = selection.baseOffset;
if ((offset > 1 && text[offset - 1] == 'n' && text[offset - 2] != 'n') ||
(offset == 1 && text[offset - 1] == 'n')) {
return;
}
final converted = _kanaKit.toKana(text);
if (converted != text) {
_japaneseController.value = _japaneseController.value.copyWith(
text: converted,
selection: TextSelection.fromPosition(
TextPosition(offset: converted.length),
),
);
}
}
void _onJapaneseFocusChange() {
if (!_japaneseFocusNode.hasFocus) {
_forceNConversion();
}
}
void _forceNConversion() {
final text = _japaneseController.text;
if (text.isNotEmpty &&
text.endsWith('n') &&
_kanaKit.toKana(text) != text) {
_japaneseController.text = _kanaKit.toKana(text);
}
}
void _saveCard() {
if (_formKey.currentState!.validate()) {
final srsData = _useInterval
? SrsData(
japaneseToEnglishNextReview: DateTime.now(),
englishToJapaneseNextReview: DateTime.now(),
listeningComprehensionNextReview: DateTime.now(),
)
: SrsData();
final newItem = CustomKanjiItem(
characters: _japaneseController.text,
meaning: _englishController.text,
kanji: _kanjiController.text.trim().isNotEmpty
? _kanjiController.text.trim()
: null,
useInterval: _useInterval,
srsData: srsData,
);
_deckRepository.addCard(newItem);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Add New Card')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _japaneseController,
focusNode: _japaneseFocusNode,
decoration: const InputDecoration(
labelText: 'Japanese (Kana)',
hintText: 'Enter Japanese vocabulary or kanji',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a Japanese term';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _kanjiController,
decoration: const InputDecoration(
labelText: 'Japanese (Kanji)',
hintText: 'Enter the kanji (optional)',
),
),
const SizedBox(height: 16),
TextFormField(
controller: _englishController,
decoration: const InputDecoration(
labelText: 'English',
hintText: 'Enter the English meaning',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an English meaning';
}
return null;
},
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Use Interval-based SRS'),
value: _useInterval,
onChanged: (value) {
setState(() {
_useInterval = value;
});
},
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _saveCard,
child: const Text('Save Card'),
),
],
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import '../models/custom_kanji_item.dart';
import '../services/custom_deck_repository.dart';
class CustomCardDetailsScreen extends StatefulWidget {
final CustomKanjiItem item;
final CustomDeckRepository repository;
const CustomCardDetailsScreen({
super.key,
required this.item,
required this.repository,
});
@override
State<CustomCardDetailsScreen> createState() =>
_CustomCardDetailsScreenState();
}
class _CustomCardDetailsScreenState extends State<CustomCardDetailsScreen> {
late TextEditingController _japaneseController;
late TextEditingController _englishController;
late TextEditingController _kanjiController;
late bool _useInterval;
@override
void initState() {
super.initState();
_japaneseController = TextEditingController(text: widget.item.characters);
_englishController = TextEditingController(text: widget.item.meaning);
_kanjiController = TextEditingController(text: widget.item.kanji);
_useInterval = widget.item.useInterval;
}
@override
void dispose() {
_japaneseController.dispose();
_englishController.dispose();
_kanjiController.dispose();
super.dispose();
}
void _saveChanges() {
final updatedItem = CustomKanjiItem(
characters: _japaneseController.text,
meaning: _englishController.text,
kanji: _kanjiController.text.trim().isNotEmpty
? _kanjiController.text.trim()
: null,
useInterval: _useInterval,
srsData: widget.item.srsData,
);
widget.repository.updateCard(updatedItem);
Navigator.of(context).pop(true);
}
void _deleteCard() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Card'),
content: const Text('Are you sure you want to delete this card?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
widget.repository.deleteCard(widget.item);
Navigator.of(context).pop();
Navigator.of(context).pop(true);
},
child: const Text('Delete'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Edit Card'),
actions: [
IconButton(icon: const Icon(Icons.delete), onPressed: _deleteCard),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
controller: _japaneseController,
decoration: const InputDecoration(labelText: 'Japanese (Kana)'),
),
TextFormField(
controller: _kanjiController,
decoration: const InputDecoration(labelText: 'Japanese (Kanji)'),
),
TextFormField(
controller: _englishController,
decoration: const InputDecoration(labelText: 'English'),
),
SwitchListTile(
title: const Text('Use Interval SRS'),
value: _useInterval,
onChanged: (value) {
setState(() {
_useInterval = value;
});
},
),
const SizedBox(height: 20),
const Text(
'SRS Levels',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Jpn→Eng: ${widget.item.srsData.japaneseToEnglish} (Next review: ${widget.item.srsData.japaneseToEnglishNextReview?.toString() ?? 'N/A'})',
),
Text(
'Eng→Jpn: ${widget.item.srsData.englishToJapanese} (Next review: ${widget.item.srsData.englishToJapaneseNextReview?.toString() ?? 'N/A'})',
),
Text(
'Listening: ${widget.item.srsData.listeningComprehension} (Next review: ${widget.item.srsData.listeningComprehensionNextReview?.toString() ?? 'N/A'})',
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _saveChanges,
child: const Text('Save Changes'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,458 @@
import 'package:flutter/material.dart';
import 'dart:math';
import '../models/custom_kanji_item.dart';
import '../widgets/options_grid.dart';
import '../widgets/kanji_card.dart';
import 'package:provider/provider.dart';
import '../services/tts_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:audioplayers/audioplayers.dart';
enum CustomQuizMode {
japaneseToEnglish,
englishToJapanese,
listeningComprehension,
}
class CustomQuizScreen extends StatefulWidget {
final List<CustomKanjiItem> deck;
final CustomQuizMode quizMode;
final Function(CustomKanjiItem) onCardReviewed;
final bool useKanji;
final bool isActive;
const CustomQuizScreen({
super.key,
required this.deck,
required this.quizMode,
required this.onCardReviewed,
required this.useKanji,
required this.isActive,
});
@override
State<CustomQuizScreen> createState() => CustomQuizScreenState();
}
class _CustomQuizState {
CustomKanjiItem? current;
List<String> options = [];
List<String> correctAnswers = [];
int score = 0;
int asked = 0;
Key key = UniqueKey();
String? selectedOption;
bool showResult = false;
Set<String> wrongItems = {};
}
class CustomQuizScreenState extends State<CustomQuizScreen>
with TickerProviderStateMixin {
final _quizState = _CustomQuizState();
List<CustomKanjiItem> _shuffledDeck = [];
int _sessionDeckSize = 0;
bool _isAnswering = false;
late AnimationController _shakeController;
late Animation<double> _shakeAnimation;
final _audioPlayer = AudioPlayer();
bool _playIncorrectSound = true;
bool _playCorrectSound = true;
bool _playNarrator = true;
@override
void initState() {
super.initState();
_shuffledDeck = widget.deck.toList()..shuffle();
_sessionDeckSize = _shuffledDeck.length;
_shakeController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_shakeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn),
);
_loadSettings();
_nextQuestion();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
_playNarrator = prefs.getBool('playNarrator') ?? true;
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
void didUpdateWidget(CustomQuizScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.deck != oldWidget.deck && !widget.isActive) {
_shuffledDeck = widget.deck.toList()..shuffle();
_sessionDeckSize = _shuffledDeck.length;
_nextQuestion();
}
if (widget.useKanji != oldWidget.useKanji) {
_nextQuestion();
}
}
void playAudio() async {
final quizState = _quizState;
if (widget.quizMode == CustomQuizMode.listeningComprehension &&
quizState.current != null &&
_playNarrator) {
final ttsService = Provider.of<TtsService>(context, listen: false);
await ttsService.speak(quizState.current!.characters);
}
}
@override
void dispose() {
_shakeController.dispose();
super.dispose();
}
void _answer(String option) async {
final quizState = _quizState;
final current = quizState.current!;
final isCorrect = quizState.correctAnswers
.map((a) => a.toLowerCase().trim())
.contains(option.toLowerCase().trim());
setState(() {
quizState.selectedOption = option;
quizState.showResult = true;
_isAnswering = true;
});
if (current.useInterval) {
_updateSrsLevel(current, isCorrect);
}
final correctDisplay = (widget.quizMode == CustomQuizMode.englishToJapanese)
? (widget.useKanji && current.kanji != null
? current.kanji!
: current.characters)
: current.meaning;
final snack = SnackBar(
content: Text(
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
style: TextStyle(
color: isCorrect
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
duration: const Duration(milliseconds: 900),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snack);
}
if (isCorrect) {
quizState.asked += 1;
if (!quizState.wrongItems.contains(current.characters)) {
quizState.score += 1;
}
if (_playCorrectSound && !_playNarrator) {
await _audioPlayer.play(AssetSource('sfx/correct.wav'));
} else if (_playNarrator) {
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
widget.quizMode == CustomQuizMode.englishToJapanese) {
await _speak(current.characters);
}
}
await Future.delayed(const Duration(milliseconds: 500));
} else {
quizState.wrongItems.add(current.characters);
_shuffledDeck.add(current);
_shuffledDeck.shuffle();
if (_playIncorrectSound) {
await _audioPlayer.play(AssetSource('sfx/incorrect.wav'));
}
_shakeController.forward(from: 0);
await Future.delayed(const Duration(milliseconds: 900));
}
Future.delayed(const Duration(milliseconds: 900), () {
if (mounted) {
_nextQuestion();
}
});
}
void _updateSrsLevel(CustomKanjiItem item, bool isCorrect) {
int currentSrsLevel = 0;
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
currentSrsLevel = item.srsData.japaneseToEnglish;
break;
case CustomQuizMode.englishToJapanese:
currentSrsLevel = item.srsData.englishToJapanese;
break;
case CustomQuizMode.listeningComprehension:
currentSrsLevel = item.srsData.listeningComprehension;
break;
}
if (isCorrect) {
currentSrsLevel++;
final interval = pow(2, currentSrsLevel).toInt();
final newNextReview = DateTime.now().add(Duration(hours: interval));
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
item.srsData.japaneseToEnglishNextReview = newNextReview;
break;
case CustomQuizMode.englishToJapanese:
item.srsData.englishToJapaneseNextReview = newNextReview;
break;
case CustomQuizMode.listeningComprehension:
item.srsData.listeningComprehensionNextReview = newNextReview;
break;
}
} else {
currentSrsLevel = max(0, currentSrsLevel - 1);
final newNextReview = DateTime.now().add(const Duration(hours: 1));
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
item.srsData.japaneseToEnglishNextReview = newNextReview;
break;
case CustomQuizMode.englishToJapanese:
item.srsData.englishToJapaneseNextReview = newNextReview;
break;
case CustomQuizMode.listeningComprehension:
item.srsData.listeningComprehensionNextReview = newNextReview;
break;
}
}
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
item.srsData.japaneseToEnglish = currentSrsLevel;
break;
case CustomQuizMode.englishToJapanese:
item.srsData.englishToJapanese = currentSrsLevel;
break;
case CustomQuizMode.listeningComprehension:
item.srsData.listeningComprehension = currentSrsLevel;
break;
}
widget.onCardReviewed(item);
}
void _nextQuestion() {
final quizState = _quizState;
if (_shuffledDeck.isEmpty) {
setState(() {
quizState.current = null;
});
return;
}
quizState.current = _shuffledDeck.removeAt(0);
quizState.key = UniqueKey();
quizState.correctAnswers = [];
quizState.options = [];
quizState.selectedOption = null;
quizState.showResult = false;
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
widget.quizMode == CustomQuizMode.listeningComprehension) {
quizState.correctAnswers = [quizState.current!.meaning];
quizState.options = [quizState.correctAnswers.first];
} else {
quizState.correctAnswers = [
widget.useKanji && quizState.current!.kanji != null
? quizState.current!.kanji!
: quizState.current!.characters,
];
quizState.options = [quizState.correctAnswers.first];
}
final otherItems = widget.deck
.where((item) => item.characters != quizState.current!.characters)
.toList();
otherItems.shuffle();
for (var i = 0; i < min(3, otherItems.length); i++) {
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
widget.quizMode == CustomQuizMode.listeningComprehension) {
quizState.options.add(otherItems[i].meaning);
} else {
quizState.options.add(
widget.useKanji && otherItems[i].kanji != null
? otherItems[i].kanji!
: otherItems[i].characters,
);
}
}
while (quizState.options.length < 4) {
quizState.options.add('---');
}
quizState.options.shuffle();
setState(() {
_isAnswering = false;
});
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
_speak(quizState.current!.characters);
}
}
Future<void> _speak(String text) async {
final ttsService = Provider.of<TtsService>(context, listen: false);
await ttsService.speak(text);
}
void _onOptionSelected(String option) {
if (!_isAnswering) {
_answer(option);
}
}
@override
Widget build(BuildContext context) {
final quizState = _quizState;
if (quizState.current == null) {
return Center(
child: Text(
'Review session complete!',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
);
}
final currentItem = quizState.current!;
Widget promptWidget;
String subtitle = '';
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
promptWidget = IconButton(
icon: const Icon(Icons.volume_up, size: 64),
onPressed: () => _speak(currentItem.characters),
);
} else if (widget.quizMode == CustomQuizMode.englishToJapanese) {
promptWidget = Text(
currentItem.meaning,
style: TextStyle(
fontSize: 48,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
);
} else {
final promptText = widget.useKanji && currentItem.kanji != null
? currentItem.kanji!
: currentItem.characters;
promptWidget = GestureDetector(
onTap: () => _speak(promptText),
child: Text(
promptText,
style: TextStyle(
fontSize: 48,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
);
}
return Padding(
key: quizState.key,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${quizState.asked} / $_sessionDeckSize',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: _sessionDeckSize > 0
? quizState.asked / _sessionDeckSize
: 0,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const SizedBox(height: 18),
Expanded(
flex: 3,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 0,
maxWidth: 500,
minHeight: 150,
),
child: KanjiCard(
characterWidget: promptWidget,
subtitle: subtitle,
),
),
),
),
const SizedBox(height: 12),
SafeArea(
top: false,
child: Column(
children: [
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_shakeAnimation.value * 10, 0),
child: child,
);
},
child: OptionsGrid(
options: quizState.options,
onSelected: _isAnswering ? (option) {} : _onOptionSelected,
selectedOption: quizState.selectedOption,
correctAnswers: quizState.correctAnswers,
showResult: quizState.showResult,
),
),
const SizedBox(height: 8),
Text(
'Score: ${quizState.score} / ${quizState.asked}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import '../models/custom_kanji_item.dart';
import '../services/custom_deck_repository.dart';
import 'custom_quiz_screen.dart';
class CustomSrsScreen extends StatefulWidget {
const CustomSrsScreen({super.key});
@override
State<CustomSrsScreen> createState() => _CustomSrsScreenState();
}
class _CustomSrsScreenState extends State<CustomSrsScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final _deckRepository = CustomDeckRepository();
List<CustomKanjiItem> _deck = [];
bool _useKanji = false;
final _quizScreenKeys = [
GlobalKey<CustomQuizScreenState>(),
GlobalKey<CustomQuizScreenState>(),
GlobalKey<CustomQuizScreenState>(),
];
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
final key = _quizScreenKeys[_tabController.index];
key.currentState?.playAudio();
}
setState(() {});
});
_loadDeck();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadDeck() async {
final deck = await _deckRepository.getCustomDeck();
setState(() {
_deck = deck;
});
}
Future<void> _updateCard(CustomKanjiItem item) async {
final index = _deck.indexWhere(
(element) => element.characters == item.characters,
);
if (index != -1) {
setState(() {
_deck[index] = item;
});
await _deckRepository.saveDeck(_deck);
}
}
@override
Widget build(BuildContext context) {
final now = DateTime.now();
final jpnToEngReviewDeck = _deck.where((item) {
if (!item.useInterval) return true;
return item.srsData.japaneseToEnglishNextReview == null ||
item.srsData.japaneseToEnglishNextReview!.isBefore(now);
}).toList();
final engToJpnReviewDeck = _deck.where((item) {
if (!item.useInterval) return true;
return item.srsData.englishToJapaneseNextReview == null ||
item.srsData.englishToJapaneseNextReview!.isBefore(now);
}).toList();
final listeningReviewDeck = _deck.where((item) {
if (!item.useInterval) return true;
return item.srsData.listeningComprehensionNextReview == null ||
item.srsData.listeningComprehensionNextReview!.isBefore(now);
}).toList();
final allDecksEmpty =
jpnToEngReviewDeck.isEmpty &&
engToJpnReviewDeck.isEmpty &&
listeningReviewDeck.isEmpty;
return Scaffold(
appBar: AppBar(
title: const Text('Custom SRS'),
actions: [
if (_tabController.index != 2)
Row(
children: [
const Text('Kanji'),
Switch(
value: _useKanji,
onChanged: (value) {
setState(() {
_useKanji = value;
});
},
),
],
),
],
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Jpn→Eng'),
Tab(text: 'Eng→Jpn'),
Tab(text: 'Listening'),
],
),
),
body: _deck.isEmpty
? const Center(child: Text('Add cards to start quizzing!'))
: allDecksEmpty
? const Center(child: Text('No cards due for review.'))
: TabBarView(
controller: _tabController,
children: [
CustomQuizScreen(
key: _quizScreenKeys[0],
deck: jpnToEngReviewDeck,
quizMode: CustomQuizMode.japaneseToEnglish,
onCardReviewed: _updateCard,
useKanji: _useKanji,
isActive: _tabController.index == 0,
),
CustomQuizScreen(
key: _quizScreenKeys[1],
deck: engToJpnReviewDeck,
quizMode: CustomQuizMode.englishToJapanese,
onCardReviewed: _updateCard,
useKanji: _useKanji,
isActive: _tabController.index == 1,
),
CustomQuizScreen(
key: _quizScreenKeys[2],
deck: listeningReviewDeck,
quizMode: CustomQuizMode.listeningComprehension,
onCardReviewed: _updateCard,
useKanji: _useKanji,
isActive: _tabController.index == 2,
),
],
),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/kanji_item.dart';
import '../models/srs_item.dart';
import '../services/deck_repository.dart';
import '../services/distractor_generator.dart';
import '../widgets/kanji_card.dart';
@@ -18,40 +19,70 @@ class _ReadingInfo {
_ReadingInfo(this.correctReadings, this.hint);
}
class _QuizState {
KanjiItem? current;
List<String> options = [];
List<String> correctAnswers = [];
String readingHint = '';
int score = 0;
int asked = 0;
Key key = UniqueKey();
String? selectedOption;
bool showResult = false;
Set<int> wrongItems = {};
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
const HomeScreen({super.key, this.distractorGenerator});
final DistractorGenerator? distractorGenerator;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<KanjiItem> _deck = [];
bool _loading = false;
bool _isAnswering = false;
String _status = 'Loading deck...';
final DistractorGenerator _dg = DistractorGenerator();
late final DistractorGenerator _dg;
final Random _random = Random();
final _audioPlayer = AudioPlayer();
QuizMode _mode = QuizMode.kanjiToEnglish;
KanjiItem? _current;
List<String> _options = [];
List<String> _correctAnswers = [];
String _readingHint = '';
int _score = 0;
int _asked = 0;
final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
final _sessionDecks = <int, List<KanjiItem>>{};
final _sessionDeckSizes = <int, int>{};
_QuizState get _currentQuizState => _quizStates[_tabController.index];
bool _playIncorrectSound = true;
bool _playCorrectSound = true;
bool _apiKeyMissing = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
setState(() {});
});
_dg = widget.distractorGenerator ?? DistractorGenerator();
_loadSettings();
_loadDeck();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
});
}
@@ -68,11 +99,10 @@ class _HomeScreenState extends State<HomeScreen> {
final apiKey = repo.apiKey;
if (apiKey == null || apiKey.isEmpty) {
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
}
setState(() {
_apiKeyMissing = true;
_loading = false;
});
return;
}
@@ -88,9 +118,55 @@ class _HomeScreenState extends State<HomeScreen> {
_deck = items;
_status = 'Loaded ${items.length} kanji';
_loading = false;
_apiKeyMissing = false;
});
_nextQuestion();
final disabledLevels = <int>{};
final itemsByLevel = <int, List<KanjiItem>>{};
for (final item in _deck) {
(itemsByLevel[item.level] ??= []).add(item);
}
itemsByLevel.forEach((level, items) {
final allSrsItems = items
.expand((item) => item.srsItems.values)
.toList();
if (allSrsItems.isNotEmpty &&
allSrsItems.every((srs) => srs.disabled)) {
disabledLevels.add(level);
}
});
for (var i = 0; i < _tabController.length; i++) {
final mode = _modeForIndex(i);
final filteredDeck = _deck.where((item) {
if (disabledLevels.contains(item.level)) {
return false;
}
if (mode == QuizMode.reading) {
final onyomiSrs = item.srsItems['${QuizMode.reading}onyomi'];
final kunyomiSrs = item.srsItems['${QuizMode.reading}kunyomi'];
final hasOnyomi =
item.onyomi.isNotEmpty &&
(onyomiSrs == null || !onyomiSrs.disabled);
final hasKunyomi =
item.kunyomi.isNotEmpty &&
(kunyomiSrs == null || !kunyomiSrs.disabled);
return hasOnyomi || hasKunyomi;
}
final srsItem = item.srsItems[mode.toString()];
return srsItem == null || !srsItem.disabled;
}).toList();
filteredDeck.shuffle(_random);
_sessionDecks[i] = filteredDeck;
_sessionDeckSizes[i] = filteredDeck.length;
}
for (var i = 0; i < _tabController.length; i++) {
_nextQuestion(i);
}
} catch (e) {
setState(() {
_status = 'Error: $e';
@@ -120,125 +196,147 @@ class _HomeScreenState extends State<HomeScreen> {
return _ReadingInfo(readingsList, hint);
}
void _nextQuestion() {
_deck.sort((a, b) {
String srsKey(KanjiItem item) {
var key = _mode.toString();
if (_mode == QuizMode.reading) {
if (item.onyomi.isNotEmpty && item.kunyomi.isNotEmpty) {
key += _random.nextBool() ? 'onyomi' : 'kunyomi';
} else if (item.onyomi.isNotEmpty) {
key += 'onyomi';
} else {
key += 'kunyomi';
}
}
return key;
}
QuizMode _modeForIndex(int index) {
switch (index) {
case 0:
return QuizMode.kanjiToEnglish;
case 1:
return QuizMode.englishToKanji;
case 2:
return QuizMode.reading;
default:
return QuizMode.kanjiToEnglish;
}
}
final aSrsItem = a.srsItems[srsKey(a)];
final bSrsItem = b.srsItems[srsKey(b)];
void _nextQuestion([int? index]) {
final tabIndex = index ?? _tabController.index;
final quizState = _quizStates[tabIndex];
final sessionDeck = _sessionDecks[tabIndex];
final mode = _modeForIndex(tabIndex);
final aStage = aSrsItem?.srsStage ?? 0;
final bStage = bSrsItem?.srsStage ?? 0;
if (sessionDeck == null || sessionDeck.isEmpty) {
setState(() {
quizState.current = null;
_status = 'Quiz complete!';
});
return;
}
if (aStage != bStage) {
return aStage.compareTo(bStage);
}
quizState.current = sessionDeck.removeAt(0);
quizState.key = UniqueKey();
final aLastAsked =
aSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0);
final bLastAsked =
bSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0);
quizState.correctAnswers = [];
quizState.options = [];
quizState.readingHint = '';
quizState.selectedOption = null;
quizState.showResult = false;
if (aLastAsked != bLastAsked) {
return aLastAsked.compareTo(bLastAsked);
}
return _random.nextDouble().compareTo(_random.nextDouble());
});
_current = _deck.first;
_correctAnswers = [];
_options = [];
_readingHint = '';
switch (_mode) {
switch (mode) {
case QuizMode.kanjiToEnglish:
_correctAnswers = [_current!.meanings.first];
_options = [
_correctAnswers.first,
..._dg.generateMeanings(_current!, _deck, 3)
].map(_toTitleCase).toList()
..shuffle();
quizState.correctAnswers = [quizState.current!.meanings.first];
quizState.options = [
quizState.correctAnswers.first,
..._dg.generateMeanings(quizState.current!, _deck, 3),
].map(_toTitleCase).toList()..shuffle();
break;
case QuizMode.englishToKanji:
_correctAnswers = [_current!.characters];
_options = [
_correctAnswers.first,
..._dg.generateKanji(_current!, _deck, 3)
quizState.correctAnswers = [quizState.current!.characters];
quizState.options = [
quizState.correctAnswers.first,
..._dg.generateKanji(quizState.current!, _deck, 3),
]..shuffle();
break;
case QuizMode.reading:
final info = _pickReading(_current!);
_correctAnswers = info.correctReadings;
_readingHint = info.hint;
final info = _pickReading(quizState.current!);
quizState.correctAnswers = info.correctReadings;
quizState.readingHint = info.hint;
final readingsSource = _readingHint.contains("on'yomi")
final readingsSource = quizState.readingHint.contains("on'yomi")
? _deck.expand((k) => k.onyomi)
: _deck.expand((k) => k.kunyomi);
final distractors = readingsSource
.where((r) => !_correctAnswers.contains(r))
.toSet()
.toList()
..shuffle();
_options = ([
_correctAnswers[_random.nextInt(_correctAnswers.length)],
...distractors.take(3)
])
..shuffle();
final distractors =
readingsSource
.where((r) => !quizState.correctAnswers.contains(r))
.toSet()
.toList()
..shuffle();
quizState.options = ([
quizState.correctAnswers[_random.nextInt(
quizState.correctAnswers.length,
)],
...distractors.take(3),
])..shuffle();
break;
default:
break;
}
setState(() {});
setState(() {
_isAnswering = false;
});
}
void _answer(String option) async {
final isCorrect = _correctAnswers
final quizState = _currentQuizState;
final mode = _modeForIndex(_tabController.index);
final isCorrect = quizState.correctAnswers
.map((a) => a.toLowerCase().trim())
.contains(option.toLowerCase().trim());
final repo = Provider.of<DeckRepository>(context, listen: false);
final current = _current!;
final current = quizState.current!;
final tabIndex = _tabController.index;
final sessionDeck = _sessionDecks[tabIndex]!;
String readingType = '';
if (_mode == QuizMode.reading) {
readingType = _readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi';
if (mode == QuizMode.reading) {
readingType = quizState.readingHint.contains("on'yomi")
? 'onyomi'
: 'kunyomi';
}
final srsKey = _mode.toString() + readingType;
final srsKey = mode.toString() + readingType;
var srsItem = current.srsItems[srsKey];
final isNew = srsItem == null;
final srsItemForUpdate = srsItem ??=
SrsItem(kanjiId: current.id, quizMode: _mode, readingType: readingType);
setState(() {
_asked += 1;
if (isCorrect) {
_score += 1;
srsItemForUpdate.srsStage += 1;
if (_playCorrectSound) {
_audioPlayer.play(AssetSource('sfx/confirm.mp3'));
}
} else {
srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1);
final srsItemForUpdate = srsItem ??= SrsItem(
subjectId: current.id,
quizMode: mode,
readingType: readingType,
);
quizState.selectedOption = option;
quizState.showResult = true;
setState(() {});
if (isCorrect) {
quizState.asked += 1;
if (!quizState.wrongItems.contains(current.id)) {
quizState.score += 1;
}
srsItemForUpdate.lastAsked = DateTime.now();
current.srsItems[srsKey] = srsItemForUpdate;
});
srsItemForUpdate.srsStage += 1;
if (_playCorrectSound) {
_audioPlayer.play(AssetSource('sfx/correct.wav'));
}
} else {
srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1);
sessionDeck.add(current);
sessionDeck.shuffle(_random);
quizState.wrongItems.add(current.id);
if (_playIncorrectSound) {
_audioPlayer.play(AssetSource('sfx/incorrect.wav'));
}
}
srsItemForUpdate.lastAsked = DateTime.now();
current.srsItems[srsKey] = srsItemForUpdate;
final scaffoldMessenger = ScaffoldMessenger.of(context);
final theme = Theme.of(context);
if (isNew) {
await repo.insertSrsItem(srsItemForUpdate);
@@ -246,152 +344,217 @@ class _HomeScreenState extends State<HomeScreen> {
await repo.updateSrsItem(srsItemForUpdate);
}
final correctDisplay = (_mode == QuizMode.kanjiToEnglish)
? _toTitleCase(_correctAnswers.first)
: (_mode == QuizMode.reading
? _correctAnswers.join(', ')
: _correctAnswers.first);
final correctDisplay = (mode == QuizMode.kanjiToEnglish)
? _toTitleCase(quizState.correctAnswers.first)
: (mode == QuizMode.reading
? quizState.correctAnswers.join(', ')
: quizState.correctAnswers.first);
final snack = SnackBar(
content: Text(
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
style: TextStyle(
color: isCorrect ? Colors.greenAccent : Colors.redAccent,
color: isCorrect
? theme.colorScheme.primary
: theme.colorScheme.error,
fontWeight: FontWeight.bold,
),
),
backgroundColor: const Color(0xFF222222),
backgroundColor: theme.colorScheme.surfaceContainerHighest,
duration: const Duration(milliseconds: 900),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snack);
scaffoldMessenger.showSnackBar(snack);
}
Future.delayed(const Duration(milliseconds: 900), _nextQuestion);
setState(() {
_isAnswering = true;
});
Future.delayed(const Duration(milliseconds: 900), () {
if (mounted) {
_nextQuestion();
}
});
}
@override
Widget build(BuildContext context) {
String prompt = '';
String subtitle = '';
if (_apiKeyMissing) {
return Scaffold(
appBar: AppBar(title: const Text('Kanji Quiz')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'WaniKani API key is not set.',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
_loadDeck();
},
child: const Text('Go to Settings'),
),
],
),
),
);
}
switch (_mode) {
case QuizMode.kanjiToEnglish:
prompt = _current?.characters ?? '';
break;
case QuizMode.englishToKanji:
prompt = _current != null ? _toTitleCase(_current!.meanings.first) : '';
break;
case QuizMode.reading:
prompt = _current?.characters ?? '';
subtitle = _readingHint;
break;
if (_loading) {
return Scaffold(
appBar: AppBar(
title: const Text('Kanji Quiz'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Kanji→English'),
Tab(text: 'English→Kanji'),
Tab(text: 'Reading'),
],
),
),
body: const Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('WaniKani Kanji SRS'),
backgroundColor: const Color(0xFF1F1F1F),
foregroundColor: Colors.white,
elevation: 2,
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
_loadSettings();
},
)
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
_status,
style: const TextStyle(color: Colors.white),
),
),
if (_loading)
const CircularProgressIndicator(color: Colors.blueAccent),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 4,
alignment: WrapAlignment.center,
children: [
_buildChoiceChip('Kanji→English', QuizMode.kanjiToEnglish),
_buildChoiceChip('English→Kanji', QuizMode.englishToKanji),
_buildChoiceChip('Reading', QuizMode.reading),
],
),
const SizedBox(height: 18),
Expanded(
flex: 3,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 0,
maxWidth: 500,
minHeight: 150,
),
child: KanjiCard(
characters: prompt,
subtitle: subtitle,
backgroundColor: const Color(0xFF1E1E1E),
textColor: Colors.white,
),
),
),
),
const SizedBox(height: 12),
SafeArea(
top: false,
child: Column(
children: [
OptionsGrid(
options: _options,
onSelected: _answer,
buttonColor: const Color(0xFF1E1E1E),
textColor: Colors.white,
),
const SizedBox(height: 8),
Text(
'Score: $_score / $_asked',
style: const TextStyle(color: Colors.white),
),
],
),
),
title: const Text('Kanji Quiz'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Kanji→English'),
Tab(text: 'English→Kanji'),
Tab(text: 'Reading'),
],
),
),
backgroundColor: Theme.of(context).colorScheme.surface,
body: TabBarView(
controller: _tabController,
children: [_buildQuizPage(0), _buildQuizPage(1), _buildQuizPage(2)],
),
);
}
ChoiceChip _buildChoiceChip(String label, QuizMode mode) {
final selected = _mode == mode;
return ChoiceChip(
label: Text(
label,
style: TextStyle(color: selected ? Colors.white : Colors.grey[400]),
Widget _buildQuizPage(int index) {
final quizState = _quizStates[index];
final mode = _modeForIndex(index);
if (quizState.current == null) {
return Center(
child: Text(
_status,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface,
),
),
);
}
String prompt = '';
String subtitle = '';
if (quizState.current != null) {
switch (mode) {
case QuizMode.kanjiToEnglish:
prompt = quizState.current!.characters;
break;
case QuizMode.englishToKanji:
prompt = _toTitleCase(quizState.current!.meanings.first);
break;
case QuizMode.reading:
prompt = quizState.current!.characters;
subtitle = quizState.readingHint;
break;
default:
break;
}
}
return Padding(
key: quizState.key,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${quizState.asked} / ${_sessionDeckSizes[index] ?? 0}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: (_sessionDeckSizes[index] ?? 0) > 0
? quizState.asked / (_sessionDeckSizes[index] ?? 1)
: 0,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const SizedBox(height: 18),
Expanded(
flex: 3,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 0,
maxWidth: 500,
minHeight: 150,
),
child: KanjiCard(
characters: prompt,
subtitle: subtitle,
backgroundColor: Theme.of(context).colorScheme.surface,
textColor: Theme.of(context).colorScheme.onSurface,
),
),
),
),
const SizedBox(height: 12),
SafeArea(
top: false,
child: Column(
children: [
OptionsGrid(
options: quizState.options,
onSelected: _isAnswering ? (option) {} : _answer,
showResult: quizState.showResult,
selectedOption: quizState.selectedOption,
correctAnswers: quizState.correctAnswers,
),
const SizedBox(height: 8),
Text(
'Score: ${quizState.score} / ${quizState.asked}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
],
),
selected: selected,
onSelected: (v) {
setState(() => _mode = mode);
_nextQuestion();
},
selectedColor: Colors.blueAccent,
backgroundColor: const Color(0xFF1E1E1E),
);
}
}

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hirameki_srs/src/models/theme_model.dart';
import 'package:hirameki_srs/src/themes.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/deck_repository.dart';
@@ -13,8 +15,9 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
final TextEditingController _apiKeyController = TextEditingController();
bool _playAudio = true;
bool _playIncorrectSound = true;
bool _playCorrectSound = true;
bool _playNarrator = true;
@override
void dispose() {
@@ -37,8 +40,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_playAudio = prefs.getBool('playAudio') ?? true;
_playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
_playNarrator = prefs.getBool('playNarrator') ?? true;
});
}
@@ -50,24 +54,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
await repo.setApiKey(apiKey);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('API key saved!')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('API key saved!')));
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
Navigator.of(
context,
).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
}
}
@override
Widget build(BuildContext context) {
final themeModel = Provider.of<ThemeModel>(context);
return Scaffold(
backgroundColor: const Color(0xFF121212),
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
title: const Text('Settings'),
backgroundColor: const Color(0xFF1F1F1F),
foregroundColor: Colors.white,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
foregroundColor: Theme.of(context).colorScheme.onSurface,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
@@ -76,15 +82,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
TextField(
controller: _apiKeyController,
obscureText: true,
style: const TextStyle(color: Colors.white),
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration(
labelText: 'WaniKani API Key',
labelStyle: const TextStyle(color: Colors.grey),
labelStyle: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
filled: true,
fillColor: const Color(0xFF1E1E1E),
fillColor: Theme.of(context).colorScheme.surfaceContainer,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Colors.grey),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
@@ -92,37 +102,43 @@ class _SettingsScreenState extends State<SettingsScreen> {
ElevatedButton(
onPressed: _saveApiKey,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
child: const Text('Save & Start Quiz'),
),
const SizedBox(height: 24),
SwitchListTile(
title: const Text(
'Play audio for vocabulary',
style: TextStyle(color: Colors.white),
title: Text(
'Play incorrect sound',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
value: _playAudio,
value: _playIncorrectSound,
onChanged: (value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool('playAudio', value);
prefs.setBool('playIncorrectSound', value);
setState(() {
_playAudio = value;
_playIncorrectSound = value;
});
},
activeThumbColor: Colors.blueAccent,
inactiveThumbColor: Colors.grey,
tileColor: const Color(0xFF1E1E1E),
activeThumbColor: Theme.of(context).colorScheme.primary,
inactiveThumbColor: Theme.of(
context,
).colorScheme.onSurfaceVariant,
tileColor: Theme.of(context).colorScheme.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(height: 12),
SwitchListTile(
title: const Text(
'Play sound on correct answer',
style: TextStyle(color: Colors.white),
title: Text(
'Play correct sound',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
value: _playCorrectSound,
onChanged: (value) async {
@@ -132,9 +148,75 @@ class _SettingsScreenState extends State<SettingsScreen> {
_playCorrectSound = value;
});
},
activeThumbColor: Colors.blueAccent,
inactiveThumbColor: Colors.grey,
tileColor: const Color(0xFF1E1E1E),
activeThumbColor: Theme.of(context).colorScheme.primary,
inactiveThumbColor: Theme.of(
context,
).colorScheme.onSurfaceVariant,
tileColor: Theme.of(context).colorScheme.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(height: 12),
SwitchListTile(
title: Text(
'Play narrator (TTS)',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
value: _playNarrator,
onChanged: (value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool('playNarrator', value);
setState(() {
_playNarrator = value;
});
},
activeThumbColor: Theme.of(context).colorScheme.primary,
inactiveThumbColor: Theme.of(
context,
).colorScheme.onSurfaceVariant,
tileColor: Theme.of(context).colorScheme.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(height: 12),
ListTile(
title: Text(
'Theme',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
trailing: DropdownButton<ThemeData>(
value: themeModel.currentTheme,
dropdownColor: Theme.of(context).colorScheme.surfaceContainer,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
items: [
DropdownMenuItem(
value: Themes.dark,
child: const Text('Dark'),
),
DropdownMenuItem(
value: Themes.light,
child: const Text('Light'),
),
DropdownMenuItem(
value: Themes.nier,
child: const Text('Nier'),
),
],
onChanged: (theme) {
if (theme != null) {
themeModel.setTheme(theme);
}
},
),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),

View File

@@ -1,134 +1,137 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/deck_repository.dart';
import 'package:hirameki_srs/src/screens/settings_screen.dart';
import 'browse_screen.dart';
import 'home_screen.dart';
import 'vocab_screen.dart';
import 'custom_srs_screen.dart';
class StartScreen extends StatefulWidget {
class StartScreen extends StatelessWidget {
const StartScreen({super.key});
@override
State<StartScreen> createState() => _StartScreenState();
}
class _StartScreenState extends State<StartScreen> {
bool _loading = true;
bool _hasApiKey = false;
@override
void initState() {
super.initState();
_checkApiKey();
}
Future<void> _checkApiKey() async {
final repo = Provider.of<DeckRepository>(context, listen: false);
await repo.loadApiKey();
// TODO: Remove this before release. This is for development purposes only.
if (repo.apiKey == null || repo.apiKey!.isEmpty) {
await repo.setApiKey('91932463-60d2-4552-95a7-4c23cf358189');
}
setState(() {
_hasApiKey = repo.apiKey != null && repo.apiKey!.isNotEmpty;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(
backgroundColor: Color(0xFF121212),
body: Center(
child: CircularProgressIndicator(color: Colors.blueAccent),
),
);
}
return Scaffold(
backgroundColor: const Color(0xFF121212),
body: Center(
appBar: AppBar(
title: const Text('Hirameki SRS'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const SettingsScreen()));
},
),
],
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(16),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.8,
children: [
_buildModeCard(
context,
title: 'Kanji Quiz',
icon: Icons.extension,
description: 'Test your knowledge of kanji characters.',
onTap: () {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const HomeScreen()));
},
),
_buildModeCard(
context,
title: 'Vocabulary Quiz',
icon: Icons.school,
description: 'Practice vocabulary from your WaniKani deck.',
onTap: () {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const VocabScreen()));
},
),
_buildModeCard(
context,
title: 'Browse Items',
icon: Icons.grid_view,
description: 'Look through your kanji and vocabulary decks.',
onTap: () {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const BrowseScreen()));
},
),
_buildModeCard(
context,
title: 'Custom SRS',
icon: Icons.create,
description: 'Create and study your own custom flashcards.',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const CustomSrsScreen()),
);
},
),
],
),
),
);
}
Widget _buildModeCard(
BuildContext context, {
required String title,
required IconData icon,
required String description,
required VoidCallback onTap,
}) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(32),
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome to WaniKani Kanji SRS!',
style: Theme.of(context)
.textTheme
.headlineMedium
?.copyWith(fontSize: 28, color: Colors.white),
textAlign: TextAlign.center,
Icon(
icon,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
_hasApiKey
? 'Your API key is set. You can start the quiz!'
: 'Before you start, please set up your WaniKani API key in the settings.',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.grey[300]),
title,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => HomeScreen()),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text(
'Kanji Quiz',
style: TextStyle(fontSize: 18),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const VocabScreen()),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text(
'Vocabulary Quiz',
style: TextStyle(fontSize: 18),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const BrowseScreen()),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurpleAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text(
'Browse Items',
style: TextStyle(fontSize: 18),
const SizedBox(height: 8),
Expanded(
child: Text(
description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
softWrap: true,
),
),
],

View File

@@ -3,14 +3,27 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/kanji_item.dart';
import '../services/deck_repository.dart';
import '../models/vocabulary_item.dart';
import '../models/srs_item.dart';
import 'package:hirameki_srs/src/services/vocab_deck_repository.dart';
import '../services/distractor_generator.dart';
import '../widgets/kanji_card.dart';
import '../widgets/options_grid.dart';
import 'package:audioplayers/audioplayers.dart';
import 'settings_screen.dart';
class _QuizState {
VocabularyItem? current;
List<String> options = [];
List<String> correctAnswers = [];
int score = 0;
int asked = 0;
Key key = UniqueKey();
String? selectedOption;
bool showResult = false;
Set<int> wrongItems = {};
}
class VocabScreen extends StatefulWidget {
const VocabScreen({super.key});
@@ -18,34 +31,53 @@ class VocabScreen extends StatefulWidget {
State<VocabScreen> createState() => _VocabScreenState();
}
class _VocabScreenState extends State<VocabScreen> {
class _VocabScreenState extends State<VocabScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<VocabularyItem> _deck = [];
bool _loading = false;
bool _isAnswering = false;
String _status = 'Loading deck...';
final DistractorGenerator _dg = DistractorGenerator();
final Random _random = Random();
final _audioPlayer = AudioPlayer();
VocabQuizMode _mode = VocabQuizMode.vocabToEnglish;
VocabularyItem? _current;
List<String> _options = [];
List<String> _correctAnswers = [];
int _score = 0;
int _asked = 0;
bool _playAudio = true;
final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
_QuizState get _currentQuizState => _quizStates[_tabController.index];
final _sessionDecks = <int, List<VocabularyItem>>{};
final _sessionDeckSizes = <int, int>{};
bool _playIncorrectSound = true;
bool _playCorrectSound = true;
bool _playNarrator = true;
bool _apiKeyMissing = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
if (_tabController.index == 2 && !_tabController.indexIsChanging) {
_playCurrentAudio();
}
setState(() {});
});
_loadSettings();
_loadDeck();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_playAudio = prefs.getBool('playAudio') ?? true;
_playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
_playNarrator = prefs.getBool('playNarrator') ?? true;
});
}
@@ -56,16 +88,15 @@ class _VocabScreenState extends State<VocabScreen> {
});
try {
final repo = Provider.of<DeckRepository>(context, listen: false);
final repo = Provider.of<VocabDeckRepository>(context, listen: false);
await repo.loadApiKey();
final apiKey = repo.apiKey;
if (apiKey == null || apiKey.isEmpty) {
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
}
setState(() {
_apiKeyMissing = true;
_loading = false;
});
return;
}
@@ -82,9 +113,49 @@ class _VocabScreenState extends State<VocabScreen> {
_deck = items;
_status = 'Loaded ${items.length} vocabulary';
_loading = false;
_apiKeyMissing = false;
});
_nextQuestion();
final disabledLevels = <int>{};
final itemsByLevel = <int, List<VocabularyItem>>{};
for (final item in _deck) {
(itemsByLevel[item.level] ??= []).add(item);
}
itemsByLevel.forEach((level, items) {
final allSrsItems = items
.expand((item) => item.srsItems.values)
.toList();
if (allSrsItems.isNotEmpty &&
allSrsItems.every((srs) => srs.disabled)) {
disabledLevels.add(level);
}
});
for (var i = 0; i < _tabController.length; i++) {
final mode = _modeForIndex(i);
var filteredDeck = _deck.where((item) {
if (disabledLevels.contains(item.level)) {
return false;
}
final srsItem = item.srsItems[mode.toString()];
return srsItem == null || !srsItem.disabled;
}).toList();
if (mode == QuizMode.audioToEnglish) {
filteredDeck = filteredDeck
.where((item) => item.pronunciationAudios.isNotEmpty)
.toList();
}
filteredDeck.shuffle(_random);
_sessionDecks[i] = filteredDeck;
_sessionDeckSizes[i] = filteredDeck.length;
}
for (var i = 0; i < _tabController.length; i++) {
_nextQuestion(i);
}
} catch (e) {
setState(() {
_status = 'Error: $e';
@@ -101,75 +172,128 @@ class _VocabScreenState extends State<VocabScreen> {
.join(' ');
}
void _nextQuestion() {
if (_deck.isEmpty) return;
QuizMode _modeForIndex(int index) {
switch (index) {
case 0:
return QuizMode.vocabToEnglish;
case 1:
return QuizMode.englishToVocab;
case 2:
return QuizMode.audioToEnglish;
default:
return QuizMode.vocabToEnglish;
}
}
_deck.sort((a, b) {
final aSrsItem = a.srsItems[_mode.toString()] ??
VocabSrsItem(vocabId: a.id, quizMode: _mode);
final bSrsItem = b.srsItems[_mode.toString()] ??
VocabSrsItem(vocabId: b.id, quizMode: _mode);
void _nextQuestion([int? index]) {
final tabIndex = index ?? _tabController.index;
final quizState = _quizStates[tabIndex];
final sessionDeck = _sessionDecks[tabIndex];
final mode = _modeForIndex(tabIndex);
final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage);
if (stageComparison != 0) {
return stageComparison;
}
return aSrsItem.lastAsked.compareTo(bSrsItem.lastAsked);
});
if (sessionDeck == null || sessionDeck.isEmpty) {
setState(() {
quizState.current = null;
_status = 'Quiz complete!';
});
return;
}
_current = _deck.first;
quizState.current = sessionDeck.removeAt(0);
quizState.key = UniqueKey();
quizState.correctAnswers = [];
quizState.options = [];
quizState.selectedOption = null;
quizState.showResult = false;
_correctAnswers = [];
_options = [];
switch (_mode) {
case VocabQuizMode.vocabToEnglish:
_correctAnswers = [_current!.meanings.first];
_options = [
_correctAnswers.first,
..._dg.generateVocabMeanings(_current!, _deck, 3)
].map(_toTitleCase).toList()
..shuffle();
switch (mode) {
case QuizMode.vocabToEnglish:
case QuizMode.audioToEnglish:
quizState.correctAnswers = [quizState.current!.meanings.first];
quizState.options = [
quizState.correctAnswers.first,
..._dg.generateVocabMeanings(quizState.current!, _deck, 3),
].map(_toTitleCase).toList()..shuffle();
break;
case VocabQuizMode.englishToVocab:
_correctAnswers = [_current!.characters];
_options = [
_correctAnswers.first,
..._dg.generateVocab(_current!, _deck, 3)
case QuizMode.englishToVocab:
quizState.correctAnswers = [quizState.current!.characters];
quizState.options = [
quizState.correctAnswers.first,
..._dg.generateVocab(quizState.current!, _deck, 3),
]..shuffle();
break;
default:
break;
}
setState(() {});
setState(() {
_isAnswering = false;
});
if (mode == QuizMode.audioToEnglish) {
_playCurrentAudio(playOnLoad: true);
}
}
Future<void> _playCurrentAudio({bool playOnLoad = false}) async {
final current = _currentQuizState.current;
if (current == null || current.pronunciationAudios.isEmpty) return;
if (playOnLoad && !_playNarrator) return;
final maleAudios = current.pronunciationAudios.where(
(a) => a.gender == 'male',
);
final audioUrl = (maleAudios.isNotEmpty
? maleAudios.first.url
: current.pronunciationAudios.first.url);
try {
await _audioPlayer.play(UrlSource(audioUrl));
} finally {}
}
void _answer(String option) async {
final isCorrect = _correctAnswers
final quizState = _currentQuizState;
final mode = _modeForIndex(_tabController.index);
final isCorrect = quizState.correctAnswers
.map((a) => a.toLowerCase().trim())
.contains(option.toLowerCase().trim());
final repo = Provider.of<DeckRepository>(context, listen: false);
final current = _current!;
final repo = Provider.of<VocabDeckRepository>(context, listen: false);
final current = quizState.current!;
final tabIndex = _tabController.index;
final sessionDeck = _sessionDecks[tabIndex]!;
final srsKey = _mode.toString();
final srsKey = mode.toString();
var srsItemNullable = current.srsItems[srsKey];
final isNew = srsItemNullable == null;
final srsItem =
srsItemNullable ?? VocabSrsItem(vocabId: current.id, quizMode: _mode);
srsItemNullable ?? SrsItem(subjectId: current.id, quizMode: mode);
setState(() {
_asked += 1;
if (isCorrect) {
_score += 1;
srsItem.srsStage += 1;
} else {
srsItem.srsStage = max(0, srsItem.srsStage - 1);
quizState.selectedOption = option;
quizState.showResult = true;
setState(() {});
if (isCorrect) {
quizState.asked += 1;
if (!quizState.wrongItems.contains(current.id)) {
quizState.score += 1;
}
srsItem.lastAsked = DateTime.now();
current.srsItems[srsKey] = srsItem;
});
srsItem.srsStage += 1;
} else {
srsItem.srsStage = max(0, srsItem.srsStage - 1);
sessionDeck.add(current);
sessionDeck.shuffle(_random);
quizState.wrongItems.add(current.id);
if (_playIncorrectSound) {
await _audioPlayer.play(AssetSource('sfx/incorrect.wav'));
}
}
srsItem.lastAsked = DateTime.now();
current.srsItems[srsKey] = srsItem;
if (isNew) {
await repo.insertVocabSrsItem(srsItem);
@@ -177,32 +301,33 @@ class _VocabScreenState extends State<VocabScreen> {
await repo.updateVocabSrsItem(srsItem);
}
final correctDisplay = (_mode == VocabQuizMode.vocabToEnglish)
? _toTitleCase(_correctAnswers.first)
: _correctAnswers.first;
final correctDisplay = (mode == QuizMode.vocabToEnglish)
? _toTitleCase(quizState.correctAnswers.first)
: quizState.correctAnswers.first;
if (!mounted) return;
final snack = SnackBar(
content: Text(
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
style: TextStyle(
color: isCorrect ? Colors.greenAccent : Colors.redAccent,
color: isCorrect
? Theme.of(context).colorScheme.tertiary
: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.bold,
),
),
backgroundColor: const Color(0xFF222222),
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
duration: const Duration(milliseconds: 900),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snack);
}
ScaffoldMessenger.of(context).showSnackBar(snack);
if (isCorrect) {
if (_playCorrectSound) {
await _audioPlayer.play(AssetSource('sfx/confirm.mp3'));
}
if (_playAudio) {
final maleAudios =
current.pronunciationAudios.where((a) => a.gender == 'male');
if (_playCorrectSound && !_playNarrator) {
await _audioPlayer.play(AssetSource('sfx/correct.wav'));
} else if (_playNarrator) {
final maleAudios = current.pronunciationAudios.where(
(a) => a.gender == 'male',
);
if (maleAudios.isNotEmpty) {
final completer = Completer<void>();
final sub = _audioPlayer.onPlayerComplete.listen((event) {
@@ -212,136 +337,212 @@ class _VocabScreenState extends State<VocabScreen> {
try {
await _audioPlayer.play(UrlSource(maleAudios.first.url));
await completer.future.timeout(const Duration(seconds: 5));
} catch (e) {
// Ignore player errors
} finally {
await sub.cancel();
}
}
}
} else {
await Future.delayed(const Duration(milliseconds: 900));
}
_nextQuestion();
setState(() {
_isAnswering = true;
});
Future.delayed(const Duration(milliseconds: 900), () {
if (mounted) {
_nextQuestion();
}
});
}
@override
Widget build(BuildContext context) {
String prompt = '';
if (_apiKeyMissing) {
return Scaffold(
appBar: AppBar(title: const Text('Vocabulary Quiz')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'WaniKani API key is not set.',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
if (!mounted) return;
_loadDeck();
},
child: const Text('Go to Settings'),
),
],
),
),
);
}
switch (_mode) {
case VocabQuizMode.vocabToEnglish:
prompt = _current?.characters ?? '';
break;
case VocabQuizMode.englishToVocab:
prompt = _current != null ? _toTitleCase(_current!.meanings.first) : '';
break;
if (_loading) {
return Scaffold(
appBar: AppBar(
title: const Text('Vocabulary Quiz'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Vocab→English'),
Tab(text: 'English→Vocab'),
Tab(text: 'Listening'),
],
),
),
body: const Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('WaniKani Vocabulary SRS'),
backgroundColor: const Color(0xFF1F1F1F),
foregroundColor: Colors.white,
elevation: 2,
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
_loadSettings();
},
)
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
_status,
style: const TextStyle(color: Colors.white),
),
),
if (_loading)
const CircularProgressIndicator(color: Colors.blueAccent),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 4,
alignment: WrapAlignment.center,
children: [
_buildChoiceChip('Vocab→English', VocabQuizMode.vocabToEnglish),
_buildChoiceChip('English→Vocab', VocabQuizMode.englishToVocab),
],
),
const SizedBox(height: 18),
Expanded(
flex: 3,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 0,
maxWidth: 500,
minHeight: 150,
),
child: KanjiCard(
characters: prompt,
subtitle: '',
backgroundColor: const Color(0xFF1E1E1E),
textColor: Colors.white,
),
),
),
),
const SizedBox(height: 12),
SafeArea(
top: false,
child: Column(
children: [
OptionsGrid(
options: _options,
onSelected: _answer,
buttonColor: const Color(0xFF1E1E1E),
textColor: Colors.white,
),
const SizedBox(height: 8),
Text(
'Score: $_score / $_asked',
style: const TextStyle(color: Colors.white),
),
],
),
),
title: const Text('Vocabulary Quiz'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Vocab→English'),
Tab(text: 'English→Vocab'),
Tab(text: 'Listening'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [_buildQuizPage(0), _buildQuizPage(1), _buildQuizPage(2)],
),
);
}
ChoiceChip _buildChoiceChip(String label, VocabQuizMode mode) {
final selected = _mode == mode;
return ChoiceChip(
label: Text(
label,
style: TextStyle(color: selected ? Colors.white : Colors.grey[400]),
Widget _buildQuizPage(int index) {
final quizState = _quizStates[index];
final mode = _modeForIndex(index);
if (quizState.current == null) {
return Center(
child: Text(
_status,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface,
),
),
);
}
Widget promptWidget;
if (quizState.current == null) {
promptWidget = const SizedBox.shrink();
} else if (mode == QuizMode.audioToEnglish) {
promptWidget = IconButton(
icon: Icon(
Icons.volume_up,
color: Theme.of(context).colorScheme.onSurface,
size: 64,
),
onPressed: _playCurrentAudio,
);
} else {
String promptText = '';
switch (mode) {
case QuizMode.vocabToEnglish:
promptText = quizState.current!.characters;
break;
case QuizMode.englishToVocab:
promptText = _toTitleCase(quizState.current!.meanings.first);
break;
case QuizMode.audioToEnglish:
break;
default:
break;
}
promptWidget = Text(
promptText,
style: TextStyle(
fontSize: 48,
color: Theme.of(context).colorScheme.onSurface,
),
);
}
return Padding(
key: quizState.key,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${quizState.asked} / ${_sessionDeckSizes[index] ?? 0}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: (_sessionDeckSizes[index] ?? 0) > 0
? quizState.asked / (_sessionDeckSizes[index] ?? 1)
: 0,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const SizedBox(height: 18),
Expanded(
flex: 3,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 0,
maxWidth: 500,
minHeight: 150,
),
child: KanjiCard(characterWidget: promptWidget, subtitle: ''),
),
),
),
const SizedBox(height: 12),
SafeArea(
top: false,
child: Column(
children: [
OptionsGrid(
options: quizState.options,
onSelected: _isAnswering ? (option) {} : _answer,
showResult: quizState.showResult,
selectedOption: quizState.selectedOption,
correctAnswers: quizState.correctAnswers,
),
const SizedBox(height: 8),
Text(
'Score: ${quizState.score} / ${quizState.asked}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
],
),
selected: selected,
onSelected: (v) {
setState(() => _mode = mode);
_nextQuestion();
},
selectedColor: Colors.blueAccent,
backgroundColor: const Color(0xFF1E1E1E),
);
}
}

View File

@@ -0,0 +1,52 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/custom_kanji_item.dart';
class CustomDeckRepository {
static const _key = 'custom_deck';
Future<List<CustomKanjiItem>> getCustomDeck() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_key);
if (jsonString != null) {
final List<dynamic> jsonList = json.decode(jsonString);
return jsonList.map((json) => CustomKanjiItem.fromJson(json)).toList();
}
return [];
}
Future<void> addCard(CustomKanjiItem item) async {
final deck = await getCustomDeck();
deck.add(item);
await saveDeck(deck);
}
Future<void> updateCard(CustomKanjiItem item) async {
await updateCards([item]);
}
Future<void> updateCards(List<CustomKanjiItem> itemsToUpdate) async {
final deck = await getCustomDeck();
for (var item in itemsToUpdate) {
final index = deck.indexWhere(
(element) => element.characters == item.characters,
);
if (index != -1) {
deck[index] = item;
}
}
await saveDeck(deck);
}
Future<void> deleteCard(CustomKanjiItem item) async {
final deck = await getCustomDeck();
deck.removeWhere((element) => element.characters == item.characters);
await saveDeck(deck);
}
Future<void> saveDeck(List<CustomKanjiItem> deck) async {
final prefs = await SharedPreferences.getInstance();
final jsonList = deck.map((item) => item.toJson()).toList();
await prefs.setString(_key, json.encode(jsonList));
}
}

View File

@@ -0,0 +1,27 @@
class DbConstants {
static const String settingsTable = 'settings';
static const String kanjiTable = 'kanji';
static const String srsItemsTable = 'srs_items';
static const String vocabularyTable = 'vocabulary';
static const String srsVocabItemsTable = 'srs_vocab_items';
static const String keyColumn = 'key';
static const String valueColumn = 'value';
static const String idColumn = 'id';
static const String levelColumn = 'level';
static const String charactersColumn = 'characters';
static const String meaningsColumn = 'meanings';
static const String onyomiColumn = 'onyomi';
static const String kunyomiColumn = 'kunyomi';
static const String readingsColumn = 'readings';
static const String pronunciationAudiosColumn = 'pronunciation_audios';
static const String kanjiIdColumn = 'kanjiId';
static const String vocabIdColumn = 'vocabId';
static const String quizModeColumn = 'quizMode';
static const String readingTypeColumn = 'readingType';
static const String srsStageColumn = 'srsStage';
static const String lastAskedColumn = 'lastAsked';
static const String disabledColumn = 'disabled';
}

View File

@@ -0,0 +1,65 @@
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'database_constants.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
static Database? _db;
factory DatabaseHelper() {
return _instance;
}
DatabaseHelper._internal();
Future<Database> get db async {
if (_db != null) return _db!;
_db = await _openDb();
return _db!;
}
Future<void> close() async {
if (_db != null) {
await _db!.close();
_db = null;
}
}
Future<Database> _openDb() async {
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, 'wanikani_srs.db');
return openDatabase(
path,
version: 8,
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE ${DbConstants.kanjiTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.onyomiColumn} TEXT, ${DbConstants.kunyomiColumn} TEXT)''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.settingsTable} (${DbConstants.keyColumn} TEXT PRIMARY KEY, ${DbConstants.valueColumn} TEXT)''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.srsItemsTable} (${DbConstants.kanjiIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.readingTypeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, ${DbConstants.disabledColumn} INTEGER DEFAULT 0, PRIMARY KEY (${DbConstants.kanjiIdColumn}, ${DbConstants.quizModeColumn}, ${DbConstants.readingTypeColumn}))''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.vocabularyTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.readingsColumn} TEXT, ${DbConstants.pronunciationAudiosColumn} TEXT)''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.srsVocabItemsTable} (${DbConstants.vocabIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, ${DbConstants.disabledColumn} INTEGER DEFAULT 0, PRIMARY KEY (${DbConstants.vocabIdColumn}, ${DbConstants.quizModeColumn}))''',
);
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 8) {
await db.execute(
'ALTER TABLE ${DbConstants.srsItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0',
);
await db.execute(
'ALTER TABLE ${DbConstants.srsVocabItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0',
);
}
},
);
}
}

View File

@@ -1,13 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import '../models/kanji_item.dart';
import '../models/srs_item.dart';
import '../api/wk_client.dart';
import 'database_constants.dart';
import 'database_helper.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class DeckRepository {
Database? _db;
String? _apiKey;
Future<void> setApiKey(String apiKey) async {
@@ -17,122 +18,98 @@ class DeckRepository {
String? get apiKey => _apiKey;
Future<Database> _openDb() async {
if (_db != null) return _db!;
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, 'wanikani_srs.db');
_db = await openDatabase(
path,
version: 6,
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)''');
await db.execute(
'''CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)''');
await db.execute(
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''');
await db.execute(
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, readings TEXT, pronunciation_audios TEXT)''');
await db.execute(
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''');
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute(
'''CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)''');
}
if (oldVersion < 3) {
// Migration from version 2 to 3 was flawed, so we just drop the columns if they exist
}
if (oldVersion < 4) {
await db.execute(
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''');
// We are not migrating the old srs data, as it was not mode-specific.
// Old columns will be dropped.
}
if (oldVersion < 5) {
await db.execute(
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, readings TEXT)''');
await db.execute(
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''');
}
if (oldVersion < 6) {
try {
await db.execute('ALTER TABLE vocabulary ADD COLUMN pronunciation_audios TEXT');
} catch (_) {
// Ignore error, column might already exist
}
}
},
);
return _db!;
}
Future<void> saveApiKey(String apiKey) async {
final db = await _openDb();
await db.insert(
'settings',
{'key': 'apiKey', 'value': apiKey},
conflictAlgorithm: ConflictAlgorithm.replace,
);
final db = await DatabaseHelper().db;
await db.insert(DbConstants.settingsTable, {
DbConstants.keyColumn: 'apiKey',
DbConstants.valueColumn: apiKey,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<String?> loadApiKey() async {
final db = await _openDb();
final rows =
await db.query('settings', where: 'key = ?', whereArgs: ['apiKey']);
final db = await DatabaseHelper().db;
final rows = await db.query(
DbConstants.settingsTable,
where: '${DbConstants.keyColumn} = ?',
whereArgs: ['apiKey'],
);
if (rows.isNotEmpty) {
_apiKey = rows.first['value'] as String;
_apiKey = rows.first[DbConstants.valueColumn] as String;
return _apiKey;
}
try {
final envApiKey = dotenv.env['WANIKANI_API_KEY'];
if (envApiKey != null && envApiKey.isNotEmpty) {
await saveApiKey(envApiKey);
_apiKey = envApiKey;
return _apiKey;
}
} catch (_) {}
return null;
}
Future<void> saveKanji(List<KanjiItem> items) async {
final db = await _openDb();
final db = await DatabaseHelper().db;
final batch = db.batch();
for (final it in items) {
batch.insert(
'kanji',
{
'id': it.id,
'characters': it.characters,
'meanings': it.meanings.join('|'),
'onyomi': it.onyomi.join('|'),
'kunyomi': it.kunyomi.join('|'),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
batch.insert(DbConstants.kanjiTable, {
DbConstants.idColumn: it.id,
DbConstants.levelColumn: it.level,
DbConstants.charactersColumn: it.characters,
DbConstants.meaningsColumn: it.meanings.join('|'),
DbConstants.onyomiColumn: it.onyomi.join('|'),
DbConstants.kunyomiColumn: it.kunyomi.join('|'),
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
await batch.commit(noResult: true);
}
Future<List<KanjiItem>> loadKanji() async {
final db = await _openDb();
final rows = await db.query('kanji');
final db = await DatabaseHelper().db;
final rows = await db.query(DbConstants.kanjiTable);
final kanjiItems = rows
.map((r) => KanjiItem(
id: r['id'] as int,
characters: r['characters'] as String,
meanings: (r['meanings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
onyomi: (r['onyomi'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
kunyomi: (r['kunyomi'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
))
.map(
(r) => KanjiItem(
id: r[DbConstants.idColumn] as int,
level: r[DbConstants.levelColumn] as int? ?? 0,
characters: r[DbConstants.charactersColumn] as String,
meanings: (r[DbConstants.meaningsColumn] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
onyomi: (r[DbConstants.onyomiColumn] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
kunyomi: (r[DbConstants.kunyomiColumn] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
),
)
.toList();
final srsRows = await db.query(DbConstants.srsItemsTable);
final srsItemsByKanjiId = <int, List<SrsItem>>{};
for (final r in srsRows) {
final srsItem = SrsItem(
subjectId: r[DbConstants.kanjiIdColumn] as int,
quizMode: QuizMode.values.firstWhere(
(e) => e.toString() == r[DbConstants.quizModeColumn] as String,
),
readingType: r[DbConstants.readingTypeColumn] as String?,
srsStage: r[DbConstants.srsStageColumn] as int,
lastAsked: DateTime.parse(r[DbConstants.lastAskedColumn] as String),
disabled: (r[DbConstants.disabledColumn] as int? ?? 0) == 1,
);
srsItemsByKanjiId.putIfAbsent(srsItem.subjectId, () => []).add(srsItem);
}
for (final item in kanjiItems) {
final srsItems = await getSrsItems(item.id);
final srsItems = srsItemsByKanjiId[item.id] ?? [];
for (final srsItem in srsItems) {
final key = srsItem.quizMode.toString() + (srsItem.readingType ?? '');
item.srsItems[key] = srsItem;
@@ -142,46 +119,68 @@ class DeckRepository {
return kanjiItems;
}
Future<List<SrsItem>> getSrsItems(int kanjiId) async {
final db = await _openDb();
final rows = await db.query('srs_items', where: 'kanjiId = ?', whereArgs: [kanjiId]);
return rows.map((r) {
return SrsItem(
kanjiId: r['kanjiId'] as int,
quizMode: QuizMode.values.firstWhere((e) => e.toString() == r['quizMode'] as String),
readingType: r['readingType'] as String?,
srsStage: r['srsStage'] as int,
lastAsked: DateTime.parse(r['lastAsked'] as String),
Future<void> updateSrsItems(List<SrsItem> items) async {
final db = await DatabaseHelper().db;
final batch = db.batch();
for (final item in items) {
var where =
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
final whereArgs = [item.subjectId, item.quizMode.toString()];
if (item.readingType != null) {
where += ' AND ${DbConstants.readingTypeColumn} = ?';
whereArgs.add(item.readingType!);
} else {
where += ' AND ${DbConstants.readingTypeColumn} IS NULL';
}
batch.update(
DbConstants.srsItemsTable,
{
DbConstants.srsStageColumn: item.srsStage,
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
DbConstants.disabledColumn: item.disabled ? 1 : 0,
},
where: where,
whereArgs: whereArgs,
);
}).toList();
}
await batch.commit(noResult: true);
}
Future<void> updateSrsItem(SrsItem item) async {
final db = await _openDb();
final db = await DatabaseHelper().db;
var where =
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
final whereArgs = [item.subjectId, item.quizMode.toString()];
if (item.readingType != null) {
where += ' AND ${DbConstants.readingTypeColumn} = ?';
whereArgs.add(item.readingType!);
} else {
where += ' AND ${DbConstants.readingTypeColumn} IS NULL';
}
await db.update(
'srs_items',
DbConstants.srsItemsTable,
{
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
DbConstants.srsStageColumn: item.srsStage,
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
DbConstants.disabledColumn: item.disabled ? 1 : 0,
},
where: 'kanjiId = ? AND quizMode = ? AND readingType = ?',
whereArgs: [item.kanjiId, item.quizMode.toString(), item.readingType],
where: where,
whereArgs: whereArgs,
);
}
Future<void> insertSrsItem(SrsItem item) async {
final db = await _openDb();
await db.insert(
'srs_items',
{
'kanjiId': item.kanjiId,
'quizMode': item.quizMode.toString(),
'readingType': item.readingType,
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
final db = await DatabaseHelper().db;
await db.insert(DbConstants.srsItemsTable, {
DbConstants.kanjiIdColumn: item.subjectId,
DbConstants.quizModeColumn: item.quizMode.toString(),
DbConstants.readingTypeColumn: item.readingType,
DbConstants.srsStageColumn: item.srsStage,
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
DbConstants.disabledColumn: item.disabled ? 1 : 0,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<List<KanjiItem>> fetchAndCacheFromWk([String? apiKey]) async {
@@ -189,8 +188,9 @@ class DeckRepository {
if (key == null) throw Exception('API key not set');
final client = WkClient(key);
final assignments =
await client.fetchAllAssignments(subjectTypes: ['kanji']);
final assignments = await client.fetchAllAssignments(
subjectTypes: ['kanji'],
);
final unlocked = <int>{};
for (final a in assignments) {
@@ -209,10 +209,12 @@ class DeckRepository {
final subjects = await client.fetchSubjectsByIds(unlocked.toList());
final items = subjects
.where((s) =>
s['object'] == 'kanji' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'kanji'))
.where(
(s) =>
s['object'] == 'kanji' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'kanji'),
)
.map((s) => KanjiItem.fromSubject(s))
.where((k) => k.characters.isNotEmpty && k.meanings.isNotEmpty)
.toList();
@@ -220,150 +222,4 @@ class DeckRepository {
await saveKanji(items);
return items;
}
Future<List<VocabSrsItem>> getVocabSrsItems(int vocabId) async {
final db = await _openDb();
final rows = await db.query('srs_vocab_items', where: 'vocabId = ?', whereArgs: [vocabId]);
return rows.map((r) {
return VocabSrsItem(
vocabId: r['vocabId'] as int,
quizMode: VocabQuizMode.values.firstWhere((e) => e.toString() == r['quizMode'] as String),
srsStage: r['srsStage'] as int,
lastAsked: DateTime.parse(r['lastAsked'] as String),
);
}).toList();
}
Future<void> updateVocabSrsItem(VocabSrsItem item) async {
final db = await _openDb();
await db.update(
'srs_vocab_items',
{
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
},
where: 'vocabId = ? AND quizMode = ?',
whereArgs: [item.vocabId, item.quizMode.toString()],
);
}
Future<void> insertVocabSrsItem(VocabSrsItem item) async {
final db = await _openDb();
await db.insert(
'srs_vocab_items',
{
'vocabId': item.vocabId,
'quizMode': item.quizMode.toString(),
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> saveVocabulary(List<VocabularyItem> items) async {
final db = await _openDb();
final batch = db.batch();
for (final it in items) {
final audios = it.pronunciationAudios
.map((a) => {'url': a.url, 'gender': a.gender})
.toList();
batch.insert(
'vocabulary',
{
'id': it.id,
'characters': it.characters,
'meanings': it.meanings.join('|'),
'readings': it.readings.join('|'),
'pronunciation_audios': jsonEncode(audios),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
}
Future<List<VocabularyItem>> loadVocabulary() async {
final db = await _openDb();
final rows = await db.query('vocabulary');
final vocabItems = rows
.map((r) {
final audiosRaw = r['pronunciation_audios'] as String?;
final List<PronunciationAudio> audios = [];
if (audiosRaw != null && audiosRaw.isNotEmpty) {
try {
final decoded = jsonDecode(audiosRaw) as List;
for (final audioData in decoded) {
audios.add(PronunciationAudio(
url: audioData['url'] as String,
gender: audioData['gender'] as String,
));
}
} catch (e) {
// Error decoding, so we'll just have no audio for this item
}
}
return VocabularyItem(
id: r['id'] as int,
characters: r['characters'] as String,
meanings: (r['meanings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
readings: (r['readings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
pronunciationAudios: audios,
);
})
.toList();
for (final item in vocabItems) {
final srsItems = await getVocabSrsItems(item.id);
for (final srsItem in srsItems) {
final key = srsItem.quizMode.toString();
item.srsItems[key] = srsItem;
}
}
return vocabItems;
}
Future<List<VocabularyItem>> fetchAndCacheVocabularyFromWk([String? apiKey]) async {
final key = apiKey ?? _apiKey;
if (key == null) throw Exception('API key not set');
final client = WkClient(key);
final assignments =
await client.fetchAllAssignments(subjectTypes: ['vocabulary']);
final unlocked = <int>{};
for (final a in assignments) {
final data = a['data'] as Map<String, dynamic>;
final sidRaw = data['subject_id'];
if (sidRaw == null) continue;
final sid = sidRaw is int ? sidRaw : int.tryParse(sidRaw.toString());
if (sid == null) continue;
final started = data['started_at'];
final srs = data['srs_stage'];
final isUnlocked = (started != null) || (srs != null && (srs as int) > 0);
if (isUnlocked) unlocked.add(sid);
}
if (unlocked.isEmpty) return [];
final subjects = await client.fetchSubjectsByIds(unlocked.toList());
final items = subjects
.where((s) =>
s['object'] == 'vocabulary' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'vocabulary'))
.map((s) => VocabularyItem.fromSubject(s))
.where((k) => k.characters.isNotEmpty && k.meanings.isNotEmpty)
.toList();
await saveVocabulary(items);
return items;
}
}
}

View File

@@ -1,17 +1,30 @@
import '../models/kanji_item.dart';
import '../models/vocabulary_item.dart';
import 'dart:math';
class DistractorGenerator {
final Random _rnd = Random();
List<String> generateMeanings(KanjiItem correct, List<KanjiItem> pool, int needed) {
List<String> generateMeanings(
KanjiItem correct,
List<KanjiItem> pool,
int needed,
) {
final correctMeaning = correct.meanings.first;
final tokens = correctMeaning.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
final tokens = correctMeaning
.split(RegExp(r'\s+'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toSet();
final candidates = <String>[];
for (final k in pool) {
if (k.id == correct.id) continue;
for (final m in k.meanings) {
final mTokens = m.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
final mTokens = m
.split(RegExp(r'\s+'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toSet();
if (mTokens.intersection(tokens).isNotEmpty) {
candidates.add(m);
}
@@ -38,8 +51,15 @@ class DistractorGenerator {
return out;
}
List<String> generateKanji(KanjiItem correct, List<KanjiItem> pool, int needed) {
final others = pool.map((k) => k.characters).where((c) => c != correct.characters).toList();
List<String> generateKanji(
KanjiItem correct,
List<KanjiItem> pool,
int needed,
) {
final others = pool
.map((k) => k.characters)
.where((c) => c != correct.characters)
.toList();
others.shuffle(_rnd);
final out = <String>[];
for (final o in others) {
@@ -52,7 +72,11 @@ class DistractorGenerator {
return out;
}
List<String> generateReadings(String correct, List<KanjiItem> pool, int needed) {
List<String> generateReadings(
String correct,
List<KanjiItem> pool,
int needed,
) {
final poolReadings = <String>[];
for (final k in pool) {
poolReadings.addAll(k.onyomi);
@@ -71,14 +95,26 @@ class DistractorGenerator {
return out;
}
List<String> generateVocabMeanings(VocabularyItem correct, List<VocabularyItem> pool, int needed) {
List<String> generateVocabMeanings(
VocabularyItem correct,
List<VocabularyItem> pool,
int needed,
) {
final correctMeaning = correct.meanings.first;
final tokens = correctMeaning.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
final tokens = correctMeaning
.split(RegExp(r'\s+'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toSet();
final candidates = <String>[];
for (final k in pool) {
if (k.id == correct.id) continue;
for (final m in k.meanings) {
final mTokens = m.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
final mTokens = m
.split(RegExp(r'\s+'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toSet();
if (mTokens.intersection(tokens).isNotEmpty) {
candidates.add(m);
}
@@ -105,8 +141,15 @@ class DistractorGenerator {
return out;
}
List<String> generateVocab(VocabularyItem correct, List<VocabularyItem> pool, int needed) {
final others = pool.map((k) => k.characters).where((c) => c != correct.characters).toList();
List<String> generateVocab(
VocabularyItem correct,
List<VocabularyItem> pool,
int needed,
) {
final others = pool
.map((k) => k.characters)
.where((c) => c != correct.characters)
.toList();
others.shuffle(_rnd);
final out = <String>[];
for (final o in others) {
@@ -120,4 +163,7 @@ class DistractorGenerator {
}
}
String _toTitleCase(String s) => s.split(' ').map((w) => w.isEmpty ? w : (w[0].toUpperCase() + w.substring(1))).join(' ');
String _toTitleCase(String s) => s
.split(' ')
.map((w) => w.isEmpty ? w : (w[0].toUpperCase() + w.substring(1)))
.join(' ');

View File

@@ -0,0 +1,56 @@
import 'package:flutter_tts/flutter_tts.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class TtsService {
FlutterTts? _flutterTts;
bool _isInitialized = false;
Future<void> initTts() async {
if (_isInitialized) return;
_flutterTts = FlutterTts();
if (_flutterTts != null) {
final isAvailable = await _flutterTts!.isLanguageAvailable("ja-JP");
if (isAvailable == true) {
await _flutterTts?.setLanguage("ja-JP");
} else {
debugPrint('Japanese (ja-JP) TTS language not available.');
}
}
_isInitialized = true;
}
Future<bool> isLanguageAvailable(String language) async {
if (_flutterTts == null) {
await initTts();
}
return await _flutterTts?.isLanguageAvailable(language) ?? false;
}
Future<void> speak(String text) async {
const int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
if (_flutterTts == null || !_isInitialized) {
await initTts();
}
await _flutterTts?.speak(text);
return;
} on PlatformException catch (_) {
debugPrint('TTS speak failed, retrying...');
await _flutterTts?.stop();
_flutterTts = null;
_isInitialized = false;
await Future.delayed(const Duration(milliseconds: 500));
}
}
debugPrint('Failed to speak after $maxRetries retries.');
}
void dispose() {
_flutterTts?.stop();
_flutterTts = null;
_isInitialized = false;
}
}

View File

@@ -0,0 +1,224 @@
import 'dart:async';
import 'dart:convert';
import 'package:sqflite/sqflite.dart';
import '../models/vocabulary_item.dart';
import '../models/srs_item.dart';
import '../api/wk_client.dart';
import 'database_helper.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class VocabDeckRepository {
String? _apiKey;
Future<void> setApiKey(String apiKey) async {
_apiKey = apiKey;
await saveApiKey(apiKey);
}
String? get apiKey => _apiKey;
Future<void> saveApiKey(String apiKey) async {
final db = await DatabaseHelper().db;
await db.insert('settings', {
'key': 'apiKey',
'value': apiKey,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<String?> loadApiKey() async {
String? envApiKey;
try {
envApiKey = dotenv.env['WANIKANI_API_KEY'];
} catch (e) {
envApiKey = null;
}
if (envApiKey != null && envApiKey.isNotEmpty) {
_apiKey = envApiKey;
return _apiKey;
}
final db = await DatabaseHelper().db;
final rows = await db.query(
'settings',
where: 'key = ?',
whereArgs: ['apiKey'],
);
if (rows.isNotEmpty) {
_apiKey = rows.first['value'] as String;
return _apiKey;
}
return null;
}
Future<List<SrsItem>> getVocabSrsItems(int vocabId) async {
final db = await DatabaseHelper().db;
final rows = await db.query(
'srs_vocab_items',
where: 'vocabId = ?',
whereArgs: [vocabId],
);
return rows.map((r) {
return SrsItem(
subjectId: r['vocabId'] as int,
quizMode: QuizMode.values.firstWhere(
(e) => e.toString() == r['quizMode'] as String,
),
srsStage: r['srsStage'] as int,
lastAsked: DateTime.parse(r['lastAsked'] as String),
disabled: (r['disabled'] as int? ?? 0) == 1,
);
}).toList();
}
Future<void> updateSrsItems(List<SrsItem> items) async {
final db = await DatabaseHelper().db;
final batch = db.batch();
for (final item in items) {
batch.update(
'srs_vocab_items',
{
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
'disabled': item.disabled ? 1 : 0,
},
where: 'vocabId = ? AND quizMode = ?',
whereArgs: [item.subjectId, item.quizMode.toString()],
);
}
await batch.commit(noResult: true);
}
Future<void> updateVocabSrsItem(SrsItem item) async {
final db = await DatabaseHelper().db;
await db.update(
'srs_vocab_items',
{
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
'disabled': item.disabled ? 1 : 0,
},
where: 'vocabId = ? AND quizMode = ?',
whereArgs: [item.subjectId, item.quizMode.toString()],
);
}
Future<void> insertVocabSrsItem(SrsItem item) async {
final db = await DatabaseHelper().db;
await db.insert('srs_vocab_items', {
'vocabId': item.subjectId,
'quizMode': item.quizMode.toString(),
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
'disabled': item.disabled ? 1 : 0,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<void> saveVocabulary(List<VocabularyItem> items) async {
final db = await DatabaseHelper().db;
final batch = db.batch();
for (final it in items) {
final audios = it.pronunciationAudios
.map((a) => {'url': a.url, 'gender': a.gender})
.toList();
batch.insert('vocabulary', {
'id': it.id,
'level': it.level,
'characters': it.characters,
'meanings': it.meanings.join('|'),
'readings': it.readings.join('|'),
'pronunciation_audios': jsonEncode(audios),
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
await batch.commit(noResult: true);
}
Future<List<VocabularyItem>> loadVocabulary() async {
final db = await DatabaseHelper().db;
final rows = await db.query('vocabulary');
final vocabItems = rows.map((r) {
final audiosRaw = r['pronunciation_audios'] as String?;
final List<PronunciationAudio> audios = [];
if (audiosRaw != null && audiosRaw.isNotEmpty) {
try {
final decoded = jsonDecode(audiosRaw) as List;
for (final audioData in decoded) {
audios.add(
PronunciationAudio(
url: audioData['url'] as String,
gender: audioData['gender'] as String,
),
);
}
} finally {}
}
return VocabularyItem(
id: r['id'] as int,
level: r['level'] as int? ?? 0,
characters: r['characters'] as String,
meanings: (r['meanings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
readings: (r['readings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
pronunciationAudios: audios,
);
}).toList();
for (final item in vocabItems) {
final srsItems = await getVocabSrsItems(item.id);
for (final srsItem in srsItems) {
final key = srsItem.quizMode.toString();
item.srsItems[key] = srsItem;
}
}
return vocabItems;
}
Future<List<VocabularyItem>> fetchAndCacheVocabularyFromWk([
String? apiKey,
]) async {
final key = apiKey ?? _apiKey;
if (key == null) throw Exception('API key not set');
final client = WkClient(key);
final assignments = await client.fetchAllAssignments(
subjectTypes: ['vocabulary'],
);
final unlocked = <int>{};
for (final a in assignments) {
final data = a['data'] as Map<String, dynamic>;
final sidRaw = data['subject_id'];
if (sidRaw == null) continue;
final sid = sidRaw is int ? sidRaw : int.tryParse(sidRaw.toString());
if (sid == null) continue;
final started = data['started_at'];
final srs = data['srs_stage'];
final isUnlocked = (started != null) || (srs != null && (srs as int) > 0);
if (isUnlocked) unlocked.add(sid);
}
if (unlocked.isEmpty) return [];
final subjects = await client.fetchSubjectsByIds(unlocked.toList());
final items = subjects
.where(
(s) =>
s['object'] == 'vocabulary' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'vocabulary'),
)
.map((s) => VocabularyItem.fromSubject(s))
.where((k) => k.characters.isNotEmpty && k.meanings.isNotEmpty)
.toList();
await saveVocabulary(items);
return items;
}
}

131
lib/src/themes.dart Normal file
View File

@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
class SrsColors {
final Color level1;
final Color level2;
final Color level3;
final Color level4;
final Color level5;
final Color level6;
final Color level7;
final Color level8;
final Color level9;
const SrsColors({
required this.level1,
required this.level2,
required this.level3,
required this.level4,
required this.level5,
required this.level6,
required this.level7,
required this.level8,
required this.level9,
});
}
extension CustomTheme on ThemeData {
SrsColors get srsColors {
if (brightness == Brightness.dark) {
return const SrsColors(
level1: Color(0xFFE57373), // red
level2: Color(0xFFFFB74D), // orange
level3: Color(0xFFFFD54F), // yellow
level4: Color(0xFFDCE775), // lime
level5: Color(0xFFAED581), // light green
level6: Color(0xFF81C784), // green
level7: Color(0xFF4DB6AC), // teal
level8: Color(0xFF4FC3F7), // light blue
level9: Color(0xFF7986CB), // indigo
);
} else if (colorScheme.primary == const Color(0xFF7B6D53)) {
// Nier theme
return const SrsColors(
level1: Color(0xFFB71C1C), // dark red
level2: Color(0xFFD84315), // deep orange
level3: Color(0xFFF57F17), // yellow
level4: Color(0xFF9E9D24), // lime
level5: Color(0xFF558B2F), // light green
level6: Color(0xFF2E7D32), // green
level7: Color(0xFF00695C), // teal
level8: Color(0xFF0277BD), // light blue
level9: Color(0xFF283593), // indigo
);
} else {
// Light theme
return const SrsColors(
level1: Colors.red,
level2: Colors.orange,
level3: Colors.yellow,
level4: Colors.lightGreen,
level5: Colors.green,
level6: Colors.teal,
level7: Colors.cyan,
level8: Colors.blue,
level9: Colors.purple,
);
}
}
}
class Themes {
static final dark = ThemeData(
colorScheme: const ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFF90CAF9),
onPrimary: Colors.black,
secondary: Color(0xFFBBDEFB),
onSecondary: Colors.black,
tertiary: Color(0xFFA5D6A7),
onTertiary: Colors.black,
error: Color(0xFFEF9A9A),
onError: Colors.black,
surface: Color(0xFF121212),
onSurface: Colors.white,
surfaceContainer: Color(0xFF1E1E1E),
surfaceContainerHighest: Color(0xFF424242),
onSurfaceVariant: Colors.white70,
),
useMaterial3: true,
);
static final light = ThemeData(
colorScheme: const ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF1976D2),
onPrimary: Colors.white,
secondary: Color(0xFF42A5F5),
onSecondary: Colors.white,
tertiary: Color(0xFF66BB6A),
onTertiary: Colors.white,
error: Color(0xFFE57373),
onError: Colors.white,
surface: Color(0xFFFFFFFF),
onSurface: Colors.black,
surfaceContainer: Color(0xFFF5F5F5),
surfaceContainerHighest: Color(0xFFE0E0E0),
onSurfaceVariant: Colors.black54,
),
useMaterial3: true,
);
static final nier = ThemeData(
colorScheme: const ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF7B6D53),
onPrimary: Colors.white,
secondary: Color(0xFFA99A7E),
onSecondary: Colors.white,
tertiary: Color(0xFFA99A7E),
onTertiary: Colors.white,
error: Color(0xFFD32F2F),
onError: Colors.white,
surface: Color(0xFFCFCBAA),
onSurface: Color(0xFF333333),
surfaceContainer: Color(0xFFBDB898),
surfaceContainerHighest: Color(0xFFA8A388),
onSurfaceVariant: Color(0xFF545454),
),
useMaterial3: true,
);
}

View File

@@ -2,13 +2,15 @@ import 'package:flutter/material.dart';
class KanjiCard extends StatelessWidget {
final String characters;
final Widget? characterWidget;
final String subtitle;
final Color? backgroundColor;
final Color? textColor;
const KanjiCard({
super.key,
required this.characters,
this.characters = '',
this.characterWidget,
this.subtitle = '',
this.backgroundColor,
this.textColor,
@@ -17,13 +19,19 @@ class KanjiCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final bgColor = backgroundColor ?? theme.cardTheme.color ?? theme.colorScheme.surface;
final fgColor = textColor ?? theme.textTheme.bodyMedium?.color ?? theme.colorScheme.onSurface;
final bgColor =
backgroundColor ?? theme.cardTheme.color ?? theme.colorScheme.surface;
final fgColor =
textColor ??
theme.textTheme.bodyMedium?.color ??
theme.colorScheme.onSurface;
return Card(
elevation: theme.cardTheme.elevation ?? 12,
color: bgColor,
shape: theme.cardTheme.shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
shape:
theme.cardTheme.shape ??
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: SizedBox(
width: 360,
height: 240,
@@ -32,19 +40,20 @@ class KanjiCard extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
characters,
style: theme.textTheme.headlineMedium?.copyWith(
fontSize: 56,
color: fgColor,
),
textAlign: TextAlign.center,
),
characterWidget ??
Text(
characters,
style: theme.textTheme.headlineMedium?.copyWith(
fontSize: 56,
color: fgColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
subtitle,
style: theme.textTheme.bodyMedium?.copyWith(
color: fgColor.withValues(alpha: 0.7),
color: fgColor.withAlpha((255 * 0.7).round()),
),
textAlign: TextAlign.center,
),

View File

@@ -5,6 +5,10 @@ class OptionsGrid extends StatelessWidget {
final void Function(String) onSelected;
final Color? buttonColor;
final Color? textColor;
final bool isDisabled;
final String? selectedOption;
final List<String>? correctAnswers;
final bool showResult;
const OptionsGrid({
super.key,
@@ -12,6 +16,10 @@ class OptionsGrid extends StatelessWidget {
required this.onSelected,
this.buttonColor,
this.textColor,
this.isDisabled = false,
this.selectedOption,
this.correctAnswers,
this.showResult = false,
});
@override
@@ -27,13 +35,28 @@ class OptionsGrid extends StatelessWidget {
runSpacing: 10,
alignment: WrapAlignment.center,
children: options.map((o) {
Color currentButtonColor = bg;
Color currentTextColor = fg;
if (showResult) {
final normalizedOption = o.trim().toLowerCase();
if (correctAnswers != null &&
correctAnswers!
.map((e) => e.trim().toLowerCase())
.contains(normalizedOption)) {
currentButtonColor = theme.colorScheme.tertiary;
}
}
return SizedBox(
width: 160,
child: ElevatedButton(
onPressed: () => onSelected(o),
onPressed: isDisabled || o == '---' ? null : () => onSelected(o),
style: ElevatedButton.styleFrom(
backgroundColor: bg,
foregroundColor: fg,
backgroundColor: currentButtonColor,
foregroundColor: currentTextColor,
disabledBackgroundColor:
theme.colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
@@ -41,7 +64,9 @@ class OptionsGrid extends StatelessWidget {
),
child: Text(
o,
style: TextStyle(fontSize: 20, color: fg),
style: theme.textTheme.titleMedium?.copyWith(
color: currentTextColor,
),
textAlign: TextAlign.center,
),
),

1
linux/.gitignore vendored
View File

@@ -1 +0,0 @@
flutter/ephemeral

View File

@@ -1,128 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "untitled1")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.untitled1")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -1,88 +0,0 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
}

View File

@@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -1,24 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -1,26 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

View File

@@ -1,6 +0,0 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@@ -1,144 +0,0 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView *view)
{
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "untitled1");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "untitled1");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000 for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

View File

@@ -1,18 +0,0 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
macos/.gitignore vendored
View File

@@ -1,7 +0,0 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -1 +0,0 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1 +0,0 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1,18 +0,0 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import audioplayers_darwin
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}

View File

@@ -1,705 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* untitled1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "untitled1.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* untitled1.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* untitled1.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/untitled1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/untitled1";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/untitled1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/untitled1";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.untitled1.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/untitled1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/untitled1";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,99 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "untitled1.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "untitled1.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "untitled1.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "untitled1.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,13 +0,0 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View File

@@ -1,68 +0,0 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Some files were not shown because too many files have changed in this diff Show More