Add kanji result support

Julien LepillerSat Jul 24 00:44:06+0200 2021

2e61731

Add kanji result support

app/build.gradle

2020
2121
dependencies {
2222
    implementation fileTree(dir: 'libs', include: ['*.jar'])
23-
    implementation 'androidx.appcompat:appcompat:1.3.0'
23+
    implementation 'androidx.appcompat:appcompat:1.3.1'
2424
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
2525
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
2626
    implementation 'androidx.preference:preference:1.1.1'
2727
    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
28+
    implementation "androidx.viewpager2:viewpager2:1.0.0"
2829
    implementation 'com.andree-surya:moji4j:1.0.0'
2930
    implementation 'com.google.android:flexbox:2.0.1'
3031
    implementation 'com.google.android.material:material:1.4.0'

app/src/main/java/eu/lepiller/nani/DictionaryActivity.java

1414
import android.widget.AdapterView;
1515
import android.widget.ArrayAdapter;
1616
import android.widget.ListView;
17-
import android.widget.ScrollView;
1817
import android.widget.Spinner;
1918
2019
import com.google.android.material.snackbar.Snackbar;

app/src/main/java/eu/lepiller/nani/MainActivity.java

55
import android.os.AsyncTask;
66
import android.os.Bundle;
77
import com.google.android.material.snackbar.Snackbar;
8+
9+
import androidx.annotation.NonNull;
810
import androidx.appcompat.app.AppCompatActivity;
911
import androidx.appcompat.widget.Toolbar;
12+
import androidx.fragment.app.FragmentManager;
1013
import androidx.preference.PreferenceManager;
14+
import androidx.viewpager2.widget.ViewPager2;
1115
12-
import android.text.Html;
1316
import android.util.Log;
1417
import android.view.View;
1518
import android.view.Menu;

1922
import android.widget.SearchView;
2023
import android.widget.TextView;
2124
25+
import com.google.android.material.tabs.TabLayout;
26+
import com.google.android.material.tabs.TabLayoutMediator;
2227
import com.moji4j.MojiConverter;
2328
import com.moji4j.MojiDetector;
2429
2530
import java.util.ArrayList;
31+
import java.util.List;
2632
2733
import eu.lepiller.nani.dictionary.DictionaryException;
2834
import eu.lepiller.nani.dictionary.DictionaryFactory;
2935
import eu.lepiller.nani.dictionary.IncompatibleFormatException;
3036
import eu.lepiller.nani.dictionary.NoDictionaryException;
3137
import eu.lepiller.nani.dictionary.NoResultDictionaryException;
38+
import eu.lepiller.nani.result.KanjiResult;
3239
import eu.lepiller.nani.result.Result;
33-
import se.fekete.furiganatextview.furiganaview.FuriganaTextView;
40+
41+
import static java.lang.Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS;
3442
3543
public class MainActivity extends AppCompatActivity implements OnTaskCompleted<SearchResult>, SharedPreferences.OnSharedPreferenceChangeListener {
36-
    private LinearLayout result_view, result_layout;
44+
    private LinearLayout result_layout;
3745
    private TextView feedback_text;
3846
    private SearchView search_form;
39-
    private static ArrayList<Result> savedResults;
4047
    private RadicalSelectorView radical_selector;
4148
    private String readingStyle = "furigana";
49+
    ViewPager2 viewPager2;
50+
    ResultPagerAdapter pagerAdapter;
4251
43-
    private static final String TAG = "MAIN";
52+
    static final String TAG = "MAIN";
4453
4554
    @Override
4655
    protected void onCreate(Bundle savedInstanceState) {

4958
        Toolbar toolbar = findViewById(R.id.toolbar);
5059
        setSupportActionBar(toolbar);
5160
52-
        result_view = findViewById(R.id.results_view);
5361
        result_layout = findViewById(R.id.result_layout);
5462
        feedback_text = findViewById(R.id.feedback);
5563
        search_form = findViewById(R.id.search_form);
5664
        radical_selector = findViewById(R.id.radical_selector);
57-
58-
        Button radical_button = findViewById(R.id.radical_button);
65+
        TabLayout tabLayout = findViewById(R.id.tab_layout);
66+
        viewPager2 = findViewById(R.id.pager);
5967
6068
        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
6169

6573
        int radSizePref = getRadSizePref(sharedPref);
6674
        readingStyle = getReadingSizePref(sharedPref);
6775
76+
        Button radical_button = findViewById(R.id.radical_button);
77+
78+
        pagerAdapter = new ResultPagerAdapter(this.getApplicationContext());
79+
        pagerAdapter.setReadingStyle(readingStyle);
80+
        pagerAdapter.notifyDataSetChanged();
81+
        viewPager2.setAdapter(pagerAdapter);
82+
83+
        tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_results));
84+
        tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_kanji));
85+
86+
        new TabLayoutMediator(tabLayout, viewPager2,
87+
                new TabLayoutMediator.TabConfigurationStrategy() {
88+
                    @Override
89+
                    public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
90+
                        tab.setText(position == 0? R.string.tab_results: R.string.tab_kanji);
91+
                    }
92+
                }
93+
        ).attach();
94+
6895
        try {
6996
            radical_selector.setRadSize(radSizePref);
7097
            radical_selector.setDictionary(DictionaryFactory.getRadicalDictionary(getApplicationContext()));

84111
                    return false;
85112
                }
86113
87-
                result_view.removeAllViews();
114+
                pagerAdapter.setKanjiResults(new ArrayList<KanjiResult>());
115+
                pagerAdapter.setResults(new ArrayList<Result>());
116+
                pagerAdapter.notifyDataSetChanged();
117+
88118
                search_form.setEnabled(false);
89119
                feedback_text.setText(R.string.feedback_progress);
90120

135165
                startActivity(intent);
136166
            }
137167
        });
138-
139-
        if(savedResults != null) {
140-
            showResults(savedResults);
141-
        }
142168
    }
143169
144170
    @Override

148174
            radical_selector.setRadSize(getRadSizePref(sharedPreferences));
149175
        } else if(key.compareTo(SettingsActivity.KEY_PREF_READING_STYLE) == 0) {
150176
            readingStyle = getReadingSizePref(sharedPreferences);
177+
            pagerAdapter.setReadingStyle(readingStyle);
178+
            pagerAdapter.notifyDataSetChanged();
151179
        }
152180
    }
153181

177205
            ArrayList<String> tried = new ArrayList<>();
178206
            tried.add(text);
179207
208+
            List<KanjiResult> kanjiResults = new ArrayList<>();
209+
            for(String c: text.split("")) {
210+
                if(Character.UnicodeBlock.of(c.codePointAt(0)) == CJK_UNIFIED_IDEOGRAPHS) {
211+
                    KanjiResult r;
212+
                    try {
213+
                        r = DictionaryFactory.searchKanji(c);
214+
                    } catch(DictionaryException e) {
215+
                        return new SearchResult(e);
216+
                    }
217+
                    Log.d(TAG, "kanji " + c + ", result: " + r);
218+
                    if(r != null)
219+
                        kanjiResults.add(r);
220+
                }
221+
            }
222+
180223
            ArrayList<Result> searchResult;
181224
            try {
182225
                searchResult = DictionaryFactory.search(text);

184227
                return new SearchResult(e);
185228
            }
186229
187-
188230
            if(searchResult.size() == 0) {
189231
                MojiConverter converter = new MojiConverter();
190232
                try {

199241
                    return new SearchResult(new NoResultDictionaryException(tried));
200242
                }
201243
202-
                return new SearchResult(searchResult, text, true);
244+
                return new SearchResult(searchResult, kanjiResults, text, true);
203245
            } else {
204-
                return new SearchResult(searchResult, text, false);
246+
                return new SearchResult(searchResult, kanjiResults, text, false);
205247
            }
206248
        }
207249

243285
244286
        feedback_text.setText("");
245287
246-
        ArrayList<Result> searchResult = r.getResults();
288+
        List<Result> searchResult = r.getResults();
289+
        List<KanjiResult> kanjiResults = r.getKanjiResults();
290+
        Log.d(TAG, "results. Kanjis: " + r.getKanjiResults().size());
247291
248292
        MojiDetector detector = new MojiDetector();
249293
        String text = r.getText();

260304
                }
261305
            });
262306
        }
263-
        savedResults = searchResult;
264-
        if(searchResult != null)
265-
            showResults(searchResult);
266-
    }
267-
268-
    void showResults(ArrayList<Result> searchResult) {
269-
        if(searchResult.size() == 0) {
270-
            feedback_text.setText(R.string.feedback_no_result);
271-
            return;
272-
        }
273-
274-
        int num = 0;
275-
        for(Result result: searchResult) {
276-
            num++;
277-
            if (num > 10)
278-
                break;
279-
            View child_result = getLayoutInflater().inflate(R.layout.layout_result, result_view, false);
280-
281-
            FuriganaTextView kanji_view = child_result.findViewById(R.id.kanji_view);
282-
            TextView reading_view = child_result.findViewById(R.id.reading_view);
283-
            LinearLayout senses_view = child_result.findViewById(R.id.sense_view);
284-
            TextView additional_info = child_result.findViewById(R.id.additional_info_view);
285-
            TextView pitch_view = child_result.findViewById(R.id.pitch_view);
286-
287-
            // Populate the data into the template view using the data object
288-
            if(readingStyle.compareTo("furigana") == 0) {
289-
                kanji_view.setFuriganaText(result.getKanjiFurigana());
290-
            } else {
291-
                kanji_view.setFuriganaText(result.getKanji());
292-
                reading_view.setVisibility(View.VISIBLE);
293-
                reading_view.setText(readingStyle.compareTo("kana") == 0? result.getReading(): result.getRomajiReading());
294-
            }
295-
296-
            // If pitch information is available, make it visible
297-
            String pitch = result.getPitch();
298-
            if(pitch != null) {
299-
                Log.d(TAG, "pitch: "+pitch);
300-
                pitch_view.setVisibility(View.VISIBLE);
301-
                pitch_view.setText(pitch);
302-
                pitch_view.setOnClickListener(new View.OnClickListener() {
303-
                    @Override
304-
                    public void onClick(View v) {
305-
                        Intent intent = new Intent(MainActivity.this, HelpPitchActivity.class);
306-
                        startActivity(intent);
307-
                    }
308-
                });
309-
            }
310-
311-
            StringBuilder additional = new StringBuilder();
312-
            boolean separator = false;
313-
            for (String s : result.getAlternatives()) {
314-
                if (separator)
315-
                    additional.append(getResources().getString(R.string.sense_separator));
316-
                else
317-
                    separator = true;
318-
                additional.append(s);
319-
            }
320-
            if(result.getAlternatives().size() > 1)
321-
                additional_info.setText(String.format(getResources().getString(R.string.sense_alternatives), additional.toString()));
322-
            else
323-
                additional_info.setVisibility(View.GONE);
324307
325-
            senses_view.removeAllViews();
326-
327-
            int sense_pos = 1;
328-
            for (Result.Sense sense : result.getSenses()) {
329-
                View child = getLayoutInflater().inflate(R.layout.layout_sense, senses_view, false);
330-
                TextView id_view = child.findViewById(R.id.id_view);
331-
                TextView lang_view = child.findViewById(R.id.lang_view);
332-
                TextView sense_view = child.findViewById(R.id.definition_view);
333-
334-
                id_view.setText(String.format(getResources().getString(R.string.sense_number), sense_pos));
335-
                lang_view.setText(sense.getLanguage());
336-
337-
                StringBuilder sb = new StringBuilder();
338-
                sb.append("<font color=\"#909090\"><i>");
339-
                boolean separator1 = false;
340-
                for(String s : sense.getInfos()) {
341-
                    if (separator1)
342-
                        sb.append(getResources().getString(R.string.sense_separator));
343-
                    else
344-
                        separator1 = true;
345-
                    sb.append(s);
346-
                }
347-
                sb.append("</i></font>");
348-
                if(separator1)
349-
                    sb.append(" ");
350-
351-
                separator1 = false;
352-
                for (String s : sense.getGlosses()) {
353-
                    if (separator1)
354-
                        sb.append(getResources().getString(R.string.sense_separator));
355-
                    else
356-
                        separator1 = true;
357-
                    sb.append(s);
358-
                }
359-
                sense_view.setText(Html.fromHtml(sb.toString()));
360-
361-
                senses_view.addView(child);
362-
                sense_pos++;
308+
        if(searchResult != null || kanjiResults != null) {
309+
            if((searchResult == null || searchResult.size() == 0) && kanjiResults.size() == 0) {
310+
                feedback_text.setText(R.string.feedback_no_result);
363311
            }
364-
365-
            result_view.addView(child_result);
366312
        }
313+
314+
        pagerAdapter.setKanjiResults(kanjiResults);
315+
        pagerAdapter.setResults(searchResult);
316+
        pagerAdapter.notifyDataSetChanged();
367317
    }
368318
369319
    @Override

app/src/main/java/eu/lepiller/nani/ResultPagerAdapter.java unknown status 1

1+
package eu.lepiller.nani;
2+
3+
import android.content.Context;
4+
import android.content.Intent;
5+
import android.os.Build;
6+
import android.text.Html;
7+
import android.util.Log;
8+
import android.view.Gravity;
9+
import android.view.LayoutInflater;
10+
import android.view.View;
11+
import android.view.ViewGroup;
12+
import android.widget.LinearLayout;
13+
import android.widget.TextView;
14+
15+
import androidx.annotation.NonNull;
16+
import androidx.recyclerview.widget.RecyclerView;
17+
18+
import org.w3c.dom.Text;
19+
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import eu.lepiller.nani.result.KanjiResult;
26+
import eu.lepiller.nani.result.Result;
27+
import se.fekete.furiganatextview.furiganaview.FuriganaTextView;
28+
29+
public class ResultPagerAdapter extends RecyclerView.Adapter<ResultPagerAdapter.ViewHolder> {
30+
    static List<Result> results = new ArrayList<>();
31+
    static List<KanjiResult> kanjiResults = new ArrayList<>();
32+
    private static String readingStyle = "furigana";
33+
    private Context context;
34+
35+
    static final String TAG = "RESULTS_PAGER";
36+
37+
    public static class ViewHolder extends RecyclerView.ViewHolder {
38+
        private LinearLayout result_view;
39+
        private Context context;
40+
41+
        ViewHolder(View view, Context context) {
42+
            super(view);
43+
            this.context = context;
44+
            result_view = view.findViewById(R.id.results_view);
45+
46+
            Log.d(TAG, "createView");
47+
        }
48+
49+
        final String getContent(List<String> content) {
50+
            StringBuilder sb = new StringBuilder();
51+
            boolean separator1 = false;
52+
            for (String s : content) {
53+
                if (separator1)
54+
                    sb.append(context.getResources().getString(R.string.sense_separator));
55+
                else
56+
                    separator1 = true;
57+
                sb.append(s);
58+
            }
59+
            return sb.toString();
60+
        }
61+
62+
        void showKanjiResults() {
63+
            result_view.removeAllViews();
64+
            if(kanjiResults == null)
65+
                return;
66+
67+
            Log.d(TAG, "(show) kanjis: " + kanjiResults.size());
68+
69+
            for(KanjiResult result: kanjiResults) {
70+
                View child_result = LayoutInflater.from(context).inflate(R.layout.layout_kanji, result_view, false);
71+
72+
                TextView kanjiView = child_result.findViewById(R.id.kanji_view);
73+
                TextView strokesView = child_result.findViewById(R.id.kanji_strokes);
74+
                LinearLayout senses_view = child_result.findViewById(R.id.sense_view);
75+
                TextView onView = child_result.findViewById(R.id.on_reading);
76+
                TextView kunView = child_result.findViewById(R.id.kun_reading);
77+
                TextView nanoriView = child_result.findViewById(R.id.nanori_reading);
78+
79+
                kanjiView.setText(result.getKanji());
80+
                strokesView.setText(String.format(context.getResources().getQuantityString(R.plurals.kanji_stroke, result.getStroke()),
81+
                        result.getStroke()));
82+
83+
                senses_view.removeAllViews();
84+
                Map<String, List<String>> meanings = new HashMap<>();
85+
                for(KanjiResult.Sense sense: result.getSenses()) {
86+
                    List<String> content = meanings.get(sense.getLang());
87+
                    if(content == null)
88+
                        content = new ArrayList<>();
89+
                    content.add(sense.getContent());
90+
                    meanings.put(sense.getLang(), content);
91+
                }
92+
93+
                int sense_pos = 1;
94+
                for(String lang: meanings.keySet()) {
95+
                    View child = LayoutInflater.from(context).inflate(R.layout.layout_sense, senses_view, false);
96+
                    TextView id_view = child.findViewById(R.id.id_view);
97+
                    TextView lang_view = child.findViewById(R.id.lang_view);
98+
                    TextView sense_view = child.findViewById(R.id.definition_view);
99+
100+
                    id_view.setText(String.format(context.getResources().getString(R.string.sense_number), sense_pos));
101+
                    lang_view.setText(lang);
102+
103+
                    StringBuilder sb = new StringBuilder();
104+
                    boolean separator1 = false;
105+
                    for (String s : meanings.get(lang)) {
106+
                        if (separator1)
107+
                            sb.append(context.getResources().getString(R.string.sense_separator));
108+
                        else
109+
                            separator1 = true;
110+
                        sb.append(s);
111+
                    }
112+
                    sense_view.setText(Html.fromHtml(sb.toString()));
113+
114+
                    senses_view.addView(child);
115+
                    sense_pos++;
116+
                }
117+
                kunView.setText(String.format(context.getResources().getString(R.string.kun_reading),
118+
                        getContent(result.getKun())));
119+
                onView.setText(String.format(context.getResources().getString(R.string.on_reading),
120+
                        getContent(result.getOn())));
121+
                nanoriView.setText(String.format(context.getResources().getString(R.string.nanori_reading),
122+
                        getContent(result.getNanori())));
123+
124+
                result_view.addView(child_result);
125+
            }
126+
        }
127+
128+
        void showResults() {
129+
            result_view.removeAllViews();
130+
            if(results == null)
131+
                return;
132+
133+
            int num = 0;
134+
            for(Result result: results) {
135+
                num++;
136+
                if (num > 10)
137+
                    break;
138+
                View child_result = LayoutInflater.from(context).inflate(R.layout.layout_result, result_view, false);
139+
140+
                FuriganaTextView kanji_view = child_result.findViewById(R.id.kanji_view);
141+
                TextView reading_view = child_result.findViewById(R.id.reading_view);
142+
                LinearLayout senses_view = child_result.findViewById(R.id.sense_view);
143+
                TextView additional_info = child_result.findViewById(R.id.additional_info_view);
144+
                TextView pitch_view = child_result.findViewById(R.id.pitch_view);
145+
146+
                // Populate the data into the template view using the data object
147+
                if(readingStyle.compareTo("furigana") == 0) {
148+
                    kanji_view.setFuriganaText(result.getKanjiFurigana());
149+
                } else {
150+
                    kanji_view.setFuriganaText(result.getKanji());
151+
                    reading_view.setVisibility(View.VISIBLE);
152+
                    reading_view.setText(readingStyle.compareTo("kana") == 0? result.getReading(): result.getRomajiReading());
153+
                }
154+
155+
                // If pitch information is available, make it visible
156+
                String pitch = result.getPitch();
157+
                if(pitch != null) {
158+
                    pitch_view.setVisibility(View.VISIBLE);
159+
                    pitch_view.setText(pitch);
160+
                    pitch_view.setOnClickListener(new View.OnClickListener() {
161+
                        @Override
162+
                        public void onClick(View v) {
163+
                            Intent intent = new Intent(context, HelpPitchActivity.class);
164+
                            context.startActivity(intent);
165+
                        }
166+
                    });
167+
                }
168+
169+
                if(result.getAlternatives().size() > 1)
170+
                    additional_info.setText(String.format(context.getResources().getString(R.string.sense_alternatives), getContent(result.getAlternatives())));
171+
                else
172+
                    additional_info.setVisibility(View.GONE);
173+
174+
                senses_view.removeAllViews();
175+
176+
                int sense_pos = 1;
177+
                for (Result.Sense sense : result.getSenses()) {
178+
                    View child = LayoutInflater.from(context).inflate(R.layout.layout_sense, senses_view, false);
179+
                    TextView id_view = child.findViewById(R.id.id_view);
180+
                    TextView lang_view = child.findViewById(R.id.lang_view);
181+
                    TextView sense_view = child.findViewById(R.id.definition_view);
182+
183+
                    id_view.setText(String.format(context.getResources().getString(R.string.sense_number), sense_pos));
184+
                    lang_view.setText(sense.getLanguage());
185+
186+
                    StringBuilder sb = new StringBuilder();
187+
                    sb.append("<font color=\"#909090\"><i>");
188+
                    sb.append(getContent(sense.getInfos()));
189+
                    sb.append("</i></font>");
190+
                    if(sense.getInfos().size() > 0)
191+
                        sb.append(" ");
192+
                    sb.append(getContent(sense.getGlosses()));
193+
                    sense_view.setText(Html.fromHtml(sb.toString()));
194+
195+
                    senses_view.addView(child);
196+
                    sense_pos++;
197+
                }
198+
199+
                result_view.addView(child_result);
200+
            }
201+
        }
202+
    }
203+
204+
    private LayoutInflater mInflater;
205+
    ResultPagerAdapter(Context context) {
206+
        this.mInflater = LayoutInflater.from(context);
207+
        this.context = context;
208+
    }
209+
210+
    @NonNull
211+
    @Override
212+
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
213+
        View view = mInflater.inflate(R.layout.fragment_results, parent, false);
214+
        return new ViewHolder(view, context);
215+
    }
216+
217+
    @Override
218+
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
219+
        if(position == 0)
220+
            holder.showResults();
221+
        else
222+
            holder.showKanjiResults();
223+
    }
224+
225+
    @Override
226+
    public int getItemCount() {
227+
        return 2;
228+
    }
229+
230+
    public void setReadingStyle(String readingStyle) {
231+
        ResultPagerAdapter.readingStyle = readingStyle;
232+
        Log.d(TAG, "reading style updated to " + readingStyle);
233+
        notifyDataSetChanged();
234+
    }
235+
236+
    public void setKanjiResults(List<KanjiResult> kanjiResults) {
237+
        if(kanjiResults == null)
238+
            return;
239+
240+
        ResultPagerAdapter.kanjiResults.clear();
241+
        ResultPagerAdapter.kanjiResults.addAll(kanjiResults);
242+
        Log.d(TAG, "kanjis: " + ResultPagerAdapter.kanjiResults.size());
243+
        notifyDataSetChanged();
244+
    }
245+
246+
    public void setResults(List<Result> results) {
247+
        if(results == null)
248+
            return;
249+
250+
        ResultPagerAdapter.results.clear();
251+
        ResultPagerAdapter.results.addAll(results);
252+
        Log.d(TAG, "results: " + ResultPagerAdapter.results.size());
253+
        notifyDataSetChanged();
254+
    }
255+
}

app/src/main/java/eu/lepiller/nani/SearchResult.java

11
package eu.lepiller.nani;
22
3-
import java.util.ArrayList;
3+
import java.util.List;
44
55
import eu.lepiller.nani.dictionary.DictionaryException;
6+
import eu.lepiller.nani.result.KanjiResult;
67
import eu.lepiller.nani.result.Result;
78
89
class SearchResult {
9-
    private ArrayList<Result> results;
10+
    private List<Result> results;
11+
    private List<KanjiResult> kanjiResults;
1012
    private DictionaryException exception;
1113
    private boolean converted;
1214
    private String text;
1315
14-
    SearchResult(ArrayList<Result> results, String text, boolean converted) {
16+
    SearchResult(List<Result> results, List<KanjiResult> kanjiResults, String text, boolean converted) {
1517
        this.results = results;
18+
        this.kanjiResults = kanjiResults;
1619
        this.converted = converted;
1720
        this.text = text;
1821
    }

2932
        return exception;
3033
    }
3134
32-
    ArrayList<Result> getResults() {
35+
    List<Result> getResults() {
3336
        return results;
3437
    }
3538
39+
    List<KanjiResult> getKanjiResults() {
40+
        return kanjiResults;
41+
    }
42+
3643
    boolean isConverted() {
3744
        return converted;
3845
    }

app/src/main/java/eu/lepiller/nani/dictionary/DictionaryFactory.java

1818
import java.util.List;
1919
import java.util.Locale;
2020
import java.util.Map;
21+
import java.util.Stack;
2122
23+
import eu.lepiller.nani.result.KanjiResult;
2224
import eu.lepiller.nani.result.Result;
2325
2426
public class DictionaryFactory {

129131
                                    chooseLanguage(synopsis),
130132
                                    chooseLanguage(description),
131133
                                    cacheDir, url, size, entries, sha256, lang);
134+
                        } else if (type.compareTo("kanjidic") == 0) {
135+
                            d = new KanjiDict(name,
136+
                                    chooseLanguage(synopsis),
137+
                                    chooseLanguage(description),
138+
                                    cacheDir, url, size, entries, sha256, lang);
132139
                        }
133140
134141
                        if(d != null) {

264271
        return new ArrayList<>(results.values());
265272
    }
266273
274+
    public static KanjiResult searchKanji(String kanji) throws DictionaryException {
275+
        if(instance == null)
276+
            throw new NoDictionaryException();
277+
278+
        int available = 0;
279+
        Stack<KanjiResult> results = new Stack<>();
280+
281+
        for(Dictionary d: dictionaries) {
282+
            if (d instanceof KanjiDict && d.isDownloaded()) {
283+
                available++;
284+
                KanjiResult kanjiResult = ((KanjiDict) d).search(kanji);
285+
                if(kanjiResult != null) {
286+
                    results.add(kanjiResult);
287+
                }
288+
            }
289+
        }
290+
291+
        Log.d(TAG, "search kanji, " + results.size() + " results");
292+
293+
        if(results.size() == 0)
294+
            return null;
295+
296+
        KanjiResult res = results.pop();
297+
        for(KanjiResult r: results) {
298+
            res.merge(r);
299+
        }
300+
301+
        return res;
302+
    }
303+
267304
    private static void augment(Result r) throws IncompatibleFormatException {
268305
        for(Dictionary d: dictionaries) {
269306
            if(d instanceof ResultAugmenterDictionary && d.isDownloaded()) {

app/src/main/java/eu/lepiller/nani/dictionary/FileDictionary.java

254254
    static<T> T searchTrie(RandomAccessFile file, long triePos, byte[] txt, TrieValsDecoder<T> decoder) throws IOException {
255255
        file.seek(triePos);
256256
        if(txt.length == 0) {
257+
            Log.v(TAG, "found trie value, reading values");
257258
            return decoder.decodeVals(file, triePos);
258259
        }
259260

266267
            byte letter = file.readByte();
267268
            Log.v(TAG, "Possible transition " + letter + "; Expected transition: " + txt[0]);
268269
            if(letter == txt[0]) {
269-
                Log.v(TAG, "Taking transition "+letter);
270+
                long nextPos = file.readInt();
271+
                Log.v(TAG, "Taking transition "+letter+" to " + nextPos);
270272
                byte[] ntxt = new byte[txt.length-1];
271273
                System.arraycopy(txt, 1, ntxt, 0, txt.length-1);
272-
                return searchTrie(file, file.readInt(), ntxt, decoder);
274+
                return searchTrie(file, nextPos, ntxt, decoder);
273275
            } else {
274276
                file.skipBytes(4);
275277
            }

app/src/main/java/eu/lepiller/nani/dictionary/KanjiDict.java unknown status 1

1+
package eu.lepiller.nani.dictionary;
2+
3+
import android.util.Log;
4+
5+
import java.io.File;
6+
import java.io.FileNotFoundException;
7+
import java.io.IOException;
8+
import java.io.RandomAccessFile;
9+
import java.util.ArrayList;
10+
import java.util.Arrays;
11+
import java.util.List;
12+
13+
import eu.lepiller.nani.R;
14+
import eu.lepiller.nani.result.KanjiResult;
15+
16+
public class KanjiDict extends FileDictionary {
17+
    final private static String TAG = "KANJIDIC";
18+
    private Huffman readingHuffman, meaningHuffman;
19+
20+
    KanjiDict(String name, String description, String fullDescription, File cacheDir, String url, int fileSize, int entries, String hash, String lang) {
21+
        super(name, description, fullDescription, cacheDir, url, fileSize, entries, hash, lang);
22+
    }
23+
24+
    @Override
25+
    public void remove() {
26+
        super.remove();
27+
        readingHuffman = null;
28+
        meaningHuffman = null;
29+
    }
30+
31+
    @Override
32+
    int getDrawableId() {
33+
        return R.drawable.ic_nani_edrdg;
34+
    }
35+
36+
    KanjiResult getValue(RandomAccessFile file, long pos, String kanji) throws IOException {
37+
        Log.d(TAG, "getValue at " + pos);
38+
        file.seek(pos);
39+
        int stroke = file.readByte();
40+
        Log.d(TAG, "strokes: " + stroke);
41+
42+
        List<String> senses = getHuffmanStringList(file, meaningHuffman);
43+
        List<KanjiResult.Sense> meanings = new ArrayList<>();
44+
        for(String s: senses) {
45+
            meanings.add(new KanjiResult.Sense(this.getLang(), s));
46+
        }
47+
        List<String> kun = getHuffmanStringList(file, readingHuffman);
48+
        List<String> on = getHuffmanStringList(file, readingHuffman);
49+
        List<String> nanori = getHuffmanStringList(file, readingHuffman);
50+
51+
        return new KanjiResult(kanji, stroke, meanings, kun, on, nanori);
52+
    }
53+
54+
    KanjiResult search(final String kanji) throws IncompatibleFormatException {
55+
        if (isDownloaded()) {
56+
            try {
57+
                Log.d(TAG, "search for kanji " + kanji);
58+
                RandomAccessFile file = new RandomAccessFile(getFile(), "r");
59+
                byte[] header = new byte[16];
60+
                int l = file.read(header);
61+
                if (l != header.length)
62+
                    return null;
63+
64+
                // Check file format version
65+
                if(!Arrays.equals(header, "NANI_KANJIDIC001".getBytes())) {
66+
                    StringBuilder error = new StringBuilder("search: incompatible header: [");
67+
                    boolean first = true;
68+
                    for(byte b: header) {
69+
                        if(first)
70+
                            first = false;
71+
                        else
72+
                            error.append(", ");
73+
                        error.append(b);
74+
                    }
75+
                    error.append("].");
76+
                    Log.d(TAG, error.toString());
77+
                    throw new IncompatibleFormatException(getName());
78+
                }
79+
80+
                Log.d(TAG, "header OK");
81+
82+
                byte[] search = kanji.toLowerCase().getBytes();
83+
                file.skipBytes(4); // size
84+
                meaningHuffman = loadHuffman(file);
85+
                readingHuffman = loadHuffman(file);
86+
                long kanjiTriePos = file.getFilePointer();
87+
88+
                Log.d(TAG, "trie pos: " + kanjiTriePos);
89+
90+
                return searchTrie(file, kanjiTriePos, search, new TrieValsDecoder<KanjiResult>() {
91+
                    @Override
92+
                    public KanjiResult decodeVals(RandomAccessFile file1, long pos) throws IOException {
93+
                        Log.d(TAG, "decoding val");
94+
                        file1.seek(pos);
95+
                        return getValue(file1, file1.readInt(), kanji);
96+
                    }
97+
98+
                    @Override
99+
                    public void skipVals(RandomAccessFile file1, long pos) throws IOException {
100+
                        file1.seek(pos);
101+
                        file1.skipBytes(4);
102+
                    }
103+
                });
104+
            } catch (FileNotFoundException e) {
105+
                e.printStackTrace();
106+
            } catch (IOException e) {
107+
                e.printStackTrace();
108+
            }
109+
        }
110+
        return null;
111+
    }
112+
}

app/src/main/java/eu/lepiller/nani/result/KanjiResult.java unknown status 1

1+
package eu.lepiller.nani.result;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
public class KanjiResult {
7+
    public static class Sense {
8+
        private String lang, content;
9+
        public Sense(String lang, String content) {
10+
            this.lang = lang;
11+
            this.content = content;
12+
        }
13+
14+
        public String getContent() {
15+
            return content;
16+
        }
17+
18+
        public String getLang() {
19+
            return lang;
20+
        }
21+
    }
22+
    private String kanji;
23+
    private int stroke;
24+
    private List<Sense> senses;
25+
    private List<String> on;
26+
    private List<String> kun;
27+
    private List<String> nanori;
28+
29+
    public KanjiResult(String kanji, int stroke, List<Sense> senses, List<String> kun, List<String> on, List<String> nanori) {
30+
        this.kanji = kanji;
31+
        this.stroke = stroke;
32+
        this.senses = senses;
33+
        this.kun = kun;
34+
        this.on = on;
35+
        this.nanori = nanori;
36+
    }
37+
38+
    public List<String> addUniq(List<String> a, List<String> b) {
39+
        List<String> res = new ArrayList<>();
40+
        for(String s: a) {
41+
            if(res.contains(s))
42+
                continue;
43+
            res.add(s);
44+
        }
45+
        for(String s: b) {
46+
            if(res.contains(s))
47+
                continue;
48+
            res.add(s);
49+
        }
50+
        return res;
51+
    }
52+
53+
    public void merge(KanjiResult other) {
54+
        this.senses.addAll(other.senses);
55+
        this.on = addUniq(this.on, other.on);
56+
        this.on = addUniq(this.kun, other.kun);
57+
        this.on = addUniq(this.nanori, other.nanori);
58+
    }
59+
60+
    public List<Sense> getSenses() {
61+
        return senses;
62+
    }
63+
64+
    public int getStroke() {
65+
        return stroke;
66+
    }
67+
68+
    public List<String> getKun() {
69+
        return kun;
70+
    }
71+
72+
    public List<String> getNanori() {
73+
        return nanori;
74+
    }
75+
76+
    public List<String> getOn() {
77+
        return on;
78+
    }
79+
80+
    public String getKanji() {
81+
        return kanji;
82+
    }
83+
}

app/src/main/res/layout/content_main.xml

4040
                android:layout_width="match_parent"
4141
                android:layout_height="wrap_content" />
4242
43-
            <ScrollView
43+
            <LinearLayout
4444
                android:layout_width="match_parent"
4545
                android:layout_height="0dp"
4646
                android:layout_weight="1">
4747
48-
                <LinearLayout
49-
                    android:id="@+id/results_view"
48+
                <androidx.viewpager2.widget.ViewPager2
5049
                    android:layout_width="match_parent"
51-
                    android:layout_height="wrap_content"
52-
                    android:layout_marginTop="16dp"
53-
                    android:layout_marginBottom="8dp"
54-
                    android:orientation="vertical" />
55-
            </ScrollView>
50+
                    android:layout_height="match_parent"
51+
                    android:id="@+id/pager" />
52+
            </LinearLayout>
5653
5754
            <LinearLayout
5855
                android:id="@+id/bottom_layout"

6057
                android:layout_height="wrap_content"
6158
                android:orientation="horizontal">
6259
63-
                <Space
60+
                <com.google.android.material.tabs.TabLayout
61+
                    android:id="@+id/tab_layout"
6462
                    android:layout_width="0dp"
6563
                    android:layout_height="wrap_content"
6664
                    android:layout_weight="1" />

7068
                    android:layout_width="wrap_content"
7169
                    android:layout_height="wrap_content"
7270
                    android:minWidth="0dp"
73-
                    android:text="???" />
71+
                    android:text="???"
72+
                    android:layout_gravity="center_vertical"/>
7473
            </LinearLayout>
7574
7675
        </LinearLayout>

app/src/main/res/layout/fragment_results.xml unknown status 1

1+
<?xml version="1.0" encoding="utf-8"?>
2+
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
3+
    xmlns:tools="http://schemas.android.com/tools"
4+
    android:layout_width="match_parent"
5+
    android:layout_height="match_parent"
6+
    android:orientation="vertical">
7+
8+
    <LinearLayout
9+
        android:id="@+id/results_view"
10+
        android:layout_width="match_parent"
11+
        android:layout_height="wrap_content"
12+
        android:orientation="vertical" />
13+
</ScrollView>
13<
014<
\ No newline at end of file

app/src/main/res/layout/layout_kanji.xml unknown status 1

1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
    android:layout_width="match_parent"
4+
    android:layout_height="wrap_content"
5+
    android:orientation="horizontal"
6+
    android:layout_marginBottom="16dp">
7+
8+
    <LinearLayout
9+
        android:layout_width="wrap_content"
10+
        android:layout_height="wrap_content"
11+
        android:orientation="vertical"
12+
        android:layout_gravity="center_vertical">
13+
        <TextView
14+
            android:layout_width="wrap_content"
15+
            android:layout_height="wrap_content"
16+
            android:id="@+id/kanji_view"
17+
            android:textSize="@dimen/title_size"
18+
            android:layout_gravity="center"
19+
            android:layout_marginLeft="8dp"
20+
            android:layout_marginStart="8dp"
21+
            android:layout_marginRight="8dp"
22+
            android:layout_marginEnd="8dp" />
23+
24+
        <TextView
25+
            android:layout_width="wrap_content"
26+
            android:layout_height="wrap_content"
27+
            android:layout_gravity="center_horizontal"
28+
            android:id="@+id/kanji_strokes" />
29+
    </LinearLayout>
30+
31+
    <LinearLayout
32+
        android:layout_width="0dp"
33+
        android:layout_height="wrap_content"
34+
        android:layout_weight="1"
35+
        android:orientation="vertical">
36+
        <LinearLayout
37+
            android:orientation="vertical"
38+
            android:id="@+id/sense_view"
39+
            android:layout_width="wrap_content"
40+
            android:layout_height="wrap_content">
41+
42+
        </LinearLayout>
43+
44+
        <TextView
45+
            android:layout_width="match_parent"
46+
            android:layout_height="wrap_content"
47+
            android:layout_marginLeft="8dp"
48+
            android:layout_marginStart="8dp"
49+
            android:id="@+id/kun_reading" />
50+
51+
        <TextView
52+
            android:layout_width="match_parent"
53+
            android:layout_height="wrap_content"
54+
            android:layout_marginLeft="8dp"
55+
            android:layout_marginStart="8dp"
56+
            android:id="@+id/on_reading" />
57+
58+
        <TextView
59+
            android:layout_width="match_parent"
60+
            android:layout_height="wrap_content"
61+
            android:layout_marginLeft="8dp"
62+
            android:layout_marginStart="8dp"
63+
            android:id="@+id/nanori_reading" />
64+
    </LinearLayout>
65+
66+
67+
</LinearLayout>
67<
068<
\ No newline at end of file

app/src/main/res/layout/layout_result.xml

11
<?xml version="1.0" encoding="utf-8"?>
22
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3-
    xmlns:tools="http://schemas.android.com/tools"
43
    android:layout_width="match_parent"
54
    android:layout_height="wrap_content"
65
    xmlns:app="http://schemas.android.com/apk/res-auto"
7-
    android:layout_marginBottom="32dp"
8-
    android:orientation="vertical">
6+
    android:orientation="vertical"
7+
    android:layout_marginBottom="8dp">
98
109
    <LinearLayout
1110
        android:layout_width="wrap_content"

app/src/main/res/layout/layout_result_list.xml unknown status 1

1+
<?xml version="1.0" encoding="utf-8"?>
2+
<ScrollView
3+
    xmlns:android="http://schemas.android.com/apk/res/android"
4+
    android:layout_width="match_parent"
5+
    android:layout_height="match_parent">
6+
7+
    <LinearLayout
8+
        android:id="@+id/results_view"
9+
        android:layout_width="match_parent"
10+
        android:layout_height="wrap_content"
11+
        android:layout_marginTop="16dp"
12+
        android:layout_marginBottom="8dp"
13+
        android:orientation="vertical" />
14+
15+
</ScrollView>
15<
016<
\ No newline at end of file

app/src/main/res/values-fr/strings.xml

5959
    <string name="dictionary_expected_size_kb">Taille : %s Ko</string>
6060
    <!-- Result view -->
6161
    <string name="sense_number">%d.</string>
62-
    <string name="sense_separator">" ; "</string>
6362
    <string name="sense_alternatives">Formes alternatives : %s</string>
6463
    <!-- About activity -->
6564
    <string name="nani_about">????Nani\????? est un dictionnaire japonais hors-ligne pour Android. Il vous aide ?? trouver et comprendre des mots sans acc??s internet, en t??l??chargeant des sources de dictionnaires.

app/src/main/res/values-uk/strings.xml

6464
    <string name="view_source">?????????????????????? ???????????????????? ??????</string>
6565
    <string name="report">???????????????????? ?????? ??????????????</string>
6666
    <string name="sense_alternatives">?????????????????????????? ??????????: %s</string>
67-
    <string name="sense_separator">"; "</string>
6867
    <string name="sense_number">%d.</string>
6968
    <string name="dictionary_expected_size_mb">???????????? ??????????: %s????</string>
7069
    <string name="dictionary_expected_size_kb">???????????? ??????????: %s????</string>

app/src/main/res/values/dimens.xml

22
    <dimen name="fab_margin">16dp</dimen>
33
    <dimen name="title_size">32sp</dimen>
44
    <dimen name="subtitle_size">24sp</dimen>
5+
    <dimen name="text_margin">16dp</dimen>
56
</resources>

app/src/main/res/values/strings.xml

8080
8181
    <!-- Result view -->
8282
    <string name="sense_number">%d.</string>
83-
    <string name="sense_separator">"; "</string>
83+
    <string name="sense_separator" translatable="false">"???"</string>
8484
    <string name="sense_alternatives">Alternative forms: %s</string>
85+
    <string name="tab_kanji">Kanji</string>
86+
    <string name="tab_results">Word</string>
87+
    <plurals name="kanji_stroke">
88+
        <item quantity="one">%d stroke</item>
89+
        <item quantity="other">%d strokes</item>
90+
    </plurals>
91+
    <string name="kun_reading">kunyomi: %s</string>
92+
    <string name="on_reading">onyomi: %s</string>
93+
    <string name="nanori_reading">nanori: %s</string>
8594
8695
    <!-- About activity -->
8796
    <string name="nani_about">???Nani???? is an offline Japanese dictionary for Android. It helps

239248
    records the position of the downstep, as there can only be one downstep in a word. </string>
240249
    <string name="help_pitch_learn_more">Learn more on Wikipedia</string>
241250
    <string name="help_pitch_wiki_link">https://en.wikipedia.org/wiki/Japanese_pitch_accent</string>
251+
    <string name="large_text">
252+
        "Material is the metaphor.\n\n"
253+
254+
        "A material metaphor is the unifying theory of a rationalized space and a system of motion."
255+
        "The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
256+
        "technologically advanced and open to imagination and magic.\n"
257+
        "Surfaces and edges of the material provide visual cues that are grounded in reality. The "
258+
        "use of familiar tactile attributes helps users quickly understand affordances. Yet the "
259+
        "flexibility of the material creates new affordances that supercede those in the physical "
260+
        "world, without breaking the rules of physics.\n"
261+
        "The fundamentals of light, surface, and movement are key to conveying how objects move, "
262+
        "interact, and exist in space and in relation to each other. Realistic lighting shows "
263+
        "seams, divides space, and indicates moving parts.\n\n"
264+
265+
        "Bold, graphic, intentional.\n\n"
266+
267+
        "The foundational elements of print based design typography, grids, space, scale, color, "
268+
        "and use of imagery guide visual treatments. These elements do far more than please the "
269+
        "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
270+
        "imagery, large scale typography, and intentional white space create a bold and graphic "
271+
        "interface that immerse the user in the experience.\n"
272+
        "An emphasis on user actions makes core functionality immediately apparent and provides "
273+
        "waypoints for the user.\n\n"
274+
275+
        "Motion provides meaning.\n\n"
276+
277+
        "Motion respects and reinforces the user as the prime mover. Primary user actions are "
278+
        "inflection points that initiate motion, transforming the whole design.\n"
279+
        "All action takes place in a single environment. Objects are presented to the user without "
280+
        "breaking the continuity of experience even as they transform and reorganize.\n"
281+
        "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
282+
        "Feedback is subtle yet clear. Transitions are ef???cient yet coherent.\n\n"
283+
284+
        "3D world.\n\n"
285+
286+
        "The material environment is a 3D space, which means all objects have x, y, and z "
287+
        "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
288+
        "positive z-axis extending towards the viewer. Every sheet of material occupies a single "
289+
        "position along the z-axis and has a standard 1dp thickness.\n"
290+
        "On the web, the z-axis is used for layering and not for perspective. The 3D world is "
291+
        "emulated by manipulating the y-axis.\n\n"
292+
293+
        "Light and shadow.\n\n"
294+
295+
        "Within the material environment, virtual lights illuminate the scene. Key lights create "
296+
        "directional shadows, while ambient light creates soft shadows from all angles.\n"
297+
        "Shadows in the material environment are cast by these two light sources. In Android "
298+
        "development, shadows occur when light sources are blocked by sheets of material at "
299+
        "various positions along the z-axis. On the web, shadows are depicted by manipulating the "
300+
        "y-axis only. The following example shows the card with a height of 6dp.\n\n"
301+
302+
        "Resting elevation.\n\n"
303+
304+
        "All material objects, regardless of size, have a resting elevation, or default elevation "
305+
        "that does not change. If an object changes elevation, it should return to its resting "
306+
        "elevation as soon as possible.\n\n"
307+
308+
        "Component elevations.\n\n"
309+
310+
        "The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
311+
        "does not vary from 6dp in one app to 16dp in another app).\n"
312+
        "Components may have different resting elevations across platforms, depending on the depth "
313+
        "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
314+
315+
        "Responsive elevation and dynamic elevation offsets.\n\n"
316+
317+
        "Some component types have responsive elevation, meaning they change elevation in response "
318+
        "to user input (e.g., normal, focused, and pressed) or system events. These elevation "
319+
        "changes are consistently implemented using dynamic elevation offsets.\n"
320+
        "Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
321+
        "to the component???s resting state. They ensure that elevation changes are consistent "
322+
        "across actions and component types. For example, all components that lift on press have "
323+
        "the same elevation change relative to their resting elevation.\n"
324+
        "Once the input event is completed or cancelled, the component will return to its resting "
325+
        "elevation.\n\n"
326+
327+
        "Avoiding elevation interference.\n\n"
328+
329+
        "Components with responsive elevations may encounter other components as they move between "
330+
        "their resting elevations and dynamic elevation offsets. Because material cannot pass "
331+
        "through other material, components avoid interfering with one another any number of ways, "
332+
        "whether on a per component basis or using the entire app layout.\n"
333+
        "On a component level, components can move or be removed before they cause interference. "
334+
        "For example, a floating action button (FAB) can disappear or move off screen before a "
335+
        "user picks up a card, or it can move if a snackbar appears.\n"
336+
        "On the layout level, design your app layout to minimize opportunities for interference. "
337+
        "For example, position the FAB to one side of stream of a cards so the FAB won???t interfere "
338+
        "when a user tries to pick up one of cards.\n\n"
339+
    </string>
242340
</resources>