some build stuff and quick fix

This commit is contained in:
Rene Kievits
2025-10-29 02:47:41 +01:00
parent d2070cd6b8
commit 6785bb9133
8 changed files with 202 additions and 182 deletions

3
.gitignore vendored
View File

@@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
*.jks
gradle.properties

View File

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

View File

@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="Hirameki SRS">
android:label="Hirameki SRS"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

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

View File

@@ -20,7 +20,7 @@ class WkApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WaniKani SRS',
title: 'Hirameki SRS',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(useMaterial3: true),
home: const StartScreen(),

View File

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

View File

@@ -1,13 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import '../models/kanji_item.dart';
import '../api/wk_client.dart';
class DeckRepository with ChangeNotifier {
class DeckRepository {
Database? _db;
String? _apiKey;
@@ -28,39 +27,50 @@ class DeckRepository with ChangeNotifier {
version: 7,
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, level INTEGER, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)''');
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, level INTEGER, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)''',
);
await db.execute(
'''CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)''');
'''CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)''',
);
await db.execute(
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''');
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''',
);
await db.execute(
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, level INTEGER, characters TEXT, meanings TEXT, readings TEXT, pronunciation_audios TEXT)''');
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, level INTEGER, characters TEXT, meanings TEXT, readings TEXT, pronunciation_audios TEXT)''',
);
await db.execute(
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''');
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''',
);
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute(
'''CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)''');
'''CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)''',
);
}
if (oldVersion < 3) {
// Migration from version 2 to 3 was flawed, so we just drop the columns if they exist
}
if (oldVersion < 4) {
await db.execute(
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''');
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''',
);
// We are not migrating the old srs data, as it was not mode-specific.
// Old columns will be dropped.
}
if (oldVersion < 5) {
await db.execute(
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, readings TEXT)''');
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, readings TEXT)''',
);
await db.execute(
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''');
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''',
);
}
if (oldVersion < 6) {
try {
await db.execute('ALTER TABLE vocabulary ADD COLUMN pronunciation_audios TEXT');
await db.execute(
'ALTER TABLE vocabulary ADD COLUMN pronunciation_audios TEXT',
);
} catch (_) {
// Ignore error, column might already exist
}
@@ -81,17 +91,19 @@ class DeckRepository with ChangeNotifier {
Future<void> saveApiKey(String apiKey) async {
final db = await _openDb();
await db.insert(
'settings',
{'key': 'apiKey', 'value': apiKey},
conflictAlgorithm: ConflictAlgorithm.replace,
);
await db.insert('settings', {
'key': 'apiKey',
'value': apiKey,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<String?> loadApiKey() async {
final db = await _openDb();
final rows =
await db.query('settings', where: 'key = ?', whereArgs: ['apiKey']);
final rows = await db.query(
'settings',
where: 'key = ?',
whereArgs: ['apiKey'],
);
if (rows.isNotEmpty) {
_apiKey = rows.first['value'] as String;
return _apiKey;
@@ -103,18 +115,14 @@ class DeckRepository with ChangeNotifier {
final db = await _openDb();
final batch = db.batch();
for (final it in items) {
batch.insert(
'kanji',
{
'id': it.id,
'level': it.level,
'characters': it.characters,
'meanings': it.meanings.join('|'),
'onyomi': it.onyomi.join('|'),
'kunyomi': it.kunyomi.join('|'),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
batch.insert('kanji', {
'id': it.id,
'level': it.level,
'characters': it.characters,
'meanings': it.meanings.join('|'),
'onyomi': it.onyomi.join('|'),
'kunyomi': it.kunyomi.join('|'),
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
await batch.commit(noResult: true);
}
@@ -123,23 +131,25 @@ class DeckRepository with ChangeNotifier {
final db = await _openDb();
final rows = await db.query('kanji');
final kanjiItems = rows
.map((r) => KanjiItem(
id: r['id'] as int,
level: r['level'] as int? ?? 0,
characters: r['characters'] as String,
meanings: (r['meanings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
onyomi: (r['onyomi'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
kunyomi: (r['kunyomi'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
))
.map(
(r) => KanjiItem(
id: r['id'] as int,
level: r['level'] as int? ?? 0,
characters: r['characters'] as String,
meanings: (r['meanings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
onyomi: (r['onyomi'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
kunyomi: (r['kunyomi'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
),
)
.toList();
for (final item in kanjiItems) {
@@ -155,11 +165,17 @@ class DeckRepository with ChangeNotifier {
Future<List<SrsItem>> getSrsItems(int kanjiId) async {
final db = await _openDb();
final rows = await db.query('srs_items', where: 'kanjiId = ?', whereArgs: [kanjiId]);
final rows = await db.query(
'srs_items',
where: 'kanjiId = ?',
whereArgs: [kanjiId],
);
return rows.map((r) {
return SrsItem(
kanjiId: r['kanjiId'] as int,
quizMode: QuizMode.values.firstWhere((e) => e.toString() == r['quizMode'] as String),
quizMode: QuizMode.values.firstWhere(
(e) => e.toString() == r['quizMode'] as String,
),
readingType: r['readingType'] as String?,
srsStage: r['srsStage'] as int,
lastAsked: DateTime.parse(r['lastAsked'] as String),
@@ -182,17 +198,13 @@ class DeckRepository with ChangeNotifier {
Future<void> insertSrsItem(SrsItem item) async {
final db = await _openDb();
await db.insert(
'srs_items',
{
'kanjiId': item.kanjiId,
'quizMode': item.quizMode.toString(),
'readingType': item.readingType,
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
await db.insert('srs_items', {
'kanjiId': item.kanjiId,
'quizMode': item.quizMode.toString(),
'readingType': item.readingType,
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<List<KanjiItem>> fetchAndCacheFromWk([String? apiKey]) async {
@@ -200,8 +212,9 @@ class DeckRepository with ChangeNotifier {
if (key == null) throw Exception('API key not set');
final client = WkClient(key);
final assignments =
await client.fetchAllAssignments(subjectTypes: ['kanji']);
final assignments = await client.fetchAllAssignments(
subjectTypes: ['kanji'],
);
final unlocked = <int>{};
for (final a in assignments) {
@@ -220,10 +233,12 @@ class DeckRepository with ChangeNotifier {
final subjects = await client.fetchSubjectsByIds(unlocked.toList());
final items = subjects
.where((s) =>
s['object'] == 'kanji' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'kanji'))
.where(
(s) =>
s['object'] == 'kanji' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'kanji'),
)
.map((s) => KanjiItem.fromSubject(s))
.where((k) => k.characters.isNotEmpty && k.meanings.isNotEmpty)
.toList();
@@ -234,11 +249,17 @@ class DeckRepository with ChangeNotifier {
Future<List<VocabSrsItem>> getVocabSrsItems(int vocabId) async {
final db = await _openDb();
final rows = await db.query('srs_vocab_items', where: 'vocabId = ?', whereArgs: [vocabId]);
final rows = await db.query(
'srs_vocab_items',
where: 'vocabId = ?',
whereArgs: [vocabId],
);
return rows.map((r) {
return VocabSrsItem(
vocabId: r['vocabId'] as int,
quizMode: VocabQuizMode.values.firstWhere((e) => e.toString() == r['quizMode'] as String),
quizMode: VocabQuizMode.values.firstWhere(
(e) => e.toString() == r['quizMode'] as String,
),
srsStage: r['srsStage'] as int,
lastAsked: DateTime.parse(r['lastAsked'] as String),
);
@@ -260,16 +281,12 @@ class DeckRepository with ChangeNotifier {
Future<void> insertVocabSrsItem(VocabSrsItem item) async {
final db = await _openDb();
await db.insert(
'srs_vocab_items',
{
'vocabId': item.vocabId,
'quizMode': item.quizMode.toString(),
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
await db.insert('srs_vocab_items', {
'vocabId': item.vocabId,
'quizMode': item.quizMode.toString(),
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<void> saveVocabulary(List<VocabularyItem> items) async {
@@ -279,18 +296,14 @@ class DeckRepository with ChangeNotifier {
final audios = it.pronunciationAudios
.map((a) => {'url': a.url, 'gender': a.gender})
.toList();
batch.insert(
'vocabulary',
{
'id': it.id,
'level': it.level,
'characters': it.characters,
'meanings': it.meanings.join('|'),
'readings': it.readings.join('|'),
'pronunciation_audios': jsonEncode(audios),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
batch.insert('vocabulary', {
'id': it.id,
'level': it.level,
'characters': it.characters,
'meanings': it.meanings.join('|'),
'readings': it.readings.join('|'),
'pronunciation_audios': jsonEncode(audios),
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
await batch.commit(noResult: true);
}
@@ -298,39 +311,39 @@ class DeckRepository with ChangeNotifier {
Future<List<VocabularyItem>> loadVocabulary() async {
final db = await _openDb();
final rows = await db.query('vocabulary');
final vocabItems = rows
.map((r) {
final audiosRaw = r['pronunciation_audios'] as String?;
final List<PronunciationAudio> audios = [];
if (audiosRaw != null && audiosRaw.isNotEmpty) {
try {
final decoded = jsonDecode(audiosRaw) as List;
for (final audioData in decoded) {
audios.add(PronunciationAudio(
url: audioData['url'] as String,
gender: audioData['gender'] as String,
));
}
} catch (e) {
// Error decoding, so we'll just have no audio for this item
}
final vocabItems = rows.map((r) {
final audiosRaw = r['pronunciation_audios'] as String?;
final List<PronunciationAudio> audios = [];
if (audiosRaw != null && audiosRaw.isNotEmpty) {
try {
final decoded = jsonDecode(audiosRaw) as List;
for (final audioData in decoded) {
audios.add(
PronunciationAudio(
url: audioData['url'] as String,
gender: audioData['gender'] as String,
),
);
}
return VocabularyItem(
id: r['id'] as int,
level: r['level'] as int? ?? 0,
characters: r['characters'] as String,
meanings: (r['meanings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
readings: (r['readings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
pronunciationAudios: audios,
);
})
.toList();
} catch (e) {
// Error decoding, so we'll just have no audio for this item
}
}
return VocabularyItem(
id: r['id'] as int,
level: r['level'] as int? ?? 0,
characters: r['characters'] as String,
meanings: (r['meanings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
readings: (r['readings'] as String)
.split('|')
.where((s) => s.isNotEmpty)
.toList(),
pronunciationAudios: audios,
);
}).toList();
for (final item in vocabItems) {
final srsItems = await getVocabSrsItems(item.id);
@@ -343,13 +356,16 @@ class DeckRepository with ChangeNotifier {
return vocabItems;
}
Future<List<VocabularyItem>> fetchAndCacheVocabularyFromWk([String? apiKey]) async {
Future<List<VocabularyItem>> fetchAndCacheVocabularyFromWk([
String? apiKey,
]) async {
final key = apiKey ?? _apiKey;
if (key == null) throw Exception('API key not set');
final client = WkClient(key);
final assignments =
await client.fetchAllAssignments(subjectTypes: ['vocabulary']);
final assignments = await client.fetchAllAssignments(
subjectTypes: ['vocabulary'],
);
final unlocked = <int>{};
for (final a in assignments) {
@@ -368,10 +384,12 @@ class DeckRepository with ChangeNotifier {
final subjects = await client.fetchSubjectsByIds(unlocked.toList());
final items = subjects
.where((s) =>
s['object'] == 'vocabulary' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'vocabulary'))
.where(
(s) =>
s['object'] == 'vocabulary' ||
(s['data'] != null &&
(s['data'] as Map)['object_type'] == 'vocabulary'),
)
.map((s) => VocabularyItem.fromSubject(s))
.where((k) => k.characters.isNotEmpty && k.meanings.isNotEmpty)
.toList();
@@ -379,4 +397,4 @@ class DeckRepository with ChangeNotifier {
await saveVocabulary(items);
return items;
}
}
}

View File

@@ -2,27 +2,27 @@ name: hirameki_srs
description: A simple and effective Spaced Repetition System (SRS) app for learning Japanese kanji and vocabulary.
version: 0.1.0+1
environment:
sdk: '>=3.3.0 <4.0.0'
sdk: ">=3.9.0 <4.0.0"
dependencies:
audioplayers: any
flutter:
sdk: flutter
shared_preferences: ^2.5.3
sqflite: ^2.4.2
path_provider: ^2.1.5
path: ^1.9.1
provider: ^6.1.5+1
http: ^1.5.0
audioplayers: ^6.0.0
http: any
path: any
path_provider: any
provider: any
shared_preferences: any
sqflite: any
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.5.0
test: ^1.26.2
build_runner: ^2.4.10
flutter_launcher_icons: ^0.14.4
flutter_lints: ^6.0.0
mockito: any
test: any
build_runner: any
flutter_launcher_icons: any
flutter_lints: any
flutter_icons:
android: true