Commit 044bb284 authored by Lückemeyer's avatar Lückemeyer
Browse files

Initial commit of claude ai assisted vocab trainer android app

parent eab02a57
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;
}
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"
}
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;
}
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;
}
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;
}
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;
}
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;
}
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 : ""; }
}
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;
}
}
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, 1f);
tv.setLayoutParams(lp);
row.addView(tv);
}
return row;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
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 ? 1f : 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 ? 1f : 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 ? 1f : 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 : ""; }
}
<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>
<?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android"
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp"
android:width="108dp" android:height="108dp"
android:height="108dp" android:viewportWidth="108"
android:viewportWidth="108" android:viewportHeight="108">
android:viewportHeight="108"> <path
<path android:pathData="M0,0h108v108h-108z"
android:fillColor="#008577" android:fillColor="#3F51B5"/>
android:pathData="M0,0h108v108h-108z" /> </vector>
<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="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>
<?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>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> android:orientation="vertical">
<TextView <com.google.android.material.appbar.MaterialToolbar
android:layout_width="wrap_content" android:id="@+id/toolbar"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Hello World!" android:layout_height="?attr/actionBarSize"
app:layout_constraintBottom_toBottomOf="parent" android:background="?attr/colorPrimary"
app:layout_constraintLeft_toLeftOf="parent" android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
app:layout_constraintRight_toRightOf="parent" app:title="VoCoach"
app:layout_constraintTop_toTopOf="parent" /> app:titleTextColor="@android:color/white" />
</android.support.constraint.ConstraintLayout> <androidx.fragment.app.FragmentContainerView
\ No newline at end of file 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>
<?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>
<?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>
This diff is collapsed.
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment