Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Lückemeyer
VoCoach
Commits
044bb284
Commit
044bb284
authored
Apr 18, 2026
by
Lückemeyer
Browse files
Initial commit of claude ai assisted vocab trainer android app
parent
eab02a57
Changes
118
Hide whitespace changes
Inline
Side-by-side
app/src/main/java/dev/lueckemeyer/vocoach/db/entity/NounDetail.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.db.entity
;
import
androidx.room.ColumnInfo
;
import
androidx.room.Entity
;
import
androidx.room.ForeignKey
;
import
androidx.room.Index
;
import
androidx.room.PrimaryKey
;
@Entity
(
tableName
=
"noun_detail"
,
foreignKeys
=
@ForeignKey
(
entity
=
Card
.
class
,
parentColumns
=
"id"
,
childColumns
=
"card_id"
,
onDelete
=
ForeignKey
.
CASCADE
),
indices
=
@Index
(
value
=
"card_id"
,
unique
=
true
))
public
class
NounDetail
{
@PrimaryKey
(
autoGenerate
=
true
)
public
int
id
;
@ColumnInfo
(
name
=
"card_id"
)
public
int
cardId
;
public
String
gender
;
@ColumnInfo
(
name
=
"plural_form"
)
public
String
pluralForm
;
@ColumnInfo
(
name
=
"is_proper"
)
public
boolean
isProper
;
}
app/src/main/java/dev/lueckemeyer/vocoach/db/entity/Session.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.db.entity
;
import
androidx.room.ColumnInfo
;
import
androidx.room.Entity
;
import
androidx.room.PrimaryKey
;
@Entity
(
tableName
=
"session"
)
public
class
Session
{
@PrimaryKey
(
autoGenerate
=
true
)
public
int
id
;
@ColumnInfo
(
name
=
"started_at"
)
public
long
startedAt
;
@ColumnInfo
(
name
=
"ended_at"
)
public
Long
endedAt
;
public
String
mode
;
// "fr_to_native" | "native_to_fr"
}
app/src/main/java/dev/lueckemeyer/vocoach/db/entity/TryRecord.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.db.entity
;
import
androidx.room.ColumnInfo
;
import
androidx.room.Entity
;
import
androidx.room.ForeignKey
;
import
androidx.room.Index
;
import
androidx.room.PrimaryKey
;
@Entity
(
tableName
=
"try_record"
,
foreignKeys
=
{
@ForeignKey
(
entity
=
LeitnerState
.
class
,
parentColumns
=
"id"
,
childColumns
=
"leitner_state_id"
,
onDelete
=
ForeignKey
.
CASCADE
),
@ForeignKey
(
entity
=
Session
.
class
,
parentColumns
=
"id"
,
childColumns
=
"session_id"
,
onDelete
=
ForeignKey
.
SET_NULL
)
},
indices
=
{
@Index
(
"leitner_state_id"
),
@Index
(
"session_id"
)})
public
class
TryRecord
{
@PrimaryKey
(
autoGenerate
=
true
)
public
int
id
;
@ColumnInfo
(
name
=
"leitner_state_id"
)
public
int
leitnerStateId
;
public
String
direction
;
public
boolean
correct
;
@ColumnInfo
(
name
=
"responded_at"
)
public
long
respondedAt
;
@ColumnInfo
(
name
=
"session_id"
)
public
Integer
sessionId
;
}
app/src/main/java/dev/lueckemeyer/vocoach/db/entity/Unit.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.db.entity
;
import
androidx.room.ColumnInfo
;
import
androidx.room.Entity
;
import
androidx.room.ForeignKey
;
import
androidx.room.Index
;
import
androidx.room.PrimaryKey
;
@Entity
(
tableName
=
"unit"
,
foreignKeys
=
@ForeignKey
(
entity
=
Book
.
class
,
parentColumns
=
"id"
,
childColumns
=
"book_id"
,
onDelete
=
ForeignKey
.
CASCADE
),
indices
=
@Index
(
"book_id"
))
public
class
Unit
{
@PrimaryKey
(
autoGenerate
=
true
)
public
int
id
;
@ColumnInfo
(
name
=
"book_id"
)
public
int
bookId
;
public
int
position
;
public
String
title
;
}
app/src/main/java/dev/lueckemeyer/vocoach/db/entity/VerbDetail.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.db.entity
;
import
androidx.room.ColumnInfo
;
import
androidx.room.Entity
;
import
androidx.room.ForeignKey
;
import
androidx.room.Index
;
import
androidx.room.PrimaryKey
;
@Entity
(
tableName
=
"verb_detail"
,
foreignKeys
=
@ForeignKey
(
entity
=
Card
.
class
,
parentColumns
=
"id"
,
childColumns
=
"card_id"
,
onDelete
=
ForeignKey
.
CASCADE
),
indices
=
@Index
(
value
=
"card_id"
,
unique
=
true
))
public
class
VerbDetail
{
@PrimaryKey
(
autoGenerate
=
true
)
public
int
id
;
@ColumnInfo
(
name
=
"card_id"
)
public
int
cardId
;
public
String
infinitive
;
@ColumnInfo
(
name
=
"verb_group"
)
public
String
verbGroup
;
@ColumnInfo
(
name
=
"participe_passe"
)
public
String
participePasse
;
@ColumnInfo
(
name
=
"participe_present"
)
public
String
participePresent
;
public
String
auxiliary
;
@ColumnInfo
(
name
=
"is_reflexive"
)
public
boolean
isReflexive
;
@ColumnInfo
(
name
=
"is_irregular"
)
public
boolean
isIrregular
;
}
app/src/main/java/dev/lueckemeyer/vocoach/db/entity/Volet.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.db.entity
;
import
androidx.room.ColumnInfo
;
import
androidx.room.Entity
;
import
androidx.room.ForeignKey
;
import
androidx.room.Index
;
import
androidx.room.PrimaryKey
;
@Entity
(
tableName
=
"volet"
,
foreignKeys
=
@ForeignKey
(
entity
=
Unit
.
class
,
parentColumns
=
"id"
,
childColumns
=
"unit_id"
,
onDelete
=
ForeignKey
.
CASCADE
),
indices
=
@Index
(
"unit_id"
))
public
class
Volet
{
@PrimaryKey
(
autoGenerate
=
true
)
public
int
id
;
@ColumnInfo
(
name
=
"unit_id"
)
public
int
unitId
;
public
int
position
;
public
String
title
;
}
app/src/main/java/dev/lueckemeyer/vocoach/repository/TrainingCard.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.repository
;
import
dev.lueckemeyer.vocoach.db.entity.*
;
/** All data the training screen needs for one card. */
public
class
TrainingCard
{
public
LeitnerState
leitnerState
;
public
Card
card
;
public
NounDetail
nounDetail
;
public
VerbDetail
verbDetail
;
public
AdjectiveDetail
adjectiveDetail
;
public
AdverbDetail
adverbDetail
;
}
app/src/main/java/dev/lueckemeyer/vocoach/repository/TrainingRepository.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.repository
;
import
android.content.Context
;
import
android.content.SharedPreferences
;
import
java.util.HashSet
;
import
java.util.List
;
import
java.util.Set
;
import
java.util.concurrent.TimeUnit
;
import
dev.lueckemeyer.vocoach.db.VocabDatabase
;
import
dev.lueckemeyer.vocoach.db.dao.TryDao
;
import
dev.lueckemeyer.vocoach.db.entity.*
;
public
class
TrainingRepository
{
private
static
final
String
PREFS
=
"vocoach_prefs"
;
private
static
final
String
KEY_SESSION_FR
=
"session_id_fr_to_native"
;
private
static
final
String
KEY_SESSION_NAT
=
"session_id_native_to_fr"
;
private
static
final
String
KEY_PENDING_FR
=
"pending_state_fr_to_native"
;
private
static
final
String
KEY_PENDING_NAT
=
"pending_state_native_to_fr"
;
private
static
final
long
SESSION_TIMEOUT_MS
=
TimeUnit
.
HOURS
.
toMillis
(
1
);
private
final
VocabDatabase
db
;
private
final
SharedPreferences
prefs
;
public
TrainingRepository
(
Context
context
)
{
db
=
VocabDatabase
.
getInstance
(
context
);
prefs
=
context
.
getSharedPreferences
(
PREFS
,
Context
.
MODE_PRIVATE
);
}
// ── Session ───────────────────────────────────────────────────────────────
public
int
getOrCreateSession
(
String
direction
)
{
int
stored
=
prefs
.
getInt
(
sessionKey
(
direction
),
-
1
);
if
(
stored
!=
-
1
)
{
Session
s
=
db
.
sessionDao
().
getByIdSync
(
stored
);
if
(
s
!=
null
&&
s
.
endedAt
==
null
)
{
long
idleMs
=
System
.
currentTimeMillis
()
-
(
s
.
startedAt
*
1000L
);
if
(
idleMs
<
SESSION_TIMEOUT_MS
)
return
s
.
id
;
s
.
endedAt
=
System
.
currentTimeMillis
()
/
1000L
;
db
.
sessionDao
().
update
(
s
);
}
}
Session
s
=
new
Session
();
s
.
startedAt
=
System
.
currentTimeMillis
()
/
1000L
;
s
.
mode
=
direction
;
int
newId
=
(
int
)
db
.
sessionDao
().
insert
(
s
);
prefs
.
edit
().
putInt
(
sessionKey
(
direction
),
newId
).
apply
();
clearPending
(
direction
);
return
newId
;
}
public
void
touchSession
(
int
sessionId
)
{
Session
s
=
db
.
sessionDao
().
getByIdSync
(
sessionId
);
if
(
s
!=
null
)
{
s
.
startedAt
=
System
.
currentTimeMillis
()
/
1000L
;
db
.
sessionDao
().
update
(
s
);
}
}
// ── Pending card ──────────────────────────────────────────────────────────
public
void
savePending
(
String
direction
,
int
leitnerStateId
)
{
prefs
.
edit
().
putInt
(
pendingKey
(
direction
),
leitnerStateId
).
apply
();
}
public
void
clearPending
(
String
direction
)
{
prefs
.
edit
().
remove
(
pendingKey
(
direction
)).
apply
();
}
// ── Scheduling ────────────────────────────────────────────────────────────
public
TrainingCard
nextCard
(
String
direction
)
{
long
dayStart
=
todayStartEpochSeconds
();
// Pending card first
int
pendingId
=
prefs
.
getInt
(
pendingKey
(
direction
),
-
1
);
if
(
pendingId
!=
-
1
)
{
LeitnerState
ls
=
db
.
leitnerStateDao
().
getByIdSync
(
pendingId
);
clearPending
(
direction
);
if
(
ls
!=
null
)
return
buildCard
(
ls
);
}
List
<
Integer
>
attemptedIds
=
db
.
tryDao
().
getAttemptedTodaySync
(
direction
,
dayStart
);
Set
<
Integer
>
attempted
=
new
HashSet
<>(
attemptedIds
);
for
(
int
box
=
1
;
box
<=
5
;
box
++)
{
for
(
LeitnerState
ls
:
db
.
leitnerStateDao
().
getByBoxOrderedSync
(
direction
,
box
))
{
if
(!
attempted
.
contains
(
ls
.
id
))
return
buildCard
(
ls
);
}
}
attempted
=
new
HashSet
<>();
for
(
int
box
=
1
;
box
<=
5
;
box
++)
{
for
(
LeitnerState
ls
:
db
.
leitnerStateDao
().
getByBoxOrderedSync
(
direction
,
box
))
{
if
(!
attempted
.
contains
(
ls
.
id
))
return
buildCard
(
ls
);
}
}
return
null
;
}
public
void
recordResult
(
LeitnerState
state
,
boolean
correct
,
int
sessionId
)
{
state
.
boxNumber
=
correct
?
Math
.
min
(
state
.
boxNumber
+
1
,
6
)
:
Math
.
max
(
state
.
boxNumber
-
1
,
1
);
state
.
updatedAt
=
System
.
currentTimeMillis
()
/
1000L
;
db
.
leitnerStateDao
().
update
(
state
);
TryRecord
tr
=
new
TryRecord
();
tr
.
leitnerStateId
=
state
.
id
;
tr
.
direction
=
state
.
direction
;
tr
.
correct
=
correct
;
tr
.
respondedAt
=
System
.
currentTimeMillis
()
/
1000L
;
tr
.
sessionId
=
sessionId
;
db
.
tryDao
().
insert
(
tr
);
}
// ── Stats ──────────────────────────────────────────────────────────────────
public
List
<
TryDao
.
VoletStat
>
getVoletStats
()
{
return
db
.
tryDao
().
getVoletStatsSync
();
}
public
List
<
TryDao
.
DayStat
>
getDayStats
()
{
return
db
.
tryDao
().
getDayStatsSync
();
}
// ── Helpers ───────────────────────────────────────────────────────────────
private
TrainingCard
buildCard
(
LeitnerState
ls
)
{
TrainingCard
tc
=
new
TrainingCard
();
tc
.
leitnerState
=
ls
;
tc
.
card
=
db
.
cardDao
().
getByIdSync
(
ls
.
cardId
);
if
(
tc
.
card
==
null
)
return
null
;
switch
(
safe
(
tc
.
card
.
wordType
))
{
case
"noun"
:
tc
.
nounDetail
=
db
.
detailDao
().
getNounSync
(
ls
.
cardId
);
break
;
case
"verb"
:
tc
.
verbDetail
=
db
.
detailDao
().
getVerbSync
(
ls
.
cardId
);
break
;
case
"adjective"
:
tc
.
adjectiveDetail
=
db
.
detailDao
().
getAdjectiveSync
(
ls
.
cardId
);
break
;
case
"adverb"
:
tc
.
adverbDetail
=
db
.
detailDao
().
getAdverbSync
(
ls
.
cardId
);
break
;
}
return
tc
;
}
private
long
todayStartEpochSeconds
()
{
long
nowMs
=
System
.
currentTimeMillis
();
long
offsetMs
=
java
.
util
.
TimeZone
.
getDefault
().
getOffset
(
nowMs
);
long
dayMs
=
TimeUnit
.
DAYS
.
toMillis
(
1
);
return
((
nowMs
+
offsetMs
)
/
dayMs
)
*
dayMs
/
1000L
-
offsetMs
/
1000L
;
}
private
String
sessionKey
(
String
d
)
{
return
"fr_to_native"
.
equals
(
d
)
?
KEY_SESSION_FR
:
KEY_SESSION_NAT
;
}
private
String
pendingKey
(
String
d
)
{
return
"fr_to_native"
.
equals
(
d
)
?
KEY_PENDING_FR
:
KEY_PENDING_NAT
;
}
private
static
String
safe
(
String
s
)
{
return
s
!=
null
?
s
:
""
;
}
}
app/src/main/java/dev/lueckemeyer/vocoach/ui/home/HomeFragment.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.ui.home
;
import
android.os.Bundle
;
import
android.view.LayoutInflater
;
import
android.view.View
;
import
android.view.ViewGroup
;
import
androidx.annotation.NonNull
;
import
androidx.annotation.Nullable
;
import
androidx.fragment.app.Fragment
;
import
androidx.navigation.Navigation
;
import
dev.lueckemeyer.vocoach.R
;
import
dev.lueckemeyer.vocoach.databinding.FragmentHomeBinding
;
public
class
HomeFragment
extends
Fragment
{
private
FragmentHomeBinding
binding
;
@Override
public
View
onCreateView
(
@NonNull
LayoutInflater
inflater
,
ViewGroup
container
,
Bundle
savedInstanceState
)
{
binding
=
FragmentHomeBinding
.
inflate
(
inflater
,
container
,
false
);
return
binding
.
getRoot
();
}
@Override
public
void
onViewCreated
(
@NonNull
View
view
,
@Nullable
Bundle
savedInstanceState
)
{
super
.
onViewCreated
(
view
,
savedInstanceState
);
binding
.
btnFrToNative
.
setOnClickListener
(
v
->
{
Bundle
args
=
new
Bundle
();
args
.
putString
(
"direction"
,
"fr_to_native"
);
Navigation
.
findNavController
(
v
)
.
navigate
(
R
.
id
.
action_home_to_training
,
args
);
});
binding
.
btnNativeToFr
.
setOnClickListener
(
v
->
{
Bundle
args
=
new
Bundle
();
args
.
putString
(
"direction"
,
"native_to_fr"
);
Navigation
.
findNavController
(
v
)
.
navigate
(
R
.
id
.
action_home_to_training
,
args
);
});
binding
.
btnStats
.
setOnClickListener
(
v
->
Navigation
.
findNavController
(
v
)
.
navigate
(
R
.
id
.
action_home_to_stats
));
}
@Override
public
void
onDestroyView
()
{
super
.
onDestroyView
();
binding
=
null
;
}
}
app/src/main/java/dev/lueckemeyer/vocoach/ui/stats/StatsFragment.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.ui.stats
;
import
android.os.Bundle
;
import
android.view.*
;
import
android.widget.*
;
import
androidx.annotation.*
;
import
androidx.fragment.app.Fragment
;
import
java.util.List
;
import
java.util.concurrent.ExecutorService
;
import
java.util.concurrent.Executors
;
import
dev.lueckemeyer.vocoach.R
;
import
dev.lueckemeyer.vocoach.databinding.FragmentStatsBinding
;
import
dev.lueckemeyer.vocoach.db.dao.TryDao
;
import
dev.lueckemeyer.vocoach.repository.TrainingRepository
;
public
class
StatsFragment
extends
Fragment
{
private
FragmentStatsBinding
binding
;
private
TrainingRepository
repo
;
private
final
ExecutorService
exec
=
Executors
.
newSingleThreadExecutor
();
@Override
public
View
onCreateView
(
@NonNull
LayoutInflater
inflater
,
ViewGroup
container
,
Bundle
savedInstanceState
)
{
binding
=
FragmentStatsBinding
.
inflate
(
inflater
,
container
,
false
);
return
binding
.
getRoot
();
}
@Override
public
void
onViewCreated
(
@NonNull
View
view
,
@Nullable
Bundle
savedInstanceState
)
{
super
.
onViewCreated
(
view
,
savedInstanceState
);
repo
=
new
TrainingRepository
(
requireContext
());
loadStats
();
}
private
void
loadStats
()
{
binding
.
progressStats
.
setVisibility
(
View
.
VISIBLE
);
exec
.
execute
(()
->
{
List
<
TryDao
.
VoletStat
>
voletStats
=
repo
.
getVoletStats
();
List
<
TryDao
.
DayStat
>
dayStats
=
repo
.
getDayStats
();
requireActivity
().
runOnUiThread
(()
->
{
binding
.
progressStats
.
setVisibility
(
View
.
GONE
);
populateVoletTable
(
voletStats
);
populateDayTable
(
dayStats
);
});
});
}
private
void
populateVoletTable
(
List
<
TryDao
.
VoletStat
>
rows
)
{
TableLayout
table
=
binding
.
tableVolet
;
// Header
table
.
addView
(
makeRow
(
true
,
"Book"
,
"Unit"
,
"Volet"
,
"Correct"
,
"Rate"
));
if
(
rows
.
isEmpty
())
{
table
.
addView
(
makeRow
(
false
,
"—"
,
"—"
,
"No data yet"
,
"—"
,
"—"
));
return
;
}
for
(
TryDao
.
VoletStat
s
:
rows
)
{
String
rate
=
s
.
total
==
0
?
"—"
:
String
.
format
(
"%.0f%%"
,
100.0
*
s
.
correct
/
s
.
total
);
table
.
addView
(
makeRow
(
false
,
s
.
bookTitle
,
s
.
unitTitle
,
s
.
voletTitle
,
s
.
correct
+
"/"
+
s
.
total
,
rate
));
}
}
private
void
populateDayTable
(
List
<
TryDao
.
DayStat
>
rows
)
{
TableLayout
table
=
binding
.
tableDay
;
table
.
addView
(
makeRow
(
true
,
"Date"
,
"Correct"
,
"Total"
,
"Rate"
,
""
));
if
(
rows
.
isEmpty
())
{
table
.
addView
(
makeRow
(
false
,
"—"
,
"—"
,
"No data yet"
,
"—"
,
""
));
return
;
}
for
(
TryDao
.
DayStat
s
:
rows
)
{
String
rate
=
s
.
total
==
0
?
"—"
:
String
.
format
(
"%.0f%%"
,
100.0
*
s
.
correct
/
s
.
total
);
table
.
addView
(
makeRow
(
false
,
s
.
day
,
String
.
valueOf
(
s
.
correct
),
String
.
valueOf
(
s
.
total
),
rate
,
""
));
}
}
private
TableRow
makeRow
(
boolean
header
,
String
...
cells
)
{
TableRow
row
=
new
TableRow
(
requireContext
());
int
bg
=
header
?
0xFF6200EE
:
android
.
R
.
color
.
transparent
;
if
(
header
)
row
.
setBackgroundColor
(
0xFF6200EE
);
for
(
String
cell
:
cells
)
{
TextView
tv
=
new
TextView
(
requireContext
());
tv
.
setText
(
cell
);
tv
.
setPadding
(
16
,
10
,
16
,
10
);
if
(
header
)
{
tv
.
setTextColor
(
0xFFFFFFFF
);
tv
.
setTypeface
(
null
,
android
.
graphics
.
Typeface
.
BOLD
);
}
TableRow
.
LayoutParams
lp
=
new
TableRow
.
LayoutParams
(
0
,
TableRow
.
LayoutParams
.
WRAP_CONTENT
,
1
f
);
tv
.
setLayoutParams
(
lp
);
row
.
addView
(
tv
);
}
return
row
;
}
@Override
public
void
onDestroyView
()
{
super
.
onDestroyView
();
binding
=
null
;
}
}
app/src/main/java/dev/lueckemeyer/vocoach/ui/training/TrainingFragment.java
0 → 100644
View file @
044bb284
package
dev.lueckemeyer.vocoach.ui.training
;
import
android.os.Bundle
;
import
android.text.TextUtils
;
import
android.view.*
;
import
android.widget.RadioButton
;
import
androidx.annotation.*
;
import
androidx.core.content.ContextCompat
;
import
androidx.fragment.app.Fragment
;
import
java.util.concurrent.ExecutorService
;
import
java.util.concurrent.Executors
;
import
dev.lueckemeyer.vocoach.R
;
import
dev.lueckemeyer.vocoach.databinding.FragmentTrainingBinding
;
import
dev.lueckemeyer.vocoach.db.entity.*
;
import
dev.lueckemeyer.vocoach.repository.TrainingCard
;
import
dev.lueckemeyer.vocoach.repository.TrainingRepository
;
public
class
TrainingFragment
extends
Fragment
{
private
FragmentTrainingBinding
binding
;
private
TrainingRepository
repo
;
private
String
direction
;
private
int
sessionId
;
private
TrainingCard
current
;
private
final
ExecutorService
exec
=
Executors
.
newSingleThreadExecutor
();
@Override
public
View
onCreateView
(
@NonNull
LayoutInflater
inflater
,
ViewGroup
container
,
Bundle
savedInstanceState
)
{
binding
=
FragmentTrainingBinding
.
inflate
(
inflater
,
container
,
false
);
return
binding
.
getRoot
();
}
@Override
public
void
onViewCreated
(
@NonNull
View
view
,
@Nullable
Bundle
savedInstanceState
)
{
super
.
onViewCreated
(
view
,
savedInstanceState
);
// Read direction argument passed via Bundle
direction
=
requireArguments
().
getString
(
"direction"
,
"fr_to_native"
);
repo
=
new
TrainingRepository
(
requireContext
());
exec
.
execute
(()
->
{
sessionId
=
repo
.
getOrCreateSession
(
direction
);
loadNextCard
();
});
binding
.
btnSubmit
.
setOnClickListener
(
v
->
onSubmit
());
binding
.
btnNext
.
setOnClickListener
(
v
->
loadNextCard
());
// Wire each "regular" checkbox to disable/enable its own panel's detail fields
binding
.
checkRegular
.
setOnCheckedChangeListener
((
cb
,
checked
)
->
setNounIrregularEnabled
(!
checked
));
binding
.
checkRegularVerb
.
setOnCheckedChangeListener
((
cb
,
checked
)
->
setVerbIrregularEnabled
(!
checked
));
binding
.
checkRegularAdj
.
setOnCheckedChangeListener
((
cb
,
checked
)
->
setAdjIrregularEnabled
(!
checked
));
}
// ── Load next card ────────────────────────────────────────────────────────
private
void
loadNextCard
()
{
binding
.
btnNext
.
setVisibility
(
View
.
GONE
);
binding
.
tvResult
.
setVisibility
(
View
.
GONE
);
binding
.
layoutSolution
.
setVisibility
(
View
.
GONE
);
exec
.
execute
(()
->
{
current
=
repo
.
nextCard
(
direction
);
requireActivity
().
runOnUiThread
(()
->
{
if
(
current
==
null
)
showAllDone
();
else
{
repo
.
savePending
(
direction
,
current
.
leitnerState
.
id
);
displayCard
(
current
);
}
});
});
}
private
void
displayCard
(
TrainingCard
tc
)
{
Card
card
=
tc
.
card
;
boolean
frToNative
=
"fr_to_native"
.
equals
(
direction
);
binding
.
tvBox
.
setText
(
"Box "
+
tc
.
leitnerState
.
boxNumber
);
binding
.
tvPromptLabel
.
setText
(
frToNative
?
"French"
:
"German"
);
binding
.
tvPrompt
.
setText
(
frToNative
?
card
.
french
:
card
.
nativeLang
);
binding
.
tvAnswerLabel
.
setText
(
frToNative
?
"German"
:
"French"
);
binding
.
etAnswer
.
setText
(
""
);
binding
.
etAnswer
.
setEnabled
(
true
);
binding
.
etAnswer
.
setVisibility
(
View
.
VISIBLE
);
binding
.
etAnswer
.
requestFocus
();
binding
.
btnSubmit
.
setVisibility
(
View
.
VISIBLE
);
// Word-type panels (only shown when answering in French direction — grammar is French)
boolean
showGrammar
=
frToNative
;
setupWordTypeFields
(
tc
,
showGrammar
);
}
private
void
setupWordTypeFields
(
TrainingCard
tc
,
boolean
showGrammar
)
{
binding
.
panelNoun
.
setVisibility
(
View
.
GONE
);
binding
.
panelVerb
.
setVisibility
(
View
.
GONE
);
binding
.
panelAdjective
.
setVisibility
(
View
.
GONE
);
if
(!
showGrammar
||
tc
.
card
.
wordType
==
null
)
return
;
switch
(
tc
.
card
.
wordType
)
{
case
"noun"
:
binding
.
panelNoun
.
setVisibility
(
View
.
VISIBLE
);
binding
.
rgGender
.
clearCheck
();
binding
.
etPlural
.
setText
(
""
);
binding
.
checkRegular
.
setChecked
(
false
);
setNounIrregularEnabled
(
true
);
break
;
case
"verb"
:
binding
.
panelVerb
.
setVisibility
(
View
.
VISIBLE
);
binding
.
etPartPasse
.
setText
(
""
);
binding
.
etPartPresent
.
setText
(
""
);
binding
.
etAuxiliary
.
setText
(
""
);
binding
.
checkRegularVerb
.
setChecked
(
false
);
setVerbIrregularEnabled
(
true
);
break
;
case
"adjective"
:
binding
.
panelAdjective
.
setVisibility
(
View
.
VISIBLE
);
binding
.
etFeminine
.
setText
(
""
);
binding
.
etPluralAdj
.
setText
(
""
);
binding
.
etFemPluralAdj
.
setText
(
""
);
binding
.
checkRegularAdj
.
setChecked
(
false
);
setAdjIrregularEnabled
(
true
);
break
;
}
}
// ── Enable/disable helpers ────────────────────────────────────────────────
private
void
setNounIrregularEnabled
(
boolean
enabled
)
{
float
a
=
enabled
?
1
f
:
0.4f
;
for
(
int
i
=
0
;
i
<
binding
.
rgGender
.
getChildCount
();
i
++)
{
binding
.
rgGender
.
getChildAt
(
i
).
setEnabled
(
enabled
);
}
binding
.
rgGender
.
setAlpha
(
a
);
binding
.
etPlural
.
setEnabled
(
enabled
);
binding
.
etPlural
.
setAlpha
(
a
);
}
private
void
setVerbIrregularEnabled
(
boolean
enabled
)
{
float
a
=
enabled
?
1
f
:
0.4f
;
binding
.
etPartPasse
.
setEnabled
(
enabled
);
binding
.
etPartPasse
.
setAlpha
(
a
);
binding
.
etPartPresent
.
setEnabled
(
enabled
);
binding
.
etPartPresent
.
setAlpha
(
a
);
binding
.
etAuxiliary
.
setEnabled
(
enabled
);
binding
.
etAuxiliary
.
setAlpha
(
a
);
}
private
void
setAdjIrregularEnabled
(
boolean
enabled
)
{
float
a
=
enabled
?
1
f
:
0.4f
;
binding
.
etFeminine
.
setEnabled
(
enabled
);
binding
.
etFeminine
.
setAlpha
(
a
);
binding
.
etPluralAdj
.
setEnabled
(
enabled
);
binding
.
etPluralAdj
.
setAlpha
(
a
);
binding
.
etFemPluralAdj
.
setEnabled
(
enabled
);
binding
.
etFemPluralAdj
.
setAlpha
(
a
);
}
// ── Submit ────────────────────────────────────────────────────────────────
private
void
onSubmit
()
{
if
(
current
==
null
)
return
;
Card
card
=
current
.
card
;
boolean
frToNative
=
"fr_to_native"
.
equals
(
direction
);
String
expected
=
(
frToNative
?
card
.
nativeLang
:
card
.
french
);
String
userAnswer
=
binding
.
etAnswer
.
getText
().
toString
().
trim
();
boolean
isCorrect
=
userAnswer
.
equalsIgnoreCase
(
safe
(
expected
));
if
(
frToNative
&&
isCorrect
)
{
isCorrect
=
checkWordTypeFields
(
current
);
}
showResult
(
isCorrect
,
current
);
boolean
finalCorrect
=
isCorrect
;
exec
.
execute
(()
->
{
repo
.
recordResult
(
current
.
leitnerState
,
finalCorrect
,
sessionId
);
repo
.
clearPending
(
direction
);
repo
.
touchSession
(
sessionId
);
});
}
private
boolean
checkWordTypeFields
(
TrainingCard
tc
)
{
if
(
tc
.
card
.
wordType
==
null
)
return
true
;
switch
(
tc
.
card
.
wordType
)
{
case
"noun"
:
{
NounDetail
d
=
tc
.
nounDetail
;
if
(
d
==
null
)
return
true
;
// Gender
int
checkedId
=
binding
.
rgGender
.
getCheckedRadioButtonId
();
if
(
checkedId
==
-
1
)
return
false
;
RadioButton
rb
=
binding
.
getRoot
().
findViewById
(
checkedId
);
if
(!
safe
(
rb
.
getTag
().
toString
()).
equalsIgnoreCase
(
safe
(
d
.
gender
)))
return
false
;
// Plural
if
(
binding
.
checkRegular
.
isChecked
())
{
return
d
.
pluralForm
==
null
||
d
.
pluralForm
.
isEmpty
();
}
return
binding
.
etPlural
.
getText
().
toString
().
trim
()
.
equalsIgnoreCase
(
safe
(
d
.
pluralForm
));
}
case
"verb"
:
{
VerbDetail
d
=
tc
.
verbDetail
;
if
(
d
==
null
)
return
true
;
if
(
binding
.
checkRegularVerb
.
isChecked
())
return
!
d
.
isIrregular
;
boolean
pp
=
binding
.
etPartPasse
.
getText
().
toString
().
trim
().
equalsIgnoreCase
(
safe
(
d
.
participePasse
));
boolean
ppr
=
binding
.
etPartPresent
.
getText
().
toString
().
trim
().
equalsIgnoreCase
(
safe
(
d
.
participePresent
));
boolean
aux
=
binding
.
etAuxiliary
.
getText
().
toString
().
trim
().
equalsIgnoreCase
(
safe
(
d
.
auxiliary
));
return
pp
&&
ppr
&&
aux
;
}
case
"adjective"
:
{
AdjectiveDetail
d
=
tc
.
adjectiveDetail
;
if
(
d
==
null
)
return
true
;
if
(
binding
.
checkRegularAdj
.
isChecked
())
return
true
;
boolean
fem
=
binding
.
etFeminine
.
getText
().
toString
().
trim
().
equalsIgnoreCase
(
safe
(
d
.
feminineForm
));
boolean
pl
=
binding
.
etPluralAdj
.
getText
().
toString
().
trim
().
equalsIgnoreCase
(
safe
(
d
.
pluralForm
));
boolean
femPl
=
binding
.
etFemPluralAdj
.
getText
().
toString
().
trim
().
equalsIgnoreCase
(
safe
(
d
.
femininePluralForm
));
return
fem
&&
pl
&&
femPl
;
}
}
return
true
;
}
// ── Show result ───────────────────────────────────────────────────────────
private
void
showResult
(
boolean
isCorrect
,
TrainingCard
tc
)
{
binding
.
etAnswer
.
setEnabled
(
false
);
binding
.
btnSubmit
.
setVisibility
(
View
.
GONE
);
binding
.
btnNext
.
setVisibility
(
View
.
VISIBLE
);
binding
.
tvResult
.
setVisibility
(
View
.
VISIBLE
);
binding
.
tvResult
.
setText
(
isCorrect
?
"✓ Correct!"
:
"✗ Wrong"
);
binding
.
tvResult
.
setTextColor
(
ContextCompat
.
getColor
(
requireContext
(),
isCorrect
?
R
.
color
.
correct
:
R
.
color
.
wrong
));
Card
card
=
tc
.
card
;
binding
.
layoutSolution
.
setVisibility
(
View
.
VISIBLE
);
binding
.
tvSolutionFrench
.
setText
(
"FR: "
+
safe
(
card
.
french
));
binding
.
tvSolutionNative
.
setText
(
"DE: "
+
safe
(
card
.
nativeLang
));
if
(!
TextUtils
.
isEmpty
(
card
.
phonetic
))
{
binding
.
tvSolutionPhonetic
.
setVisibility
(
View
.
VISIBLE
);
binding
.
tvSolutionPhonetic
.
setText
(
"["
+
card
.
phonetic
+
"]"
);
}
else
{
binding
.
tvSolutionPhonetic
.
setVisibility
(
View
.
GONE
);
}
if
(!
TextUtils
.
isEmpty
(
card
.
exampleSentence
))
{
binding
.
tvSolutionExample
.
setVisibility
(
View
.
VISIBLE
);
binding
.
tvSolutionExample
.
setText
(
"e.g. "
+
card
.
exampleSentence
);
}
else
{
binding
.
tvSolutionExample
.
setVisibility
(
View
.
GONE
);
}
int
oldBox
=
tc
.
leitnerState
.
boxNumber
;
int
newBox
=
isCorrect
?
Math
.
min
(
oldBox
+
1
,
6
)
:
Math
.
max
(
oldBox
-
1
,
1
);
binding
.
tvBoxChange
.
setText
(
"Box: "
+
oldBox
+
" → "
+
newBox
);
}
private
void
showAllDone
()
{
binding
.
panelNoun
.
setVisibility
(
View
.
GONE
);
binding
.
panelVerb
.
setVisibility
(
View
.
GONE
);
binding
.
panelAdjective
.
setVisibility
(
View
.
GONE
);
binding
.
tvBox
.
setText
(
""
);
binding
.
tvPromptLabel
.
setText
(
""
);
binding
.
tvPrompt
.
setText
(
"All words for today done! 🎉"
);
binding
.
tvAnswerLabel
.
setText
(
""
);
binding
.
etAnswer
.
setVisibility
(
View
.
GONE
);
binding
.
btnSubmit
.
setVisibility
(
View
.
GONE
);
binding
.
btnNext
.
setVisibility
(
View
.
GONE
);
}
@Override
public
void
onDestroyView
()
{
super
.
onDestroyView
();
binding
=
null
;
}
private
static
String
safe
(
String
s
)
{
return
s
!=
null
?
s
:
""
;
}
}
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted
100644 → 0
View file @
eab02a57
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:aapt=
"http://schemas.android.com/aapt"
android:width=
"108dp"
android:height=
"108dp"
android:viewportWidth=
"108"
android:viewportHeight=
"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:strokeWidth=
"1"
android:strokeColor=
"#00000000"
>
<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:strokeWidth=
"1"
android:strokeColor=
"#00000000"
/>
</vector>
app/src/main/res/drawable/ic_launcher_background.xml
View file @
044bb284
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:width=
"108dp"
android:height=
"108dp"
android:viewportWidth=
"108"
android:viewportHeight=
"108"
>
<path
android:fillColor=
"#008577"
android:pathData=
"M0,0h108v108h-108z"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M9,0L9,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M19,0L19,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M29,0L29,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M39,0L39,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M49,0L49,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M59,0L59,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M69,0L69,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M79,0L79,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M89,0L89,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M99,0L99,108"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,9L108,9"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,19L108,19"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,29L108,29"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,39L108,39"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,49L108,49"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,59L108,59"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,69L108,69"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,79L108,79"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,89L108,89"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M0,99L108,99"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M19,29L89,29"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M19,39L89,39"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M19,49L89,49"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M19,59L89,59"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M19,69L89,69"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M19,79L89,79"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M29,19L29,89"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M39,19L39,89"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M49,19L49,89"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M59,19L59,89"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M69,19L69,89"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
<path
android:fillColor=
"#00000000"
android:pathData=
"M79,19L79,89"
android:strokeWidth=
"0.8"
android:strokeColor=
"#33FFFFFF"
/>
</vector>
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:width=
"108dp"
android:height=
"108dp"
android:viewportWidth=
"108"
android:viewportHeight=
"108"
>
<path
android:pathData=
"M0,0h108v108h-108z"
android:fillColor=
"#3F51B5"
/>
</vector>
app/src/main/res/drawable/ic_launcher_foreground.xml
0 → 100644
View file @
044bb284
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:width=
"108dp"
android:height=
"108dp"
android:viewportWidth=
"108"
android:viewportHeight=
"108"
>
<path
android:pathData=
"M54,30C41.85,30 32,38.95 32,50C32,55.41 34.42,60.31 38.35,63.85L36,74L47.5,68.25C49.54,69.38 51.7,70 54,70C66.15,70 76,61.05 76,50C76,38.95 66.15,30 54,30ZM48,58L40,50L42.83,47.17L48,52.34L61.17,39.17L64,42L48,58Z"
android:fillColor=
"#ffffff"
/>
</vector>
app/src/main/res/drawable/panel_bg.xml
0 → 100644
View file @
044bb284
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<solid
android:color=
"#0D6200EE"
/>
<corners
android:radius=
"8dp"
/>
<stroke
android:width=
"1dp"
android:color=
"#336200EE"
/>
</shape>
app/src/main/res/layout/activity_main.xml
View file @
044bb284
<?xml version="1.0" encoding="utf-8"?>
<
android.support.constraint.Constraint
Layout
xmlns:android=
"http://schemas.android.com/apk/res/android"
<
Linear
Layout
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
"
>
android:orientation=
"vertical
"
>
<
TextView
android:
layout_width=
"wrap_content
"
android:layout_
height=
"wrap_cont
ent"
android:
text=
"Hello World!
"
a
pp:layout_constraintBottom_toBottomOf=
"parent
"
a
pp:layout_constraintLeft_toLeftOf=
"parent
"
app:
layout_constraintRight_toRightOf=
"parent
"
app:
layout_constraintTop_toTopOf=
"parent
"
/>
<
com.google.android.material.appbar.MaterialToolbar
android:
id=
"@+id/toolbar
"
android:layout_
width=
"match_par
ent"
android:
layout_height=
"?attr/actionBarSize
"
a
ndroid:background=
"?attr/colorPrimary
"
a
ndroid:theme=
"@style/ThemeOverlay.MaterialComponents.Dark.ActionBar
"
app:
title=
"VoCoach
"
app:
titleTextColor=
"@android:color/white
"
/>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<androidx.fragment.app.FragmentContainerView
android:id=
"@+id/nav_host_fragment"
android:name=
"androidx.navigation.fragment.NavHostFragment"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
app:defaultNavHost=
"true"
app:navGraph=
"@navigation/nav_graph"
/>
</LinearLayout>
app/src/main/res/layout/fragment_home.xml
0 → 100644
View file @
044bb284
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:orientation=
"vertical"
android:gravity=
"center"
android:padding=
"32dp"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"What would you like to do?"
android:textAppearance=
"?attr/textAppearanceHeadline6"
android:layout_marginBottom=
"40dp"
/>
<com.google.android.material.button.MaterialButton
android:id=
"@+id/btn_fr_to_native"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:text=
"Train French → German"
android:layout_marginBottom=
"16dp"
style=
"@style/Widget.MaterialComponents.Button"
/>
<com.google.android.material.button.MaterialButton
android:id=
"@+id/btn_native_to_fr"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:text=
"Train German → French"
android:layout_marginBottom=
"32dp"
style=
"@style/Widget.MaterialComponents.Button"
/>
<com.google.android.material.button.MaterialButton
android:id=
"@+id/btn_stats"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:text=
"Stats"
style=
"@style/Widget.MaterialComponents.Button.OutlinedButton"
/>
</LinearLayout>
app/src/main/res/layout/fragment_stats.xml
0 → 100644
View file @
044bb284
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:orientation=
"vertical"
>
<ProgressBar
android:id=
"@+id/progress_stats"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_gravity=
"center_horizontal"
android:layout_marginTop=
"32dp"
android:visibility=
"gone"
/>
<ScrollView
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:padding=
"12dp"
>
<LinearLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Success rate by Volet (all-time)"
android:textAppearance=
"?attr/textAppearanceSubtitle1"
android:textStyle=
"bold"
android:layout_marginBottom=
"8dp"
/>
<HorizontalScrollView
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
>
<TableLayout
android:id=
"@+id/table_volet"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:stretchColumns=
"2"
/>
</HorizontalScrollView>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Success rate by Day"
android:textAppearance=
"?attr/textAppearanceSubtitle1"
android:textStyle=
"bold"
android:layout_marginTop=
"24dp"
android:layout_marginBottom=
"8dp"
/>
<TableLayout
android:id=
"@+id/table_day"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:stretchColumns=
"0"
/>
</LinearLayout>
</ScrollView>
</LinearLayout>
app/src/main/res/layout/fragment_training.xml
0 → 100644
View file @
044bb284
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
>
<LinearLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
android:padding=
"20dp"
>
<!-- Box indicator -->
<TextView
android:id=
"@+id/tv_box"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceCaption"
android:textColor=
"?attr/colorPrimary"
android:layout_marginBottom=
"8dp"
/>
<!-- Prompt -->
<TextView
android:id=
"@+id/tv_prompt_label"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceCaption"
android:layout_marginBottom=
"2dp"
/>
<TextView
android:id=
"@+id/tv_prompt"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceHeadline5"
android:textStyle=
"bold"
android:layout_marginBottom=
"24dp"
/>
<!-- Answer -->
<TextView
android:id=
"@+id/tv_answer_label"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceCaption"
android:layout_marginBottom=
"2dp"
/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Your answer"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_marginBottom=
"12dp"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_answer"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
android:imeOptions=
"actionDone"
/>
</com.google.android.material.textfield.TextInputLayout>
<!-- ══ NOUN PANEL ════════════════════════════════════════════════════ -->
<LinearLayout
android:id=
"@+id/panel_noun"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
android:visibility=
"gone"
android:background=
"@drawable/panel_bg"
android:padding=
"12dp"
android:layout_marginBottom=
"12dp"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Noun details"
android:textStyle=
"bold"
android:textAppearance=
"?attr/textAppearanceCaption"
android:layout_marginBottom=
"6dp"
/>
<CheckBox
android:id=
"@+id/check_regular"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Regular plural (+s)"
android:layout_marginBottom=
"8dp"
/>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Gender"
android:textStyle=
"bold"
android:layout_marginBottom=
"4dp"
/>
<RadioGroup
android:id=
"@+id/rg_gender"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:orientation=
"horizontal"
android:layout_marginBottom=
"8dp"
>
<RadioButton
android:id=
"@+id/rb_masc"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Masculine (le)"
android:tag=
"m"
android:layout_marginEnd=
"16dp"
/>
<RadioButton
android:id=
"@+id/rb_fem"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Feminine (la)"
android:tag=
"f"
/>
</RadioGroup>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Irregular plural form"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_plural"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- ══ VERB PANEL ════════════════════════════════════════════════════ -->
<LinearLayout
android:id=
"@+id/panel_verb"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
android:visibility=
"gone"
android:background=
"@drawable/panel_bg"
android:padding=
"12dp"
android:layout_marginBottom=
"12dp"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Verb details"
android:textStyle=
"bold"
android:textAppearance=
"?attr/textAppearanceCaption"
android:layout_marginBottom=
"6dp"
/>
<CheckBox
android:id=
"@+id/check_regular_verb"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Regular verb (groupe 1, -er)"
android:layout_marginBottom=
"8dp"
/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Participe passé"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_marginBottom=
"8dp"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_part_passe"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Participe présent"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_marginBottom=
"8dp"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_part_present"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Auxiliary (avoir / être)"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_auxiliary"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- ══ ADJECTIVE PANEL ═══════════════════════════════════════════════ -->
<LinearLayout
android:id=
"@+id/panel_adjective"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
android:visibility=
"gone"
android:background=
"@drawable/panel_bg"
android:padding=
"12dp"
android:layout_marginBottom=
"12dp"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Adjective details"
android:textStyle=
"bold"
android:textAppearance=
"?attr/textAppearanceCaption"
android:layout_marginBottom=
"6dp"
/>
<CheckBox
android:id=
"@+id/check_regular_adj"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Regular adjective (+e / +s / +es)"
android:layout_marginBottom=
"8dp"
/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Feminine form (e.g. grande)"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_marginBottom=
"8dp"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_feminine"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Masculine plural (e.g. grands)"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_marginBottom=
"8dp"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_plural_adj"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:hint=
"Feminine plural (e.g. grandes)"
style=
"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
>
<com.google.android.material.textfield.TextInputEditText
android:id=
"@+id/et_fem_plural_adj"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inputType=
"text"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- Buttons -->
<com.google.android.material.button.MaterialButton
android:id=
"@+id/btn_submit"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:text=
"Check answer"
android:layout_marginTop=
"8dp"
/>
<com.google.android.material.button.MaterialButton
android:id=
"@+id/btn_next"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:text=
"Next word →"
android:visibility=
"gone"
android:layout_marginTop=
"8dp"
style=
"@style/Widget.MaterialComponents.Button.OutlinedButton"
/>
<!-- Result -->
<TextView
android:id=
"@+id/tv_result"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceHeadline6"
android:layout_marginTop=
"16dp"
android:visibility=
"gone"
/>
<!-- Solution card -->
<LinearLayout
android:id=
"@+id/layout_solution"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
android:background=
"@drawable/panel_bg"
android:padding=
"14dp"
android:layout_marginTop=
"12dp"
android:visibility=
"gone"
>
<TextView
android:id=
"@+id/tv_solution_french"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceBody1"
android:textStyle=
"bold"
/>
<TextView
android:id=
"@+id/tv_solution_native"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceBody1"
android:layout_marginTop=
"4dp"
/>
<TextView
android:id=
"@+id/tv_solution_phonetic"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceCaption"
android:textColor=
"?attr/colorSecondary"
android:layout_marginTop=
"4dp"
android:visibility=
"gone"
/>
<TextView
android:id=
"@+id/tv_solution_example"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceBody2"
android:fontFamily=
"serif"
android:textStyle=
"italic"
android:layout_marginTop=
"8dp"
android:visibility=
"gone"
/>
<TextView
android:id=
"@+id/tv_box_change"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textAppearance=
"?attr/textAppearanceCaption"
android:layout_marginTop=
"8dp"
/>
</LinearLayout>
</LinearLayout>
</ScrollView>
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
View file @
044bb284
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<background
android:drawable=
"@drawable/ic_launcher_background"
/>
<foreground
android:drawable=
"@drawable/ic_launcher_foreground"
/>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<background
android:drawable=
"@drawable/ic_launcher_background"
/>
<foreground
android:drawable=
"@drawable/ic_launcher_foreground"
/>
<monochrome
android:drawable=
"@drawable/ic_launcher_foreground"
/>
</adaptive-icon>
\ No newline at end of file
Prev
1
2
3
4
5
6
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment