Compare commits
5 Commits
049eaa1807
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2179cc6dcf | |||
| ede3fc61f1 | |||
| b1d4f9f71b | |||
| c1651550d1 | |||
| 64368b1f29 |
@@ -77,7 +77,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Sync Capacitor to Android
|
- name: Sync Capacitor to Android
|
||||||
working-directory: client
|
working-directory: client
|
||||||
run: npx cap sync android
|
run: |
|
||||||
|
npx cap add android
|
||||||
|
npx cap sync android
|
||||||
|
|
||||||
- name: Decode Keystore
|
- name: Decode Keystore
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
|
||||||
|
|
||||||
# Built application files
|
|
||||||
*.apk
|
|
||||||
*.aar
|
|
||||||
*.ap_
|
|
||||||
*.aab
|
|
||||||
|
|
||||||
# Files for the ART/Dalvik VM
|
|
||||||
*.dex
|
|
||||||
|
|
||||||
# Java class files
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
bin/
|
|
||||||
gen/
|
|
||||||
out/
|
|
||||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
|
||||||
# release/
|
|
||||||
|
|
||||||
# Gradle files
|
|
||||||
.gradle/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
|
||||||
local.properties
|
|
||||||
|
|
||||||
# Proguard folder generated by Eclipse
|
|
||||||
proguard/
|
|
||||||
|
|
||||||
# Log Files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Android Studio Navigation editor temp files
|
|
||||||
.navigation/
|
|
||||||
|
|
||||||
# Android Studio captures folder
|
|
||||||
captures/
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
*.iml
|
|
||||||
.idea/workspace.xml
|
|
||||||
.idea/tasks.xml
|
|
||||||
.idea/gradle.xml
|
|
||||||
.idea/assetWizardSettings.xml
|
|
||||||
.idea/dictionaries
|
|
||||||
.idea/libraries
|
|
||||||
# Android Studio 3 in .gitignore file.
|
|
||||||
.idea/caches
|
|
||||||
.idea/modules.xml
|
|
||||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
|
||||||
.idea/navEditor.xml
|
|
||||||
|
|
||||||
# Keystore files
|
|
||||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
|
||||||
#*.jks
|
|
||||||
#*.keystore
|
|
||||||
|
|
||||||
# External native build folder generated in Android Studio 2.2 and later
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx/
|
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
|
||||||
# google-services.json
|
|
||||||
|
|
||||||
# Freeline
|
|
||||||
freeline.py
|
|
||||||
freeline/
|
|
||||||
freeline_project_description.json
|
|
||||||
|
|
||||||
# fastlane
|
|
||||||
fastlane/report.xml
|
|
||||||
fastlane/Preview.html
|
|
||||||
fastlane/screenshots
|
|
||||||
fastlane/test_output
|
|
||||||
fastlane/readme.md
|
|
||||||
|
|
||||||
# Version control
|
|
||||||
vcs.xml
|
|
||||||
|
|
||||||
# lint
|
|
||||||
lint/intermediates/
|
|
||||||
lint/generated/
|
|
||||||
lint/outputs/
|
|
||||||
lint/tmp/
|
|
||||||
# lint/reports/
|
|
||||||
|
|
||||||
# Android Profiling
|
|
||||||
*.hprof
|
|
||||||
|
|
||||||
# Cordova plugins for Capacitor
|
|
||||||
capacitor-cordova-android-plugins
|
|
||||||
|
|
||||||
# Copied web assets
|
|
||||||
app/src/main/assets/public
|
|
||||||
|
|
||||||
# Generated Config files
|
|
||||||
app/src/main/assets/capacitor.config.json
|
|
||||||
app/src/main/assets/capacitor.plugins.json
|
|
||||||
app/src/main/res/xml/config.xml
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
/build/*
|
|
||||||
!/build/.npmkeep
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace "com.zenkanji.app"
|
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.zenkanji.app"
|
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
aaptOptions {
|
|
||||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
signingConfigs {
|
|
||||||
release {
|
|
||||||
if (file("my-release-key.jks").exists()) {
|
|
||||||
storeFile file("my-release-key.jks")
|
|
||||||
if (project.hasProperty("RELEASE_KEY_PASSWORD")) {
|
|
||||||
storePassword RELEASE_KEY_PASSWORD
|
|
||||||
keyPassword RELEASE_KEY_PASSWORD
|
|
||||||
} else if (System.getenv("RELEASE_KEY_PASSWORD") != null) {
|
|
||||||
storePassword System.getenv("RELEASE_KEY_PASSWORD")
|
|
||||||
keyPassword System.getenv("RELEASE_KEY_PASSWORD")
|
|
||||||
} else {
|
|
||||||
storePassword "missing_password"
|
|
||||||
keyPassword "missing_password"
|
|
||||||
}
|
|
||||||
if (project.hasProperty("RELEASE_KEY_ALIAS")) {
|
|
||||||
keyAlias RELEASE_KEY_ALIAS
|
|
||||||
} else if (System.getenv("RELEASE_KEY_ALIAS") != null) {
|
|
||||||
keyAlias System.getenv("RELEASE_KEY_ALIAS")
|
|
||||||
} else {
|
|
||||||
keyAlias "my-key-alias"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applicationVariants.all { variant ->
|
|
||||||
variant.outputs.all {
|
|
||||||
outputFileName = "ZenKanji-${variant.versionName}.apk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
flatDir{
|
|
||||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
|
||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
|
||||||
implementation project(':capacitor-android')
|
|
||||||
testImplementation "junit:junit:$junitVersion"
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
||||||
implementation project(':capacitor-cordova-android-plugins')
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: 'capacitor.build.gradle'
|
|
||||||
|
|
||||||
try {
|
|
||||||
def servicesJSON = file('google-services.json')
|
|
||||||
if (servicesJSON.text) {
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations.all {
|
|
||||||
resolutionStrategy {
|
|
||||||
force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
|
|
||||||
force 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22'
|
|
||||||
force 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
|
||||||
targetCompatibility JavaVersion.VERSION_21
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
|
||||||
dependencies {
|
|
||||||
implementation project(':capacitor-app')
|
|
||||||
implementation project(':capgo-capacitor-updater')
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (hasProperty('postBuildExtras')) {
|
|
||||||
postBuildExtras()
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.getcapacitor.myapp;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ExampleInstrumentedTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void useAppContext() throws Exception {
|
|
||||||
// Context of the app under test.
|
|
||||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
||||||
|
|
||||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/AppTheme"
|
|
||||||
android:usesCleartextTraffic="true">
|
|
||||||
<activity
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/title_activity_main"
|
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.fileprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
|
||||||
</provider>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
<!-- Permissions -->
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
</manifest>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.zenkanji.app;
|
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {}
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 35 KiB |
@@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="78.5885"
|
|
||||||
android:endY="90.9159"
|
|
||||||
android:startX="48.7653"
|
|
||||||
android:startY="61.0927"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#26A69A"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 5.6 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background>
|
|
||||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
|
||||||
</background>
|
|
||||||
<foreground>
|
|
||||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
|
||||||
</foreground>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background>
|
|
||||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
|
||||||
</background>
|
|
||||||
<foreground>
|
|
||||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
|
||||||
</foreground>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 530 B |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 349 B |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 700 B |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#FFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">Zen Kanji</string>
|
|
||||||
<string name="title_activity_main">Zen Kanji</string>
|
|
||||||
<string name="package_name">com.zenkanji.app</string>
|
|
||||||
<string name="custom_url_scheme">com.zenkanji.app</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
|
||||||
<item name="windowActionBar">false</item>
|
|
||||||
<item name="windowNoTitle">true</item>
|
|
||||||
<item name="android:background">@null</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
|
||||||
<item name="android:background">@drawable/splash</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<external-path name="my_images" path="." />
|
|
||||||
<cache-path name="my_cache_images" path="." />
|
|
||||||
</paths>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.getcapacitor.myapp;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
public class ExampleUnitTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addition_isCorrect() throws Exception {
|
|
||||||
assertEquals(4, 2 + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:8.7.2'
|
|
||||||
classpath 'com.google.gms:google-services:4.4.2'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "variables.gradle"
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task clean(type: Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
|
||||||
include ':capacitor-android'
|
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
|
||||||
|
|
||||||
include ':capacitor-app'
|
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
|
||||||
|
|
||||||
include ':capgo-capacitor-updater'
|
|
||||||
project(':capgo-capacitor-updater').projectDir = new File('../node_modules/@capgo/capacitor-updater/android')
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536m
|
|
||||||
android.useAndroidX=true
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
include ':app'
|
|
||||||
include ':capacitor-cordova-android-plugins'
|
|
||||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
|
||||||
|
|
||||||
apply from: 'capacitor.settings.gradle'
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
ext {
|
|
||||||
minSdkVersion = 23
|
|
||||||
compileSdkVersion = 35
|
|
||||||
targetSdkVersion = 35
|
|
||||||
androidxActivityVersion = '1.9.2'
|
|
||||||
androidxAppCompatVersion = '1.7.0'
|
|
||||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
|
||||||
androidxCoreVersion = '1.15.0'
|
|
||||||
androidxFragmentVersion = '1.8.4'
|
|
||||||
coreSplashScreenVersion = '1.0.1'
|
|
||||||
androidxWebkitVersion = '1.12.1'
|
|
||||||
junitVersion = '4.13.2'
|
|
||||||
androidxJunitVersion = '1.2.1'
|
|
||||||
androidxEspressoCoreVersion = '3.6.1'
|
|
||||||
cordovaAndroidVersion = '10.1.1'
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@mdi/font": "^7.3.67",
|
"@mdi/font": "^7.3.67",
|
||||||
"capacitor": "^0.5.6",
|
"capacitor": "^0.1.4",
|
||||||
"eslint-plugin-jsonc": "^2.21.0",
|
"eslint-plugin-jsonc": "^2.21.0",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
@@ -651,9 +651,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@capacitor/cli": {
|
"node_modules/@capacitor/cli": {
|
||||||
"version": "7.4.4",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.5.0.tgz",
|
||||||
"integrity": "sha512-J7ciBE7GlJ70sr2s8oz1+H4ZdNk4MGG41fsakUlDHWva5UWgFIZYMiEdDvGbYazAYTaxN3lVZpH9zil9FfZj+Q==",
|
"integrity": "sha512-mlohsvLZjWrO5eAVTn1+dABNQwQawcphVp6NQVJZ3I4x2BAoNmJj53QflX7PYGUipL9gF9EM9Yiku3m1McxFZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -665,13 +665,13 @@
|
|||||||
"env-paths": "^2.2.0",
|
"env-paths": "^2.2.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"kleur": "^4.1.5",
|
"kleur": "^4.1.5",
|
||||||
"native-run": "^2.0.1",
|
"native-run": "^2.0.3",
|
||||||
"open": "^8.4.0",
|
"open": "^8.4.0",
|
||||||
"plist": "^3.1.0",
|
"plist": "^3.1.0",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"tar": "^6.1.11",
|
"tar": "^7.5.3",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
@@ -1551,27 +1551,17 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/balanced-match": {
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "ISC",
|
||||||
"engines": {
|
|
||||||
"node": "20 || >=22"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/brace-expansion": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/balanced-match": "^4.0.1"
|
"minipass": "^7.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
@@ -2038,9 +2028,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||||
"integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
|
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2051,9 +2041,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
|
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2064,9 +2054,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
|
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2077,9 +2067,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||||
"integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
|
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2090,9 +2080,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
|
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2103,9 +2093,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||||
"integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
|
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2116,9 +2106,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||||
"integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
|
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2129,9 +2119,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||||
"integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
|
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2142,9 +2132,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
|
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2155,9 +2145,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
|
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2168,9 +2158,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
|
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -2181,9 +2184,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
|
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -2194,9 +2210,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
|
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -2207,9 +2223,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
|
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -2220,9 +2236,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
|
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -2233,9 +2249,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
|
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2246,9 +2262,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
|
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2258,10 +2274,23 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
|
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2272,9 +2301,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||||
"integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
|
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2285,9 +2314,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||||
"integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
|
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -2298,9 +2327,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
|
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2311,9 +2340,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||||
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
|
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2693,9 +2722,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
@@ -3286,14 +3315,24 @@
|
|||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
"node_modules/capacitor": {
|
"node_modules/capacitor": {
|
||||||
"version": "0.5.6",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/capacitor/-/capacitor-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/capacitor/-/capacitor-0.1.4.tgz",
|
||||||
"integrity": "sha512-xjyv2ztJxD1VayTMGIkh+TNfpdJU9BT3jO4Y5kEsh5MpF+zEdBQYLC/pFD9lNyS6mi8Pm3b/bj0ZUSOKblkfcw==",
|
"integrity": "sha512-wJjJmi0h0Rcob3WiaZTSEj50W2AhiZH+7C+7nE6zqrxb4rBLZDcvZ+i568LR7ioWCsI+8TAiIKYVKQ0uScmYUw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"immutable": "^3.7.3",
|
"lodash": "^2.4.1",
|
||||||
"lodash": "^4.17.21"
|
"signals": "=1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/capacitor/node_modules/lodash": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==",
|
||||||
|
"engines": [
|
||||||
|
"node",
|
||||||
|
"rhino"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -3336,13 +3375,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
@@ -5067,32 +5106,6 @@
|
|||||||
"node": ">=14.14"
|
"node": ">=14.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs-minipass": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"minipass": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fs-minipass/node_modules/minipass": {
|
|
||||||
"version": "3.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
|
||||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@@ -5258,17 +5271,40 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
"version": "10.1.1",
|
"version": "10.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/brace-expansion": "^5.0.0"
|
"brace-expansion": "^5.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@@ -5584,13 +5620,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "3.8.2",
|
"version": "5.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||||
"integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==",
|
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||||
"license": "MIT",
|
"devOptional": true,
|
||||||
"engines": {
|
"license": "MIT"
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
@@ -6480,9 +6514,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
@@ -6617,9 +6651,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
@@ -6649,30 +6683,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib": {
|
"node_modules/minizlib": {
|
||||||
"version": "2.1.2",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minipass": "^3.0.0",
|
"minipass": "^7.1.2"
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 18"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minizlib/node_modules/minipass": {
|
|
||||||
"version": "3.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
|
||||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
@@ -6681,19 +6701,6 @@
|
|||||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"mkdirp": "bin/cmd.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/moo": {
|
"node_modules/moo": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
||||||
@@ -6744,9 +6751,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/native-run": {
|
"node_modules/native-run": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz",
|
||||||
"integrity": "sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==",
|
"integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7722,9 +7729,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.53.5",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||||
"integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
|
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
@@ -7737,28 +7744,31 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.53.5",
|
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||||
"@rollup/rollup-android-arm64": "4.53.5",
|
"@rollup/rollup-android-arm64": "4.59.0",
|
||||||
"@rollup/rollup-darwin-arm64": "4.53.5",
|
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.53.5",
|
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.53.5",
|
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.53.5",
|
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.53.5",
|
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.53.5",
|
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.53.5",
|
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.53.5",
|
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.53.5",
|
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.53.5",
|
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.53.5",
|
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.53.5",
|
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.53.5",
|
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.53.5",
|
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.53.5",
|
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.53.5",
|
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.53.5",
|
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.53.5",
|
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.53.5",
|
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||||
|
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -7901,13 +7911,6 @@
|
|||||||
"@parcel/watcher": "^2.4.1"
|
"@parcel/watcher": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass/node_modules/immutable": {
|
|
||||||
"version": "5.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
|
||||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz",
|
||||||
@@ -8080,6 +8083,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/signals": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signals/-/signals-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-dE3lBiqgrgIvpGHYBy6/kiYKfh0HXRmbg0ocakBKiOefbal6ZeTtNlQlxsu9ADkNzv5OmRwRKu+IaTPSqJdZDg=="
|
||||||
|
},
|
||||||
"node_modules/sirv": {
|
"node_modules/sirv": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||||
@@ -8741,9 +8749,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/table/node_modules/ajv": {
|
"node_modules/table/node_modules/ajv": {
|
||||||
"version": "8.17.1",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -8765,31 +8773,20 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "6.2.1",
|
"version": "7.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
|
||||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
"integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chownr": "^2.0.0",
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
"fs-minipass": "^2.0.0",
|
"chownr": "^3.0.0",
|
||||||
"minipass": "^5.0.0",
|
"minipass": "^7.1.2",
|
||||||
"minizlib": "^2.1.1",
|
"minizlib": "^3.1.0",
|
||||||
"mkdirp": "^1.0.3",
|
"yallist": "^5.0.0"
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=18"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tar/node_modules/minipass": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
@@ -9815,11 +9812,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yauzl": {
|
"node_modules/yauzl": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@mdi/font": "^7.3.67",
|
"@mdi/font": "^7.3.67",
|
||||||
"capacitor": "^0.5.6",
|
"capacitor": "^0.1.4",
|
||||||
"eslint-plugin-jsonc": "^2.21.0",
|
"eslint-plugin-jsonc": "^2.21.0",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
|||||||
@@ -36,6 +36,16 @@
|
|||||||
v-list-item-title.font-weight-bold
|
v-list-item-title.font-weight-bold
|
||||||
ScrambleText(:text="$t('nav.collection')")
|
ScrambleText(:text="$t('nav.collection')")
|
||||||
|
|
||||||
|
v-list-item.mb-2(
|
||||||
|
to="/vocabularies"
|
||||||
|
rounded="lg"
|
||||||
|
:active="$route.path === '/vocabularies'"
|
||||||
|
)
|
||||||
|
template(v-slot:prepend)
|
||||||
|
v-icon(color="#00cec9" icon="mdi-translate")
|
||||||
|
v-list-item-title.font-weight-bold
|
||||||
|
ScrambleText(:text="$t('nav.vocabularies')")
|
||||||
|
|
||||||
v-divider.my-4.border-subtle
|
v-divider.my-4.border-subtle
|
||||||
|
|
||||||
.d-flex.flex-column.gap-2
|
.d-flex.flex-column.gap-2
|
||||||
@@ -116,6 +126,13 @@
|
|||||||
)
|
)
|
||||||
ScrambleText(:text="$t('nav.collection')")
|
ScrambleText(:text="$t('nav.collection')")
|
||||||
|
|
||||||
|
v-btn.mx-1(
|
||||||
|
to="/vocabularies"
|
||||||
|
variant="text"
|
||||||
|
:color="$route.path === '/vocabularies' ? '#00cec9' : 'grey'"
|
||||||
|
)
|
||||||
|
ScrambleText(:text="$t('nav.vocabularies')")
|
||||||
|
|
||||||
v-divider.mx-2.my-auto(vertical length="20" color="grey-darken-2")
|
v-divider.mx-2.my-auto(vertical length="20" color="grey-darken-2")
|
||||||
|
|
||||||
v-tooltip(:text="$t('nav.settings')" location="bottom")
|
v-tooltip(:text="$t('nav.settings')" location="bottom")
|
||||||
@@ -230,24 +247,43 @@
|
|||||||
ScrambleText(:text="$t('settings.items')")
|
ScrambleText(:text="$t('settings.items')")
|
||||||
|
|
||||||
.text-caption.text-grey.mb-2
|
.text-caption.text-grey.mb-2
|
||||||
ScrambleText(:text="$t('settings.drawingTolerance')")
|
ScrambleText(:text="$t('settings.strokeStrictness')")
|
||||||
v-slider(
|
.d-flex.justify-center.gap-2.mb-2
|
||||||
v-model="tempDrawingAccuracy"
|
v-btn(
|
||||||
:min="1"
|
v-for="p in presetNames"
|
||||||
:max="20"
|
:key="p"
|
||||||
:step="1"
|
size="small"
|
||||||
thumb-label
|
:variant="tempPreset === p ? 'flat' : 'outlined'"
|
||||||
color="#00cec9"
|
:color="tempPreset === p ? '#00cec9' : 'grey'"
|
||||||
track-color="grey-darken-3"
|
@click="selectPreset(p)"
|
||||||
|
) {{ p.charAt(0).toUpperCase() + p.slice(1) }}
|
||||||
|
|
||||||
|
v-btn.mb-3(
|
||||||
|
variant="text"
|
||||||
|
size="x-small"
|
||||||
|
color="grey"
|
||||||
|
:prepend-icon="showAdvanced ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||||
|
@click="showAdvanced = !showAdvanced"
|
||||||
)
|
)
|
||||||
.d-flex.justify-space-between.text-caption.text-grey-lighten-1.mb-6.px-1
|
ScrambleText(:text="$t('settings.advanced')")
|
||||||
span
|
|
||||||
ScrambleText(:text="$t('settings.strict')")
|
v-expand-transition
|
||||||
| (5)
|
.advanced-sliders(v-show="showAdvanced")
|
||||||
span.font-weight-bold.text-body-1(color="#00cec9") {{ tempDrawingAccuracy }}
|
.slider-row(v-for="facet in facetSliders" :key="facet.key")
|
||||||
span
|
.d-flex.justify-space-between.align-center.mb-1
|
||||||
ScrambleText(:text="$t('settings.loose')")
|
.text-caption.text-grey {{ facet.label }}
|
||||||
| (20)
|
.text-caption.font-weight-bold.text-teal-accent-3 {{ facet.value }}%
|
||||||
|
v-slider(
|
||||||
|
:model-value="facet.value"
|
||||||
|
@update:model-value="v => updateFacetSlider(facet.key, v)"
|
||||||
|
:min="5"
|
||||||
|
:max="95"
|
||||||
|
:step="5"
|
||||||
|
color="#00cec9"
|
||||||
|
track-color="grey-darken-3"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
)
|
||||||
|
|
||||||
.text-caption.text-grey.mb-2
|
.text-caption.text-grey.mb-2
|
||||||
ScrambleText(:text="$t('settings.language')")
|
ScrambleText(:text="$t('settings.language')")
|
||||||
@@ -321,13 +357,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
import {
|
import {
|
||||||
ref, watch, onMounted, computed, onUnmounted,
|
ref, watch, onMounted, computed, onUnmounted, reactive,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { App as CapacitorApp } from '@capacitor/app';
|
import { App as CapacitorApp } from '@capacitor/app';
|
||||||
import { useAppStore } from '@/stores/appStore';
|
import { useAppStore } from '@/stores/appStore';
|
||||||
import { checkForUpdates } from '@/utils/autoUpdate';
|
import { checkForUpdates } from '@/utils/autoUpdate';
|
||||||
import { SoundManager } from '@/utils/SoundManager';
|
import { SoundManager } from '@/utils/SoundManager';
|
||||||
|
import {
|
||||||
|
PRESET_NAMES, PRESETS, FACET_KEYS, thresholdToSlider, sliderToThreshold,
|
||||||
|
} from '@/utils/StrokeConfig.js';
|
||||||
import logo from '@/assets/icon.svg';
|
import logo from '@/assets/icon.svg';
|
||||||
|
|
||||||
const drawer = ref(false);
|
const drawer = ref(false);
|
||||||
@@ -345,7 +384,46 @@ const showLogoutDialog = ref(false);
|
|||||||
const snackbar = ref({ show: false, text: '', color: 'success' });
|
const snackbar = ref({ show: false, text: '', color: 'success' });
|
||||||
|
|
||||||
const tempBatchSize = ref(store.batchSize);
|
const tempBatchSize = ref(store.batchSize);
|
||||||
const tempDrawingAccuracy = ref(store.drawingAccuracy);
|
const tempPreset = ref(store.strokeStrictness.preset || 'medium');
|
||||||
|
const tempOverrides = reactive({});
|
||||||
|
const showAdvanced = ref(false);
|
||||||
|
const presetNames = PRESET_NAMES;
|
||||||
|
|
||||||
|
// Human-readable facet labels
|
||||||
|
const facetLabels = {
|
||||||
|
proximityThreshold: 'Position',
|
||||||
|
directionThreshold: 'Direction',
|
||||||
|
shapeThreshold: 'Shape',
|
||||||
|
lengthThreshold: 'Length',
|
||||||
|
curvatureThreshold: 'Curvature',
|
||||||
|
};
|
||||||
|
|
||||||
|
const facetSliders = computed(() => FACET_KEYS.map((key) => {
|
||||||
|
const preset = PRESETS[tempPreset.value] || PRESETS.medium;
|
||||||
|
const raw = tempOverrides[key] !== undefined ? tempOverrides[key] : preset[key];
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label: facetLabels[key] || key,
|
||||||
|
value: thresholdToSlider(raw),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
function selectPreset(p) {
|
||||||
|
tempPreset.value = p;
|
||||||
|
// Clear all overrides when selecting a named preset
|
||||||
|
FACET_KEYS.forEach((k) => { delete tempOverrides[k]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFacetSlider(key, sliderVal) {
|
||||||
|
tempOverrides[key] = sliderToThreshold(sliderVal);
|
||||||
|
// If overrides differ from current preset, mark as custom
|
||||||
|
const preset = PRESETS[tempPreset.value] || PRESETS.medium;
|
||||||
|
const isCustom = FACET_KEYS.some((k) => {
|
||||||
|
if (tempOverrides[k] === undefined) return false;
|
||||||
|
return Math.abs(tempOverrides[k] - preset[k]) > 0.001;
|
||||||
|
});
|
||||||
|
if (isCustom) tempPreset.value = 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
const availableLocales = ['en', 'de', 'ja'];
|
const availableLocales = ['en', 'de', 'ja'];
|
||||||
const tempLocale = ref(locale.value);
|
const tempLocale = ref(locale.value);
|
||||||
@@ -460,7 +538,14 @@ const parallaxStyle = computed(() => {
|
|||||||
watch(showSettings, (isOpen) => {
|
watch(showSettings, (isOpen) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
tempBatchSize.value = store.batchSize;
|
tempBatchSize.value = store.batchSize;
|
||||||
tempDrawingAccuracy.value = store.drawingAccuracy;
|
tempPreset.value = store.strokeStrictness.preset || 'medium';
|
||||||
|
// Restore any saved overrides
|
||||||
|
FACET_KEYS.forEach((k) => { delete tempOverrides[k]; });
|
||||||
|
if (store.strokeStrictness.overrides) {
|
||||||
|
Object.entries(store.strokeStrictness.overrides).forEach(([k, v]) => {
|
||||||
|
tempOverrides[k] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
tempLocale.value = locale.value;
|
tempLocale.value = locale.value;
|
||||||
soundEnabled.value = !SoundManager.isMuted;
|
soundEnabled.value = !SoundManager.isMuted;
|
||||||
}
|
}
|
||||||
@@ -516,9 +601,28 @@ function saveSettings() {
|
|||||||
showSettings.value = false;
|
showSettings.value = false;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
// Build the overrides object (only include keys that differ from preset)
|
||||||
|
const overrides = {};
|
||||||
|
const preset = PRESETS[tempPreset.value];
|
||||||
|
if (preset) {
|
||||||
|
FACET_KEYS.forEach((k) => {
|
||||||
|
if (tempOverrides[k] !== undefined && Math.abs(tempOverrides[k] - preset[k]) > 0.001) {
|
||||||
|
overrides[k] = tempOverrides[k];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Custom preset — keep all overrides
|
||||||
|
FACET_KEYS.forEach((k) => {
|
||||||
|
if (tempOverrides[k] !== undefined) overrides[k] = tempOverrides[k];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
store.saveSettings({
|
store.saveSettings({
|
||||||
batchSize: tempBatchSize.value,
|
batchSize: tempBatchSize.value,
|
||||||
drawingAccuracy: tempDrawingAccuracy.value,
|
strokeStrictness: {
|
||||||
|
preset: tempPreset.value,
|
||||||
|
overrides,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
SoundManager.setMuted(!soundEnabled.value);
|
SoundManager.setMuted(!soundEnabled.value);
|
||||||
@@ -678,6 +782,7 @@ function confirmLogout() {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
filter: blur(10px);
|
filter: blur(10px);
|
||||||
|
|||||||
@@ -4,17 +4,20 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onBeforeUnmount } from 'vue';
|
import { ref, watch, onBeforeUnmount } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
const displayText = ref(props.text);
|
const displayText = ref(props.text);
|
||||||
let frameId = null;
|
let frameId = null;
|
||||||
|
let lastLocale = locale.value;
|
||||||
|
|
||||||
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZアイウエオカキクケコ漢字書道';
|
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZアイウエオカキクケコ漢字書道';
|
||||||
const DURATION = 400; // total animation time in ms
|
const DURATION = 400;
|
||||||
const STEPS_PER_CHAR = 3; // how many random chars before settling
|
const STEPS_PER_CHAR = 3;
|
||||||
|
|
||||||
function scrambleTo(newText) {
|
function scrambleTo(newText) {
|
||||||
if (frameId) cancelAnimationFrame(frameId);
|
if (frameId) cancelAnimationFrame(frameId);
|
||||||
@@ -22,8 +25,6 @@ function scrambleTo(newText) {
|
|||||||
const oldText = displayText.value;
|
const oldText = displayText.value;
|
||||||
const maxLen = Math.max(oldText.length, newText.length);
|
const maxLen = Math.max(oldText.length, newText.length);
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
// Each character position settles at a staggered time
|
|
||||||
const charDelay = DURATION / (maxLen || 1);
|
const charDelay = DURATION / (maxLen || 1);
|
||||||
|
|
||||||
const tick = (now) => {
|
const tick = (now) => {
|
||||||
@@ -35,15 +36,12 @@ function scrambleTo(newText) {
|
|||||||
const charElapsed = elapsed - charSettleTime;
|
const charElapsed = elapsed - charSettleTime;
|
||||||
|
|
||||||
if (i >= newText.length && elapsed > charSettleTime + charDelay) {
|
if (i >= newText.length && elapsed > charSettleTime + charDelay) {
|
||||||
// This position should disappear — don't add anything
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charElapsed >= charDelay) {
|
if (charElapsed >= charDelay) {
|
||||||
// This character has settled — show final
|
|
||||||
if (i < newText.length) result += newText[i];
|
if (i < newText.length) result += newText[i];
|
||||||
} else if (charElapsed > 0) {
|
} else if (charElapsed > 0) {
|
||||||
// This character is still scrambling
|
|
||||||
const step = Math.floor((charElapsed / charDelay) * STEPS_PER_CHAR);
|
const step = Math.floor((charElapsed / charDelay) * STEPS_PER_CHAR);
|
||||||
if (step >= STEPS_PER_CHAR - 1 && i < newText.length) {
|
if (step >= STEPS_PER_CHAR - 1 && i < newText.length) {
|
||||||
result += newText[i];
|
result += newText[i];
|
||||||
@@ -51,7 +49,6 @@ function scrambleTo(newText) {
|
|||||||
result += CHARS[Math.floor(Math.random() * CHARS.length)];
|
result += CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Hasn't started yet — show old char or space
|
|
||||||
result += i < oldText.length ? oldText[i] : ' ';
|
result += i < oldText.length ? oldText[i] : ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,8 +66,21 @@ function scrambleTo(newText) {
|
|||||||
frameId = requestAnimationFrame(tick);
|
frameId = requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only scramble when locale changes, otherwise update silently
|
||||||
watch(() => props.text, (newVal) => {
|
watch(() => props.text, (newVal) => {
|
||||||
scrambleTo(newVal);
|
if (locale.value !== lastLocale) {
|
||||||
|
lastLocale = locale.value;
|
||||||
|
scrambleTo(newVal);
|
||||||
|
} else {
|
||||||
|
displayText.value = newVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also watch locale directly to catch the change
|
||||||
|
watch(locale, () => {
|
||||||
|
// The text prop will update right after locale changes,
|
||||||
|
// so we just mark it and let the text watcher handle animation
|
||||||
|
lastLocale = '__pending__';
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ function handlePointerUp(e) {
|
|||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
if (e.cancelable) e.preventDefault();
|
if (e.cancelable) e.preventDefault();
|
||||||
|
|
||||||
e.target.releasePointerCapture(e.pointerId);
|
try {
|
||||||
|
e.target.releasePointerCapture(e.pointerId);
|
||||||
|
} catch (_) { /* pointer was never captured */ }
|
||||||
|
|
||||||
controller.endStroke();
|
controller.endStroke();
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,7 @@ function handlePointerUp(e) {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
controller = new KanjiController({
|
controller = new KanjiController({
|
||||||
size: props.size,
|
size: props.size,
|
||||||
accuracy: store.drawingAccuracy,
|
config: store.resolvedStrokeConfig,
|
||||||
onComplete: () => emit('complete'),
|
onComplete: () => emit('complete'),
|
||||||
onMistake: (needsHint) => {
|
onMistake: (needsHint) => {
|
||||||
isShaking.value = true;
|
isShaking.value = true;
|
||||||
@@ -110,7 +112,10 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
controller = null;
|
if (controller) {
|
||||||
|
controller.reset();
|
||||||
|
controller = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.char, (newChar) => {
|
watch(() => props.char, (newChar) => {
|
||||||
@@ -129,13 +134,18 @@ watch(() => props.size, (newSize) => {
|
|||||||
if (controller) controller.resize(newSize);
|
if (controller) controller.resize(newSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => store.drawingAccuracy, (newVal) => {
|
watch(() => store.resolvedStrokeConfig, (newConfig) => {
|
||||||
if (controller) controller.setAccuracy(newVal);
|
if (controller) controller.setConfig(newConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
reset: () => controller?.reset(),
|
reset: () => controller?.reset(),
|
||||||
showHint: () => controller?.showHint(),
|
showHint: () => controller?.showHint(),
|
||||||
drawGuide: () => controller?.showHint(),
|
getLastEvaluation: () => controller?.getLastEvaluation(),
|
||||||
|
drawGuide: (enableAutoHint) => {
|
||||||
|
if (!controller) return;
|
||||||
|
if (enableAutoHint) controller.setAutoHint(true);
|
||||||
|
controller.showHint();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* useStrokeEvaluation.js
|
||||||
|
*
|
||||||
|
* Vue composable that provides reactive access to stroke evaluation
|
||||||
|
* results and the resolved config from the store.
|
||||||
|
*
|
||||||
|
* Usage in a component:
|
||||||
|
* const { config, lastResult, hasFeedback } = useStrokeEvaluation();
|
||||||
|
*
|
||||||
|
* The `lastResult` ref is updated whenever a KanjiCanvas emits an
|
||||||
|
* evaluation result via its `getLastEvaluation()` exposed method.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useAppStore } from '@/stores/appStore';
|
||||||
|
|
||||||
|
export function useStrokeEvaluation() {
|
||||||
|
const store = useAppStore();
|
||||||
|
|
||||||
|
/** Resolved frozen config object, reactively derived from the store. */
|
||||||
|
const config = computed(() => store.resolvedStrokeConfig);
|
||||||
|
|
||||||
|
/** Last evaluation result, manually updated by the parent component. */
|
||||||
|
const lastResult = ref(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this after each stroke (on @complete or @mistake) to capture
|
||||||
|
* the detailed score breakdown from the canvas controller.
|
||||||
|
*
|
||||||
|
* @param {import('@/components/kanji/KanjiCanvas.vue').default} canvasRef
|
||||||
|
*/
|
||||||
|
function captureResult(canvasRef) {
|
||||||
|
if (canvasRef?.getLastEvaluation) {
|
||||||
|
lastResult.value = canvasRef.getLastEvaluation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether we have a non-null result to display. */
|
||||||
|
const hasFeedback = computed(() => lastResult.value !== null);
|
||||||
|
|
||||||
|
/** Per-facet pass/fail booleans for UI badges. */
|
||||||
|
const facetResults = computed(() => {
|
||||||
|
if (!lastResult.value) return null;
|
||||||
|
const { scores, thresholds } = lastResult.value;
|
||||||
|
return {
|
||||||
|
proximity: scores.proximity >= thresholds.proximity,
|
||||||
|
direction: scores.direction >= thresholds.direction,
|
||||||
|
shape: scores.shape >= thresholds.shape,
|
||||||
|
length: scores.length >= thresholds.length,
|
||||||
|
curvature: scores.curvature >= thresholds.curvature,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
lastResult.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
lastResult,
|
||||||
|
hasFeedback,
|
||||||
|
facetResults,
|
||||||
|
captureResult,
|
||||||
|
clear,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import Dashboard from './views/Dashboard.vue';
|
|||||||
import Collection from './views/Collection.vue';
|
import Collection from './views/Collection.vue';
|
||||||
import Review from './views/Review.vue';
|
import Review from './views/Review.vue';
|
||||||
import Lesson from './views/Lesson.vue';
|
import Lesson from './views/Lesson.vue';
|
||||||
|
import Vocabularies from './views/Vocabularies.vue';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
@@ -26,6 +27,7 @@ const router = createRouter({
|
|||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Dashboard },
|
{ path: '/', component: Dashboard },
|
||||||
{ path: '/collection', component: Collection },
|
{ path: '/collection', component: Collection },
|
||||||
|
{ path: '/vocabularies', component: Vocabularies },
|
||||||
{ path: '/review', component: Review },
|
{ path: '/review', component: Review },
|
||||||
{ path: '/lesson', component: Lesson },
|
{ path: '/lesson', component: Lesson },
|
||||||
{ path: '/:pathMatch(.*)*', redirect: '/' },
|
{ path: '/:pathMatch(.*)*', redirect: '/' },
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const messages = {
|
|||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
review: 'Review',
|
review: 'Review',
|
||||||
collection: 'Collection',
|
collection: 'Collection',
|
||||||
|
vocabularies: 'Vocabularies',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
@@ -99,6 +100,8 @@ const messages = {
|
|||||||
items: 'Items',
|
items: 'Items',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
drawingTolerance: 'Drawing Tolerance',
|
drawingTolerance: 'Drawing Tolerance',
|
||||||
|
strokeStrictness: 'Stroke Strictness',
|
||||||
|
advanced: 'Advanced',
|
||||||
strict: 'Strict',
|
strict: 'Strict',
|
||||||
loose: 'Loose',
|
loose: 'Loose',
|
||||||
save: 'Save & Close',
|
save: 'Save & Close',
|
||||||
@@ -142,6 +145,32 @@ const messages = {
|
|||||||
startLesson: 'Start Lesson',
|
startLesson: 'Start Lesson',
|
||||||
redoLesson: 'Redo Lesson',
|
redoLesson: 'Redo Lesson',
|
||||||
},
|
},
|
||||||
|
vocabulary: {
|
||||||
|
searchLabel: 'Search Vocabulary...',
|
||||||
|
placeholder: "e.g. '大人', 'otona', 'adult'",
|
||||||
|
loading: 'Loading Vocabularies...',
|
||||||
|
noMatches: 'No matches found',
|
||||||
|
tryDifferent: 'Try searching for a different word or reading.',
|
||||||
|
levelHeader: 'LEVEL',
|
||||||
|
levelLabel: 'Level',
|
||||||
|
primary: 'Primary',
|
||||||
|
mnemonics: 'Mnemonics',
|
||||||
|
meaningMnemonic: 'Meaning Mnemonic',
|
||||||
|
readingMnemonic: 'Reading Mnemonic',
|
||||||
|
components: 'Component Kanji',
|
||||||
|
exampleSentences: 'Example Sentences',
|
||||||
|
filters: 'Filters',
|
||||||
|
filterType: 'Word Type',
|
||||||
|
filterVerbClass: 'Verb Class',
|
||||||
|
filterTransitivity: 'Transitivity',
|
||||||
|
filterEnding: 'Reading Ending',
|
||||||
|
filterLength: 'Character Count',
|
||||||
|
clearAll: 'Clear All',
|
||||||
|
matchCount: '{n} results',
|
||||||
|
listenMale: 'Male',
|
||||||
|
listenFemale: 'Female',
|
||||||
|
close: 'Close',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
common: {
|
common: {
|
||||||
@@ -152,6 +181,7 @@ const messages = {
|
|||||||
dashboard: 'Übersicht',
|
dashboard: 'Übersicht',
|
||||||
review: 'Lernen',
|
review: 'Lernen',
|
||||||
collection: 'Sammlung',
|
collection: 'Sammlung',
|
||||||
|
vocabularies: 'Vokabeln',
|
||||||
settings: 'Einstellungen',
|
settings: 'Einstellungen',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
logout: 'Abmelden',
|
logout: 'Abmelden',
|
||||||
@@ -241,6 +271,8 @@ const messages = {
|
|||||||
items: 'Einträge',
|
items: 'Einträge',
|
||||||
language: 'Sprache',
|
language: 'Sprache',
|
||||||
drawingTolerance: 'Zeichentoleranz',
|
drawingTolerance: 'Zeichentoleranz',
|
||||||
|
strokeStrictness: 'Strichgenauigkeit',
|
||||||
|
advanced: 'Erweitert',
|
||||||
strict: 'Strikt',
|
strict: 'Strikt',
|
||||||
loose: 'Locker',
|
loose: 'Locker',
|
||||||
save: 'Speichern & Schließen',
|
save: 'Speichern & Schließen',
|
||||||
@@ -284,6 +316,32 @@ const messages = {
|
|||||||
startLesson: 'Lektion starten',
|
startLesson: 'Lektion starten',
|
||||||
redoLesson: 'Lektion wiederholen',
|
redoLesson: 'Lektion wiederholen',
|
||||||
},
|
},
|
||||||
|
vocabulary: {
|
||||||
|
searchLabel: 'Vokabel suchen...',
|
||||||
|
placeholder: "z.B. '大人', 'otona', 'Erwachsener'",
|
||||||
|
loading: 'Lade Vokabeln...',
|
||||||
|
noMatches: 'Keine Treffer',
|
||||||
|
tryDifferent: 'Versuche einen anderen Suchbegriff.',
|
||||||
|
levelHeader: 'STUFE',
|
||||||
|
levelLabel: 'Stufe',
|
||||||
|
primary: 'Primär',
|
||||||
|
mnemonics: 'Eselsbrücken',
|
||||||
|
meaningMnemonic: 'Bedeutungs-Eselsbrücke',
|
||||||
|
readingMnemonic: 'Lesungs-Eselsbrücke',
|
||||||
|
components: 'Kanji-Bestandteile',
|
||||||
|
exampleSentences: 'Beispielsätze',
|
||||||
|
filters: 'Filter',
|
||||||
|
filterType: 'Wortart',
|
||||||
|
filterVerbClass: 'Verbklasse',
|
||||||
|
filterTransitivity: 'Transitivität',
|
||||||
|
filterEnding: 'Lesung-Endung',
|
||||||
|
filterLength: 'Zeichenanzahl',
|
||||||
|
clearAll: 'Zurücksetzen',
|
||||||
|
matchCount: '{n} Ergebnisse',
|
||||||
|
listenMale: 'Männlich',
|
||||||
|
listenFemale: 'Weiblich',
|
||||||
|
close: 'Schließen',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ja: {
|
ja: {
|
||||||
common: {
|
common: {
|
||||||
@@ -294,6 +352,7 @@ const messages = {
|
|||||||
dashboard: 'ダッシュボード',
|
dashboard: 'ダッシュボード',
|
||||||
review: '復習',
|
review: '復習',
|
||||||
collection: 'コレクション',
|
collection: 'コレクション',
|
||||||
|
vocabularies: '単語',
|
||||||
settings: '設定',
|
settings: '設定',
|
||||||
sync: '同期',
|
sync: '同期',
|
||||||
logout: 'ログアウト',
|
logout: 'ログアウト',
|
||||||
@@ -383,6 +442,8 @@ const messages = {
|
|||||||
items: '個',
|
items: '個',
|
||||||
language: '言語 (Language)',
|
language: '言語 (Language)',
|
||||||
drawingTolerance: '描画許容範囲',
|
drawingTolerance: '描画許容範囲',
|
||||||
|
strokeStrictness: '筆画の厳密さ',
|
||||||
|
advanced: '詳細設定',
|
||||||
strict: '厳しい',
|
strict: '厳しい',
|
||||||
loose: '甘い',
|
loose: '甘い',
|
||||||
save: '保存して閉じる',
|
save: '保存して閉じる',
|
||||||
@@ -426,6 +487,32 @@ const messages = {
|
|||||||
startLesson: 'レッスン開始',
|
startLesson: 'レッスン開始',
|
||||||
redoLesson: 'レッスンをやり直す',
|
redoLesson: 'レッスンをやり直す',
|
||||||
},
|
},
|
||||||
|
vocabulary: {
|
||||||
|
searchLabel: '単語を検索...',
|
||||||
|
placeholder: "例: '大人', 'おとな', 'adult'",
|
||||||
|
loading: '単語を読み込み中...',
|
||||||
|
noMatches: '見つかりませんでした',
|
||||||
|
tryDifferent: '別のキーワードで検索してください。',
|
||||||
|
levelHeader: 'レベル',
|
||||||
|
levelLabel: 'レベル',
|
||||||
|
primary: '主要',
|
||||||
|
mnemonics: '覚え方',
|
||||||
|
meaningMnemonic: '意味の覚え方',
|
||||||
|
readingMnemonic: '読みの覚え方',
|
||||||
|
components: '構成漢字',
|
||||||
|
exampleSentences: '例文',
|
||||||
|
filters: 'フィルター',
|
||||||
|
filterType: '品詞',
|
||||||
|
filterVerbClass: '動詞の種類',
|
||||||
|
filterTransitivity: '自他',
|
||||||
|
filterEnding: '読みの語尾',
|
||||||
|
filterLength: '文字数',
|
||||||
|
clearAll: 'クリア',
|
||||||
|
matchCount: '{n}件',
|
||||||
|
listenMale: '男性',
|
||||||
|
listenFemale: '女性',
|
||||||
|
close: '閉じる',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
import { createConfig, migrateOldAccuracy } from '@/utils/StrokeConfig.js';
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ export const useAppStore = defineStore('app', {
|
|||||||
queue: [],
|
queue: [],
|
||||||
lessonQueue: [],
|
lessonQueue: [],
|
||||||
collection: [],
|
collection: [],
|
||||||
|
vocabularies: [],
|
||||||
stats: {
|
stats: {
|
||||||
distribution: {},
|
distribution: {},
|
||||||
forecast: [],
|
forecast: [],
|
||||||
@@ -19,10 +21,25 @@ export const useAppStore = defineStore('app', {
|
|||||||
ghosts: [],
|
ghosts: [],
|
||||||
},
|
},
|
||||||
batchSize: parseInt(localStorage.getItem('zen_batch_size'), 10) || 20,
|
batchSize: parseInt(localStorage.getItem('zen_batch_size'), 10) || 20,
|
||||||
drawingAccuracy: parseInt(localStorage.getItem('zen_drawing_accuracy'), 10) || 10,
|
strokeStrictness: (() => {
|
||||||
|
// Try new format first, then migrate from old drawingAccuracy
|
||||||
|
const saved = localStorage.getItem('zen_stroke_strictness');
|
||||||
|
if (saved) {
|
||||||
|
try { return JSON.parse(saved); } catch (e) { /* fall through */ }
|
||||||
|
}
|
||||||
|
const oldVal = parseInt(localStorage.getItem('zen_drawing_accuracy'), 10);
|
||||||
|
if (oldVal) return migrateOldAccuracy(oldVal);
|
||||||
|
return { preset: 'medium', overrides: {} };
|
||||||
|
})(),
|
||||||
loading: false,
|
loading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
resolvedStrokeConfig(state) {
|
||||||
|
return createConfig(state.strokeStrictness.preset, state.strokeStrictness.overrides);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async login(apiKey) {
|
async login(apiKey) {
|
||||||
const res = await fetch(`${BASE_URL}/api/auth/login`, {
|
const res = await fetch(`${BASE_URL}/api/auth/login`, {
|
||||||
@@ -39,10 +56,16 @@ export const useAppStore = defineStore('app', {
|
|||||||
|
|
||||||
if (data.user.settings) {
|
if (data.user.settings) {
|
||||||
this.batchSize = data.user.settings.batchSize || 20;
|
this.batchSize = data.user.settings.batchSize || 20;
|
||||||
this.drawingAccuracy = data.user.settings.drawingAccuracy || 10;
|
|
||||||
|
// Handle both old and new settings format from server
|
||||||
|
if (data.user.settings.strokeStrictness) {
|
||||||
|
this.strokeStrictness = data.user.settings.strokeStrictness;
|
||||||
|
} else if (data.user.settings.drawingAccuracy) {
|
||||||
|
this.strokeStrictness = migrateOldAccuracy(data.user.settings.drawingAccuracy);
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem('zen_batch_size', this.batchSize);
|
localStorage.setItem('zen_batch_size', this.batchSize);
|
||||||
localStorage.setItem('zen_drawing_accuracy', this.drawingAccuracy);
|
localStorage.setItem('zen_stroke_strictness', JSON.stringify(this.strokeStrictness));
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('zen_token', data.token);
|
localStorage.setItem('zen_token', data.token);
|
||||||
@@ -70,10 +93,22 @@ export const useAppStore = defineStore('app', {
|
|||||||
this.token = '';
|
this.token = '';
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.stats = {};
|
this.lessonQueue = [];
|
||||||
|
this.collection = [];
|
||||||
|
this.vocabularies = [];
|
||||||
|
this.stats = {
|
||||||
|
distribution: {},
|
||||||
|
forecast: [],
|
||||||
|
queueLength: 0,
|
||||||
|
lessonCount: 0,
|
||||||
|
streak: {},
|
||||||
|
accuracy: {},
|
||||||
|
ghosts: [],
|
||||||
|
};
|
||||||
localStorage.removeItem('zen_token');
|
localStorage.removeItem('zen_token');
|
||||||
localStorage.removeItem('zen_batch_size');
|
localStorage.removeItem('zen_batch_size');
|
||||||
localStorage.removeItem('zen_drawing_accuracy');
|
localStorage.removeItem('zen_stroke_strictness');
|
||||||
|
localStorage.removeItem('zen_drawing_accuracy'); // clean up old key
|
||||||
},
|
},
|
||||||
|
|
||||||
getHeaders() {
|
getHeaders() {
|
||||||
@@ -151,6 +186,20 @@ export const useAppStore = defineStore('app', {
|
|||||||
this.collection = await res.json();
|
this.collection = await res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchVocabularies() {
|
||||||
|
if (!this.token) return;
|
||||||
|
|
||||||
|
const res = await fetch(`${BASE_URL}/api/vocabulary`, { headers: this.getHeaders() });
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
await this.logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
this.vocabularies = Array.isArray(data) ? data : [];
|
||||||
|
},
|
||||||
|
|
||||||
async submitReview(subjectId, success) {
|
async submitReview(subjectId, success) {
|
||||||
const res = await fetch(`${BASE_URL}/api/review`, {
|
const res = await fetch(`${BASE_URL}/api/review`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -186,9 +235,9 @@ export const useAppStore = defineStore('app', {
|
|||||||
this.batchSize = settings.batchSize;
|
this.batchSize = settings.batchSize;
|
||||||
localStorage.setItem('zen_batch_size', settings.batchSize);
|
localStorage.setItem('zen_batch_size', settings.batchSize);
|
||||||
}
|
}
|
||||||
if (settings.drawingAccuracy !== undefined) {
|
if (settings.strokeStrictness !== undefined) {
|
||||||
this.drawingAccuracy = settings.drawingAccuracy;
|
this.strokeStrictness = settings.strokeStrictness;
|
||||||
localStorage.setItem('zen_drawing_accuracy', settings.drawingAccuracy);
|
localStorage.setItem('zen_stroke_strictness', JSON.stringify(settings.strokeStrictness));
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetch(`${BASE_URL}/api/settings`, {
|
await fetch(`${BASE_URL}/api/settings`, {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ html,
|
|||||||
color: $color-text-white;
|
color: $color-text-white;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-card {
|
.v-card {
|
||||||
@@ -67,3 +68,10 @@ html,
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-15px);
|
transform: translateY(-15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix for flickering on mobile during transitions
|
||||||
|
.v-navigation-drawer--temporary,
|
||||||
|
.v-dialog > .v-card {
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
.readings-container {
|
.readings-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||||
gap: $spacing-sm;
|
gap: $spacing-sm;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@use 'dashboard';
|
@use 'dashboard';
|
||||||
@use 'collection';
|
@use 'collection';
|
||||||
|
@use 'vocabularies';
|
||||||
@use 'review';
|
@use 'review';
|
||||||
@use 'lesson';
|
@use 'lesson';
|
||||||
|
|||||||
@@ -19,3 +19,151 @@
|
|||||||
.lesson-canvas-wrapper {
|
.lesson-canvas-wrapper {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase Stepper ──────────────────────────────────
|
||||||
|
|
||||||
|
.lesson-stepper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-stepper__step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-stepper__dot {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: $radius-circle;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: $font-xs;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
background: $color-surface-light;
|
||||||
|
color: $color-text-grey;
|
||||||
|
border: $border-width-md solid transparent;
|
||||||
|
transition: all $duration-normal $ease-default;
|
||||||
|
|
||||||
|
.is-active & {
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-text-dark;
|
||||||
|
border-color: $color-primary;
|
||||||
|
box-shadow: $shadow-glow-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-done & {
|
||||||
|
background: $color-success;
|
||||||
|
color: $color-text-white;
|
||||||
|
border-color: $color-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-stepper__label {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
color: $color-text-grey;
|
||||||
|
opacity: $opacity-inactive;
|
||||||
|
transition: opacity $duration-normal $ease-default;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.is-active & {
|
||||||
|
opacity: $opacity-active;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-done & {
|
||||||
|
opacity: 0.6;
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-stepper__line {
|
||||||
|
flex: 1;
|
||||||
|
height: $border-width-md;
|
||||||
|
background: $color-surface-light;
|
||||||
|
border-radius: $radius-pill;
|
||||||
|
transition: background $duration-normal $ease-default;
|
||||||
|
margin: 0.875rem $spacing-xs 0;
|
||||||
|
|
||||||
|
&.is-done {
|
||||||
|
background: $color-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Practice Dots ──────────────────────────────────
|
||||||
|
|
||||||
|
.practice-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-dot {
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
border-radius: $radius-circle;
|
||||||
|
background: $color-surface-light;
|
||||||
|
transition: all $duration-normal $ease-out-back;
|
||||||
|
|
||||||
|
&.is-filled {
|
||||||
|
background: $color-primary;
|
||||||
|
box-shadow: 0 0 0.5rem $color-primary;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mnemonic Box ───────────────────────────────────
|
||||||
|
|
||||||
|
.mnemonic-box {
|
||||||
|
width: 100%;
|
||||||
|
background: $bg-glass-subtle;
|
||||||
|
border: $border-width-sm solid $color-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lesson Summary ─────────────────────────────────
|
||||||
|
|
||||||
|
.lesson-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-summary-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: $spacing-md $spacing-sm;
|
||||||
|
background: $bg-glass-subtle;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
border: $border-width-sm solid $color-border;
|
||||||
|
transition: border-color $duration-normal $ease-default;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-summary-char {
|
||||||
|
font-size: $font-xl;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-summary-meaning {
|
||||||
|
font-size: $font-xs;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Utility ────────────────────────────────────────
|
||||||
|
|
||||||
|
.stepper-spacer {
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,322 @@
|
|||||||
|
@use '../abstracts' as *;
|
||||||
|
|
||||||
|
// ── Page layout ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-page {
|
||||||
|
max-width: $max-width-desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
align-items: stretch;
|
||||||
|
position: sticky;
|
||||||
|
top: $spacing-sm;
|
||||||
|
z-index: $z-sticky;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
|
||||||
|
.vocab-search {
|
||||||
|
flex: 1;
|
||||||
|
box-shadow: $shadow-md;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-filter-toggle {
|
||||||
|
height: auto !important;
|
||||||
|
min-width: 7rem;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
font-size: $font-xs;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: $tracking-normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Active filter chips bar ──────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-active-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
padding: $spacing-xs 0;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filter panel ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-filter-panel {
|
||||||
|
background: $bg-glass-dark;
|
||||||
|
border: $border-width-sm solid $color-border;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: $spacing-md $spacing-lg;
|
||||||
|
margin-bottom: $spacing-lg;
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: $font-xs;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
color: $color-text-grey;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: $tracking-wider;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $spacing-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: $spacing-sm;
|
||||||
|
border-top: $border-width-sm solid $color-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Level header ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-level-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
|
||||||
|
.vocab-level-badge {
|
||||||
|
font-size: $font-xs;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
color: $color-primary;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: $tracking-wider;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: $spacing-2xs $spacing-sm;
|
||||||
|
background: rgba(0, 206, 201, 0.1);
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
border: $border-width-sm solid rgba(0, 206, 201, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-level-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: $color-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-level-count {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $color-text-grey;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Grid ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
||||||
|
gap: $spacing-sm;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-sm) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Card ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-card {
|
||||||
|
@include card-base;
|
||||||
|
@include hover-lift;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $spacing-md $spacing-sm $spacing-sm;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 5.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: $border-width-sm solid transparent;
|
||||||
|
transition:
|
||||||
|
transform $duration-fast $ease-default,
|
||||||
|
background $duration-fast $ease-default,
|
||||||
|
box-shadow $duration-fast $ease-default,
|
||||||
|
border-color $duration-fast $ease-default;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(0, 206, 201, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-2xs;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-char {
|
||||||
|
font-size: $font-lg;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
color: $color-primary;
|
||||||
|
line-height: $leading-tight;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-reading {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $color-text-grey;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-2xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-meaning {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $color-text-grey;
|
||||||
|
text-align: center;
|
||||||
|
line-height: $leading-normal;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-2xs;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.vc-tag {
|
||||||
|
font-size: 0.5625rem;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
color: rgba(0, 206, 201, 0.7);
|
||||||
|
background: rgba(0, 206, 201, 0.08);
|
||||||
|
padding: 0 $spacing-xs;
|
||||||
|
border-radius: $radius-xs;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
letter-spacing: $tracking-wide;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detail modal ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
.vocab-detail-char {
|
||||||
|
font-family: 'Noto Serif JP', serif;
|
||||||
|
letter-spacing: $tracking-wide;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-readings-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.vocab-reading-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: $bg-glass-dark;
|
||||||
|
padding: $spacing-xs $spacing-md;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
|
||||||
|
.vocab-reading-value {
|
||||||
|
font-size: $font-md;
|
||||||
|
color: $color-text-grey;
|
||||||
|
|
||||||
|
&.is-primary {
|
||||||
|
color: $color-text-white;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-mnemonic-section {
|
||||||
|
background: $bg-glass-dark;
|
||||||
|
padding: $spacing-md;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
|
||||||
|
.mnemonic-text {
|
||||||
|
line-height: $leading-loose;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-components-section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-collapsible {
|
||||||
|
.vocab-collapse-toggle {
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: $tracking-normal;
|
||||||
|
font-size: $font-sm;
|
||||||
|
border: $border-width-sm solid $color-border;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-collapse-content {
|
||||||
|
margin-top: $spacing-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-audio-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-audio-btn {
|
||||||
|
transition: all $duration-fast $ease-default;
|
||||||
|
text-transform: none !important;
|
||||||
|
letter-spacing: $tracking-normal !important;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
box-shadow: $shadow-glow-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-sentences-section {
|
||||||
|
.vocab-sentence {
|
||||||
|
background: $bg-glass-dark;
|
||||||
|
padding: $spacing-md;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-sentence-ja {
|
||||||
|
font-size: $font-md;
|
||||||
|
color: $color-text-white;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
line-height: $leading-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-sentence-en {
|
||||||
|
font-size: $font-sm;
|
||||||
|
color: $color-text-grey;
|
||||||
|
line-height: $leading-normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { SoundManager } from './SoundManager.js';
|
import { SoundManager } from './SoundManager.js';
|
||||||
|
import { evaluateStroke } from './StrokeEvaluator.js';
|
||||||
|
import { createConfig } from './StrokeConfig.js';
|
||||||
|
|
||||||
export const KANJI_CONSTANTS = {
|
export const KANJI_CONSTANTS = {
|
||||||
BASE_SIZE: 109,
|
BASE_SIZE: 109,
|
||||||
@@ -12,10 +14,6 @@ export const KANJI_CONSTANTS = {
|
|||||||
ANIMATION_DURATION: 300,
|
ANIMATION_DURATION: 300,
|
||||||
SAMPLE_POINTS: 60,
|
SAMPLE_POINTS: 60,
|
||||||
|
|
||||||
VALIDATION: {
|
|
||||||
SAMPLES: 20,
|
|
||||||
},
|
|
||||||
|
|
||||||
COLORS: {
|
COLORS: {
|
||||||
USER: { r: 255, g: 118, b: 117 },
|
USER: { r: 255, g: 118, b: 117 },
|
||||||
FINAL: { r: 0, g: 206, b: 201 },
|
FINAL: { r: 0, g: 206, b: 201 },
|
||||||
@@ -27,10 +25,13 @@ export const KANJI_CONSTANTS = {
|
|||||||
export class KanjiController {
|
export class KanjiController {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.size = options.size || 300;
|
this.size = options.size || 300;
|
||||||
this.accuracy = options.accuracy || 10;
|
|
||||||
this.onComplete = options.onComplete || (() => {});
|
this.onComplete = options.onComplete || (() => {});
|
||||||
this.onMistake = options.onMistake || (() => {});
|
this.onMistake = options.onMistake || (() => {});
|
||||||
|
|
||||||
|
// Stroke evaluation config (from StrokeConfig presets)
|
||||||
|
this.config = options.config || createConfig('medium');
|
||||||
|
this.lastEvaluation = null;
|
||||||
|
|
||||||
this.scale = this.size / KANJI_CONSTANTS.BASE_SIZE;
|
this.scale = this.size / KANJI_CONSTANTS.BASE_SIZE;
|
||||||
this.paths = [];
|
this.paths = [];
|
||||||
this.currentStrokeIdx = 0;
|
this.currentStrokeIdx = 0;
|
||||||
@@ -101,8 +102,12 @@ export class KanjiController {
|
|||||||
this.resize(this.size);
|
this.resize(this.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccuracy(val) {
|
setConfig(config) {
|
||||||
this.accuracy = val;
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastEvaluation() {
|
||||||
|
return this.lastEvaluation;
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(newSize) {
|
resize(newSize) {
|
||||||
@@ -306,12 +311,19 @@ export class KanjiController {
|
|||||||
|
|
||||||
validateStroke() {
|
validateStroke() {
|
||||||
const targetD = this.paths[this.currentStrokeIdx];
|
const targetD = this.paths[this.currentStrokeIdx];
|
||||||
const userNormalized = this.userPath.map((p) => ({
|
const pathEl = KanjiController.createPathElement(targetD);
|
||||||
x: p.x / this.scale,
|
|
||||||
y: p.y / this.scale,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (this.checkMatch(userNormalized, targetD)) {
|
const result = evaluateStroke(
|
||||||
|
this.userPath,
|
||||||
|
pathEl,
|
||||||
|
this.scale,
|
||||||
|
this.config,
|
||||||
|
{ isFirstStroke: this.currentStrokeIdx === 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.lastEvaluation = result;
|
||||||
|
|
||||||
|
if (result.pass) {
|
||||||
if (this.hintAnimationFrame) {
|
if (this.hintAnimationFrame) {
|
||||||
cancelAnimationFrame(this.hintAnimationFrame);
|
cancelAnimationFrame(this.hintAnimationFrame);
|
||||||
this.hintAnimationFrame = null;
|
this.hintAnimationFrame = null;
|
||||||
@@ -345,179 +357,8 @@ export class KanjiController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkMatch(userPts, targetD) {
|
// Old checkMatch, normalizeAngle, classifyOrientation, measureCurvature
|
||||||
if (userPts.length < 3) return false;
|
// have been replaced by StrokeEvaluator.evaluateStroke().
|
||||||
|
|
||||||
const pathEl = KanjiController.createPathElement(targetD);
|
|
||||||
const len = pathEl.getTotalLength();
|
|
||||||
const { SAMPLES } = KANJI_CONSTANTS.VALIDATION;
|
|
||||||
|
|
||||||
const dist = (p1, p2) => Math.hypot(p1.x - p2.x, p1.y - p2.y);
|
|
||||||
|
|
||||||
// ── 1. Start / end point check ──────────────────
|
|
||||||
const targetStart = pathEl.getPointAtLength(0);
|
|
||||||
const targetEnd = pathEl.getPointAtLength(len);
|
|
||||||
const userStart = userPts[0];
|
|
||||||
const userEnd = userPts[userPts.length - 1];
|
|
||||||
|
|
||||||
const isFirstStroke = this.currentStrokeIdx === 0;
|
|
||||||
const startThreshold = this.accuracy * (isFirstStroke ? 3.5 : 2.5);
|
|
||||||
// End tolerance scales with stroke length — long strokes get more forgiveness
|
|
||||||
const lengthBonus = Math.min(len / 30, 1.5);
|
|
||||||
const endThreshold = this.accuracy * (2.5 + lengthBonus);
|
|
||||||
|
|
||||||
if (dist(userStart, targetStart) > startThreshold) return false;
|
|
||||||
if (dist(userEnd, targetEnd) > endThreshold) return false;
|
|
||||||
|
|
||||||
// ── 2. Overall direction check ──────────────────
|
|
||||||
const userAngle = Math.atan2(userEnd.y - userStart.y, userEnd.x - userStart.x);
|
|
||||||
const targetAngle = Math.atan2(targetEnd.y - targetStart.y, targetEnd.x - targetStart.x);
|
|
||||||
const angleDiff = KanjiController.normalizeAngle(userAngle - targetAngle);
|
|
||||||
|
|
||||||
// Reject if direction is off by more than 90 degrees
|
|
||||||
if (angleDiff > Math.PI / 2) return false;
|
|
||||||
|
|
||||||
// ── 3. Orientation classification guard ─────────
|
|
||||||
const userOrientation = KanjiController.classifyOrientation(userStart, userEnd);
|
|
||||||
const targetOrientation = KanjiController.classifyOrientation(targetStart, targetEnd);
|
|
||||||
|
|
||||||
// Only block if both strokes are clearly H or V (not diagonal or dot)
|
|
||||||
if (userOrientation !== 'other' && targetOrientation !== 'other'
|
|
||||||
&& userOrientation !== targetOrientation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 4. Sequential sample comparison ─────────────
|
|
||||||
const userResampled = KanjiController.resamplePoints(userPts, SAMPLES + 1);
|
|
||||||
let totalError = 0;
|
|
||||||
let maxError = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i <= SAMPLES; i++) {
|
|
||||||
const t = i / SAMPLES;
|
|
||||||
const targetPt = pathEl.getPointAtLength(t * len);
|
|
||||||
const userPt = userResampled[i];
|
|
||||||
const d = dist(targetPt, userPt);
|
|
||||||
totalError += d;
|
|
||||||
if (d > maxError) maxError = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgError = totalError / (SAMPLES + 1);
|
|
||||||
const avgDistThreshold = this.accuracy * 1.0;
|
|
||||||
const maxDistThreshold = this.accuracy * 3.5;
|
|
||||||
|
|
||||||
if (avgError > avgDistThreshold) return false;
|
|
||||||
// No single point should be wildly off — catches "right average, wrong shape"
|
|
||||||
if (maxError > maxDistThreshold) return false;
|
|
||||||
|
|
||||||
// ── 4b. Stroke length ratio ─────────────────────
|
|
||||||
let userLen = 0;
|
|
||||||
for (let i = 1; i < userPts.length; i++) {
|
|
||||||
userLen += dist(userPts[i - 1], userPts[i]);
|
|
||||||
}
|
|
||||||
const lengthRatio = userLen / len;
|
|
||||||
// User stroke should be between 30% and 300% of target length
|
|
||||||
// Long straight strokes especially benefit from the upper bound
|
|
||||||
if (lengthRatio < 0.3 || lengthRatio > 3.0) return false;
|
|
||||||
|
|
||||||
// ── 4c. Curvature similarity ────────────────────
|
|
||||||
const targetCurvature = KanjiController.measureCurvature(
|
|
||||||
userResampled.map((_, i) => {
|
|
||||||
const t = i / SAMPLES;
|
|
||||||
const pt = pathEl.getPointAtLength(t * len);
|
|
||||||
return { x: pt.x, y: pt.y };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const userCurvature = KanjiController.measureCurvature(userPts);
|
|
||||||
|
|
||||||
// If target is clearly curved but user drew straight (or vice versa)
|
|
||||||
const curvatureDiff = Math.abs(targetCurvature - userCurvature);
|
|
||||||
// Scale threshold: strict = 0.2, loose = 0.5
|
|
||||||
const curvatureThreshold = 0.15 + (this.accuracy / 20) * 0.35;
|
|
||||||
if (curvatureDiff > curvatureThreshold) return false;
|
|
||||||
|
|
||||||
// ── 5. Endpoint direction check (hook detection) ─
|
|
||||||
const tailFraction = 0.2;
|
|
||||||
const targetTailStart = pathEl.getPointAtLength(len * (1 - tailFraction));
|
|
||||||
const targetTailAngle = Math.atan2(
|
|
||||||
targetEnd.y - targetTailStart.y,
|
|
||||||
targetEnd.x - targetTailStart.x,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if target has a significant direction change at the end (hook)
|
|
||||||
const mainTargetAngle = Math.atan2(
|
|
||||||
targetEnd.y - targetStart.y,
|
|
||||||
targetEnd.x - targetStart.x,
|
|
||||||
);
|
|
||||||
const targetHookAngle = KanjiController.normalizeAngle(targetTailAngle - mainTargetAngle);
|
|
||||||
const hasHook = targetHookAngle > Math.PI / 6; // > 30 degrees = hook
|
|
||||||
|
|
||||||
if (hasHook) {
|
|
||||||
// User must also have a direction change in the last portion
|
|
||||||
const tailIdx = Math.max(0, Math.floor(userPts.length * (1 - tailFraction)));
|
|
||||||
const userTailStart = userPts[tailIdx];
|
|
||||||
const userTailAngle = Math.atan2(
|
|
||||||
userEnd.y - userTailStart.y,
|
|
||||||
userEnd.x - userTailStart.x,
|
|
||||||
);
|
|
||||||
const mainUserAngle = Math.atan2(
|
|
||||||
userEnd.y - userStart.y,
|
|
||||||
userEnd.x - userStart.x,
|
|
||||||
);
|
|
||||||
const userHookAngle = KanjiController.normalizeAngle(userTailAngle - mainUserAngle);
|
|
||||||
|
|
||||||
// User's hook should be at least half as pronounced as the target's
|
|
||||||
// but use a generous threshold scaled by accuracy
|
|
||||||
const hookThreshold = Math.max(Math.PI / 12, targetHookAngle * 0.4);
|
|
||||||
if (userHookAngle < hookThreshold) {
|
|
||||||
// Only reject on strict accuracy (< 12), forgive on loose settings
|
|
||||||
if (this.accuracy < 12) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Static helpers for stroke validation ────────────
|
|
||||||
|
|
||||||
static normalizeAngle(angle) {
|
|
||||||
let a = Math.abs(angle);
|
|
||||||
if (a > Math.PI) a = 2 * Math.PI - a;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
static classifyOrientation(start, end) {
|
|
||||||
const dx = Math.abs(end.x - start.x);
|
|
||||||
const dy = Math.abs(end.y - start.y);
|
|
||||||
const totalDist = Math.hypot(dx, dy);
|
|
||||||
|
|
||||||
// Very short strokes (dots) — don't classify
|
|
||||||
if (totalDist < 5) return 'other';
|
|
||||||
|
|
||||||
const ratio = dx / (dy || 0.001);
|
|
||||||
if (ratio > 2.5) return 'horizontal';
|
|
||||||
if (ratio < 0.4) return 'vertical';
|
|
||||||
return 'other'; // diagonal or ambiguous
|
|
||||||
}
|
|
||||||
|
|
||||||
static measureCurvature(points) {
|
|
||||||
if (points.length < 3) return 0;
|
|
||||||
|
|
||||||
// Curvature = ratio of actual path length to straight-line distance
|
|
||||||
// A perfectly straight line = 0, a semicircle ≈ 0.57
|
|
||||||
let pathLen = 0;
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
|
||||||
pathLen += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
|
|
||||||
}
|
|
||||||
|
|
||||||
const straightDist = Math.hypot(
|
|
||||||
points[points.length - 1].x - points[0].x,
|
|
||||||
points[points.length - 1].y - points[0].y,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (straightDist < 1) return 0; // dot-like stroke
|
|
||||||
// Returns 0 for straight, >0 for curved (ratio - 1)
|
|
||||||
return Math.max(0, (pathLen / straightDist) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
animateErrorFade(userPath, onComplete) {
|
animateErrorFade(userPath, onComplete) {
|
||||||
this.isAnimating = true;
|
this.isAnimating = true;
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* StrokeConfig.js
|
||||||
|
*
|
||||||
|
* Strictness configuration for the stroke evaluation system.
|
||||||
|
* Provides preset difficulty levels and a factory to build
|
||||||
|
* resolved configs with optional per-facet overrides.
|
||||||
|
*
|
||||||
|
* Each threshold is a minimum score (0-1) that the corresponding
|
||||||
|
* evaluation facet must exceed for a stroke to pass.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Presets ──────────────────────────────────────────────────
|
||||||
|
export const PRESETS = {
|
||||||
|
easy: {
|
||||||
|
proximityThreshold: 0.25,
|
||||||
|
directionThreshold: 0.35,
|
||||||
|
shapeThreshold: 0.35,
|
||||||
|
lengthThreshold: 0.25,
|
||||||
|
curvatureThreshold: 0.20,
|
||||||
|
sampleCount: 32,
|
||||||
|
dtwBandWidth: 0.35,
|
||||||
|
firstStrokeLeniency: 1.5,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
proximityThreshold: 0.40,
|
||||||
|
directionThreshold: 0.50,
|
||||||
|
shapeThreshold: 0.50,
|
||||||
|
lengthThreshold: 0.40,
|
||||||
|
curvatureThreshold: 0.35,
|
||||||
|
sampleCount: 32,
|
||||||
|
dtwBandWidth: 0.30,
|
||||||
|
firstStrokeLeniency: 1.35,
|
||||||
|
},
|
||||||
|
hard: {
|
||||||
|
proximityThreshold: 0.55,
|
||||||
|
directionThreshold: 0.65,
|
||||||
|
shapeThreshold: 0.65,
|
||||||
|
lengthThreshold: 0.55,
|
||||||
|
curvatureThreshold: 0.50,
|
||||||
|
sampleCount: 48,
|
||||||
|
dtwBandWidth: 0.25,
|
||||||
|
firstStrokeLeniency: 1.2,
|
||||||
|
},
|
||||||
|
expert: {
|
||||||
|
proximityThreshold: 0.70,
|
||||||
|
directionThreshold: 0.80,
|
||||||
|
shapeThreshold: 0.78,
|
||||||
|
lengthThreshold: 0.65,
|
||||||
|
curvatureThreshold: 0.65,
|
||||||
|
sampleCount: 48,
|
||||||
|
dtwBandWidth: 0.20,
|
||||||
|
firstStrokeLeniency: 1.1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Factory ─────────────────────────────────────────────────
|
||||||
|
export function createConfig(preset = 'medium', overrides = {}) {
|
||||||
|
const base = PRESETS[preset] || PRESETS.medium;
|
||||||
|
return Object.freeze({ ...base, ...overrides });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Slider helpers ──────────────────────────────────────────
|
||||||
|
export function sliderToThreshold(v) {
|
||||||
|
return Math.max(0, Math.min(1, v / 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function thresholdToSlider(t) {
|
||||||
|
return Math.round(Math.max(0, Math.min(1, t)) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Migration from old accuracy (1-20) ─────────────────────
|
||||||
|
export function migrateOldAccuracy(accuracy) {
|
||||||
|
const val = Math.max(1, Math.min(20, accuracy));
|
||||||
|
let preset;
|
||||||
|
if (val <= 5) preset = 'expert';
|
||||||
|
else if (val <= 10) preset = 'hard';
|
||||||
|
else if (val <= 15) preset = 'medium';
|
||||||
|
else preset = 'easy';
|
||||||
|
return { preset, overrides: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PRESET_NAMES = ['easy', 'medium', 'hard', 'expert'];
|
||||||
|
|
||||||
|
export const FACET_KEYS = [
|
||||||
|
'proximityThreshold',
|
||||||
|
'directionThreshold',
|
||||||
|
'shapeThreshold',
|
||||||
|
'lengthThreshold',
|
||||||
|
'curvatureThreshold',
|
||||||
|
];
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
/**
|
||||||
|
* StrokeEvaluator.js
|
||||||
|
*
|
||||||
|
* Pure-function module for evaluating user-drawn strokes against
|
||||||
|
* SVG reference paths. No DOM, Canvas, or Vue dependencies — only
|
||||||
|
* math and array operations (except where an SVG path element is
|
||||||
|
* passed in for sampling).
|
||||||
|
*
|
||||||
|
* Core algorithm: Dynamic Time Warping (DTW) with Sakoe-Chiba band.
|
||||||
|
*
|
||||||
|
* Five evaluation facets:
|
||||||
|
* 1. Start/End Proximity
|
||||||
|
* 2. Directionality (angle match + reversal detection)
|
||||||
|
* 3. Shape Similarity (DTW)
|
||||||
|
* 4. Length Ratio
|
||||||
|
* 5. Curvature (sinuosity) Match
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Helpers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Euclidean distance between two {x, y} points. */
|
||||||
|
function dist(a, b) {
|
||||||
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize an angle difference to [0, PI].
|
||||||
|
* @param {number} angle - raw difference in radians
|
||||||
|
* @returns {number} absolute difference clamped to [0, PI]
|
||||||
|
*/
|
||||||
|
function normalizeAngle(angle) {
|
||||||
|
let a = Math.abs(angle);
|
||||||
|
if (a > Math.PI) a = 2 * Math.PI - a;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total arc-length of a polyline [{x,y}, ...].
|
||||||
|
*/
|
||||||
|
function polylineLength(pts) {
|
||||||
|
let len = 0;
|
||||||
|
for (let i = 1; i < pts.length; i++) {
|
||||||
|
len += dist(pts[i - 1], pts[i]);
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resampling ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resample a polyline to `count` equidistant points along its arc.
|
||||||
|
*
|
||||||
|
* @param {Array<{x:number, y:number}>} points
|
||||||
|
* @param {number} count - desired number of output points
|
||||||
|
* @returns {Array<{x:number, y:number}>}
|
||||||
|
*/
|
||||||
|
export function resamplePath(points, count) {
|
||||||
|
if (!points || points.length === 0) return [];
|
||||||
|
if (points.length === 1 || count <= 1) return [{ ...points[0] }];
|
||||||
|
|
||||||
|
// Build cumulative distance array
|
||||||
|
let totalLen = 0;
|
||||||
|
const cumDist = [0];
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
totalLen += dist(points[i - 1], points[i]);
|
||||||
|
cumDist.push(totalLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalLen === 0) return Array.from({ length: count }, () => ({ ...points[0] }));
|
||||||
|
|
||||||
|
const step = totalLen / (count - 1);
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const targetDist = i * step;
|
||||||
|
|
||||||
|
// Binary search for the segment containing targetDist
|
||||||
|
let lo = 0;
|
||||||
|
let hi = cumDist.length - 1;
|
||||||
|
while (lo < hi - 1) {
|
||||||
|
const mid = (lo + hi) >> 1;
|
||||||
|
if (cumDist[mid] <= targetDist) lo = mid;
|
||||||
|
else hi = mid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segStart = lo;
|
||||||
|
const segLen = cumDist[segStart + 1] - cumDist[segStart];
|
||||||
|
const t = segLen === 0 ? 0 : (targetDist - cumDist[segStart]) / segLen;
|
||||||
|
|
||||||
|
const p1 = points[segStart];
|
||||||
|
const p2 = points[Math.min(segStart + 1, points.length - 1)];
|
||||||
|
result.push({
|
||||||
|
x: p1.x + (p2.x - p1.x) * t,
|
||||||
|
y: p1.y + (p2.y - p1.y) * t,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dynamic Time Warping ────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute DTW distance between two sequences of {x, y} points
|
||||||
|
* using a Sakoe-Chiba band constraint.
|
||||||
|
*
|
||||||
|
* Returns the total accumulated cost normalized by (N + M),
|
||||||
|
* producing a length-independent distance measure.
|
||||||
|
*
|
||||||
|
* @param {Array<{x:number, y:number}>} seq1
|
||||||
|
* @param {Array<{x:number, y:number}>} seq2
|
||||||
|
* @param {number} bandFraction - band width as fraction of max(N, M), default 0.3
|
||||||
|
* @returns {number} normalized DTW distance
|
||||||
|
*/
|
||||||
|
export function computeDTW(seq1, seq2, bandFraction = 0.3) {
|
||||||
|
const n = seq1.length;
|
||||||
|
const m = seq2.length;
|
||||||
|
if (n === 0 || m === 0) return Infinity;
|
||||||
|
|
||||||
|
const w = Math.max(1, Math.floor(Math.max(n, m) * bandFraction));
|
||||||
|
|
||||||
|
// Use two-row optimization to save memory (O(m) instead of O(n*m))
|
||||||
|
let prev = new Float64Array(m).fill(Infinity);
|
||||||
|
let curr = new Float64Array(m).fill(Infinity);
|
||||||
|
|
||||||
|
prev[0] = dist(seq1[0], seq2[0]);
|
||||||
|
|
||||||
|
// Fill first row within band
|
||||||
|
for (let j = 1; j < Math.min(w + 1, m); j++) {
|
||||||
|
prev[j] = prev[j - 1] + dist(seq1[0], seq2[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < n; i++) {
|
||||||
|
curr.fill(Infinity);
|
||||||
|
const jMin = Math.max(0, i - w);
|
||||||
|
const jMax = Math.min(m - 1, i + w);
|
||||||
|
|
||||||
|
for (let j = jMin; j <= jMax; j++) {
|
||||||
|
const cost = dist(seq1[i], seq2[j]);
|
||||||
|
const top = prev[j]; // (i-1, j)
|
||||||
|
const left = j > 0 ? curr[j - 1] : Infinity; // (i, j-1)
|
||||||
|
const diag = (j > 0) ? prev[j - 1] : Infinity; // (i-1, j-1)
|
||||||
|
curr[j] = cost + Math.min(top, left, diag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap rows
|
||||||
|
[prev, curr] = [curr, prev];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result is in prev after last swap
|
||||||
|
return prev[m - 1] / (n + m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Facet 1: Proximity ──────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate how close the user's start/end points are to the target's.
|
||||||
|
*
|
||||||
|
* Score is the average of start-proximity and end-proximity,
|
||||||
|
* each mapped through a smooth decay: score = exp(-d / radius).
|
||||||
|
*
|
||||||
|
* @param {{x:number,y:number}} userStart
|
||||||
|
* @param {{x:number,y:number}} userEnd
|
||||||
|
* @param {{x:number,y:number}} targetStart
|
||||||
|
* @param {{x:number,y:number}} targetEnd
|
||||||
|
* @param {number} canvasSize - size of the drawing area (for normalization)
|
||||||
|
* @returns {number} 0-1 score (1 = perfect alignment)
|
||||||
|
*/
|
||||||
|
export function evaluateProximity(userStart, userEnd, targetStart, targetEnd, canvasSize) {
|
||||||
|
// Radius = 15% of canvas size — distances beyond this decay rapidly
|
||||||
|
const radius = canvasSize * 0.15;
|
||||||
|
|
||||||
|
const startDist = dist(userStart, targetStart);
|
||||||
|
const endDist = dist(userEnd, targetEnd);
|
||||||
|
|
||||||
|
const startScore = Math.exp(-startDist / radius);
|
||||||
|
const endScore = Math.exp(-endDist / radius);
|
||||||
|
|
||||||
|
return (startScore + endScore) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Facet 2: Directionality ─────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate the directional match between user and target strokes.
|
||||||
|
*
|
||||||
|
* Uses the angle between start→end vectors. Also detects
|
||||||
|
* reversal (stroke drawn backwards) and orientation mismatch
|
||||||
|
* (horizontal vs vertical).
|
||||||
|
*
|
||||||
|
* @param {{x:number,y:number}} userStart
|
||||||
|
* @param {{x:number,y:number}} userEnd
|
||||||
|
* @param {{x:number,y:number}} targetStart
|
||||||
|
* @param {{x:number,y:number}} targetEnd
|
||||||
|
* @returns {{ score: number, reversed: boolean }}
|
||||||
|
*/
|
||||||
|
export function evaluateDirection(userStart, userEnd, targetStart, targetEnd) {
|
||||||
|
const userAngle = Math.atan2(userEnd.y - userStart.y, userEnd.x - userStart.x);
|
||||||
|
const targetAngle = Math.atan2(targetEnd.y - targetStart.y, targetEnd.x - targetStart.x);
|
||||||
|
|
||||||
|
const angleDiff = normalizeAngle(userAngle - targetAngle);
|
||||||
|
|
||||||
|
// Reversed if angle diff > 120 degrees
|
||||||
|
const reversed = angleDiff > (2 * Math.PI / 3);
|
||||||
|
|
||||||
|
// Score: 1.0 at 0 deg, 0.0 at 180 deg (linear mapping)
|
||||||
|
const score = Math.max(0, 1.0 - angleDiff / Math.PI);
|
||||||
|
|
||||||
|
return { score, reversed };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Facet 3: Shape Similarity (DTW) ─────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate shape similarity using DTW on resampled, translation-
|
||||||
|
* and scale-normalized paths.
|
||||||
|
*
|
||||||
|
* Both paths are centered at origin and scaled to unit diagonal
|
||||||
|
* before comparison, so DTW only measures *shape* — not position
|
||||||
|
* or size (those are handled by other facets).
|
||||||
|
*
|
||||||
|
* @param {Array<{x:number,y:number}>} userResampled
|
||||||
|
* @param {Array<{x:number,y:number}>} targetResampled
|
||||||
|
* @param {number} bandFraction
|
||||||
|
* @returns {number} 0-1 score (1 = identical shape)
|
||||||
|
*/
|
||||||
|
export function evaluateShape(userResampled, targetResampled, bandFraction) {
|
||||||
|
const normalizeSeq = (pts) => {
|
||||||
|
let minX = Infinity; let maxX = -Infinity;
|
||||||
|
let minY = Infinity; let maxY = -Infinity;
|
||||||
|
for (const p of pts) {
|
||||||
|
if (p.x < minX) minX = p.x;
|
||||||
|
if (p.x > maxX) maxX = p.x;
|
||||||
|
if (p.y < minY) minY = p.y;
|
||||||
|
if (p.y > maxY) maxY = p.y;
|
||||||
|
}
|
||||||
|
const cx = (minX + maxX) / 2;
|
||||||
|
const cy = (minY + maxY) / 2;
|
||||||
|
const diag = Math.hypot(maxX - minX, maxY - minY) || 1;
|
||||||
|
return pts.map((p) => ({ x: (p.x - cx) / diag, y: (p.y - cy) / diag }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const normUser = normalizeSeq(userResampled);
|
||||||
|
const normTarget = normalizeSeq(targetResampled);
|
||||||
|
|
||||||
|
const dtwDist = computeDTW(normUser, normTarget, bandFraction);
|
||||||
|
|
||||||
|
// Convert distance to 0-1 score using a tuned sigmoid
|
||||||
|
// sensitivity controls how fast score drops with distance
|
||||||
|
const sensitivity = 0.06;
|
||||||
|
return 1.0 / (1.0 + dtwDist / sensitivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Facet 4: Length Ratio ───────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate how well the user's stroke length matches the target.
|
||||||
|
*
|
||||||
|
* Uses a ratio-based score that peaks at 1.0 when lengths are equal
|
||||||
|
* and decays symmetrically for too-short or too-long strokes.
|
||||||
|
*
|
||||||
|
* @param {number} userLen - arc-length of user stroke
|
||||||
|
* @param {number} targetLen - arc-length of target stroke
|
||||||
|
* @returns {number} 0-1 score
|
||||||
|
*/
|
||||||
|
export function evaluateLength(userLen, targetLen) {
|
||||||
|
if (targetLen === 0) return userLen === 0 ? 1 : 0;
|
||||||
|
if (userLen === 0) return 0;
|
||||||
|
|
||||||
|
const ratio = userLen / targetLen;
|
||||||
|
// Score = 1 at ratio=1, drops off for ratios far from 1
|
||||||
|
// Using: 1 - |ln(ratio)| / ln(K), clamped to [0, 1]
|
||||||
|
// K=4 means ratio of 4x or 0.25x gives score=0
|
||||||
|
const K = 4;
|
||||||
|
const score = 1 - Math.abs(Math.log(ratio)) / Math.log(K);
|
||||||
|
return Math.max(0, Math.min(1, score));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Facet 5: Curvature ──────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure sinuosity of a polyline: ratio of arc-length to
|
||||||
|
* straight-line distance minus 1. Returns 0 for straight lines,
|
||||||
|
* larger values for curvier paths.
|
||||||
|
*/
|
||||||
|
function measureSinuosity(pts) {
|
||||||
|
if (pts.length < 3) return 0;
|
||||||
|
const arcLen = polylineLength(pts);
|
||||||
|
const chord = dist(pts[0], pts[pts.length - 1]);
|
||||||
|
if (chord < 1) return 0; // effectively a dot
|
||||||
|
return Math.max(0, (arcLen / chord) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate curvature similarity between user and target strokes.
|
||||||
|
*
|
||||||
|
* Compares sinuosity values — if the target is curved and the
|
||||||
|
* user drew straight (or vice versa), the score drops.
|
||||||
|
*
|
||||||
|
* @param {Array<{x:number,y:number}>} userPts
|
||||||
|
* @param {Array<{x:number,y:number}>} targetPts
|
||||||
|
* @returns {number} 0-1 score
|
||||||
|
*/
|
||||||
|
export function evaluateCurvature(userPts, targetPts) {
|
||||||
|
const userSin = measureSinuosity(userPts);
|
||||||
|
const targetSin = measureSinuosity(targetPts);
|
||||||
|
|
||||||
|
const diff = Math.abs(userSin - targetSin);
|
||||||
|
|
||||||
|
// Tolerance: curvature differences below 0.05 are perfect,
|
||||||
|
// differences above 0.6 are completely wrong
|
||||||
|
const tolerance = 0.6;
|
||||||
|
const score = 1 - Math.min(diff / tolerance, 1);
|
||||||
|
return Math.max(0, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Master Evaluation ───────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all 5 evaluation facets on a user stroke and return a
|
||||||
|
* detailed result.
|
||||||
|
*
|
||||||
|
* @param {Array<{x:number,y:number}>} userPoints - raw user stroke (canvas coords)
|
||||||
|
* @param {SVGPathElement} targetPathEl - SVG path element for the target stroke
|
||||||
|
* @param {number} scale - canvas-to-SVG scale factor
|
||||||
|
* @param {Object} config - resolved StrokeConfig
|
||||||
|
* @param {{ isFirstStroke: boolean }} options
|
||||||
|
* @returns {{
|
||||||
|
* pass: boolean,
|
||||||
|
* scores: { proximity: number, direction: number, shape: number, length: number, curvature: number },
|
||||||
|
* thresholds: { proximity: number, direction: number, shape: number, length: number, curvature: number },
|
||||||
|
* details: { reversed: boolean }
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function evaluateStroke(userPoints, targetPathEl, scale, config, options = {}) {
|
||||||
|
const { isFirstStroke = false } = options;
|
||||||
|
|
||||||
|
// Minimum points to be considered a real stroke
|
||||||
|
if (userPoints.length < 3) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
scores: {
|
||||||
|
proximity: 0, direction: 0, shape: 0, length: 0, curvature: 0,
|
||||||
|
},
|
||||||
|
thresholds: _getThresholds(config, isFirstStroke),
|
||||||
|
details: { reversed: false, reason: 'too_few_points' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Normalize user points to SVG coordinate space ──────
|
||||||
|
const userNorm = userPoints.map((p) => ({ x: p.x / scale, y: p.y / scale }));
|
||||||
|
|
||||||
|
// ── Extract target geometry ────────────────────────────
|
||||||
|
const targetLen = targetPathEl.getTotalLength();
|
||||||
|
const targetStart = targetPathEl.getPointAtLength(0);
|
||||||
|
const targetEnd = targetPathEl.getPointAtLength(targetLen);
|
||||||
|
|
||||||
|
const userStart = userNorm[0];
|
||||||
|
const userEnd = userNorm[userNorm.length - 1];
|
||||||
|
|
||||||
|
// SVG viewBox is 109x109, use that as the "canvas size" for normalization
|
||||||
|
const svgSize = 109;
|
||||||
|
|
||||||
|
// ── 1. Proximity ───────────────────────────────────────
|
||||||
|
const proximityScore = evaluateProximity(
|
||||||
|
userStart, userEnd, targetStart, targetEnd, svgSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 2. Direction ───────────────────────────────────────
|
||||||
|
const { score: directionScore, reversed } = evaluateDirection(
|
||||||
|
userStart, userEnd, targetStart, targetEnd,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 3. Shape (DTW) ────────────────────────────────────
|
||||||
|
const sampleCount = config.sampleCount || 32;
|
||||||
|
const userResampled = resamplePath(userNorm, sampleCount);
|
||||||
|
|
||||||
|
const targetResampled = [];
|
||||||
|
for (let i = 0; i < sampleCount; i++) {
|
||||||
|
const pt = targetPathEl.getPointAtLength((i / (sampleCount - 1)) * targetLen);
|
||||||
|
targetResampled.push({ x: pt.x, y: pt.y });
|
||||||
|
}
|
||||||
|
|
||||||
|
const shapeScore = evaluateShape(
|
||||||
|
userResampled, targetResampled, config.dtwBandWidth || 0.3,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 4. Length ──────────────────────────────────────────
|
||||||
|
const userLen = polylineLength(userNorm);
|
||||||
|
const lengthScore = evaluateLength(userLen, targetLen);
|
||||||
|
|
||||||
|
// ── 5. Curvature ───────────────────────────────────────
|
||||||
|
const curvatureScore = evaluateCurvature(userResampled, targetResampled);
|
||||||
|
|
||||||
|
// ── Thresholds (adjusted for first stroke if applicable) ──
|
||||||
|
const thresholds = _getThresholds(config, isFirstStroke);
|
||||||
|
|
||||||
|
// ── Pass/fail determination ────────────────────────────
|
||||||
|
const pass = proximityScore >= thresholds.proximity
|
||||||
|
&& directionScore >= thresholds.direction
|
||||||
|
&& shapeScore >= thresholds.shape
|
||||||
|
&& lengthScore >= thresholds.length
|
||||||
|
&& curvatureScore >= thresholds.curvature;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
scores: {
|
||||||
|
proximity: proximityScore,
|
||||||
|
direction: directionScore,
|
||||||
|
shape: shapeScore,
|
||||||
|
length: lengthScore,
|
||||||
|
curvature: curvatureScore,
|
||||||
|
},
|
||||||
|
thresholds,
|
||||||
|
details: { reversed },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the threshold object from config, applying first-stroke
|
||||||
|
* leniency to the proximity threshold if applicable.
|
||||||
|
*/
|
||||||
|
function _getThresholds(config, isFirstStroke) {
|
||||||
|
const leniency = isFirstStroke ? (config.firstStrokeLeniency || 1) : 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
proximity: Math.max(0, config.proximityThreshold / leniency),
|
||||||
|
direction: config.directionThreshold,
|
||||||
|
shape: config.shapeThreshold,
|
||||||
|
length: config.lengthThreshold,
|
||||||
|
curvature: config.curvatureThreshold,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -157,13 +157,14 @@
|
|||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { useAppStore } from '@/stores/appStore';
|
import { useAppStore } from '@/stores/appStore';
|
||||||
import KanjiSvgViewer from '@/components/kanji/KanjiSvgViewer.vue';
|
import KanjiSvgViewer from '@/components/kanji/KanjiSvgViewer.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
@@ -184,6 +185,10 @@ const accuracyStats = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.fetchCollection();
|
await store.fetchCollection();
|
||||||
|
// Pre-fill search from query param (e.g. navigated from Vocabularies component kanji)
|
||||||
|
if (route.query.search) {
|
||||||
|
searchQuery.value = route.query.search;
|
||||||
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
elevation="8"
|
elevation="8"
|
||||||
)
|
)
|
||||||
|
|
||||||
.mb-4.d-flex.gap-2
|
.mb-2.d-flex.gap-2
|
||||||
v-btn.text-caption.font-weight-bold.opacity-80(
|
v-btn.text-caption.font-weight-bold.opacity-80(
|
||||||
variant="text"
|
variant="text"
|
||||||
color="amber-lighten-1"
|
color="amber-lighten-1"
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
v-sheet.d-flex.flex-column.align-center.justify-center(
|
v-sheet.d-flex.flex-column.align-center.justify-center(
|
||||||
width="100%"
|
width="100%"
|
||||||
min-height="80"
|
min-height="56"
|
||||||
color="transparent"
|
color="transparent"
|
||||||
)
|
)
|
||||||
transition(name="fade-slide" mode="out-in")
|
transition(name="fade-slide" mode="out-in")
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
ScrambleText(:text="$t('review.' + statusCode)")
|
ScrambleText(:text="$t('review.' + statusCode)")
|
||||||
|
|
||||||
transition(name="scale")
|
transition(name="scale")
|
||||||
v-chip.mt-2.font-weight-bold.elevation-2(
|
v-chip.mt-1.font-weight-bold.elevation-2(
|
||||||
v-if="showNext && rankChange"
|
v-if="showNext && rankChange"
|
||||||
:color="rankChange.type === 'up' ? 'teal-accent-4' : 'red-darken-1'"
|
:color="rankChange.type === 'up' ? 'teal-accent-4' : 'red-darken-1'"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
v-icon(start size="14") {{ rankChange.type === 'up' ? 'mdi-arrow-up-thick' : 'mdi-arrow-down-thick' }}
|
v-icon(start size="14") {{ rankChange.type === 'up' ? 'mdi-arrow-up-thick' : 'mdi-arrow-down-thick' }}
|
||||||
| Level {{ currentItem.srsLevel }} → {{ rankChange.level }}
|
| Level {{ currentItem.srsLevel }} → {{ rankChange.level }}
|
||||||
|
|
||||||
v-progress-linear.mt-4.progress-bar(
|
v-progress-linear.mt-2.progress-bar(
|
||||||
v-model="progressPercent"
|
v-model="progressPercent"
|
||||||
color="primary"
|
color="primary"
|
||||||
height="4"
|
height="4"
|
||||||
@@ -402,4 +402,24 @@ const getStatusClass = (status) => {
|
|||||||
font-size: $font-sm;
|
font-size: $font-sm;
|
||||||
margin-left: $spacing-sm;
|
margin-left: $spacing-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Landscape optimizations (iPad, tablets)
|
||||||
|
@media (orientation: landscape) and (max-height: 900px) {
|
||||||
|
.review-view-container {
|
||||||
|
padding-top: $spacing-xs !important;
|
||||||
|
padding-bottom: $spacing-xs !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-card {
|
||||||
|
padding: $spacing-md !important;
|
||||||
|
|
||||||
|
.text-h3 {
|
||||||
|
font-size: $font-xl !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: $spacing-sm !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,622 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
v-container.vocab-page
|
||||||
|
|
||||||
|
//- Search + Filter toolbar
|
||||||
|
.vocab-toolbar
|
||||||
|
v-text-field.vocab-search(
|
||||||
|
v-model="searchQuery"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
:label="$t('vocabulary.searchLabel')"
|
||||||
|
:placeholder="$t('vocabulary.placeholder')"
|
||||||
|
variant="solo-filled"
|
||||||
|
density="comfortable"
|
||||||
|
bg-color="#2f3542"
|
||||||
|
color="white"
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
|
)
|
||||||
|
|
||||||
|
v-btn.vocab-filter-toggle(
|
||||||
|
:variant="showFilters ? 'flat' : 'tonal'"
|
||||||
|
:color="hasActiveFilters ? '#00cec9' : 'grey'"
|
||||||
|
@click="showFilters = !showFilters"
|
||||||
|
)
|
||||||
|
v-icon(start) mdi-filter-variant
|
||||||
|
| {{ $t('vocabulary.filters') }}
|
||||||
|
v-badge.ml-2(
|
||||||
|
v-if="activeFilterCount > 0"
|
||||||
|
:content="activeFilterCount"
|
||||||
|
color="#00cec9"
|
||||||
|
inline
|
||||||
|
)
|
||||||
|
|
||||||
|
//- Active filter chips (always visible when filters active)
|
||||||
|
.vocab-active-filters(v-if="activeFilterLabels.length > 0 && !showFilters")
|
||||||
|
v-chip.mr-1.mb-1(
|
||||||
|
v-for="f in activeFilterLabels"
|
||||||
|
:key="f.key + f.value"
|
||||||
|
size="small"
|
||||||
|
color="#00cec9"
|
||||||
|
variant="tonal"
|
||||||
|
closable
|
||||||
|
@click:close="removeFilter(f.key, f.value)"
|
||||||
|
) {{ f.label }}
|
||||||
|
v-btn.ml-1(
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
color="grey"
|
||||||
|
@click="clearAllFilters"
|
||||||
|
) {{ $t('vocabulary.clearAll') }}
|
||||||
|
|
||||||
|
//- Filter panel
|
||||||
|
v-expand-transition
|
||||||
|
.vocab-filter-panel(v-show="showFilters")
|
||||||
|
.filter-section
|
||||||
|
.filter-label {{ $t('vocabulary.filterType') }}
|
||||||
|
.filter-chips
|
||||||
|
v-chip(
|
||||||
|
v-for="t in TYPE_OPTIONS"
|
||||||
|
:key="t.value"
|
||||||
|
size="small"
|
||||||
|
:variant="filterTypes.includes(t.value) ? 'flat' : 'outlined'"
|
||||||
|
:color="filterTypes.includes(t.value) ? '#00cec9' : 'grey'"
|
||||||
|
@click="toggleFilter('types', t.value)"
|
||||||
|
) {{ t.label }}
|
||||||
|
|
||||||
|
.filter-section
|
||||||
|
.filter-label {{ $t('vocabulary.filterVerbClass') }}
|
||||||
|
.filter-chips
|
||||||
|
v-chip(
|
||||||
|
v-for="t in VERB_CLASS_OPTIONS"
|
||||||
|
:key="t.value"
|
||||||
|
size="small"
|
||||||
|
:variant="filterVerbClass.includes(t.value) ? 'flat' : 'outlined'"
|
||||||
|
:color="filterVerbClass.includes(t.value) ? '#00cec9' : 'grey'"
|
||||||
|
@click="toggleFilter('verbClass', t.value)"
|
||||||
|
) {{ t.label }}
|
||||||
|
|
||||||
|
.filter-section
|
||||||
|
.filter-label {{ $t('vocabulary.filterTransitivity') }}
|
||||||
|
.filter-chips
|
||||||
|
v-chip(
|
||||||
|
v-for="t in TRANSITIVITY_OPTIONS"
|
||||||
|
:key="t.value"
|
||||||
|
size="small"
|
||||||
|
:variant="filterTransitivity.includes(t.value) ? 'flat' : 'outlined'"
|
||||||
|
:color="filterTransitivity.includes(t.value) ? '#00cec9' : 'grey'"
|
||||||
|
@click="toggleFilter('transitivity', t.value)"
|
||||||
|
) {{ t.label }}
|
||||||
|
|
||||||
|
.filter-section
|
||||||
|
.filter-label {{ $t('vocabulary.filterEnding') }}
|
||||||
|
.filter-chips
|
||||||
|
v-chip(
|
||||||
|
v-for="t in ENDING_OPTIONS"
|
||||||
|
:key="t.value"
|
||||||
|
size="small"
|
||||||
|
:variant="filterEndings.includes(t.value) ? 'flat' : 'outlined'"
|
||||||
|
:color="filterEndings.includes(t.value) ? '#00cec9' : 'grey'"
|
||||||
|
@click="toggleFilter('endings', t.value)"
|
||||||
|
) {{ t.label }}
|
||||||
|
|
||||||
|
.filter-section
|
||||||
|
.filter-label {{ $t('vocabulary.filterLength') }}
|
||||||
|
.filter-chips
|
||||||
|
v-chip(
|
||||||
|
v-for="t in LENGTH_OPTIONS"
|
||||||
|
:key="t.value"
|
||||||
|
size="small"
|
||||||
|
:variant="filterLength.includes(t.value) ? 'flat' : 'outlined'"
|
||||||
|
:color="filterLength.includes(t.value) ? '#00cec9' : 'grey'"
|
||||||
|
@click="toggleFilter('length', t.value)"
|
||||||
|
) {{ t.label }}
|
||||||
|
|
||||||
|
.filter-actions
|
||||||
|
.text-caption.text-grey {{ $t('vocabulary.matchCount', { n: filteredVocabularies.length }) }}
|
||||||
|
v-btn(
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="grey"
|
||||||
|
@click="clearAllFilters"
|
||||||
|
) {{ $t('vocabulary.clearAll') }}
|
||||||
|
|
||||||
|
//- Loading
|
||||||
|
.text-center.mt-10(v-if="loading")
|
||||||
|
v-progress-circular(indeterminate color="primary" size="48")
|
||||||
|
.text-body-2.text-grey.mt-4
|
||||||
|
ScrambleText(:text="$t('vocabulary.loading')")
|
||||||
|
|
||||||
|
//- Empty state
|
||||||
|
.text-center.mt-10.text-grey-lighten-1(
|
||||||
|
v-else-if="!loading && Object.keys(groupedItems).length === 0"
|
||||||
|
)
|
||||||
|
v-icon.mb-4(size="64" color="grey-darken-2") mdi-text-search-variant
|
||||||
|
.text-h6
|
||||||
|
ScrambleText(:text="$t('vocabulary.noMatches')")
|
||||||
|
.text-body-2
|
||||||
|
ScrambleText(:text="$t('vocabulary.tryDifferent')")
|
||||||
|
|
||||||
|
//- Vocabulary grid
|
||||||
|
.mb-8.fade-slide-up(
|
||||||
|
v-else
|
||||||
|
v-for="(group, level) in groupedItems"
|
||||||
|
:key="level"
|
||||||
|
)
|
||||||
|
.vocab-level-header
|
||||||
|
.vocab-level-badge {{ $t('vocabulary.levelHeader') }} {{ level }}
|
||||||
|
.vocab-level-line
|
||||||
|
.vocab-level-count {{ group.length }}
|
||||||
|
|
||||||
|
.vocab-grid
|
||||||
|
.vocab-card(
|
||||||
|
v-for="item in group"
|
||||||
|
:key="item._id"
|
||||||
|
@click="openDetail(item)"
|
||||||
|
)
|
||||||
|
.vc-top
|
||||||
|
.vc-char {{ item.characters }}
|
||||||
|
.vc-reading {{ getPrimaryReading(item) }}
|
||||||
|
.vc-bottom
|
||||||
|
.vc-meaning {{ item.meanings[0] }}
|
||||||
|
|
||||||
|
//- Detail modal
|
||||||
|
v-dialog(
|
||||||
|
v-model="showModal"
|
||||||
|
max-width="480"
|
||||||
|
transition="dialog-bottom-transition"
|
||||||
|
)
|
||||||
|
v-card.pa-5.pt-6.rounded-xl.border-subtle.vocab-detail-card(color="#1e1e24")
|
||||||
|
.d-flex.justify-space-between.align-center.px-2.mb-2
|
||||||
|
.text-caption.text-grey
|
||||||
|
ScrambleText(:text="$t('vocabulary.levelLabel') + ' ' + selectedItem?.level")
|
||||||
|
.d-flex.flex-wrap.gap-2
|
||||||
|
v-chip(
|
||||||
|
v-for="pos in selectedItem?.partsOfSpeech"
|
||||||
|
:key="pos"
|
||||||
|
size="x-small"
|
||||||
|
color="#00cec9"
|
||||||
|
variant="tonal"
|
||||||
|
) {{ formatPOS(pos) }}
|
||||||
|
|
||||||
|
.text-h3.font-weight-bold.text-center.mb-2.vocab-detail-char {{ selectedItem?.characters }}
|
||||||
|
|
||||||
|
.d-flex.flex-wrap.gap-2.justify-center.mb-4
|
||||||
|
v-chip(
|
||||||
|
v-for="(m, idx) in selectedItem?.meanings"
|
||||||
|
:key="idx"
|
||||||
|
:color="idx === 0 ? '#00cec9' : 'grey'"
|
||||||
|
:variant="idx === 0 ? 'flat' : 'outlined'"
|
||||||
|
size="small"
|
||||||
|
) {{ m }}
|
||||||
|
|
||||||
|
.vocab-readings-container.mb-4
|
||||||
|
.vocab-reading-group(v-for="r in selectedItem?.readings" :key="r.reading")
|
||||||
|
.vocab-reading-value(:class="{ 'is-primary': r.primary }") {{ r.reading }}
|
||||||
|
v-chip.ml-2(
|
||||||
|
v-if="r.primary"
|
||||||
|
size="x-small"
|
||||||
|
color="#00cec9"
|
||||||
|
variant="tonal"
|
||||||
|
)
|
||||||
|
ScrambleText(:text="$t('vocabulary.primary')")
|
||||||
|
|
||||||
|
.vocab-audio-row.mb-5
|
||||||
|
v-btn.vocab-audio-btn(
|
||||||
|
variant="tonal"
|
||||||
|
:color="hasMaleAudio ? '#00cec9' : 'grey-darken-2'"
|
||||||
|
:loading="audioPlayingGender === 'male'"
|
||||||
|
:disabled="!hasMaleAudio"
|
||||||
|
size="small"
|
||||||
|
prepend-icon="mdi-account"
|
||||||
|
@click="playAudio('male')"
|
||||||
|
)
|
||||||
|
| {{ $t('vocabulary.listenMale') }}
|
||||||
|
v-btn.vocab-audio-btn(
|
||||||
|
variant="tonal"
|
||||||
|
:color="hasFemaleAudio ? '#00cec9' : 'grey-darken-2'"
|
||||||
|
:loading="audioPlayingGender === 'female'"
|
||||||
|
:disabled="!hasFemaleAudio"
|
||||||
|
size="small"
|
||||||
|
prepend-icon="mdi-account-outline"
|
||||||
|
@click="playAudio('female')"
|
||||||
|
)
|
||||||
|
| {{ $t('vocabulary.listenFemale') }}
|
||||||
|
|
||||||
|
.vocab-sentences-section.mb-5(v-if="selectedItem?.contextSentences?.length")
|
||||||
|
.text-caption.text-grey-darken-1.text-uppercase.mb-3.font-weight-bold
|
||||||
|
ScrambleText(:text="$t('vocabulary.exampleSentences')")
|
||||||
|
.vocab-sentence(v-for="(s, idx) in selectedItem.contextSentences" :key="idx")
|
||||||
|
.vocab-sentence-ja {{ s.ja }}
|
||||||
|
.vocab-sentence-en {{ s.en }}
|
||||||
|
|
||||||
|
//- Collapsible mnemonics
|
||||||
|
.vocab-collapsible.mb-5(
|
||||||
|
v-if="selectedItem?.meaningMnemonic || selectedItem?.readingMnemonic"
|
||||||
|
)
|
||||||
|
v-btn.vocab-collapse-toggle(
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="grey"
|
||||||
|
block
|
||||||
|
:prepend-icon="showMnemonics ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||||
|
@click="showMnemonics = !showMnemonics"
|
||||||
|
)
|
||||||
|
ScrambleText(:text="$t('vocabulary.mnemonics')")
|
||||||
|
|
||||||
|
v-expand-transition
|
||||||
|
.vocab-collapse-content(v-show="showMnemonics")
|
||||||
|
.vocab-mnemonic-section.mb-3(v-if="selectedItem?.meaningMnemonic")
|
||||||
|
.text-caption.text-grey-darken-1.text-uppercase.mb-2.font-weight-bold
|
||||||
|
ScrambleText(:text="$t('vocabulary.meaningMnemonic')")
|
||||||
|
.text-body-2.text-grey-lighten-1.mnemonic-text {{ selectedItem.meaningMnemonic }}
|
||||||
|
|
||||||
|
.vocab-mnemonic-section(v-if="selectedItem?.readingMnemonic")
|
||||||
|
.text-caption.text-grey-darken-1.text-uppercase.mb-2.font-weight-bold
|
||||||
|
ScrambleText(:text="$t('vocabulary.readingMnemonic')")
|
||||||
|
.text-body-2.text-grey-lighten-1.mnemonic-text {{ selectedItem.readingMnemonic }}
|
||||||
|
|
||||||
|
.vocab-components-section.mb-5(v-if="componentKanji.length > 0")
|
||||||
|
.text-caption.text-grey-darken-1.text-uppercase.mb-2.font-weight-bold
|
||||||
|
ScrambleText(:text="$t('vocabulary.components')")
|
||||||
|
.d-flex.flex-wrap.gap-2.justify-center
|
||||||
|
v-chip(
|
||||||
|
v-for="k in componentKanji"
|
||||||
|
:key="k._id || k.wkSubjectId"
|
||||||
|
variant="outlined"
|
||||||
|
color="grey-lighten-1"
|
||||||
|
@click.stop="navigateToKanji(k)"
|
||||||
|
)
|
||||||
|
span.font-weight-bold.mr-1 {{ k.char }}
|
||||||
|
span.text-caption.text-grey {{ k.meaning }}
|
||||||
|
|
||||||
|
v-btn.text-white.mt-2(
|
||||||
|
block
|
||||||
|
color="#2f3542"
|
||||||
|
@click="showModal = false"
|
||||||
|
)
|
||||||
|
ScrambleText(:text="$t('vocabulary.close')")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useAppStore } from '@/stores/appStore';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useAppStore();
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const showModal = ref(false);
|
||||||
|
const selectedItem = ref(null);
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const audioPlayingGender = ref(null);
|
||||||
|
const showFilters = ref(false);
|
||||||
|
const showMnemonics = ref(false);
|
||||||
|
|
||||||
|
let currentAudio = null;
|
||||||
|
|
||||||
|
// ── Filter definitions ──────────────────────────────────────
|
||||||
|
|
||||||
|
const TYPE_OPTIONS = [
|
||||||
|
{ value: 'noun', label: 'Noun' },
|
||||||
|
{ value: 'verb', label: 'Verb' },
|
||||||
|
{ value: 'i_adjective', label: 'い Adj.' },
|
||||||
|
{ value: 'na_adjective', label: 'な Adj.' },
|
||||||
|
{ value: 'adverb', label: 'Adverb' },
|
||||||
|
{ value: 'numeral', label: 'Numeral' },
|
||||||
|
{ value: 'counter', label: 'Counter' },
|
||||||
|
{ value: 'suffix', label: 'Suffix' },
|
||||||
|
{ value: 'prefix', label: 'Prefix' },
|
||||||
|
{ value: 'expression', label: 'Expression' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const VERB_CLASS_OPTIONS = [
|
||||||
|
{ value: 'godan_verb', label: 'Godan (五段)' },
|
||||||
|
{ value: 'ichidan_verb', label: 'Ichidan (一段)' },
|
||||||
|
{ value: 'suru_verb', label: 'Suru (する)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TRANSITIVITY_OPTIONS = [
|
||||||
|
{ value: 'transitive_verb', label: 'Transitive' },
|
||||||
|
{ value: 'intransitive_verb', label: 'Intransitive' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ENDING_OPTIONS = [
|
||||||
|
{ value: 'eru', label: '〜える' },
|
||||||
|
{ value: 'iru', label: '〜いる' },
|
||||||
|
{ value: 'aru', label: '〜ある' },
|
||||||
|
{ value: 'uru', label: '〜うる' },
|
||||||
|
{ value: 'oru', label: '〜おる' },
|
||||||
|
{ value: 'su', label: '〜す' },
|
||||||
|
{ value: 'ku', label: '〜く' },
|
||||||
|
{ value: 'gu', label: '〜ぐ' },
|
||||||
|
{ value: 'mu', label: '〜む' },
|
||||||
|
{ value: 'tsu', label: '〜つ' },
|
||||||
|
{ value: 'nu', label: '〜ぬ' },
|
||||||
|
{ value: 'bu', label: '〜ぶ' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LENGTH_OPTIONS = [
|
||||||
|
{ value: '1', label: '1 Char' },
|
||||||
|
{ value: '2', label: '2 Char' },
|
||||||
|
{ value: '3', label: '3 Char' },
|
||||||
|
{ value: '4+', label: '4+ Char' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Use individual refs for reliable Vue reactivity tracking
|
||||||
|
const filterTypes = ref([]);
|
||||||
|
const filterVerbClass = ref([]);
|
||||||
|
const filterTransitivity = ref([]);
|
||||||
|
const filterEndings = ref([]);
|
||||||
|
const filterLength = ref([]);
|
||||||
|
|
||||||
|
const filterRefs = {
|
||||||
|
types: filterTypes,
|
||||||
|
verbClass: filterVerbClass,
|
||||||
|
transitivity: filterTransitivity,
|
||||||
|
endings: filterEndings,
|
||||||
|
length: filterLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENDING_MAP = {
|
||||||
|
eru: ['える', 'エル'],
|
||||||
|
iru: ['いる', 'イル'],
|
||||||
|
aru: ['ある', 'アル'],
|
||||||
|
uru: ['うる', 'ウル'],
|
||||||
|
oru: ['おる', 'オル'],
|
||||||
|
su: ['す', 'ス'],
|
||||||
|
ku: ['く', 'ク'],
|
||||||
|
gu: ['ぐ', 'グ'],
|
||||||
|
mu: ['む', 'ム'],
|
||||||
|
tsu: ['つ', 'ツ'],
|
||||||
|
nu: ['ぬ', 'ヌ'],
|
||||||
|
bu: ['ぶ', 'ブ'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Filter actions ──────────────────────────────────────────
|
||||||
|
|
||||||
|
const toggleFilter = (key, value) => {
|
||||||
|
const r = filterRefs[key];
|
||||||
|
const idx = r.value.indexOf(value);
|
||||||
|
if (idx >= 0) {
|
||||||
|
r.value = r.value.filter((v) => v !== value);
|
||||||
|
} else {
|
||||||
|
r.value = [...r.value, value];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFilter = (key, value) => {
|
||||||
|
const r = filterRefs[key];
|
||||||
|
r.value = r.value.filter((v) => v !== value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAllFilters = () => {
|
||||||
|
filterTypes.value = [];
|
||||||
|
filterVerbClass.value = [];
|
||||||
|
filterTransitivity.value = [];
|
||||||
|
filterEndings.value = [];
|
||||||
|
filterLength.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Filter computeds ────────────────────────────────────────
|
||||||
|
|
||||||
|
const activeFilterCount = computed(() => (
|
||||||
|
filterTypes.value.length
|
||||||
|
+ filterVerbClass.value.length
|
||||||
|
+ filterTransitivity.value.length
|
||||||
|
+ filterEndings.value.length
|
||||||
|
+ filterLength.value.length
|
||||||
|
));
|
||||||
|
|
||||||
|
const hasActiveFilters = computed(() => activeFilterCount.value > 0);
|
||||||
|
|
||||||
|
const activeFilterLabels = computed(() => {
|
||||||
|
const labels = [];
|
||||||
|
const addLabels = (key, arr, options) => {
|
||||||
|
arr.forEach((v) => {
|
||||||
|
const opt = options.find((o) => o.value === v);
|
||||||
|
if (opt) labels.push({ key, value: v, label: opt.label });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addLabels('types', filterTypes.value, TYPE_OPTIONS);
|
||||||
|
addLabels('verbClass', filterVerbClass.value, VERB_CLASS_OPTIONS);
|
||||||
|
addLabels('transitivity', filterTransitivity.value, TRANSITIVITY_OPTIONS);
|
||||||
|
addLabels('endings', filterEndings.value, ENDING_OPTIONS);
|
||||||
|
addLabels('length', filterLength.value, LENGTH_OPTIONS);
|
||||||
|
return labels;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Helpers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const getPrimaryReading = (item) => {
|
||||||
|
if (!item.readings?.length) return '';
|
||||||
|
const primary = item.readings.find((r) => r.primary);
|
||||||
|
return primary ? primary.reading : item.readings[0].reading;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAudiosByGender = (gender) => {
|
||||||
|
const audios = selectedItem.value?.pronunciationAudios || [];
|
||||||
|
return audios.filter((a) => a.gender === gender);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasMaleAudio = computed(() => getAudiosByGender('male').length > 0);
|
||||||
|
const hasFemaleAudio = computed(() => getAudiosByGender('female').length > 0);
|
||||||
|
|
||||||
|
// ── Data loading ────────────────────────────────────────────
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await store.fetchVocabularies();
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Filtered data ───────────────────────────────────────────
|
||||||
|
|
||||||
|
const filteredVocabularies = computed(() => {
|
||||||
|
let items = store.vocabularies;
|
||||||
|
|
||||||
|
// Text search
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const q = searchQuery.value.toLowerCase().trim();
|
||||||
|
items = items.filter((item) => {
|
||||||
|
if (item.characters && item.characters.includes(q)) return true;
|
||||||
|
if (item.meanings && item.meanings.some((m) => m.toLowerCase().includes(q))) return true;
|
||||||
|
if (item.readings && item.readings.some((r) => r.reading.includes(q))) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all filter refs explicitly so Vue tracks them
|
||||||
|
const fTypes = filterTypes.value;
|
||||||
|
const fVerbClass = filterVerbClass.value;
|
||||||
|
const fTransitivity = filterTransitivity.value;
|
||||||
|
const fEndings = filterEndings.value;
|
||||||
|
const fLength = filterLength.value;
|
||||||
|
|
||||||
|
if (fTypes.length + fVerbClass.length + fTransitivity.length + fEndings.length + fLength.length === 0) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.filter((item) => {
|
||||||
|
const pos = item.partsOfSpeech || [];
|
||||||
|
// Normalize: convert spaces to underscores to match filter values
|
||||||
|
const normalizedPos = pos.map((p) => p.replace(/\s+/g, '_'));
|
||||||
|
|
||||||
|
// Type filter (OR within category)
|
||||||
|
if (fTypes.length > 0) {
|
||||||
|
const typeMatch = fTypes.some((ft) => {
|
||||||
|
if (ft === 'verb') return normalizedPos.some((p) => p.includes('verb'));
|
||||||
|
return normalizedPos.includes(ft);
|
||||||
|
});
|
||||||
|
if (!typeMatch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verb class filter
|
||||||
|
if (fVerbClass.length > 0) {
|
||||||
|
if (!fVerbClass.some((v) => normalizedPos.includes(v))) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transitivity filter
|
||||||
|
if (fTransitivity.length > 0) {
|
||||||
|
if (!fTransitivity.some((v) => normalizedPos.includes(v))) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ending filter
|
||||||
|
if (fEndings.length > 0) {
|
||||||
|
const reading = getPrimaryReading(item);
|
||||||
|
const endingMatch = fEndings.some((e) => {
|
||||||
|
const suffixes = ENDING_MAP[e] || [];
|
||||||
|
return suffixes.some((s) => reading.endsWith(s));
|
||||||
|
});
|
||||||
|
if (!endingMatch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length filter
|
||||||
|
if (fLength.length > 0) {
|
||||||
|
const len = (item.characters || '').length;
|
||||||
|
const lenMatch = fLength.some((l) => {
|
||||||
|
if (l === '1') return len === 1;
|
||||||
|
if (l === '2') return len === 2;
|
||||||
|
if (l === '3') return len === 3;
|
||||||
|
if (l === '4+') return len >= 4;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (!lenMatch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedItems = computed(() => {
|
||||||
|
const groups = {};
|
||||||
|
filteredVocabularies.value.forEach((i) => {
|
||||||
|
if (!groups[i.level]) groups[i.level] = [];
|
||||||
|
groups[i.level].push(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = {};
|
||||||
|
Object.keys(groups)
|
||||||
|
.sort((a, b) => parseInt(a) - parseInt(b))
|
||||||
|
.forEach((k) => { sorted[k] = groups[k]; });
|
||||||
|
return sorted;
|
||||||
|
});
|
||||||
|
|
||||||
|
const componentKanji = computed(() => {
|
||||||
|
if (!selectedItem.value?.componentSubjectIds?.length) return [];
|
||||||
|
return selectedItem.value.componentSubjectIds
|
||||||
|
.map((id) => store.collection.find((k) => k.wkSubjectId === id))
|
||||||
|
.filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Actions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const openDetail = (item) => {
|
||||||
|
selectedItem.value = item;
|
||||||
|
showMnemonics.value = false;
|
||||||
|
showModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const playAudio = (gender) => {
|
||||||
|
const audios = getAudiosByGender(gender);
|
||||||
|
if (audios.length === 0) return;
|
||||||
|
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pick = audios[Math.floor(Math.random() * audios.length)];
|
||||||
|
|
||||||
|
audioPlayingGender.value = gender;
|
||||||
|
currentAudio = new Audio(pick.url);
|
||||||
|
|
||||||
|
currentAudio.addEventListener('ended', () => {
|
||||||
|
audioPlayingGender.value = null;
|
||||||
|
currentAudio = null;
|
||||||
|
});
|
||||||
|
currentAudio.addEventListener('error', () => {
|
||||||
|
audioPlayingGender.value = null;
|
||||||
|
currentAudio = null;
|
||||||
|
});
|
||||||
|
currentAudio.play().catch(() => {
|
||||||
|
audioPlayingGender.value = null;
|
||||||
|
currentAudio = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToKanji = (kanji) => {
|
||||||
|
showModal.value = false;
|
||||||
|
router.push({ path: '/collection', query: { search: kanji.char } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize POS: handle both "na adjective" and "na_adjective"
|
||||||
|
const formatPOS = (pos) => pos
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.split(' ')
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
const formatPOSShort = (pos) => {
|
||||||
|
const normalized = pos.replace(/\s+/g, '_');
|
||||||
|
const POS_SHORT = {
|
||||||
|
noun: 'N',
|
||||||
|
transitive_verb: 'Vt',
|
||||||
|
intransitive_verb: 'Vi',
|
||||||
|
godan_verb: '五',
|
||||||
|
ichidan_verb: '一',
|
||||||
|
suru_verb: 'する',
|
||||||
|
i_adjective: 'い',
|
||||||
|
na_adjective: 'な',
|
||||||
|
adverb: 'Adv',
|
||||||
|
numeral: '#',
|
||||||
|
counter: 'Ctr',
|
||||||
|
suffix: 'Sfx',
|
||||||
|
prefix: 'Pfx',
|
||||||
|
expression: 'Exp',
|
||||||
|
};
|
||||||
|
return POS_SHORT[normalized] || pos.charAt(0).toUpperCase();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -11,23 +11,37 @@ services:
|
|||||||
- zen-network
|
- zen-network
|
||||||
|
|
||||||
server:
|
server:
|
||||||
depends_on:
|
build:
|
||||||
- mongo
|
context: ./server
|
||||||
environment:
|
container_name: zen_server
|
||||||
- MONGO_URI=mongodb://mongo:27017/zenkanji
|
|
||||||
volumes:
|
|
||||||
- ./server:/app
|
|
||||||
- /app/node_modules
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
command: npm run dev
|
env_file:
|
||||||
|
- ./server/.env
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
networks:
|
||||||
|
- zen-network
|
||||||
|
|
||||||
client:
|
client:
|
||||||
build:
|
build:
|
||||||
|
context: ./client
|
||||||
target: dev-stage
|
target: dev-stage
|
||||||
|
container_name: zen_client
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
|
env_file:
|
||||||
|
- ./client/.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./client:/app
|
- ./client/src:/app/src
|
||||||
- /app/node_modules
|
depends_on:
|
||||||
command: npm run dev -- --host
|
- server
|
||||||
|
networks:
|
||||||
|
- zen-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
zen-network:
|
||||||
|
driver: bridge
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "zen-kanji",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||