second commit

This commit is contained in:
AlizarMuharrom 2025-06-19 14:28:01 +07:00
parent 879a6028c4
commit f1fa31b3c0
213 changed files with 23337 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "c519ee916eaeb88923e67befb89c0f1dabfa83e6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: android
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: ios
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: linux
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: macos
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: web
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: windows
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

32
analysis_options.yaml Normal file
View File

@ -0,0 +1,32 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
errors:
collection_methods_unrelated_type: ignore
library_private_types_in_public_api: ignore
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

44
android/app/build.gradle Normal file
View File

@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "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.bahasajepang"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.bahasajepang"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Benkyou"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.bahasajepang
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View File

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

25
android/settings.gradle Normal file
View File

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

BIN
assets/dictionary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/email.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/food.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
assets/image_splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
assets/kanji.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/padlock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/subject-matter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/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

@ -0,0 +1,26 @@
<?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>12.0</string>
</dict>
</plist>

View File

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

View File

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

View File

@ -0,0 +1,616 @@
// !$*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 = 12.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.bahasajepang;
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.bahasajepang.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.bahasajepang.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.bahasajepang.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 = 12.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 = 12.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.bahasajepang;
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.bahasajepang;
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

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

View File

@ -0,0 +1,8 @@
<?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

@ -0,0 +1,8 @@
<?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

@ -0,0 +1,98 @@
<?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"
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"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
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

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

View File

@ -0,0 +1,8 @@
<?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

@ -0,0 +1,8 @@
<?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

@ -0,0 +1,13 @@
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

@ -0,0 +1,122 @@
{
"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.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"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.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# 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

@ -0,0 +1,37 @@
<?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

@ -0,0 +1,26 @@
<?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>

49
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,49 @@
<?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>Bahasajepang</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>bahasajepang</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

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

View File

@ -0,0 +1,12 @@
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.
}
}

71
lib/main.dart Normal file
View File

@ -0,0 +1,71 @@
import 'package:bahasajepang/pages/forgot_password.dart';
import 'package:bahasajepang/pages/level_page.dart';
import 'package:bahasajepang/pages/n4/kanji/kanji%20jukugo/detail_jukugo.dart';
import 'package:bahasajepang/pages/n4/kanji/kanji%20jukugo/kanji_jukugo.dart';
import 'package:bahasajepang/pages/n4/kanji/kanji%20okurigana/detail_okurigana.dart';
import 'package:bahasajepang/pages/n4/kanji/kanji%20okurigana/kanji_okurigana.dart';
import 'package:bahasajepang/pages/n4/kanji/kanji%20tandoku/detail_tandoku.dart';
import 'package:bahasajepang/pages/n4/kanji/kanji%20tandoku/kanji_tandoku.dart';
import 'package:bahasajepang/pages/n4/main_page.dart';
import 'package:bahasajepang/pages/n5/kanji/kanji%20jukugo/detail_jukugo.dart';
import 'package:bahasajepang/pages/n5/kanji/kanji%20jukugo/kanji_jukugo.dart';
import 'package:bahasajepang/pages/n5/kanji/kanji%20okurigana/detail_okurigana.dart';
import 'package:bahasajepang/pages/n5/kanji/kanji%20okurigana/kanji_okurigana.dart';
import 'package:bahasajepang/pages/n5/kanji/kanji%20tandoku/detail_tandoku.dart';
import 'package:bahasajepang/pages/n5/kanji/kanji%20tandoku/kanji_tandoku.dart';
import 'package:bahasajepang/pages/n5/main_page.dart';
import 'package:bahasajepang/pages/reset_password.dart';
import 'package:bahasajepang/pages/sign_in_page.dart';
import 'package:bahasajepang/pages/sign_up_page.dart';
import 'package:bahasajepang/pages/pemula/main_page.dart';
import 'package:bahasajepang/pages/splash_page.dart';
import 'package:bahasajepang/pages/token_verification.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/': (context) => SplashPage(),
'/sign-in': (context) => SignInPage(),
'/sign-up': (context) => SignUpPage(),
'/level': (context) => LevelSelectionPage(),
'/pemula': (context) => PemulaPage(),
'/n5': (context) => NlimaPage(),
'/n4': (context) => NEmpatPage(),
'/kanji-tandoku5': (context) => KanjiTandokuPage(),
'/kanji-okurigana5': (context) => KanjiOkuriganaPage(),
'/kanji-jukugo5': (context) => KanjiJukugoPage(),
'/detail-okurigana': (context) => DetailOkuriganaPage(),
'/detail-tandoku': (context) => DetailTandokuPage(),
'/detail-jukugo': (context) => DetailJukugoPage(),
'/kanji-tandoku4': (context) => KanjiTandoku4Page(),
'/kanji-okurigana4': (context) => KanjiOkurigana4Page(),
'/kanji-jukugo4': (context) => KanjiJukugo4Page(),
'/detail-tandoku4': (context) => DetailTandoku4Page(),
'/detail-okurigana4': (context) => DetailOkurigana4Page(),
'/detail-jukugo4': (context) => DetailJukugo4Page(),
'/forgot-password': (context) => ForgotPasswordPage(),
'/token-verification': (context) {
final args = ModalRoute.of(context)!.settings.arguments
as Map<String, dynamic>;
return TokenVerificationPage(email: args['email']);
},
'/reset-password': (context) {
final args = ModalRoute.of(context)!.settings.arguments
as Map<String, dynamic>;
return ResetPasswordPage(
email: args['email'],
token: args['token'],
);
},
},
);
}
}

View File

@ -0,0 +1,28 @@
class UserModel {
final int id;
final String fullname;
final String username;
final String email;
final String password;
final int? level_id;
UserModel({
required this.id,
required this.fullname,
required this.username,
required this.email,
required this.password,
this.level_id,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] ?? 0, // Berikan nilai default jika null
fullname: json['fullname'] ?? "", // Berikan nilai default jika null
username: json['username'] ?? "", // Berikan nilai default jika null
email: json['email'] ?? "", // Berikan nilai default jika null
password: json['password'] ?? "", // Berikan nilai default jika null
level_id: json['level_id'], // Biarkan null jika tidak ada
);
}
}

View File

View File

@ -0,0 +1,125 @@
import 'package:bahasajepang/service/API_config.dart';
import 'package:bahasajepang/theme.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class ForgotPasswordPage extends StatefulWidget {
@override
State<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
}
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
final TextEditingController _emailController = TextEditingController();
bool _isLoading = false;
Future<void> _sendEmail() async {
// Validasi email kosong
if (_emailController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Email tidak boleh kosong')),
);
return;
}
setState(() {
_isLoading = true;
});
try {
final url = Uri.parse('${ApiConfig.baseUrl}/forgot-password');
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Connection': 'keep-alive',
'ngrok-skip-browser-warning': 'true' // Untuk ngrok
},
body: json.encode({'email': _emailController.text}),
);
final responseData = json.decode(response.body);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(responseData['message'])),
);
Navigator.pushNamed(context, '/token-verification', arguments: {
'email': _emailController.text,
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(responseData['message'] ?? 'Terjadi kesalahan')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal terhubung ke server: $e')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Forgot Password'),
backgroundColor: bgColor3,
),
backgroundColor: bgColor1,
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Masukkan email Anda untuk reset password',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 8),
const Text(
'Pastikan Anda menginputkan email yang terdaftar dan bukan email palsu.',
style: TextStyle(
fontSize: 14,
color: Colors.redAccent,
),
),
const SizedBox(height: 20),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
backgroundColor: bgColor2,
),
onPressed: _isLoading ? null : _sendEmail,
child: _isLoading
? CircularProgressIndicator(color: bgColor2)
: Text(
'Kirim Token',
style: TextStyle(fontSize: 16, color: Colors.black),
),
),
),
],
),
),
);
}
}

341
lib/pages/level_page.dart Normal file
View File

@ -0,0 +1,341 @@
import 'dart:convert';
import 'package:bahasajepang/pages/pretest_page.dart';
import 'package:bahasajepang/service/API_config.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class LevelSelectionPage extends StatefulWidget {
const LevelSelectionPage({super.key});
@override
// ignore: library_private_types_in_public_api
_LevelSelectionPageState createState() => _LevelSelectionPageState();
}
class _LevelSelectionPageState extends State<LevelSelectionPage> {
int? userId;
int? level_id; // Tambahkan variabel levelId
@override
void initState() {
super.initState();
loadUserData();
}
Future<void> loadUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.getKeys().forEach((key) {
print("$key : ${prefs.get(key)}");
});
setState(() {
userId = prefs.getInt('id');
level_id = prefs.getInt('level_id');
});
// Hanya arahkan jika levelId memiliki nilai yang valid (1, 2, atau 3)
if (level_id != null && level_id! > 0 && level_id! <= 3) {
print("LEVEL ID : $level_id");
WidgetsBinding.instance.addPostFrameCallback((_) {
switch (level_id) {
case 1:
Navigator.pushNamedAndRemoveUntil(
context, '/pemula', (route) => false);
break;
case 2:
Navigator.pushNamedAndRemoveUntil(context, '/n5', (route) => false);
break;
case 3:
Navigator.pushNamedAndRemoveUntil(context, '/n4', (route) => false);
break;
}
});
} else {
// Jika levelId null, 0, atau tidak valid, tetap di halaman level selection
print("Level ID belum dipilih atau tidak valid: $level_id");
}
}
Future<void> sendNumberToDatabase(int number) async {
const endpoint = "/update-level";
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
int? userId = prefs.getInt('id');
if (userId == null) {
print('User ID tidak ditemukan');
return;
}
var response = await http.post(
Uri.parse(ApiConfig.baseUrl + endpoint),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'user_id': userId, 'level_id': number}),
);
if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body);
print('Response: ${jsonResponse}');
} else {
print('Failed to send number. Status Code: ${response.statusCode}');
}
} catch (e) {
print('Error: $e');
}
}
Future<void> updateUserLevel(int userId, int level_id) async {
const endpoint = "/update-level";
try {
print('Sending data: user_id=$userId, level_id=$level_id');
var response = await http.post(
Uri.parse(ApiConfig.baseUrl + endpoint),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'user_id': userId, 'level_id': level_id}),
);
if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body);
print('Response: ${jsonResponse}');
} else {
print('Failed to update level. Status Code: ${response.statusCode}');
}
} catch (e) {
print('Error: $e');
}
}
Future<void> sendLevelToDatabase(int level_id) async {
const endpoint = "/update-level";
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
int? userId = prefs.getInt('id');
if (userId == null) {
print('User ID tidak ditemukan');
return;
}
var response = await http.post(
Uri.parse(ApiConfig.baseUrl + endpoint),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'user_id': userId, 'level_id': level_id}),
);
if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body);
print('Response: ${jsonResponse}');
// Simpan levelId ke SharedPreferences
await prefs.setInt('levelId', level_id);
// Arahkan ke halaman level yang sesuai
switch (level_id) {
case 1:
Navigator.pushNamedAndRemoveUntil(
context, '/pemula', (route) => false);
break;
case 2:
Navigator.pushNamedAndRemoveUntil(context, '/n5', (route) => false);
break;
case 3:
Navigator.pushNamedAndRemoveUntil(context, '/n4', (route) => false);
break;
default:
print("level_page2");
Navigator.pushNamedAndRemoveUntil(
context, '/level', (route) => false);
}
} else {
print('Failed to send level. Status Code: ${response.statusCode}');
}
} catch (e) {
print('Error: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue.shade50,
Colors.blue.shade100,
Colors.blue.shade200,
],
),
),
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/image_splash.png',
width: 180,
height: 180,
fit: BoxFit.contain,
),
const SizedBox(height: 30),
const Text(
"Pilih Level Kemampuanmu",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.blueGrey,
),
),
const SizedBox(height: 8),
const Text(
"Silakan pilih level yang sesuai dengan kemampuan bahasa Jepang Anda",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.blueGrey,
),
),
const SizedBox(height: 30),
_buildLevelCard(
title: "Tes Kemampuan",
subtitle: "Ikuti pretest untuk mengetahui level Anda",
color: Colors.blue.shade200,
icon: Icons.quiz,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PretestPage()),
);
},
),
// const SizedBox(height: 16),
// _buildLevelCard(
// title: "Pemula",
// subtitle: "Belum pernah belajar bahasa Jepang",
// color: Colors.blue.shade200,
// icon: Icons.school,
// onPressed: () async {
// SharedPreferences prefs =
// await SharedPreferences.getInstance();
// int? userId = prefs.getInt('id');
// if (userId != null) {
// await sendLevelToDatabase(1);
// } else {
// print('User ID tidak ditemukan');
// }
// },
// ),
// const SizedBox(height: 16),
// _buildLevelCard(
// title: "N5",
// subtitle: "Mengetahui huruf dasar bahasa Jepang",
// color: const Color.fromRGBO(100, 181, 246, 1),
// icon: Icons.language,
// onPressed: () async {
// SharedPreferences prefs =
// await SharedPreferences.getInstance();
// int? userId = prefs.getInt('id');
// if (userId != null) {
// await sendLevelToDatabase(2);
// } else {
// print('User ID tidak ditemukan');
// }
// },
// ),
// const SizedBox(height: 16),
// _buildLevelCard(
// title: "N4",
// subtitle: "Mengetahui lebih dari 100 kanji dan 800 kosakata",
// color: Colors.blue.shade400,
// icon: Icons.auto_awesome,
// onPressed: () async {
// SharedPreferences prefs =
// await SharedPreferences.getInstance();
// int? userId = prefs.getInt('id');
// if (userId != null) {
// await sendLevelToDatabase(3);
// } else {
// print('User ID tidak ditemukan');
// }
// },
// ),
],
),
),
),
),
);
}
Widget _buildLevelCard({
required String title,
required String subtitle,
required Color color,
required IconData icon,
required VoidCallback onPressed,
}) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onPressed,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.5), width: 1),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.3),
shape: BoxShape.circle,
),
child: Icon(icon, color: color.withOpacity(0.8), size: 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color.withOpacity(0.9),
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.blueGrey.shade700,
),
),
],
),
),
Icon(Icons.chevron_right, color: color.withOpacity(0.7)),
],
),
),
),
);
}
}

View File

@ -0,0 +1,348 @@
import 'package:bahasajepang/pages/n5/kamus/kamus_service.dart';
import 'package:bahasajepang/service/API_config.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:audioplayers/audioplayers.dart';
class DetailKamus4Page extends StatefulWidget {
final int kamusId;
final AudioPlayer audioPlayer = AudioPlayer();
DetailKamus4Page({super.key, required this.kamusId});
@override
State<DetailKamus4Page> createState() => _DetailKamus4PageState();
}
class _DetailKamus4PageState extends State<DetailKamus4Page>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late Future<dynamic> _kamusFuture;
final KamusService _kamusService = KamusService();
@override
void initState() {
super.initState();
_kamusFuture = _kamusService.fetchKamusById(widget.kamusId);
widget.audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {});
});
}
@override
void dispose() {
widget.audioPlayer.dispose();
super.dispose();
}
Future<void> _playAudio(String? audioUrl) async {
if (audioUrl == null || audioUrl.isEmpty) return;
try {
final fullUrl = "${ApiConfig.url}/$audioUrl";
await widget.audioPlayer.play(UrlSource(fullUrl));
if (!mounted) return;
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Audio Tidak Tersedia'),
backgroundColor: bgColor3,
),
);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: bgColor1.withValues(alpha: 0.95),
appBar: AppBar(
title: const Text(
'Detail Kamus N4',
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold, fontSize: 18),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withValues(alpha: 0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: FutureBuilder<dynamic>(
future: _kamusFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(bgColor2),
),
);
} else if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 50),
const SizedBox(height: 16),
Text(
'Error: ${snapshot.error}',
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
],
),
);
} else if (!snapshot.hasData) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, color: Colors.grey, size: 50),
const SizedBox(height: 16),
const Text(
'Data tidak ditemukan',
style: TextStyle(fontSize: 16),
),
],
),
);
}
final item = snapshot.data!;
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informasi Utama Kamus
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.menu_book,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Informasi Kamus',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 16),
_buildDetailRow('Judul', item["judul"]),
_buildDetailRow('Nama', item["nama"]),
_buildDetailRow('Baca', item["baca"]),
],
),
),
const SizedBox(height: 24),
// Contoh Penggunaan
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
shape: BoxShape.circle,
),
child: Icon(
Icons.format_quote,
color: bgColor2,
size: 24,
),
),
const SizedBox(width: 12),
Text(
'Contoh Penggunaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 16),
if (item["detail_kamuses"] != null &&
(item["detail_kamuses"] as List).isNotEmpty)
...(item["detail_kamuses"] as List<dynamic>)
.map<Widget>((kamus) {
return _buildExampleCard(kamus);
})
else
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
'Tidak ada contoh penggunaan',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
],
),
),
],
),
);
},
),
);
}
Widget _buildDetailRow(String label, String? value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
color: Colors.white.withValues(alpha: 0.8),
),
),
const SizedBox(height: 4),
Text(
value ?? '-',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Divider(
color: Colors.white24,
height: 1,
),
],
),
);
}
Widget _buildExampleCard(Map<String, dynamic> kamus) {
return Card(
color: bgColor1,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kamus["kanji"] ?? '-',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (kamus["romaji"] != null && kamus["romaji"].isNotEmpty)
Text(
kamus["romaji"],
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.7),
),
),
],
),
),
IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
size: 30,
),
onPressed: () => _playAudio(kamus["voice_record"]),
),
],
),
const SizedBox(height: 8),
Text(
kamus["arti"] ?? '-',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,225 @@
import 'package:bahasajepang/pages/n5/kamus/kamus_service.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:bahasajepang/pages/n4/kamus/detail_kamus.dart';
class Kamus4Page extends StatefulWidget {
const Kamus4Page({super.key});
@override
State<Kamus4Page> createState() => _Kamus4PageState();
}
class _Kamus4PageState extends State<Kamus4Page> {
final TextEditingController _searchController = TextEditingController();
List<dynamic> _filteredKamus = [];
List<dynamic> _allKamus = [];
final KamusService _kamusService = KamusService();
bool _isLoading = true;
String _errorMessage = '';
@override
void initState() {
super.initState();
_loadKamusData();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadKamusData() async {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
final kamuses = await _kamusService.fetchKamusesByLevel(3); // Level N5
setState(() {
_allKamus = kamuses;
_filteredKamus = kamuses;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString().replaceAll('Exception: ', '');
});
}
}
void _onSearchChanged() {
final query = _searchController.text;
if (query.isEmpty) {
setState(() {
_filteredKamus = _allKamus;
});
return;
}
setState(() {
_filteredKamus = _allKamus.where((kamus) {
final nama = kamus["nama"]?.toString().toLowerCase() ?? '';
final judul = kamus["judul"]?.toString().toLowerCase() ?? '';
final queryLower = query.toLowerCase();
return nama.contains(queryLower) || judul.contains(queryLower);
}).toList();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha:0.95),
appBar: AppBar(
title: const Text(
'Kamus N4',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withValues(alpha:0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.white.withValues(alpha:0.9)),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Cari kamus...",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: bgColor2,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
),
// List Kamus
Expanded(
child: _isLoading
? Center(
child: CircularProgressIndicator(
color: bgColor2,
),
)
: _errorMessage.isNotEmpty
? Center(
child: Text(
_errorMessage,
style: const TextStyle(color: Colors.red),
),
)
: _filteredKamus.isEmpty
? const Center(
child: Text('Tidak ada hasil yang ditemukan'),
)
: ListView.builder(
itemCount: _filteredKamus.length,
itemBuilder: (context, index) {
final item = _filteredKamus[index];
return Card(
color: bgColor2,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailKamus4Page(
kamusId: item["id"]),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item["judul"] ??
'Tidak ada judul',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
item["nama"] ??
'Tidak ada nama',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: bgColor1,
size: 20,
),
],
),
),
),
);
},
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,79 @@
List<Map<String, dynamic>> detailKamusList = [
{
"judul": "母親",
"nama": "Ibu",
"baca": "ははおや",
"contohPenggunaan": [
{"kanji": "母は市場に行きます", "arti": "Ibu pergi ke pasar"},
{"kanji": "母が料理をしている", "arti": "Ibu sedang memasak"},
{"kanji": "母は庭にいる", "arti": "Ibu berada di kebun"},
{"kanji": "私の母は40歳です", "arti": "Ibuku berumur 40 tahun"}
]
},
{
"judul": "父親",
"nama": "Ayah",
"baca": "ちちおや",
"contohPenggunaan": [
{"kanji": "お父さんは仕事をしています", "arti": "Ayah sedang bekerja"},
{"kanji": "私の父は45歳です", "arti": "Ayahku berumur 45 tahun"},
{"kanji": "お父さんは庭にいる", "arti": "Ayah berada di kebun"},
{"kanji": "お父さんは寝ています", "arti": "Ayah sedang tidur"}
]
},
{
"judul": "走る",
"nama": "Berlari",
"baca": "はしる",
"contohPenggunaan": [
{"kanji": "彼は速く走る", "arti": "Dia berlari dengan cepat"},
{"kanji": "公園で走るのが好きです", "arti": "Saya suka berlari di taman"},
{"kanji": "犬が道を走っています", "arti": "Anjing sedang berlari di jalan"},
{"kanji": "毎朝5キロ走ります", "arti": "Saya berlari 5 km setiap pagi"}
]
},
{
"judul": "歩く",
"nama": "Berjalan",
"baca": "あるく",
"contohPenggunaan": [
{"kanji": "私は学校まで歩く", "arti": "Saya berjalan ke sekolah"},
{"kanji": "公園を歩くのが楽しい", "arti": "Berjalan di taman itu menyenangkan"},
{"kanji": "彼はゆっくり歩いている", "arti": "Dia berjalan dengan pelan"},
{"kanji": "夜に散歩を歩く", "arti": "Saya berjalan-jalan di malam hari"}
]
},
{
"judul": "作る",
"nama": "Membuat",
"baca": "つくる",
"contohPenggunaan": [
{"kanji": "母がケーキを作る", "arti": "Ibu membuat kue"},
{"kanji": "彼は模型を作るのが好きです", "arti": "Dia suka membuat model"},
{"kanji": "新しいアプリを作っています", "arti": "Saya sedang membuat aplikasi baru"},
{"kanji": "彼女は美しい絵を作った", "arti": "Dia membuat lukisan yang indah"}
]
},
{
"judul": "使用",
"nama": "Menggunakan",
"baca": "しよう",
"contohPenggunaan": [
{"kanji": "このカードを使用できます", "arti": "Kartu ini bisa digunakan"},
{"kanji": "パスワードを正しく使用してください", "arti": "Gunakan kata sandi dengan benar"},
{"kanji": "水の使用を減らしましょう", "arti": "Mari kurangi penggunaan air"},
{"kanji": "携帯電話の使用は禁止されています", "arti": "Penggunaan ponsel dilarang"}
]
},
{
"judul": "高い",
"nama": "Tinggi / Mahal",
"baca": "たかい",
"contohPenggunaan": [
{"kanji": "このビルはとても高い", "arti": "Gedung ini sangat tinggi"},
{"kanji": "この時計は高いですね", "arti": "Jam ini mahal ya"},
{"kanji": "彼は高いところが苦手です", "arti": "Dia tidak suka tempat tinggi"},
{"kanji": "この店の服は高すぎる", "arti": "Baju di toko ini terlalu mahal"}
]
},
];

View File

@ -0,0 +1,556 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:bahasajepang/service/API_config.dart';
class DetailJukugo4Page extends StatefulWidget {
final AudioPlayer audioPlayer = AudioPlayer();
DetailJukugo4Page({super.key});
@override
State<DetailJukugo4Page> createState() => _DetailJukugo4PageState();
}
class _DetailJukugo4PageState extends State<DetailJukugo4Page> {
late Map<String, dynamic> kanjiData;
@override
void didChangeDependencies() {
super.didChangeDependencies();
kanjiData =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
widget.audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {});
});
}
@override
void dispose() {
widget.audioPlayer.dispose();
super.dispose();
}
void _showWritingModal(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return WritingModal();
},
);
}
Future<void> _playVoice(String? audioUrl) async {
if (audioUrl == null || audioUrl.isEmpty) return;
try {
final fullUrl = "${ApiConfig.url}/$audioUrl";
await widget.audioPlayer.play(UrlSource(fullUrl));
if (!mounted) return;
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Audio Tidak Tersedia'),
backgroundColor: bgColor3,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha: 0.95),
appBar: AppBar(
title: const Text(
'Detail Kanji Jukugo',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withValues(alpha: 0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Kanji Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Kanji Display
Container(
width: 120,
height: 140,
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
kanjiData["judul"] ?? '?',
style: const TextStyle(
fontSize: 48,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
kanjiData["nama"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
const SizedBox(width: 16),
// Kanji Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReadingCard('Kunyomi', kanjiData["kunyomi"]),
const SizedBox(height: 8),
_buildReadingCard('Onyomi', kanjiData["onyomi"]),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanjiData["voice_record"]);
},
),
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Contoh Penggunaan
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.format_quote,
color: bgColor1,
size: 24,
),
const SizedBox(width: 8),
Text(
'Contoh Penggunaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
if (kanjiData["detail_kanji"] != null &&
(kanjiData["detail_kanji"] as List).isNotEmpty)
...(kanjiData["detail_kanji"] as List).map<Widget>((kanji) {
return _buildExampleUsage(kanji);
})
else
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Tidak ada contoh penggunaan',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _showWritingModal(context),
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: const Text(
"Coba Menulis Kanji",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
Widget _buildReadingCard(String title, String? reading) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Text(
"$title: ",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
reading ?? '-',
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
);
}
Widget _buildExampleUsage(Map<String, dynamic> kanji) {
return Card(
color: bgColor1,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color:
Colors.black.withValues(alpha: 0.2), // Warna bayangan
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 1), // Posisi bayangan (x, y)
),
],
color: Colors.white),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanji["voice_record"]);
},
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kanji["kanji"] ?? '',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
kanji["romaji"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
Expanded(
child: Text(
kanji["arti"] ?? '',
style: const TextStyle(fontSize: 14, color: Colors.white),
textAlign: TextAlign.end,
),
),
],
),
),
);
}
}
class WritingModal extends StatefulWidget {
const WritingModal({super.key});
@override
_WritingModalState createState() => _WritingModalState();
}
class _WritingModalState extends State<WritingModal> {
List<Offset> points = [];
final GlobalKey _paintKey = GlobalKey();
Rect? _drawingArea;
void _clearDrawing() {
setState(() {
points.clear();
});
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Latihan Menulis Kanji",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) {
// Simpan ukuran area gambar setelah layout selesai
WidgetsBinding.instance.addPostFrameCallback((_) {
final renderBox = _paintKey.currentContext?.findRenderObject()
as RenderBox?;
if (renderBox != null) {
final offset = renderBox.localToGlobal(Offset.zero);
setState(() {
_drawingArea = Rect.fromLTWH(
offset.dx,
offset.dy,
renderBox.size.width,
renderBox.size.height,
);
});
}
});
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: GestureDetector(
onPanStart: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
}
},
onPanUpdate: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
} else {
// Tambahkan titik kosong ketika keluar area
setState(() {
points.add(Offset.zero);
});
}
},
onPanEnd: (details) {
setState(() {
points.add(Offset.zero);
});
},
child: CustomPaint(
key: _paintKey,
size: Size.infinite,
painter: DrawingPainter(points),
),
),
);
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: _clearDrawing,
child: const Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 4),
Text("Hapus", style: TextStyle(color: Colors.red)),
],
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
),
child: const Text("Selesai"),
),
],
),
],
),
),
);
}
Offset _globalToLocal(Offset global) {
if (_drawingArea == null) return Offset.zero;
return Offset(
global.dx - _drawingArea!.left,
global.dy - _drawingArea!.top,
);
}
}
class DrawingPainter extends CustomPainter {
final List<Offset> points;
DrawingPainter(this.points);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..strokeWidth = 4.0
..strokeCap = StrokeCap.round;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != Offset.zero && points[i + 1] != Offset.zero) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@ -0,0 +1,268 @@
import 'package:bahasajepang/pages/n5/kanji/kanji_service.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
class KanjiJukugo4Page extends StatefulWidget {
const KanjiJukugo4Page({super.key});
@override
_KanjiJukugo4PageState createState() => _KanjiJukugo4PageState();
}
class _KanjiJukugo4PageState extends State<KanjiJukugo4Page> {
final TextEditingController _searchController = TextEditingController();
final KanjiService _kanjiService = KanjiService();
List<dynamic> _filteredKanji = [];
List<dynamic> _allKanji = [];
bool _isLoading = true;
String _errorMessage = '';
@override
void initState() {
super.initState();
_fetchKanji();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _fetchKanji() async {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
var kanjiList = await _kanjiService.fetchKanjiByKategori('jukugo');
var filteredKanji = kanjiList
.where((kanji) =>
kanji["kategori"] == "jukugo" && kanji["level_id"] == 3)
.toList();
setState(() {
_allKanji = filteredKanji;
_filteredKanji = filteredKanji;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Gagal memuat data kanji';
});
}
}
void _onSearchChanged() {
final query = _searchController.text;
if (query.isEmpty) {
setState(() {
_filteredKanji = _allKanji;
});
return;
}
setState(() {
_filteredKanji = _allKanji.where((kanji) {
final judul = kanji["judul"]?.toString().toLowerCase() ?? '';
final nama = kanji["nama"]?.toString().toLowerCase() ?? '';
final kunyomi = kanji["kunyomi"]?.toString().toLowerCase() ?? '';
final queryLower = query.toLowerCase();
return judul.contains(queryLower) ||
nama.contains(queryLower) ||
kunyomi.contains(queryLower);
}).toList();
});
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final cardSize =
screenWidth / 3.5; // Menyesuaikan ukuran card berdasarkan lebar layar
return Scaffold(
backgroundColor: bgColor1.withOpacity(0.95),
appBar: AppBar(
title: const Text(
'Kanji Jukugo N4',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withOpacity(0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: Column(
children: [
// Search Bar
Container(
padding: const EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Cari kanji...",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: bgColor2.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
),
),
// Content
Expanded(
child: _isLoading
? Center(
child: CircularProgressIndicator(
color: bgColor2,
),
)
: _errorMessage.isNotEmpty
? Center(
child: Text(
_errorMessage,
style: const TextStyle(color: Colors.red),
),
)
: _filteredKanji.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 50,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Tidak ada kanji yang ditemukan',
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: GridView.builder(
padding: const EdgeInsets.only(bottom: 20),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio:
0.85, // Nilai yang lebih optimal
mainAxisExtent:
cardSize, // Gunakan ukuran dinamis
),
itemCount: _filteredKanji.length,
itemBuilder: (context, index) {
return _kanjiCard(
_filteredKanji[index], context);
},
),
),
),
],
),
);
}
Widget _kanjiCard(dynamic kanji, BuildContext context) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.pushNamed(
context,
'/detail-jukugo4',
arguments: kanji,
);
},
child: Container(
padding: const EdgeInsets.all(8), // Padding yang lebih kecil
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
bgColor2.withOpacity(0.7),
bgColor2.withOpacity(0.9),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["judul"] ?? '?',
style: const TextStyle(
fontSize: 30, // Ukuran font yang lebih moderat
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["nama"] ?? '',
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.9),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,302 @@
List<Map<String, dynamic>> detailJukugoList = [
{
"judul": "",
"nama": "Giat",
"kunyomi": ["つと"],
"onyomi": ["べん"],
"kanjiGabungan": [
{"kanji": "勉強", "furigana": "べんきょう", "arti": "Belajar"},
{"kanji": "勤勉", "furigana": "きんべん", "arti": "Rajin, Tekun"},
{"kanji": "勉学", "furigana": "べんがく", "arti": "Pembelajaran"},
{"kanji": "勉める", "furigana": "つとめる", "arti": "Berusaha keras"}
]
},
{
"judul": "",
"nama": "Maju",
"kunyomi": ["すす"],
"onyomi": ["しん"],
"kanjiGabungan": [
{"kanji": "進学", "furigana": "しんがく", "arti": "Melanjutkan pendidikan"},
{"kanji": "進化", "furigana": "しんか", "arti": "Evolusi, Kemajuan"},
{"kanji": "前進", "furigana": "ぜんしん", "arti": "Majuan ke depan"},
{"kanji": "進歩", "furigana": "しんぽ", "arti": "Kemajuan"}
]
},
{
"judul": "",
"nama": "Berkumpul",
"kunyomi": ["あつ"],
"onyomi": ["しゅう"],
"kanjiGabungan": [
{"kanji": "集合", "furigana": "しゅうごう", "arti": "Perkumpulan"},
{"kanji": "集会", "furigana": "しゅうかい", "arti": "Rapat, Pertemuan"},
{"kanji": "収集", "furigana": "しゅうしゅう", "arti": "Pengumpulan"},
{"kanji": "集中", "furigana": "しゅうちゅう", "arti": "Konsentrasi"}
]
},
{
"judul": "",
"nama": "Kalimat",
"kunyomi": ["ふみ"],
"onyomi": ["ぶん", "もん"],
"kanjiGabungan": [
{"kanji": "文章", "furigana": "ぶんしょう", "arti": "Teks, Karangan"},
{"kanji": "文学", "furigana": "ぶんがく", "arti": "Sastra"},
{"kanji": "文化", "furigana": "ぶんか", "arti": "Budaya"},
{"kanji": "文法", "furigana": "ぶんぽう", "arti": "Tata bahasa"}
]
},
{
"judul": "",
"nama": "Kabar",
"kunyomi": ["むく"],
"onyomi": ["ほう"],
"kanjiGabungan": [
{"kanji": "報告", "furigana": "ほうこく", "arti": "Laporan"},
{"kanji": "情報", "furigana": "じょうほう", "arti": "Informasi"},
{"kanji": "電報", "furigana": "でんぽう", "arti": "Telegram"},
{"kanji": "新聞", "furigana": "しんぶん", "arti": "Surat kabar"}
]
},
{
"judul": "",
"nama": "Ampuh",
"kunyomi": [""],
"onyomi": ["こう"],
"kanjiGabungan": [
{"kanji": "効果", "furigana": "こうか", "arti": "Efek, Dampak"},
{"kanji": "有効", "furigana": "ゆうこう", "arti": "Berlaku, Efektif"},
{"kanji": "効能", "furigana": "こうのう", "arti": "Khasiat"},
{"kanji": "効率", "furigana": "こうりつ", "arti": "Efisiensi"}
]
},
{
"judul": "",
"nama": "Gedung",
"kunyomi": ["やかた"],
"onyomi": ["かん"],
"kanjiGabungan": [
{"kanji": "図書館", "furigana": "としょかん", "arti": "Perpustakaan"},
{"kanji": "映画館", "furigana": "えいがかん", "arti": "Bioskop"},
{"kanji": "大使館", "furigana": "たいしかん", "arti": "Kedutaan"},
{"kanji": "体育館", "furigana": "たいいくかん", "arti": "Gedung olahraga"}
]
},
{
"judul": "",
"nama": "Ada, Memiliki",
"kunyomi": [""],
"onyomi": ["ゆう"],
"kanjiGabungan": [
{"kanji": "有名", "furigana": "ゆうめい", "arti": "Terkenal"},
{"kanji": "有料", "furigana": "ゆうりょう", "arti": "Berbayar"},
{"kanji": "所有", "furigana": "しょゆう", "arti": "Kepemilikan"},
{"kanji": "有する", "furigana": "ゆうする", "arti": "Memiliki"}
]
},
{
"judul": "",
"nama": "Lalu, Pergi",
"kunyomi": [""],
"onyomi": ["きょ", ""],
"kanjiGabungan": [
{"kanji": "去年", "furigana": "きょねん", "arti": "Tahun lalu"},
{"kanji": "過去", "furigana": "かこ", "arti": "Masa lalu"},
{"kanji": "死去", "furigana": "しきょ", "arti": "Meninggal dunia"},
{"kanji": "去る", "furigana": "さる", "arti": "Pergi, Meninggalkan"}
]
},
{
"judul": "",
"nama": "Benar, Nyata",
"kunyomi": [""],
"onyomi": ["しん"],
"kanjiGabungan": [
{"kanji": "真実", "furigana": "しんじつ", "arti": "Kebenaran"},
{"kanji": "写真", "furigana": "しゃしん", "arti": "Foto"},
{"kanji": "真面目", "furigana": "まじめ", "arti": "Serius, Rajin"},
{"kanji": "真っ赤", "furigana": "まっか", "arti": "Merah terang"}
]
},
{
"judul": "",
"nama": "Orang, Cara",
"kunyomi": ["かた"],
"onyomi": ["ほう"],
"kanjiGabungan": [
{"kanji": "方法", "furigana": "ほうほう", "arti": "Metode, Cara"},
{"kanji": "方向", "furigana": "ほうこう", "arti": "Arah, Tujuan"},
{"kanji": "読み方", "furigana": "よみかた", "arti": "Cara membaca"},
{"kanji": "使い方", "furigana": "つかいかた", "arti": "Cara menggunakan"}
]
},
{
"judul": "",
"nama": "Dokter",
"kunyomi": [""],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "医者", "furigana": "いしゃ", "arti": "Dokter"},
{"kanji": "医学", "furigana": "いがく", "arti": "Ilmu Kedokteran"},
{"kanji": "医院", "furigana": "いいん", "arti": "Klinik"},
{"kanji": "名医", "furigana": "めいい", "arti": "Dokter Ahli"}
]
},
{
"judul": "",
"nama": "Orang",
"kunyomi": ["もの"],
"onyomi": ["しゃ"],
"kanjiGabungan": [
{"kanji": "患者", "furigana": "かんじゃ", "arti": "Pasien"},
{"kanji": "記者", "furigana": "きしゃ", "arti": "Wartawan"},
{"kanji": "学者", "furigana": "がくしゃ", "arti": "Sarjana"},
{"kanji": "医者", "furigana": "いしゃ", "arti": "Dokter"}
]
},
{
"judul": "",
"nama": "Pegawai",
"kunyomi": [],
"onyomi": ["いん"],
"kanjiGabungan": [
{"kanji": "社員", "furigana": "しゃいん", "arti": "Karyawan"},
{"kanji": "店員", "furigana": "てんいん", "arti": "Pramuniaga"},
{"kanji": "銀行員", "furigana": "ぎんこういん", "arti": "Pegawai Bank"},
{"kanji": "会員", "furigana": "かいいん", "arti": "Anggota"}
]
},
{
"judul": "",
"nama": "Rakyat",
"kunyomi": ["たみ"],
"onyomi": ["みん"],
"kanjiGabungan": [
{"kanji": "国民", "furigana": "こくみん", "arti": "Warga Negara"},
{"kanji": "住民", "furigana": "じゅうみん", "arti": "Penduduk"},
{"kanji": "市民", "furigana": "しみん", "arti": "Warga Kota"},
{"kanji": "民族", "furigana": "みんぞく", "arti": "Etnis"}
]
},
{
"judul": "",
"nama": "Keberuntungan",
"kunyomi": ["はこ"],
"onyomi": ["うん"],
"kanjiGabungan": [
{"kanji": "運転", "furigana": "うんてん", "arti": "Mengemudi"},
{"kanji": "運命", "furigana": "うんめい", "arti": "Takdir"},
{"kanji": "運動", "furigana": "うんどう", "arti": "Olahraga"},
{"kanji": "幸運", "furigana": "こううん", "arti": "Keberuntungan"}
]
},
{
"judul": "",
"nama": "Penjelasan",
"kunyomi": [""],
"onyomi": ["せつ"],
"kanjiGabungan": [
{"kanji": "説明", "furigana": "せつめい", "arti": "Penjelasan"},
{"kanji": "小説", "furigana": "しょうせつ", "arti": "Novel"},
{"kanji": "伝説", "furigana": "でんせつ", "arti": "Legenda"},
{"kanji": "解説", "furigana": "かいせつ", "arti": "Komentar"}
]
},
{
"judul": "",
"nama": "Prefektur",
"kunyomi": [],
"onyomi": ["けん"],
"kanjiGabungan": [
{"kanji": "県庁", "furigana": "けんちょう", "arti": "Kantor Prefektur"},
{"kanji": "県立", "furigana": "けんりつ", "arti": "Milik Prefektur"},
{"kanji": "愛知県", "furigana": "あいちけん", "arti": "Prefektur Aichi"},
{"kanji": "福岡県", "furigana": "ふくおかけん", "arti": "Prefektur Fukuoka"}
]
},
{
"judul": "",
"nama": "Pemerintahan",
"kunyomi": [],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "政府", "furigana": "せいふ", "arti": "Pemerintah"},
{"kanji": "大阪府", "furigana": "おおさかふ", "arti": "Prefektur Osaka"},
{"kanji": "京都府", "furigana": "きょうとふ", "arti": "Prefektur Kyoto"},
{"kanji": "府立", "furigana": "ふりつ", "arti": "Milik Prefektur"}
]
},
{
"judul": "",
"nama": "Ibu Kota",
"kunyomi": ["みやこ"],
"onyomi": ["", ""],
"kanjiGabungan": [
{"kanji": "首都", "furigana": "しゅと", "arti": "Ibu Kota"},
{"kanji": "東京都", "furigana": "とうきょうと", "arti": "Metropolitan"},
{"kanji": "都心", "furigana": "としん", "arti": "Pusat Kota"},
{"kanji": "都会", "furigana": "とかい", "arti": "Kota Besar"}
]
},
{
"judul": "",
"nama": "Ibu Kota",
"kunyomi": [],
"onyomi": ["きょう"],
"kanjiGabungan": [
{"kanji": "東京", "furigana": "とうきょう", "arti": "Tokyo"},
{"kanji": "京都", "furigana": "きょうと", "arti": "Kyoto"},
{"kanji": "上京", "furigana": "じょうきょう", "arti": "Pergi ke Tokyo"},
{"kanji": "京大", "furigana": "きょうだい", "arti": "Universitas Kyoto"}
]
},
{
"judul": "",
"nama": "Alasan",
"kunyomi": [],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "理由", "furigana": "りゆう", "arti": "Alasan"},
{"kanji": "理科", "furigana": "りか", "arti": "Ilmu Pengetahuan"},
{"kanji": "心理", "furigana": "しんり", "arti": "Psikologi"},
{"kanji": "管理", "furigana": "かんり", "arti": "Manajemen"}
]
},
{
"judul": "",
"nama": "Bahan",
"kunyomi": [],
"onyomi": ["りょう"],
"kanjiGabungan": [
{"kanji": "材料", "furigana": "ざいりょう", "arti": "Bahan"},
{"kanji": "料理", "furigana": "りょうり", "arti": "Masakan"},
{"kanji": "給料", "furigana": "きゅうりょう", "arti": "Gaji"},
{"kanji": "無料", "furigana": "むりょう", "arti": "Gratis"}
]
},
{
"judul": "",
"nama": "Film",
"kunyomi": ["うつ"],
"onyomi": ["えい"],
"kanjiGabungan": [
{"kanji": "映画", "furigana": "えいが", "arti": "Film"},
{"kanji": "映る", "furigana": "うつる", "arti": "Terpantul"},
{"kanji": "映像", "furigana": "えいぞう", "arti": "Gambar / Visual"},
{"kanji": "上映", "furigana": "じょうえい", "arti": "Pemutaran Film"}
]
},
{
"judul": "",
"nama": "Gambar",
"kunyomi": [],
"onyomi": ["", "かく"],
"kanjiGabungan": [
{"kanji": "映画", "furigana": "えいが", "arti": "Film"},
{"kanji": "漫画", "furigana": "まんが", "arti": "Komik"},
{"kanji": "画家", "furigana": "がか", "arti": "Pelukis"},
{"kanji": "計画", "furigana": "けいかく", "arti": "Rencana"}
]
},
];

View File

@ -0,0 +1,557 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:bahasajepang/service/API_config.dart';
class DetailOkurigana4Page extends StatefulWidget {
final AudioPlayer audioPlayer = AudioPlayer();
DetailOkurigana4Page({super.key});
@override
State<DetailOkurigana4Page> createState() => _DetailOkurigana4PageState();
}
class _DetailOkurigana4PageState extends State<DetailOkurigana4Page> {
late Map<String, dynamic> kanjiData;
@override
void didChangeDependencies() {
super.didChangeDependencies();
kanjiData =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
widget.audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {
});
});
}
@override
void dispose() {
widget.audioPlayer.dispose();
super.dispose();
}
void _showWritingModal(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return WritingModal();
},
);
}
Future<void> _playVoice(String? audioUrl) async {
if (audioUrl == null || audioUrl.isEmpty) return;
try {
final fullUrl = "${ApiConfig.url}/$audioUrl";
await widget.audioPlayer.play(UrlSource(fullUrl));
if (!mounted) return;
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Audio Tidak Tersedia'),
backgroundColor: bgColor3,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha:0.95),
appBar: AppBar(
title: const Text(
'Detail Kanji Okurigana',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withValues(alpha:0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Kanji Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha:0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Kanji Display
Container(
width: 120,
height: 140,
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
kanjiData["judul"] ?? '?',
style: const TextStyle(
fontSize: 48,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
kanjiData["nama"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
const SizedBox(width: 16),
// Kanji Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReadingCard('Kunyomi', kanjiData["kunyomi"]),
const SizedBox(height: 8),
_buildReadingCard('Onyomi', kanjiData["onyomi"]),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanjiData["voice_record"]);
},
),
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Contoh Penggunaan
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.format_quote,
color: bgColor1,
size: 24,
),
const SizedBox(width: 8),
Text(
'Contoh Penggunaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
if (kanjiData["detail_kanji"] != null &&
(kanjiData["detail_kanji"] as List).isNotEmpty)
...(kanjiData["detail_kanji"] as List).map<Widget>((kanji) {
return _buildExampleUsage(kanji);
})
else
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Tidak ada contoh penggunaan',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _showWritingModal(context),
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: const Text(
"Coba Menulis Kanji",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
Widget _buildReadingCard(String title, String? reading) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Text(
"$title: ",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
reading ?? '-',
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
);
}
Widget _buildExampleUsage(Map<String, dynamic> kanji) {
return Card(
color: bgColor1,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.2), // Warna bayangan
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 1), // Posisi bayangan (x, y)
),
],
color: Colors.white),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanji["voice_record"]);
},
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kanji["kanji"] ?? '',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
kanji["romaji"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
Expanded(
child: Text(
kanji["arti"] ?? '',
style: const TextStyle(fontSize: 14, color: Colors.white),
textAlign: TextAlign.end,
),
),
],
),
),
);
}
}
class WritingModal extends StatefulWidget {
const WritingModal({super.key});
@override
_WritingModalState createState() => _WritingModalState();
}
class _WritingModalState extends State<WritingModal> {
List<Offset> points = [];
final GlobalKey _paintKey = GlobalKey();
Rect? _drawingArea;
void _clearDrawing() {
setState(() {
points.clear();
});
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.2),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Latihan Menulis Kanji",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) {
// Simpan ukuran area gambar setelah layout selesai
WidgetsBinding.instance.addPostFrameCallback((_) {
final renderBox = _paintKey.currentContext?.findRenderObject()
as RenderBox?;
if (renderBox != null) {
final offset = renderBox.localToGlobal(Offset.zero);
setState(() {
_drawingArea = Rect.fromLTWH(
offset.dx,
offset.dy,
renderBox.size.width,
renderBox.size.height,
);
});
}
});
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: GestureDetector(
onPanStart: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
}
},
onPanUpdate: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
} else {
// Tambahkan titik kosong ketika keluar area
setState(() {
points.add(Offset.zero);
});
}
},
onPanEnd: (details) {
setState(() {
points.add(Offset.zero);
});
},
child: CustomPaint(
key: _paintKey,
size: Size.infinite,
painter: DrawingPainter(points),
),
),
);
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: _clearDrawing,
child: const Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 4),
Text("Hapus", style: TextStyle(color: Colors.red)),
],
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
),
child: const Text("Selesai"),
),
],
),
],
),
),
);
}
Offset _globalToLocal(Offset global) {
if (_drawingArea == null) return Offset.zero;
return Offset(
global.dx - _drawingArea!.left,
global.dy - _drawingArea!.top,
);
}
}
class DrawingPainter extends CustomPainter {
final List<Offset> points;
DrawingPainter(this.points);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..strokeWidth = 4.0
..strokeCap = StrokeCap.round;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != Offset.zero && points[i + 1] != Offset.zero) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@ -0,0 +1,268 @@
import 'package:bahasajepang/pages/n5/kanji/kanji_service.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
class KanjiOkurigana4Page extends StatefulWidget {
const KanjiOkurigana4Page({super.key});
@override
_KanjiOkurigana4PageState createState() => _KanjiOkurigana4PageState();
}
class _KanjiOkurigana4PageState extends State<KanjiOkurigana4Page> {
final TextEditingController _searchController = TextEditingController();
final KanjiService _kanjiService = KanjiService();
List<dynamic> _filteredKanji = [];
List<dynamic> _allKanji = [];
bool _isLoading = true;
String _errorMessage = '';
@override
void initState() {
super.initState();
_fetchKanji();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _fetchKanji() async {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
var kanjiList = await _kanjiService.fetchKanjiByKategori('okurigana');
var filteredKanji = kanjiList
.where((kanji) =>
kanji["kategori"] == "okurigana" && kanji["level_id"] == 3)
.toList();
setState(() {
_allKanji = filteredKanji;
_filteredKanji = filteredKanji;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Gagal memuat data kanji';
});
}
}
void _onSearchChanged() {
final query = _searchController.text;
if (query.isEmpty) {
setState(() {
_filteredKanji = _allKanji;
});
return;
}
setState(() {
_filteredKanji = _allKanji.where((kanji) {
final judul = kanji["judul"]?.toString().toLowerCase() ?? '';
final nama = kanji["nama"]?.toString().toLowerCase() ?? '';
final kunyomi = kanji["kunyomi"]?.toString().toLowerCase() ?? '';
final queryLower = query.toLowerCase();
return judul.contains(queryLower) ||
nama.contains(queryLower) ||
kunyomi.contains(queryLower);
}).toList();
});
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final cardSize =
screenWidth / 3.5; // Menyesuaikan ukuran card berdasarkan lebar layar
return Scaffold(
backgroundColor: bgColor1.withOpacity(0.95),
appBar: AppBar(
title: const Text(
'Kanji Okurigana N4',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withOpacity(0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: Column(
children: [
// Search Bar
Container(
padding: const EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Cari kanji...",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: bgColor2.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
),
),
// Content
Expanded(
child: _isLoading
? Center(
child: CircularProgressIndicator(
color: bgColor2,
),
)
: _errorMessage.isNotEmpty
? Center(
child: Text(
_errorMessage,
style: const TextStyle(color: Colors.red),
),
)
: _filteredKanji.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 50,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Tidak ada kanji yang ditemukan',
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: GridView.builder(
padding: const EdgeInsets.only(bottom: 20),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio:
0.85, // Nilai yang lebih optimal
mainAxisExtent:
cardSize, // Gunakan ukuran dinamis
),
itemCount: _filteredKanji.length,
itemBuilder: (context, index) {
return _kanjiCard(
_filteredKanji[index], context);
},
),
),
),
],
),
);
}
Widget _kanjiCard(dynamic kanji, BuildContext context) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.pushNamed(
context,
'/detail-okurigana4',
arguments: kanji,
);
},
child: Container(
padding: const EdgeInsets.all(8), // Padding yang lebih kecil
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
bgColor2.withOpacity(0.7),
bgColor2.withOpacity(0.9),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["judul"] ?? '?',
style: const TextStyle(
fontSize: 30, // Ukuran font yang lebih moderat
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["nama"] ?? '',
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.9),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,306 @@
List<Map<String, dynamic>> detailOkuriganaList = [
{
"judul": "",
"nama": "Lalu lintas",
"kunyomi": ["とお", "かよ"],
"onyomi": ["つう"],
"kanjiGabungan": [
{"kanji": "通う", "furigana": "かよう", "arti": "Pulang pergi"},
{"kanji": "交通", "furigana": "こうつう", "arti": "Lalu lintas"},
{"kanji": "通学", "furigana": "つうがく", "arti": "Pergi ke sekolah"},
{"kanji": "通信", "furigana": "つうしん", "arti": "Komunikasi"}
]
},
{
"judul": "",
"nama": "Turun (Salju/Hujan)",
"kunyomi": ["", ""],
"onyomi": ["こう"],
"kanjiGabungan": [
{"kanji": "降雪", "furigana": "こうせつ", "arti": "Turun salju"},
{"kanji": "降雨", "furigana": "こうう", "arti": "Turun hujan"},
{"kanji": "降車", "furigana": "こうしゃ", "arti": "Turun (kendaraan)"},
{"kanji": "降下", "furigana": "こうか", "arti": "Turun ke bawah"}
]
},
{
"judul": "",
"nama": "Memakai",
"kunyomi": ["", ""],
"onyomi": ["ちゃく"],
"kanjiGabungan": [
{"kanji": "着物", "furigana": "きもの", "arti": "Kimono"},
{"kanji": "到着", "furigana": "とうちゃく", "arti": "Kedatangan"},
{"kanji": "着陸", "furigana": "ちゃくりく", "arti": "Pendaratan"},
{"kanji": "着用", "furigana": "ちゃくよう", "arti": "Pemakaian"}
]
},
{
"judul": "",
"nama": "Bergerak",
"kunyomi": ["うご"],
"onyomi": ["どう"],
"kanjiGabungan": [
{"kanji": "活動", "furigana": "かつどう", "arti": "Aktivitas"},
{"kanji": "運動", "furigana": "うんどう", "arti": "Olahraga, Gerakan"},
{"kanji": "動物", "furigana": "どうぶつ", "arti": "Hewan"},
{"kanji": "動作", "furigana": "どうさ", "arti": "Gerakan, Aksi"}
]
},
{
"judul": "",
"nama": "Membangun",
"kunyomi": ["", "けん"],
"onyomi": ["けん", "こん"],
"kanjiGabungan": [
{"kanji": "建物", "furigana": "たてもの", "arti": "Bangunan"},
{"kanji": "建築", "furigana": "けんちく", "arti": "Arsitektur"},
{"kanji": "建設", "furigana": "けんせつ", "arti": "Konstruksi"},
{"kanji": "建国", "furigana": "けんこく", "arti": "Pendirian negara"}
]
},
{
"judul": "",
"nama": "Sama",
"kunyomi": ["おな"],
"onyomi": ["どう"],
"kanjiGabungan": [
{"kanji": "同時", "furigana": "どうじ", "arti": "Pada waktu yang sama"},
{"kanji": "同意", "furigana": "どうい", "arti": "Persetujuan"},
{"kanji": "同情", "furigana": "どうじょう", "arti": "Simpati"},
{"kanji": "同級生", "furigana": "どうきゅうせい", "arti": "Teman sekelas"}
]
},
{
"judul": "",
"nama": "Berputar",
"kunyomi": ["まわ"],
"onyomi": ["かい"],
"kanjiGabungan": [
{"kanji": "回転", "furigana": "かいてん", "arti": "Berputar"},
{"kanji": "回数", "furigana": "かいすう", "arti": "Frekuensi, Jumlah kali"},
{"kanji": "回復", "furigana": "かいふく", "arti": "Pemulihan"},
{"kanji": "回想", "furigana": "かいそう", "arti": "Mengingat kembali"}
]
},
{
"judul": "",
"nama": "Membuat",
"kunyomi": ["つく"],
"onyomi": ["さく", ""],
"kanjiGabungan": [
{"kanji": "作品", "furigana": "さくひん", "arti": "Karya, Hasil karya"},
{"kanji": "作業", "furigana": "さぎょう", "arti": "Pekerjaan"},
{"kanji": "作家", "furigana": "さっか", "arti": "Penulis, Sastrawan"},
{"kanji": "作成", "furigana": "さくせい", "arti": "Pembuatan, Penyusunan"}
]
},
{
"judul": "使",
"nama": "Menggunakan",
"kunyomi": ["つか"],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "使用", "furigana": "しよう", "arti": "Penggunaan"},
{"kanji": "使者", "furigana": "ししゃ", "arti": "Utusan"},
{"kanji": "使役", "furigana": "しえき", "arti": "Menugaskan seseorang"},
{"kanji": "使い道", "furigana": "つかいみち", "arti": "Cara penggunaan"}
]
},
{
"judul": "",
"nama": "Rendah",
"kunyomi": ["ひく"],
"onyomi": ["てい"],
"kanjiGabungan": [
{"kanji": "低下", "furigana": "ていか", "arti": "Penurunan"},
{"kanji": "最低", "furigana": "さいてい", "arti": "Paling rendah"},
{"kanji": "低温", "furigana": "ていおん", "arti": "Suhu rendah"},
{"kanji": "低地", "furigana": "ていち", "arti": "Dataran rendah"}
]
},
{
"judul": "",
"nama": "Pendek",
"kunyomi": ["みじか"],
"onyomi": ["たん"],
"kanjiGabungan": [
{"kanji": "短縮", "furigana": "たんしゅく", "arti": "Pemendekan"},
{"kanji": "短期", "furigana": "たんき", "arti": "Jangka pendek"},
{"kanji": "短時間", "furigana": "たんじかん", "arti": "Waktu singkat"},
{"kanji": "短所", "furigana": "たんしょ", "arti": "Kelemahan"}
]
},
{
"judul": "",
"nama": "Dekat",
"kunyomi": ["ちか"],
"onyomi": ["きん"],
"kanjiGabungan": [
{"kanji": "近所", "furigana": "きんじょ", "arti": "Lingkungan sekitar"},
{"kanji": "最近", "furigana": "さいきん", "arti": "Belakangan ini"},
{"kanji": "接近", "furigana": "せっきん", "arti": "Pendekatan"},
{"kanji": "近代", "furigana": "きんだい", "arti": "Zaman modern"}
]
},
{
"judul": "",
"nama": "Jauh",
"kunyomi": ["とお"],
"onyomi": ["えん"],
"kanjiGabungan": [
{"kanji": "遠足", "furigana": "えんそく", "arti": "Piknik"},
{"kanji": "遠方", "furigana": "えんぽう", "arti": "Tempat yang jauh"},
{"kanji": "遠距離", "furigana": "えんきょり", "arti": "Jarak jauh"},
{"kanji": "遠視", "furigana": "えんし", "arti": "Rabun jauh"}
]
},
{
"judul": "",
"nama": "Lemah",
"kunyomi": ["よわ"],
"onyomi": ["じゃく"],
"kanjiGabungan": [
{"kanji": "弱点", "furigana": "じゃくてん", "arti": "Titik lemah"},
{"kanji": "弱気", "furigana": "よわき", "arti": "Kurang percaya diri"},
{"kanji": "弱音", "furigana": "よわね", "arti": "Keluhan"},
{"kanji": "弱体", "furigana": "じゃくたい", "arti": "Lemah secara fisik"}
]
},
{
"judul": "",
"nama": "Kuat",
"kunyomi": ["つよ"],
"onyomi": ["きょう", "ごう"],
"kanjiGabungan": [
{"kanji": "強力", "furigana": "きょうりょく", "arti": "Kekuatan besar"},
{"kanji": "勉強", "furigana": "べんきょう", "arti": "Belajar"},
{"kanji": "強風", "furigana": "きょうふう", "arti": "Angin kencang"},
{"kanji": "強制", "furigana": "きょうせい", "arti": "Paksaan"}
]
},
{
"judul": "",
"nama": "Gelap",
"kunyomi": ["くら"],
"onyomi": ["あん"],
"kanjiGabungan": [
{"kanji": "暗黒", "furigana": "あんこく", "arti": "Kegelapan"},
{"kanji": "暗示", "furigana": "あんじ", "arti": "Petunjuk tersembunyi"},
{"kanji": "暗記", "furigana": "あんき", "arti": "Menghafal"},
{"kanji": "暗殺", "furigana": "あんさつ", "arti": "Pembunuhan diam-diam"}
]
},
{
"judul": "",
"nama": "Terang",
"kunyomi": ["あか", "あき"],
"onyomi": ["めい", "みょう"],
"kanjiGabungan": [
{"kanji": "明日", "furigana": "あした", "arti": "Besok"},
{"kanji": "明白", "furigana": "めいはく", "arti": "Jelas, Nyata"},
{"kanji": "明確", "furigana": "めいかく", "arti": "Tegas, Jelas"},
{"kanji": "説明", "furigana": "せつめい", "arti": "Penjelasan"}
]
},
{
"judul": "",
"nama": "Buruk",
"kunyomi": ["わる"],
"onyomi": ["あく", ""],
"kanjiGabungan": [
{"kanji": "悪人", "furigana": "あくにん", "arti": "Orang jahat"},
{"kanji": "最悪", "furigana": "さいあく", "arti": "Paling buruk"},
{"kanji": "悪化", "furigana": "あっか", "arti": "Memburuk"},
{"kanji": "悪意", "furigana": "あくい", "arti": "Niat jahat"}
]
},
{
"judul": "",
"nama": "Menyalin",
"kunyomi": ["うつ"],
"onyomi": ["しゃ"],
"kanjiGabungan": [
{"kanji": "写真", "furigana": "しゃしん", "arti": "Foto"},
{"kanji": "写す", "furigana": "うつす", "arti": "Menyalin, Memotret"},
{
"kanji": "写る",
"furigana": "うつる",
"arti": "Tercetak, Tertangkap (kamera)"
},
{"kanji": "写実", "furigana": "しゃじつ", "arti": "Realistis"}
]
},
{
"judul": "",
"nama": "Nasi",
"kunyomi": ["めし"],
"onyomi": ["はん"],
"kanjiGabungan": [
{"kanji": "ご飯", "furigana": "ごはん", "arti": "Nasi, Makanan"},
{"kanji": "朝飯", "furigana": "あさめし", "arti": "Sarapan"},
{"kanji": "昼飯", "furigana": "ひるめし", "arti": "Makan siang"},
{"kanji": "晩飯", "furigana": "ばんめし", "arti": "Makan malam"}
]
},
{
"judul": "",
"nama": "Terguling",
"kunyomi": ["ころ"],
"onyomi": ["てん"],
"kanjiGabungan": [
{"kanji": "自転車", "furigana": "じてんしゃ", "arti": "Sepeda"},
{"kanji": "転職", "furigana": "てんしょく", "arti": "Pindah Pekerjaan"},
{"kanji": "転がる", "furigana": "ころがる", "arti": "Berguling"},
{"kanji": "回転", "furigana": "かいてん", "arti": "Rotasi"}
]
},
{
"judul": "",
"nama": "Mencuci",
"kunyomi": ["あら"],
"onyomi": ["せん"],
"kanjiGabungan": [
{"kanji": "洗う", "furigana": "あらう", "arti": "Mencuci"},
{"kanji": "洗濯", "furigana": "せんたく", "arti": "Cuci Pakaian"},
{"kanji": "洗面所", "furigana": "せんめんじょ", "arti": "Tempat Cuci Muka"},
{"kanji": "洗礼", "furigana": "せんれい", "arti": "Pembaptisan"}
]
},
{
"judul": "",
"nama": "Mengukur",
"kunyomi": ["はか"],
"onyomi": ["けい"],
"kanjiGabungan": [
{"kanji": "計る", "furigana": "はかる", "arti": "Mengukur"},
{"kanji": "時計", "furigana": "とけい", "arti": "Jam"},
{"kanji": "計算", "furigana": "けいさん", "arti": "Perhitungan"},
{"kanji": "設計", "furigana": "せっけい", "arti": "Desain / Rancangan"}
]
},
{
"judul": "",
"nama": "Rencana",
"kunyomi": ["はか"],
"onyomi": ["", ""],
"kanjiGabungan": [
{"kanji": "図る", "furigana": "はかる", "arti": "Rencana"},
{"kanji": "地図", "furigana": "ちず", "arti": "Peta"},
{"kanji": "図書館", "furigana": "としょかん", "arti": "Perpustakaan"},
{"kanji": "図面", "furigana": "ずめん", "arti": "Denah"}
]
},
{
"judul": "",
"nama": "Mengabdi",
"kunyomi": ["つか"],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "仕える", "furigana": "つかえる", "arti": "Mengabdi"},
{"kanji": "仕事", "furigana": "しごと", "arti": "Pekerjaan"},
{"kanji": "仕組み", "furigana": "しくみ", "arti": "Struktur / Mekanisme"},
{"kanji": "仕方", "furigana": "しかた", "arti": "Cara / Metode"}
]
},
];

View File

@ -0,0 +1,556 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:bahasajepang/service/API_config.dart';
class DetailTandoku4Page extends StatefulWidget {
final AudioPlayer audioPlayer = AudioPlayer();
DetailTandoku4Page({super.key});
@override
State<DetailTandoku4Page> createState() => _DetailTandoku4PageState();
}
class _DetailTandoku4PageState extends State<DetailTandoku4Page> {
late Map<String, dynamic> kanjiData;
@override
void didChangeDependencies() {
super.didChangeDependencies();
kanjiData =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
widget.audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {
});
});
}
@override
void dispose() {
widget.audioPlayer.dispose();
super.dispose();
}
void _showWritingModal(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return WritingModal();
},
);
}
Future<void> _playVoice(String? audioUrl) async {
if (audioUrl == null || audioUrl.isEmpty) return;
try {
final fullUrl = "${ApiConfig.url}/$audioUrl";
await widget.audioPlayer.play(UrlSource(fullUrl));
if (!mounted) return;
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Audio Tidak Tersedia'),
backgroundColor: bgColor3,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha:0.95),
appBar: AppBar(
title: const Text(
'Detail Kanji Tandoku',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withValues(alpha:0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Kanji Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha:0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Kanji Display
Container(
width: 120,
height: 140,
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
kanjiData["judul"] ?? '?',
style: const TextStyle(
fontSize: 48,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
kanjiData["nama"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
const SizedBox(width: 16),
// Kanji Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReadingCard('Kunyomi', kanjiData["kunyomi"]),
const SizedBox(height: 8),
_buildReadingCard('Onyomi', kanjiData["onyomi"]),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanjiData["voice_record"]);
},
),
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Contoh Penggunaan
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.format_quote,
color: bgColor1,
size: 24,
),
const SizedBox(width: 8),
Text(
'Contoh Penggunaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
if (kanjiData["detail_kanji"] != null &&
(kanjiData["detail_kanji"] as List).isNotEmpty)
...(kanjiData["detail_kanji"] as List).map<Widget>((kanji) {
return _buildExampleUsage(kanji);
})
else
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Tidak ada contoh penggunaan',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _showWritingModal(context),
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: const Text(
"Coba Menulis Kanji",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
Widget _buildReadingCard(String title, String? reading) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Text(
"$title: ",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
reading ?? '-',
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
);
}
Widget _buildExampleUsage(Map<String, dynamic> kanji) {
return Card(
color: bgColor1,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.2), // Warna bayangan
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 1), // Posisi bayangan (x, y)
),
],
color: Colors.white),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanji["voice_record"]);
},
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kanji["kanji"] ?? '',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
kanji["romaji"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
Expanded(
child: Text(
kanji["arti"] ?? '',
style: const TextStyle(fontSize: 14, color: Colors.white),
textAlign: TextAlign.end,
),
),
],
),
),
);
}
}
class WritingModal extends StatefulWidget {
const WritingModal({super.key});
@override
_WritingModalState createState() => _WritingModalState();
}
class _WritingModalState extends State<WritingModal> {
List<Offset> points = [];
final GlobalKey _paintKey = GlobalKey();
Rect? _drawingArea;
void _clearDrawing() {
setState(() {
points.clear();
});
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.2),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Latihan Menulis Kanji",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) {
// Simpan ukuran area gambar setelah layout selesai
WidgetsBinding.instance.addPostFrameCallback((_) {
final renderBox = _paintKey.currentContext?.findRenderObject()
as RenderBox?;
if (renderBox != null) {
final offset = renderBox.localToGlobal(Offset.zero);
setState(() {
_drawingArea = Rect.fromLTWH(
offset.dx,
offset.dy,
renderBox.size.width,
renderBox.size.height,
);
});
}
});
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: GestureDetector(
onPanStart: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
}
},
onPanUpdate: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
} else {
// Tambahkan titik kosong ketika keluar area
setState(() {
points.add(Offset.zero);
});
}
},
onPanEnd: (details) {
setState(() {
points.add(Offset.zero);
});
},
child: CustomPaint(
key: _paintKey,
size: Size.infinite,
painter: DrawingPainter(points),
),
),
);
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: _clearDrawing,
child: const Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 4),
Text("Hapus", style: TextStyle(color: Colors.red)),
],
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
),
child: const Text("Selesai"),
),
],
),
],
),
),
);
}
Offset _globalToLocal(Offset global) {
if (_drawingArea == null) return Offset.zero;
return Offset(
global.dx - _drawingArea!.left,
global.dy - _drawingArea!.top,
);
}
}
class DrawingPainter extends CustomPainter {
final List<Offset> points;
DrawingPainter(this.points);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..strokeWidth = 4.0
..strokeCap = StrokeCap.round;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != Offset.zero && points[i + 1] != Offset.zero) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@ -0,0 +1,268 @@
import 'package:bahasajepang/pages/n5/kanji/kanji_service.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
class KanjiTandoku4Page extends StatefulWidget {
const KanjiTandoku4Page({super.key});
@override
_KanjiTandoku4PageState createState() => _KanjiTandoku4PageState();
}
class _KanjiTandoku4PageState extends State<KanjiTandoku4Page> {
final TextEditingController _searchController = TextEditingController();
final KanjiService _kanjiService = KanjiService();
List<dynamic> _filteredKanji = [];
List<dynamic> _allKanji = [];
bool _isLoading = true;
String _errorMessage = '';
@override
void initState() {
super.initState();
_fetchKanji();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _fetchKanji() async {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
var kanjiList = await _kanjiService.fetchKanjiByKategori('tandoku');
var filteredKanji = kanjiList
.where((kanji) =>
kanji["kategori"] == "tandoku" && kanji["level_id"] == 3)
.toList();
setState(() {
_allKanji = filteredKanji;
_filteredKanji = filteredKanji;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Gagal memuat data kanji';
});
}
}
void _onSearchChanged() {
final query = _searchController.text;
if (query.isEmpty) {
setState(() {
_filteredKanji = _allKanji;
});
return;
}
setState(() {
_filteredKanji = _allKanji.where((kanji) {
final judul = kanji["judul"]?.toString().toLowerCase() ?? '';
final nama = kanji["nama"]?.toString().toLowerCase() ?? '';
final kunyomi = kanji["kunyomi"]?.toString().toLowerCase() ?? '';
final queryLower = query.toLowerCase();
return judul.contains(queryLower) ||
nama.contains(queryLower) ||
kunyomi.contains(queryLower);
}).toList();
});
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final cardSize =
screenWidth / 3.5; // Menyesuaikan ukuran card berdasarkan lebar layar
return Scaffold(
backgroundColor: bgColor1.withOpacity(0.95),
appBar: AppBar(
title: const Text(
'Kanji Tandoku N4',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withOpacity(0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: Column(
children: [
// Search Bar
Container(
padding: const EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Cari kanji...",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: bgColor2.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
),
),
// Content
Expanded(
child: _isLoading
? Center(
child: CircularProgressIndicator(
color: bgColor2,
),
)
: _errorMessage.isNotEmpty
? Center(
child: Text(
_errorMessage,
style: const TextStyle(color: Colors.red),
),
)
: _filteredKanji.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 50,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Tidak ada kanji yang ditemukan',
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: GridView.builder(
padding: const EdgeInsets.only(bottom: 20),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio:
0.85, // Nilai yang lebih optimal
mainAxisExtent:
cardSize, // Gunakan ukuran dinamis
),
itemCount: _filteredKanji.length,
itemBuilder: (context, index) {
return _kanjiCard(
_filteredKanji[index], context);
},
),
),
),
],
),
);
}
Widget _kanjiCard(dynamic kanji, BuildContext context) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.pushNamed(
context,
'/detail-tandoku4',
arguments: kanji,
);
},
child: Container(
padding: const EdgeInsets.all(8), // Padding yang lebih kecil
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
bgColor2.withOpacity(0.7),
bgColor2.withOpacity(0.9),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["judul"] ?? '?',
style: const TextStyle(
fontSize: 30, // Ukuran font yang lebih moderat
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["nama"] ?? '',
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.9),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,278 @@
List<Map<String, dynamic>> detailTandokuList = [
{
"judul": "",
"nama": "Barang, Benda",
"kunyomi": ["もの"],
"onyomi": ["ぶつ", "もつ"],
"kanjiGabungan": [
{"kanji": "動物", "furigana": "どうぶつ", "arti": "Hewan"},
{"kanji": "食物", "furigana": "しょくもつ", "arti": "Makanan"},
{"kanji": "物語", "furigana": "ものがたり", "arti": "Cerita, Kisah"},
{"kanji": "買い物", "furigana": "かいもの", "arti": "Belanja"}
]
},
{
"judul": "",
"nama": "Barang, Produk",
"kunyomi": ["しな"],
"onyomi": ["ひん"],
"kanjiGabungan": [
{"kanji": "商品", "furigana": "しょうひん", "arti": "Produk, Barang dagangan"},
{"kanji": "作品", "furigana": "さくひん", "arti": "Karya, Produk seni"},
{"kanji": "食品", "furigana": "しょくひん", "arti": "Makanan (Produk pangan)"},
{"kanji": "品物", "furigana": "しなもの", "arti": "Barang"}
]
},
{
"judul": "",
"nama": "Malam",
"kunyomi": ["", "よる"],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "夜空", "furigana": "よぞら", "arti": "Langit malam"},
{"kanji": "夜明け", "furigana": "よあけ", "arti": "Fajar, Matahari terbit"},
{"kanji": "深夜", "furigana": "しんや", "arti": "Tengah malam"},
{"kanji": "夜中", "furigana": "よなか", "arti": "Tengah malam"}
]
},
{
"judul": "",
"nama": "Perjalanan",
"kunyomi": ["たび"],
"onyomi": ["りょ"],
"kanjiGabungan": [
{"kanji": "旅行", "furigana": "りょこう", "arti": "Perjalanan, Travelling"},
{"kanji": "旅館", "furigana": "りょかん", "arti": "Penginapan Jepang"},
{"kanji": "旅人", "furigana": "たびびと", "arti": "Pelancong, Pengembara"},
{"kanji": "旅立つ", "furigana": "たびだつ", "arti": "Dalam perjalanan"}
]
},
{
"judul": "",
"nama": "Sore",
"kunyomi": ["ゆう"],
"onyomi": ["せき"],
"kanjiGabungan": [
{"kanji": "", "furigana": "ゆう", "arti": "Sore"},
{"kanji": "夕食", "furigana": "ゆうしょく", "arti": "Makan malam"},
{"kanji": "夕日", "furigana": "ゆうひ", "arti": "Matahari sore"},
{"kanji": "夕暮れ", "furigana": "ゆうぐれ", "arti": "Senja"}
]
},
{
"judul": "",
"nama": "Utama",
"kunyomi": ["ぬし", "あるじ"],
"onyomi": ["しゅ", ""],
"kanjiGabungan": [
{"kanji": "", "furigana": "おも", "arti": "Utama"},
{"kanji": "主人", "furigana": "しゅじん", "arti": "Suami, Tuan rumah"},
{"kanji": "主食", "furigana": "しゅしょく", "arti": "Makanan pokok"},
{"kanji": "主題", "furigana": "しゅだい", "arti": "Tema utama"}
]
},
{
"judul": "",
"nama": "Bernyanyi",
"kunyomi": ["うた", "うたう"],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "", "furigana": "うた", "arti": "Lagu"},
{"kanji": "歌手", "furigana": "かしゅ", "arti": "Penyanyi"},
{"kanji": "国歌", "furigana": "こっか", "arti": "Lagu Kebangsaan"},
{"kanji": "歌詞", "furigana": "かし", "arti": "Lirik Lagu"}
]
},
{
"judul": "",
"nama": "Rumah",
"kunyomi": ["いえ", ""],
"onyomi": ["", ""],
"kanjiGabungan": [
{"kanji": "", "furigana": "いえ", "arti": "Rumah"},
{"kanji": "家具", "furigana": "かぐ", "arti": "Perabot Rumah"},
{"kanji": "家賃", "furigana": "やちん", "arti": "Sewa Rumah"},
{"kanji": "画家", "furigana": "がか", "arti": "Pelukis"}
]
},
{
"judul": "",
"nama": "Kota",
"kunyomi": ["まち"],
"onyomi": ["ちょう"],
"kanjiGabungan": [
{"kanji": "", "furigana": "まち", "arti": "Kota"},
{"kanji": "下町", "furigana": "したまち", "arti": "Kota Tua"},
{"kanji": "町内", "furigana": "ちょうない", "arti": "Dalam Kota"},
{"kanji": "町民", "furigana": "ちょうみん", "arti": "Penduduk Kota"}
]
},
{
"judul": "",
"nama": "Musim Dingin",
"kunyomi": ["ふゆ"],
"onyomi": ["とう"],
"kanjiGabungan": [
{"kanji": "", "furigana": "ふゆ", "arti": "Musim Dingin"},
{"kanji": "冬眠", "furigana": "とうみん", "arti": "Hibernasi"},
{"kanji": "冬季", "furigana": "とうき", "arti": "Musim Dingin"},
{"kanji": "真冬", "furigana": "まふゆ", "arti": "Puncak Musim Dingin"}
]
},
{
"judul": "",
"nama": "Musim Gugur",
"kunyomi": ["あき"],
"onyomi": ["しゅう"],
"kanjiGabungan": [
{"kanji": "", "furigana": "あき", "arti": "Musim Gugur"},
{"kanji": "秋分", "furigana": "しゅうぶん", "arti": "Titik Balik Musim Gugur"},
{"kanji": "初秋", "furigana": "しょしゅう", "arti": "Awal Musim Gugur"},
{"kanji": "晩秋", "furigana": "ばんしゅう", "arti": "Akhir Musim Gugur"}
]
},
{
"judul": "",
"nama": "Musim Panas",
"kunyomi": ["なつ"],
"onyomi": ["", ""],
"kanjiGabungan": [
{"kanji": "", "furigana": "なつ", "arti": "Musim Panas"},
{"kanji": "真夏", "furigana": "まなつ", "arti": "Puncak Musim Panas"},
{"kanji": "初夏", "furigana": "しょか", "arti": "Awal Musim Panas"},
{"kanji": "冷夏", "furigana": "れいか", "arti": "Musim Panas Dingin"}
]
},
{
"judul": "",
"nama": "Musim Semi",
"kunyomi": ["はる"],
"onyomi": ["しゅん"],
"kanjiGabungan": [
{"kanji": "", "furigana": "はる", "arti": "Musim Semi"},
{"kanji": "春休み", "furigana": "はるやすみ", "arti": "Liburan Musim Semi"},
{"kanji": "青春", "furigana": "せいしゅん", "arti": "Masa Muda"},
{"kanji": "立春", "furigana": "りっしゅん", "arti": "Awal Musim Semi"}
]
},
{
"judul": "",
"nama": "Siang",
"kunyomi": ["ひる"],
"onyomi": ["ちゅう"],
"kanjiGabungan": [
{"kanji": "", "furigana": "ひる", "arti": "Siang"},
{"kanji": "昼ご飯", "furigana": "ひるごはん", "arti": "Makan Siang"},
{"kanji": "昼寝", "furigana": "ひるね", "arti": "Tidur Siang"},
{"kanji": "昼食", "furigana": "ちゅうしょく", "arti": "Makan Siang"}
]
},
{
"judul": "",
"nama": "Pagi",
"kunyomi": ["あさ"],
"onyomi": ["ちょう"],
"kanjiGabungan": [
{"kanji": "", "furigana": "あさ", "arti": "Pagi"},
{"kanji": "朝ご飯", "furigana": "あさごはん", "arti": "Sarapan"},
{"kanji": "今朝", "furigana": "けさ", "arti": "Pagi ini"},
{"kanji": "朝食", "furigana": "ちょうしょく", "arti": "Makan Pagi"}
]
},
{
"judul": "",
"nama": "Pasar",
"kunyomi": ["いち"],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "", "furigana": "いち", "arti": "Pasar"},
{"kanji": "市長", "furigana": "しちょう", "arti": "Walikota"},
{"kanji": "都市", "furigana": "とし", "arti": "Kota"},
{"kanji": "市民", "furigana": "しみん", "arti": "Warga Kota"}
]
},
{
"judul": "",
"nama": "Sayur",
"kunyomi": [""],
"onyomi": ["さい"],
"kanjiGabungan": [
{"kanji": "", "furigana": "", "arti": "Sayuran"},
{"kanji": "野菜", "furigana": "やさい", "arti": "Sayuran"},
{"kanji": "菜園", "furigana": "さいえん", "arti": "Kebun Sayur"},
{"kanji": "青菜", "furigana": "あおな", "arti": "Sayur Hijau"}
]
},
{
"judul": "",
"nama": "Lapangan",
"kunyomi": [""],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "", "furigana": "", "arti": "Lapangan"},
{"kanji": "野菜", "furigana": "やさい", "arti": "Sayuran"},
{"kanji": "野球", "furigana": "やきゅう", "arti": "Baseball"},
{"kanji": "野原", "furigana": "のはら", "arti": "Padang Rumput"}
]
},
{
"judul": "",
"nama": "Beras",
"kunyomi": ["こめ"],
"onyomi": ["べい", "まい"],
"kanjiGabungan": [
{"kanji": "", "furigana": "こめ", "arti": "Beras"},
{"kanji": "米国", "furigana": "べいこく", "arti": "Amerika Serikat"},
{"kanji": "新米", "furigana": "しんまい", "arti": "Beras Baru / Pemula"},
{"kanji": "玄米", "furigana": "げんまい", "arti": "Beras Coklat"}
]
},
{
"judul": "",
"nama": "Sake",
"kunyomi": ["さけ", "さか"],
"onyomi": ["しゅ"],
"kanjiGabungan": [
{"kanji": "", "furigana": "さけ", "arti": "Sake"},
{"kanji": "日本酒", "furigana": "にほんしゅ", "arti": "Sake Jepang"},
{"kanji": "飲酒", "furigana": "いんしゅ", "arti": "Minum Alkohol"},
{"kanji": "酒場", "furigana": "さかば", "arti": "Bar / Tempat Minum"}
]
},
{
"judul": "",
"nama": "Daging",
"kunyomi": [],
"onyomi": ["にく"],
"kanjiGabungan": [
{"kanji": "", "furigana": "にく", "arti": "Daging"},
{"kanji": "牛肉", "furigana": "ぎゅうにく", "arti": "Daging Sapi"},
{"kanji": "鶏肉", "furigana": "とりにく", "arti": "Daging Ayam"},
{"kanji": "肉体", "furigana": "にくたい", "arti": "Tubuh"}
]
},
{
"judul": "",
"nama": "Rasa",
"kunyomi": ["あじ"],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "", "furigana": "あじ", "arti": "Rasa"},
{"kanji": "意味", "furigana": "いみ", "arti": "Makna / Arti"},
{"kanji": "趣味", "furigana": "しゅみ", "arti": "Hobi"},
{"kanji": "味覚", "furigana": "みかく", "arti": "Indra Perasa"}
]
},
{
"judul": "",
"nama": "Hal",
"kunyomi": ["こと"],
"onyomi": [""],
"kanjiGabungan": [
{"kanji": "", "furigana": "こと", "arti": "Hal / Perkara"},
{"kanji": "食事", "furigana": "しょくじ", "arti": "Makan"},
{"kanji": "大事", "furigana": "だいじ", "arti": "Penting"},
{"kanji": "出来事", "furigana": "できごと", "arti": "Kejadian"}
]
}
];

View File

@ -0,0 +1,77 @@
import 'package:bahasajepang/theme.dart';
import 'package:flutter/material.dart';
class KanjiPage extends StatefulWidget {
const KanjiPage({super.key});
@override
_KanjiPageState createState() => _KanjiPageState();
}
class _KanjiPageState extends State<KanjiPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Kanji N4',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withOpacity(0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.9)),
),
backgroundColor: bgColor1,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/image_splash.png',
width: 200,
),
_buildKanjiButton(
"Kanji Tandoku", Colors.blue.shade200, '/kanji-tandoku4'),
const SizedBox(height: 10),
_buildKanjiButton("Kanji Okurigana",
const Color.fromRGBO(100, 181, 246, 1), '/kanji-okurigana4'),
const SizedBox(height: 10),
_buildKanjiButton(
"Kanji Jukugo", Colors.blue.shade400, '/kanji-jukugo4'),
],
),
),
);
}
Widget _buildKanjiButton(String text, Color color, String route) {
return SizedBox(
width: 300,
height: 60,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () => Navigator.pushNamed(context, route),
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
);
}
}

103
lib/pages/n4/main_page.dart Normal file
View File

@ -0,0 +1,103 @@
import 'package:bahasajepang/pages/n4/kamus/kamus_page.dart';
import 'package:bahasajepang/pages/n4/kanji/kanji_page.dart';
import 'package:bahasajepang/pages/n4/materi/materi_page.dart';
import 'package:bahasajepang/pages/pemula/setting/setting_page.dart';
import 'package:bahasajepang/theme.dart';
import 'package:flutter/material.dart';
class NEmpatPage extends StatefulWidget {
const NEmpatPage({super.key});
@override
State<NEmpatPage> createState() => _NEmpatPageState();
}
class _NEmpatPageState extends State<NEmpatPage> {
int currentIndex = 0;
@override
Widget build(BuildContext context) {
Widget customBottomNav() {
return ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
child: BottomNavigationBar(
backgroundColor: bgColor3,
currentIndex: currentIndex,
onTap: (value) {
setState(() {
currentIndex = value;
});
},
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Container(
padding: EdgeInsets.only(top: 10),
child: Icon(
Icons.book,
size: 35,
color: currentIndex == 0 ? bgColor2 : bgColor1,
),
),
label: '',
),
BottomNavigationBarItem(
icon: Container(
padding: EdgeInsets.only(top: 10),
child: Icon(
Icons.menu_book,
size: 35,
color: currentIndex == 1 ? bgColor2 : bgColor1,
),
),
label: '',
),
BottomNavigationBarItem(
icon: Container(
padding: EdgeInsets.only(top: 10),
child: Icon(
Icons.translate,
size: 35,
color: currentIndex == 2 ? bgColor2 : bgColor1,
),
),
label: '',
),
BottomNavigationBarItem(
icon: Container(
padding: EdgeInsets.only(top: 10),
child: Icon(
Icons.settings,
size: 35,
color: currentIndex == 3 ? bgColor2 : bgColor1,
),
),
label: '',
),
],
),
);
}
Widget body() {
switch (currentIndex) {
case 0:
return MateriN4Page();
case 1:
return Kamus4Page();
case 2:
return KanjiPage();
case 3:
return SettingPage();
default:
return MateriN4Page();
}
}
return Scaffold(
backgroundColor: bgColor1,
bottomNavigationBar: customBottomNav(),
body: body(),
);
}
}

View File

@ -0,0 +1,417 @@
import 'package:bahasajepang/pages/n5/materi/materi_service.dart';
import 'package:bahasajepang/pages/n4/materi/isi_materi.dart';
import 'package:bahasajepang/theme.dart';
import 'package:flutter/material.dart';
class DetailMateriN4Page extends StatefulWidget {
final int materiId;
const DetailMateriN4Page({super.key, required this.materiId});
@override
State<DetailMateriN4Page> createState() => _DetailMateriN4PageState();
}
class _DetailMateriN4PageState extends State<DetailMateriN4Page>
with SingleTickerProviderStateMixin {
final MateriService _materiService = MateriService();
bool _isLoading = true;
dynamic _materiData;
bool _isExpanded = false;
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_loadMateriDetails();
}
Future<dynamic> _loadMateriDetails() async {
try {
final data = await _materiService.getMateriDetails(widget.materiId);
if (data == null) {
throw Exception('Data materi tidak ditemukan');
}
if (!mounted) return; // Cek apakah widget masih ada
setState(() {
_materiData = data;
_isLoading = false;
});
return data;
} catch (e) {
if (!mounted) return; // Tambahkan ini juga di catch block
setState(() {
_isLoading = false;
});
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Text('Error',
style: TextStyle(fontWeight: FontWeight.bold)),
content: Text('Gagal memuat materi: ${e.toString()}'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: bgColor2,
),
child: const Text('OK'),
),
],
),
);
rethrow;
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleExpand() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha: 0.95),
appBar: AppBar(
title: Text(
_isLoading ? 'Loading...' : _materiData['judul'] ?? 'Materi N4',
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withValues(alpha: 0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: Builder(
builder: (context) {
if (_isLoading) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(bgColor2),
),
);
}
return RefreshIndicator(
color: bgColor2,
backgroundColor: bgColor1,
onRefresh: _loadMateriDetails,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
// Header clickable
InkWell(
onTap: _toggleExpand,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Daftar Sub Materi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
RotationTransition(
turns: _animation,
child: Icon(
Icons.expand_more,
color: Colors.white,
size: 28,
),
),
],
),
),
),
// Sub materi list
SizeTransition(
sizeFactor: _animation,
child: Column(
children: _materiData['details'] != null
? _materiData['details']
.asMap()
.entries
.map<Widget>((entry) {
final index = entry.key;
final detail = entry.value;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 4.0),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.circular(12),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
IsiMateriN4Page(
items: _materiData['details'],
initialIndex: index,
),
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: bgColor1.withValues(
alpha: 0.8),
borderRadius:
BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: bgColor2.withValues(
alpha: 0.2),
shape: BoxShape.circle,
border: Border.all(
color:
bgColor2.withValues(
alpha: 0.5),
width: 1.5,
),
),
child: Text(
'${index + 1}',
style: TextStyle(
fontSize: 12,
fontWeight:
FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
detail['judul'] ??
'Sub Materi',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight:
FontWeight.w500,
),
),
),
Icon(
Icons
.arrow_forward_ios_rounded,
size: 16,
color: Colors.white
.withValues(alpha: 0.7),
),
],
),
),
),
),
);
}).toList()
: [],
),
),
],
),
),
// Informasi tambahan tentang level N4
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline_rounded,
color: Colors.white.withValues(alpha: 0.9),
size: 24,
),
const SizedBox(width: 8),
Text(
'Tentang JLPT N4',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
Text(
'JLPT N4 adalah tingkat kedua dari bawah dalam ujian kemampuan bahasa Jepang. Level ini menunjukkan bahwa seseorang sudah memahami bahasa Jepang dasar yang digunakan dalam kehidupan sehari-hari dan dapat memahami percakapan sederhana dengan konteks umum. Dapat dikatakan level N4 ketika sudah menguasai 1500 kosakata dan 300 kanji',
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: 0.9),
height: 1.5,
),
),
],
),
),
// Tombol navigasi cepat
if (_materiData['details'] != null &&
_materiData['details'].isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mulai Belajar',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 12),
SizedBox(
height: 48,
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => IsiMateriN4Page(
items: _materiData['details'],
initialIndex: 0,
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: bgColor2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: const Text(
'Mulai dari Sub Materi Pertama',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
),
),
);
},
),
);
}
}

View File

@ -0,0 +1,336 @@
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
class IsiMateriN4Page extends StatefulWidget {
final List<dynamic> items;
final int initialIndex;
final String level;
const IsiMateriN4Page({
super.key,
required this.items,
required this.initialIndex,
this.level = 'N4',
});
@override
State<IsiMateriN4Page> createState() => _IsiMateriN4PageState();
}
class _IsiMateriN4PageState extends State<IsiMateriN4Page> {
late int currentIndex;
late PageController _pageController;
@override
void initState() {
super.initState();
currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _goToPrevious() {
if (currentIndex > 0) {
_pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _goToNext() {
if (currentIndex < widget.items.length - 1) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withOpacity(0.95),
appBar: AppBar(
title: Text(
'Materi JLPT ${widget.level}',
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withOpacity(0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: Column(
children: [
// Progress indicator with percentage
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
color: bgColor1.withOpacity(0.2),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Progress: ${((currentIndex + 1) / widget.items.length * 100).toStringAsFixed(0)}%',
style: TextStyle(
color: Colors.blue.shade400,
fontWeight: FontWeight.w500,
),
),
Text(
'${currentIndex + 1}/${widget.items.length}',
style: TextStyle(
color: Colors.blue.shade400,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 6),
LinearProgressIndicator(
value: (currentIndex + 1) / widget.items.length,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(bgColor2),
minHeight: 6,
borderRadius: BorderRadius.circular(10),
),
],
),
),
// Page content
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: widget.items.length,
onPageChanged: (index) {
setState(() {
currentIndex = index;
});
},
itemBuilder: (context, index) {
final item = widget.items[index];
return SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Judul materi
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2.withOpacity(0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Text(
item['judul'] ?? 'Judul Materi',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 20),
// Konten materi
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Text(
item['isi'] ?? 'Tidak ada konten',
style: TextStyle(
fontSize: 16,
height: 1.6,
color: Colors.grey[800],
),
),
),
// Contoh penggunaan untuk materi bahasa Jepang
if (item['contoh'] != null) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: bgColor2.withOpacity(0.3),
width: 1.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.lightbulb_outline,
color: bgColor2,
size: 24,
),
const SizedBox(width: 8),
Text(
'Contoh Penggunaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: bgColor2,
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: bgColor2.withOpacity(0.2),
),
),
child: Text(
item['contoh'],
style: TextStyle(
fontSize: 16,
color: Colors.grey[800],
fontStyle: FontStyle.italic,
),
),
),
],
),
),
],
],
),
);
},
),
),
// Navigation buttons
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: bgColor3,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Previous button
ElevatedButton(
onPressed: currentIndex > 0 ? _goToPrevious : null,
style: ElevatedButton.styleFrom(
backgroundColor:
currentIndex > 0 ? bgColor1 : Colors.grey.shade400,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 14),
elevation: 2,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.arrow_back,
size: 18,
color: bgColor2,
),
SizedBox(width: 8),
Text('Sebelumnya'),
],
),
),
// Next button
ElevatedButton(
onPressed: () => _goToNext(),
style: ElevatedButton.styleFrom(
backgroundColor: bgColor1,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 14),
elevation: 2,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
currentIndex < widget.items.length - 1
? 'Selanjutnya'
: 'Selesai',
),
if (currentIndex < widget.items.length - 1)
const SizedBox(width: 8),
if (currentIndex < widget.items.length - 1)
Icon(
Icons.arrow_forward,
size: 18,
color: bgColor2,
),
],
),
),
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,402 @@
import 'package:bahasajepang/pages/n4/materi/detail_materi.dart';
import 'package:bahasajepang/pages/n5/materi/materi_service.dart';
import 'package:bahasajepang/pages/n4/materi/ujian.dart';
import 'package:bahasajepang/service/ujian_service.dart';
import 'package:bahasajepang/theme.dart';
import 'package:flutter/material.dart';
class MateriN4Page extends StatefulWidget {
const MateriN4Page({super.key});
@override
State<MateriN4Page> createState() => _MateriN4PageState();
}
class _MateriN4PageState extends State<MateriN4Page> {
final MateriService _materiService = MateriService();
late Future<List<dynamic>> _materiFuture;
bool _isLoading = true;
List<dynamic> _materiList = [];
String _errorMessage = '';
@override
void initState() {
super.initState();
_loadInitialData();
}
void _loadInitialData() {
_materiFuture = _loadMateriData();
_materiFuture.catchError((error) {});
}
Future<List<dynamic>> _loadMateriData() async {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
final data = await _materiService.getMateriByLevel('N4');
setState(() {
_materiList = data;
_isLoading = false;
});
return data;
} catch (e) {
final errorMessage = e.toString().replaceAll('Exception: ', '');
setState(() {
_isLoading = false;
_errorMessage = errorMessage;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Colors.red[400],
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
throw e;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha:0.95),
appBar: AppBar(
title: const Text(
'Materi N4',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withValues(alpha:0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.white.withValues(alpha:0.9)),
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(bgColor2),
strokeWidth: 3,
),
);
}
if (_errorMessage.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
color: Colors.red[400],
size: 50,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
_errorMessage,
style: TextStyle(
color: Colors.red[400],
fontSize: 16,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loadMateriData,
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: const Text(
'Coba Lagi',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
if (_materiList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.library_books_outlined,
color: bgColor2.withValues(alpha:0.7),
size: 60,
),
const SizedBox(height: 16),
Text(
'Tidak ada materi tersedia',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
],
),
);
}
return RefreshIndicator(
color: bgColor2,
backgroundColor: bgColor1,
onRefresh: _loadMateriData,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
child: ListView.separated(
itemCount: _materiList.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 16),
itemBuilder: (context, index) {
final materi = _materiList[index];
return _buildMateriCard(materi);
},
),
),
_buildLatihanSoalCard(),
],
),
),
);
}
Widget _buildMateriCard(Map<String, dynamic> materi) {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailMateriN4Page(materiId: materi['id']),
),
);
},
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 4),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: bgColor2.withValues(alpha:0.9),
shadowColor: bgColor2.withValues(alpha:0.3),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.article_outlined,
color: Colors.white.withValues(alpha:0.9),
size: 28,
),
const SizedBox(width: 16),
Expanded(
child: Text(
materi['judul'] ?? 'Judul tidak tersedia',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
Icon(
Icons.chevron_right,
color: Colors.white.withValues(alpha:0.7),
),
],
),
),
),
);
}
Widget _buildLatihanSoalCard() {
final UjianService ujianService = UjianService();
return FutureBuilder<List<dynamic>>(
future: ujianService.getUjianByLevel(3), // Level pemula
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(bgColor2),
),
);
}
if (snapshot.hasError) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Icon(
Icons.error_outline,
color: Colors.red[400],
size: 40,
),
const SizedBox(height: 8),
Text(
'Error: ${snapshot.error}',
style: TextStyle(color: Colors.red[400]),
textAlign: TextAlign.center,
),
],
),
);
}
final ujianList = snapshot.data ?? [];
if (ujianList.isEmpty) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Icon(
Icons.quiz_outlined,
color: bgColor2.withValues(alpha:0.7),
size: 40,
),
const SizedBox(height: 8),
Text(
'Tidak ada ujian tersedia',
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, top: 16, bottom: 8),
child: Text(
'Latihan Soal',
style: TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
...ujianList.map((ujian) {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UjianN4Page(ujianId: ujian['id']),
),
);
},
child: Card(
margin: const EdgeInsets.symmetric(vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: bgColor2.withValues(alpha:0.9),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: bgColor1.withValues(alpha:0.3),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.quiz_outlined,
color: Colors.white,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ujian['judul'] ?? 'Latihan Soal',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
ujian['deskripsi'] ??
'Jumlah soal: ${ujian['jumlah_soal']}',
style: TextStyle(
color: Colors.white.withValues(alpha:0.8),
fontSize: 14,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: Colors.white.withValues(alpha:0.7),
),
],
),
),
),
);
}).toList(),
],
);
},
);
}
}

View File

@ -0,0 +1,651 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'package:bahasajepang/service/ujian_service.dart';
import 'package:bahasajepang/service/API_config.dart';
import 'package:bahasajepang/theme.dart';
class UjianN4Page extends StatefulWidget {
final int ujianId;
const UjianN4Page({Key? key, required this.ujianId}) : super(key: key);
@override
State<UjianN4Page> createState() => _UjianN4PageState();
}
class _UjianN4PageState extends State<UjianN4Page> {
final UjianService _ujianService = UjianService();
int _currentQuestionIndex = 0;
String? _selectedAnswer;
bool _isLoading = true;
bool _isSubmitting = false;
List<dynamic> _soalList = [];
List<Map<String, dynamic>> _jawabanUser = [];
Map<String, dynamic>? _hasilUjian;
String? _token;
String? _errorMessage;
late Duration _duration;
late Timer _timer;
bool _timeUp = false;
@override
void initState() {
super.initState();
_duration = const Duration(minutes: 5);
_loadInitialData();
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
void _startTimer() {
const oneSecond = Duration(seconds: 1);
_timer = Timer.periodic(oneSecond, (timer) {
if (_duration.inSeconds == 0) {
_timer.cancel();
setState(() {
_timeUp = true;
});
_autoSubmitAnswers();
} else {
setState(() {
_duration = _duration - oneSecond;
});
}
});
}
Future<void> _autoSubmitAnswers() async {
try {
setState(() => _isSubmitting = true);
_hasilUjian = await _ujianService.submitUjian(
widget.ujianId,
_jawabanUser,
_token!,
);
setState(() => _isSubmitting = false);
} catch (e) {
setState(() => _isSubmitting = false);
_showErrorSnackbar(
'Error: ${e.toString().replaceAll('Exception: ', '')}');
}
}
Future<void> _loadInitialData() async {
try {
setState(() {
_isLoading = true;
_errorMessage = null;
});
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
if (_token == null) {
throw Exception('Silakan login kembali');
}
_soalList = await _ujianService.getSoalUjian(widget.ujianId, _token!);
if (_soalList.isEmpty) {
throw Exception('Tidak ada soal tersedia untuk ujian ini');
}
_startTimer();
setState(() => _isLoading = false);
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString().replaceAll('Exception: ', '');
});
_showErrorSnackbar(_errorMessage!);
}
}
void _showErrorSnackbar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
void _handleAnswerSelection(String answer) {
setState(() => _selectedAnswer = answer);
}
Future<void> sendLevelToDatabase(int level_id) async {
const endpoint = "/update-level";
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
int? userId = prefs.getInt('id');
if (userId == null) {
throw Exception('User ID tidak ditemukan');
}
var response = await http.post(
Uri.parse(ApiConfig.baseUrl + endpoint),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'user_id': userId, 'level_id': level_id}),
);
if (response.statusCode == 200) {
await prefs.setInt('levelId', level_id);
Navigator.pushNamedAndRemoveUntil(
context, level_id == 3 ? '/n4' : '/level', (route) => false);
} else {
throw Exception('Failed to update level: ${response.statusCode}');
}
} catch (e) {
_showErrorSnackbar('Error: ${e.toString()}');
}
}
Future<void> _submitAnswer() async {
if (_selectedAnswer == null) {
_showErrorSnackbar('Pilih jawaban terlebih dahulu');
return;
}
try {
_jawabanUser.add({
'soal_id': _soalList[_currentQuestionIndex]['id'],
'jawaban_user': _selectedAnswer!,
});
if (_currentQuestionIndex == _soalList.length - 1 || _timeUp) {
setState(() => _isSubmitting = true);
_hasilUjian = await _ujianService.submitUjian(
widget.ujianId,
_jawabanUser,
_token!,
);
setState(() => _isSubmitting = false);
} else {
setState(() {
_currentQuestionIndex++;
_selectedAnswer = null;
});
}
} catch (e) {
setState(() => _isSubmitting = false);
_showErrorSnackbar(
'Error: ${e.toString().replaceAll('Exception: ', '')}');
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "$twoDigitMinutes:$twoDigitSeconds";
}
Widget _buildLoading() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
bgColor2,
),
strokeWidth: 3,
),
const SizedBox(height: 20),
Text(
'Memuat soal...',
style: TextStyle(
color: Colors.blue.shade400,
fontSize: 16,
),
),
],
),
);
}
Widget _buildError() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
color: Colors.red[400],
size: 50,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
_errorMessage ?? 'Terjadi kesalahan',
style: TextStyle(
color: Colors.red[400],
fontSize: 16,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loadInitialData,
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: const Text(
'Coba Lagi',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Widget _buildQuestion() {
final currentQuestion = _soalList[_currentQuestionIndex];
final pilihanJawaban =
currentQuestion['pilihan_jawaban'] as Map<String, dynamic>? ?? {};
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Timer and progress
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.timer,
size: 20,
color:
_duration.inSeconds <= 30 ? Colors.red : bgColor2,
),
const SizedBox(width: 8),
Text(
_formatDuration(_duration),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _duration.inSeconds <= 30
? Colors.red
: bgColor2,
),
),
],
),
Text(
'Soal ${_currentQuestionIndex + 1}/${_soalList.length}',
style: TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: (_currentQuestionIndex + 1) / _soalList.length,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(bgColor2),
minHeight: 6,
borderRadius: BorderRadius.circular(10),
),
],
),
),
const SizedBox(height: 24),
// Question card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
currentQuestion['soal'] ?? 'Pertanyaan tidak tersedia',
style: const TextStyle(
fontSize: 18,
height: 1.5,
),
),
),
),
const SizedBox(height: 24),
// Answer options
Expanded(
child: ListView.separated(
itemCount: pilihanJawaban.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final optionKey = pilihanJawaban.keys.elementAt(index);
final optionText = pilihanJawaban[optionKey];
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _handleAnswerSelection(optionKey),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _selectedAnswer == optionKey
? bgColor2.withOpacity(0.2)
: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _selectedAnswer == optionKey
? bgColor2
: Colors.grey.withOpacity(0.3),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _selectedAnswer == optionKey
? bgColor2
: Colors.grey.withOpacity(0.2),
border: Border.all(
color: _selectedAnswer == optionKey
? bgColor2
: Colors.grey.withOpacity(0.5),
),
),
child: _selectedAnswer == optionKey
? const Icon(
Icons.check,
size: 16,
color: Colors.white,
)
: null,
),
const SizedBox(width: 16),
Expanded(
child: Text(
optionText ?? 'Opsi tidak tersedia',
style: TextStyle(
fontSize: 16,
color: Colors.grey[800],
),
),
),
],
),
),
),
);
},
),
),
const SizedBox(height: 16),
// Submit button
ElevatedButton(
onPressed: _isSubmitting ? null : _submitAnswer,
style: ElevatedButton.styleFrom(
backgroundColor: bgColor3,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: _isSubmitting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 3,
color: Colors.white,
),
)
: Text(
_currentQuestionIndex == _soalList.length - 1
? 'Selesai Ujian'
: 'Lanjut ke Soal Berikutnya',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
Widget _buildResult() {
final score = _hasilUjian?['score'] ?? 0;
final isPerfectScore = score == 100;
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
bgColor1.withOpacity(0.1),
bgColor1.withOpacity(0.3),
],
),
),
child: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Icon(
isPerfectScore
? Icons.star
: _timeUp
? Icons.timer_off
: Icons.assignment_turned_in,
color: isPerfectScore
? Colors.amber
: _timeUp
? Colors.orange
: Colors.green,
size: 60,
),
const SizedBox(height: 20),
Text(
isPerfectScore
? 'Sempurna!'
: _timeUp
? 'Waktu Habis!'
: 'Ujian Selesai!',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 30),
Text(
'Skor Anda',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
Text(
'$score',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: bgColor2,
),
),
const SizedBox(height: 20),
Text(
'${_hasilUjian?['jumlah_benar'] ?? '0'} dari ${_soalList.length} soal benar',
style: const TextStyle(
fontSize: 16,
),
),
if (_timeUp) ...[
const SizedBox(height: 20),
Text(
'Waktu ujian telah habis, jawaban otomatis dikirim',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 30),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 40, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Kembali ke Materi'),
),
],
),
),
],
),
),
),
);
}
Widget _buildLevelButton(String text, Color color, VoidCallback onPressed) {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.arrow_forward),
const SizedBox(width: 8),
Text(
text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withOpacity(0.95),
appBar: AppBar(
title: const Text(
'Latihan Soal N4',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withOpacity(0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: _isLoading
? _buildLoading()
: _errorMessage != null
? _buildError()
: _hasilUjian != null
? _buildResult()
: _buildQuestion(),
);
}
}

View File

@ -0,0 +1,348 @@
import 'package:bahasajepang/pages/n5/kamus/kamus_service.dart';
import 'package:bahasajepang/service/API_config.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:audioplayers/audioplayers.dart';
class DetailKamus5Page extends StatefulWidget {
final int kamusId;
final AudioPlayer audioPlayer = AudioPlayer();
DetailKamus5Page({super.key, required this.kamusId});
@override
State<DetailKamus5Page> createState() => _DetailKamus5PageState();
}
class _DetailKamus5PageState extends State<DetailKamus5Page>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late Future<dynamic> _kamusFuture;
final KamusService _kamusService = KamusService();
@override
void initState() {
super.initState();
_kamusFuture = _kamusService.fetchKamusById(widget.kamusId);
widget.audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {});
});
}
@override
void dispose() {
widget.audioPlayer.dispose();
super.dispose();
}
Future<void> _playAudio(String? audioUrl) async {
if (audioUrl == null || audioUrl.isEmpty) return;
try {
final fullUrl = "${ApiConfig.url}/$audioUrl";
await widget.audioPlayer.play(UrlSource(fullUrl));
if (!mounted) return;
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Audio Tidak Tersedia'),
backgroundColor: bgColor3,
),
);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: bgColor1.withValues(alpha: 0.95),
appBar: AppBar(
title: const Text(
'Detail Kamus N5',
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold, fontSize: 18),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withValues(alpha: 0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: FutureBuilder<dynamic>(
future: _kamusFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(bgColor2),
),
);
} else if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 50),
const SizedBox(height: 16),
Text(
'Error: ${snapshot.error}',
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
],
),
);
} else if (!snapshot.hasData) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, color: Colors.grey, size: 50),
const SizedBox(height: 16),
const Text(
'Data tidak ditemukan',
style: TextStyle(fontSize: 16),
),
],
),
);
}
final item = snapshot.data!;
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informasi Utama Kamus
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.menu_book,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Informasi Kamus',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 16),
_buildDetailRow('Judul', item["judul"]),
_buildDetailRow('Nama', item["nama"]),
_buildDetailRow('Baca', item["baca"]),
],
),
),
const SizedBox(height: 24),
// Contoh Penggunaan
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
shape: BoxShape.circle,
),
child: Icon(
Icons.format_quote,
color: bgColor2,
size: 24,
),
),
const SizedBox(width: 12),
Text(
'Contoh Penggunaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 16),
if (item["detail_kamuses"] != null &&
(item["detail_kamuses"] as List).isNotEmpty)
...(item["detail_kamuses"] as List<dynamic>)
.map<Widget>((kamus) {
return _buildExampleCard(kamus);
})
else
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
'Tidak ada contoh penggunaan',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
],
),
),
],
),
);
},
),
);
}
Widget _buildDetailRow(String label, String? value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
color: Colors.white.withValues(alpha: 0.8),
),
),
const SizedBox(height: 4),
Text(
value ?? '-',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Divider(
color: Colors.white24,
height: 1,
),
],
),
);
}
Widget _buildExampleCard(Map<String, dynamic> kamus) {
return Card(
color: bgColor1,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kamus["kanji"] ?? '-',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (kamus["romaji"] != null && kamus["romaji"].isNotEmpty)
Text(
kamus["romaji"],
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.7),
),
),
],
),
),
IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
size: 30,
),
onPressed: () => _playAudio(kamus["voice_record"]),
),
],
),
const SizedBox(height: 8),
Text(
kamus["arti"] ?? '-',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,243 @@
import 'package:bahasajepang/pages/n5/kamus/kamus_service.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:bahasajepang/pages/n5/kamus/detail_kamus.dart';
class Kamus5Page extends StatefulWidget {
const Kamus5Page({super.key});
@override
State<Kamus5Page> createState() => _Kamus5PageState();
}
class _Kamus5PageState extends State<Kamus5Page> {
final TextEditingController _searchController = TextEditingController();
late Future<List<dynamic>> _kamusesFuture;
List<dynamic> _filteredKamus = [];
List<dynamic> _allKamus = [];
final KamusService _kamusService = KamusService();
bool _isLoading = true;
String _errorMessage = '';
@override
void initState() {
super.initState();
_loadKamusData();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadKamusData() async {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
final kamuses = await _kamusService.fetchKamusesByLevel(2); // Level N5
setState(() {
_allKamus = kamuses;
_filteredKamus = kamuses;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString().replaceAll('Exception: ', '');
});
}
}
void _onSearchChanged() {
final query = _searchController.text;
if (query.isEmpty) {
setState(() {
_filteredKamus = _allKamus;
});
return;
}
setState(() {
_filteredKamus = _allKamus.where((kamus) {
final nama = kamus["nama"]?.toString().toLowerCase() ?? '';
final judul = kamus["judul"]?.toString().toLowerCase() ?? '';
final queryLower = query.toLowerCase();
return nama.contains(queryLower) || judul.contains(queryLower);
}).toList();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha:0.95),
appBar: AppBar(
title: const Text(
'Kamus N5',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor2.withValues(alpha:0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.white.withValues(alpha:0.9)),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Cari kamus...",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: bgColor2,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
),
// List Kamus
Expanded(
child: _isLoading
? Center(
child: CircularProgressIndicator(
color: bgColor2,
),
)
: _errorMessage.isNotEmpty
? Center(
child: Text(
_errorMessage,
style: const TextStyle(color: Colors.red),
),
)
: _filteredKamus.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey,
),
SizedBox(height: 12),
Text(
'Tidak ada kamus yang ditemukan',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
)
: ListView.builder(
itemCount: _filteredKamus.length,
itemBuilder: (context, index) {
final item = _filteredKamus[index];
return Card(
color: bgColor2,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailKamus5Page(
kamusId: item["id"]),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item["judul"] ??
'Tidak ada judul',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
item["nama"] ??
'Tidak ada nama',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: bgColor1,
size: 20,
),
],
),
),
),
);
},
),
)
],
),
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:bahasajepang/service/API_config.dart';
class KamusService {
// Gunakan baseUrl dari ApiConfig
final String baseUrl = ApiConfig.baseUrl + "/kamuses";
// Ambil semua kamus tanpa filter
Future<List<dynamic>> fetchKamuses() async {
final response = await http.get(Uri.parse(baseUrl));
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load kamuses');
}
}
// Ambil kamus berdasarkan ID
Future<dynamic> fetchKamusById(int id) async {
final response = await http.get(Uri.parse('$baseUrl/$id'));
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load kamus');
}
}
// Ambil kamus berdasarkan level_id
Future<List<dynamic>> fetchKamusesByLevel(int levelId) async {
final response = await http.get(Uri.parse('$baseUrl?level_id=$levelId'));
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load kamuses by level');
}
}
}

View File

@ -0,0 +1,79 @@
List<Map<String, dynamic>> detailKamusList = [
{
"judul": "母親",
"nama": "Ibu",
"baca": "ははおや",
"contohPenggunaan": [
{"kanji": "母は市場に行きます", "arti": "Ibu pergi ke pasar"},
{"kanji": "母が料理をしている", "arti": "Ibu sedang memasak"},
{"kanji": "母は庭にいる", "arti": "Ibu berada di kebun"},
{"kanji": "私の母は40歳です", "arti": "Ibuku berumur 40 tahun"}
]
},
{
"judul": "父親",
"nama": "Ayah",
"baca": "ちちおや",
"contohPenggunaan": [
{"kanji": "お父さんは仕事をしています", "arti": "Ayah sedang bekerja"},
{"kanji": "私の父は45歳です", "arti": "Ayahku berumur 45 tahun"},
{"kanji": "お父さんは庭にいる", "arti": "Ayah berada di kebun"},
{"kanji": "お父さんは寝ています", "arti": "Ayah sedang tidur"}
]
},
{
"judul": "走る",
"nama": "Berlari",
"baca": "はしる",
"contohPenggunaan": [
{"kanji": "彼は速く走る", "arti": "Dia berlari dengan cepat"},
{"kanji": "公園で走るのが好きです", "arti": "Saya suka berlari di taman"},
{"kanji": "犬が道を走っています", "arti": "Anjing sedang berlari di jalan"},
{"kanji": "毎朝5キロ走ります", "arti": "Saya berlari 5 km setiap pagi"}
]
},
{
"judul": "歩く",
"nama": "Berjalan",
"baca": "あるく",
"contohPenggunaan": [
{"kanji": "私は学校まで歩く", "arti": "Saya berjalan ke sekolah"},
{"kanji": "公園を歩くのが楽しい", "arti": "Berjalan di taman itu menyenangkan"},
{"kanji": "彼はゆっくり歩いている", "arti": "Dia berjalan dengan pelan"},
{"kanji": "夜に散歩を歩く", "arti": "Saya berjalan-jalan di malam hari"}
]
},
{
"judul": "作る",
"nama": "Membuat",
"baca": "つくる",
"contohPenggunaan": [
{"kanji": "母がケーキを作る", "arti": "Ibu membuat kue"},
{"kanji": "彼は模型を作るのが好きです", "arti": "Dia suka membuat model"},
{"kanji": "新しいアプリを作っています", "arti": "Saya sedang membuat aplikasi baru"},
{"kanji": "彼女は美しい絵を作った", "arti": "Dia membuat lukisan yang indah"}
]
},
{
"judul": "使用",
"nama": "Menggunakan",
"baca": "しよう",
"contohPenggunaan": [
{"kanji": "このカードを使用できます", "arti": "Kartu ini bisa digunakan"},
{"kanji": "パスワードを正しく使用してください", "arti": "Gunakan kata sandi dengan benar"},
{"kanji": "水の使用を減らしましょう", "arti": "Mari kurangi penggunaan air"},
{"kanji": "携帯電話の使用は禁止されています", "arti": "Penggunaan ponsel dilarang"}
]
},
{
"judul": "高い",
"nama": "Tinggi / Mahal",
"baca": "たかい",
"contohPenggunaan": [
{"kanji": "このビルはとても高い", "arti": "Gedung ini sangat tinggi"},
{"kanji": "この時計は高いですね", "arti": "Jam ini mahal ya"},
{"kanji": "彼は高いところが苦手です", "arti": "Dia tidak suka tempat tinggi"},
{"kanji": "この店の服は高すぎる", "arti": "Baju di toko ini terlalu mahal"}
]
},
];

View File

@ -0,0 +1,560 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
import 'package:bahasajepang/service/API_config.dart';
class DetailJukugoPage extends StatefulWidget {
final AudioPlayer audioPlayer = AudioPlayer();
DetailJukugoPage({super.key});
@override
State<DetailJukugoPage> createState() => _DetailJukugoPageState();
}
class _DetailJukugoPageState extends State<DetailJukugoPage> {
late Map<String, dynamic> kanjiData;
bool _isPlaying = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
kanjiData =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
widget.audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {
_isPlaying = state == PlayerState.playing;
});
});
}
@override
void dispose() {
widget.audioPlayer.dispose();
super.dispose();
}
void _showWritingModal(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return WritingModal();
},
);
}
Future<void> _playVoice(String? voiceRecordPath) async {
if (voiceRecordPath == null || voiceRecordPath.isEmpty) return;
try {
String fullUrl = ApiConfig.url + "/" + voiceRecordPath;
await widget.audioPlayer.play(UrlSource(fullUrl));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Audio Tidak Tersedia'),
backgroundColor: bgColor3,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor1.withValues(alpha:0.95),
appBar: AppBar(
title: const Text(
'Detail Kanji Jukugo',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withValues(alpha:0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Kanji Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor2.withValues(alpha:0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Kanji Display
Container(
width: 120,
height: 140,
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
kanjiData["judul"] ?? '?',
style: const TextStyle(
fontSize: 48,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
kanjiData["nama"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
const SizedBox(width: 16),
// Kanji Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReadingCard('Kunyomi', kanjiData["kunyomi"]),
const SizedBox(height: 8),
_buildReadingCard('Onyomi', kanjiData["onyomi"]),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanjiData["voice_record"]);
},
),
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Contoh Penggunaan
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor2,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha:0.8),
shape: BoxShape.circle,
),
child: Icon(
Icons.format_quote,
color: bgColor2,
size: 24,
),
),
const SizedBox(width: 8),
Text(
'Contoh Penggunaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
if (kanjiData["detail_kanji"] != null &&
(kanjiData["detail_kanji"] as List).isNotEmpty)
...(kanjiData["detail_kanji"] as List).map<Widget>((kanji) {
return _buildExampleUsage(kanji);
}).toList()
else
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Tidak ada contoh penggunaan',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _showWritingModal(context),
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: const Text(
"Coba Menulis Kanji",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
Widget _buildReadingCard(String title, String? reading) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: bgColor1,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Text(
"$title: ",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
reading ?? '-',
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
);
}
Widget _buildExampleUsage(Map<String, dynamic> kanji) {
return Card(
color: bgColor1,
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.2), // Warna bayangan
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 1), // Posisi bayangan (x, y)
),
],
color: Colors.white),
child: IconButton(
icon: Icon(
Icons.volume_up,
color: bgColor2,
),
onPressed: () {
_playVoice(kanji["voice_record"]);
},
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kanji["kanji"] ?? '',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
kanji["romaji"] ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
Expanded(
child: Text(
kanji["arti"] ?? '',
style: const TextStyle(fontSize: 14, color: Colors.white),
textAlign: TextAlign.end,
),
),
],
),
),
);
}
}
class WritingModal extends StatefulWidget {
@override
_WritingModalState createState() => _WritingModalState();
}
class _WritingModalState extends State<WritingModal> {
List<Offset> points = [];
final GlobalKey _paintKey = GlobalKey();
Rect? _drawingArea;
void _clearDrawing() {
setState(() {
points.clear();
});
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.2),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Latihan Menulis Kanji",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) {
// Simpan ukuran area gambar setelah layout selesai
WidgetsBinding.instance.addPostFrameCallback((_) {
final renderBox = _paintKey.currentContext?.findRenderObject()
as RenderBox?;
if (renderBox != null) {
final offset = renderBox.localToGlobal(Offset.zero);
setState(() {
_drawingArea = Rect.fromLTWH(
offset.dx,
offset.dy,
renderBox.size.width,
renderBox.size.height,
);
});
}
});
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: GestureDetector(
onPanStart: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
}
},
onPanUpdate: (details) {
if (_drawingArea?.contains(details.globalPosition) ??
false) {
setState(() {
final localPosition =
_globalToLocal(details.globalPosition);
points.add(localPosition);
});
} else {
// Tambahkan titik kosong ketika keluar area
setState(() {
points.add(Offset.zero);
});
}
},
onPanEnd: (details) {
setState(() {
points.add(Offset.zero);
});
},
child: CustomPaint(
key: _paintKey,
size: Size.infinite,
painter: DrawingPainter(points),
),
),
);
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: _clearDrawing,
child: const Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 4),
Text("Hapus", style: TextStyle(color: Colors.red)),
],
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: bgColor2,
foregroundColor: Colors.white,
),
child: const Text("Selesai"),
),
],
),
],
),
),
);
}
Offset _globalToLocal(Offset global) {
if (_drawingArea == null) return Offset.zero;
return Offset(
global.dx - _drawingArea!.left,
global.dy - _drawingArea!.top,
);
}
}
class DrawingPainter extends CustomPainter {
final List<Offset> points;
DrawingPainter(this.points);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..strokeWidth = 4.0
..strokeCap = StrokeCap.round;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != Offset.zero && points[i + 1] != Offset.zero) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@ -0,0 +1,268 @@
import 'package:bahasajepang/pages/n5/kanji/kanji_service.dart';
import 'package:flutter/material.dart';
import 'package:bahasajepang/theme.dart';
class KanjiJukugoPage extends StatefulWidget {
const KanjiJukugoPage({super.key});
@override
_KanjiJukugoPageState createState() => _KanjiJukugoPageState();
}
class _KanjiJukugoPageState extends State<KanjiJukugoPage> {
final TextEditingController _searchController = TextEditingController();
final KanjiService _kanjiService = KanjiService();
List<dynamic> _filteredKanji = [];
List<dynamic> _allKanji = [];
bool _isLoading = true;
String _errorMessage = '';
@override
void initState() {
super.initState();
_fetchKanji();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _fetchKanji() async {
try {
setState(() {
_isLoading = true;
_errorMessage = '';
});
var kanjiList = await _kanjiService.fetchKanjiByKategori('jukugo');
var filteredKanji = kanjiList
.where((kanji) =>
kanji["kategori"] == "jukugo" && kanji["level_id"] == 2)
.toList();
setState(() {
_allKanji = filteredKanji;
_filteredKanji = filteredKanji;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Gagal memuat data kanji';
});
}
}
void _onSearchChanged() {
final query = _searchController.text;
if (query.isEmpty) {
setState(() {
_filteredKanji = _allKanji;
});
return;
}
setState(() {
_filteredKanji = _allKanji.where((kanji) {
final judul = kanji["judul"]?.toString().toLowerCase() ?? '';
final nama = kanji["nama"]?.toString().toLowerCase() ?? '';
final kunyomi = kanji["kunyomi"]?.toString().toLowerCase() ?? '';
final queryLower = query.toLowerCase();
return judul.contains(queryLower) ||
nama.contains(queryLower) ||
kunyomi.contains(queryLower);
}).toList();
});
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final cardSize =
screenWidth / 3.5; // Menyesuaikan ukuran card berdasarkan lebar layar
return Scaffold(
backgroundColor: bgColor1.withOpacity(0.95),
appBar: AppBar(
title: const Text(
'Kanji Jukugo N5',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: bgColor3,
elevation: 4,
shadowColor: bgColor3.withOpacity(0.5),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: Column(
children: [
// Search Bar
Container(
padding: const EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Cari kanji...",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: bgColor2.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
),
),
// Content
Expanded(
child: _isLoading
? Center(
child: CircularProgressIndicator(
color: bgColor2,
),
)
: _errorMessage.isNotEmpty
? Center(
child: Text(
_errorMessage,
style: const TextStyle(color: Colors.red),
),
)
: _filteredKanji.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 50,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Tidak ada kanji yang ditemukan',
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: GridView.builder(
padding: const EdgeInsets.only(bottom: 20),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio:
0.85, // Nilai yang lebih optimal
mainAxisExtent:
cardSize, // Gunakan ukuran dinamis
),
itemCount: _filteredKanji.length,
itemBuilder: (context, index) {
return _kanjiCard(
_filteredKanji[index], context);
},
),
),
),
],
),
);
}
Widget _kanjiCard(dynamic kanji, BuildContext context) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.pushNamed(
context,
'/detail-jukugo',
arguments: kanji,
);
},
child: Container(
padding: const EdgeInsets.all(8), // Padding yang lebih kecil
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
bgColor2.withOpacity(0.7),
bgColor2.withOpacity(0.9),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["judul"] ?? '?',
style: const TextStyle(
fontSize: 30, // Ukuran font yang lebih moderat
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
kanji["nama"] ?? '',
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.9),
),
),
),
],
),
),
),
),
);
}
}

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