diff --git a/app/build.gradle b/app/build.gradle index 5f65a282..a1291ebb 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "dnd.jon.spellbook" minSdkVersion 24 targetSdkVersion 33 - versionCode 300060 - versionName "3.0.6" + versionCode 300100 + versionName "3.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release } diff --git a/app/src/main/java/dnd/jon/spellbook/DisplayUtils.java b/app/src/main/java/dnd/jon/spellbook/DisplayUtils.java index f622c5b9..444d810d 100644 --- a/app/src/main/java/dnd/jon/spellbook/DisplayUtils.java +++ b/app/src/main/java/dnd/jon/spellbook/DisplayUtils.java @@ -182,4 +182,20 @@ static Range rangeFromString(Context context, String s) { return Range.fromString(s, (t) -> getDisplayName(context, t), (us) -> unitFromString(context, LengthUnit.class, us)); } + + // Spell prompt text + public static String locationPrompt(Context context, int nLocations) { + return context.getString(nLocations == 1 ? R.string.location : R.string.location); + } + public static String concentrationPrompt(Context context) { return context.getString(R.string.concentration); } + public static String castingTimePrompt(Context context) { return context.getString(R.string.casting_time); } + public static String rangePrompt(Context context) { return context.getString(R.string.range); } + public static String componentsPrompt(Context context) { return context.getString(R.string.components); } + public static String materialsPrompt(Context context) { return context.getString(R.string.materials); } + public static String royaltyPrompt(Context context) { return context.getString(R.string.royalty); } + public static String durationPrompt(Context context) { return context.getString(R.string.duration); } + public static String classesPrompt(Context context) { return context.getString(R.string.classes); } + public static String tceExpandedClassesPrompt(Context context) { return context.getString(R.string.tce_expanded_classes); } + public static String descriptionPrompt(Context context) { return context.getString(R.string.description); } + public static String higherLevelsPrompt(Context context) { return context.getString(R.string.higher_level); } } diff --git a/app/src/main/java/dnd/jon/spellbook/GlobalInfo.java b/app/src/main/java/dnd/jon/spellbook/GlobalInfo.java index aa810d5b..1d0404f3 100644 --- a/app/src/main/java/dnd/jon/spellbook/GlobalInfo.java +++ b/app/src/main/java/dnd/jon/spellbook/GlobalInfo.java @@ -2,13 +2,13 @@ class GlobalInfo { - static final Version VERSION = new Version(3,0,6); + static final Version VERSION = new Version(3,1,0); static final String VERSION_CODE = VERSION.string(); // We don't always want to show an update message // i.e. for updates that are pure bugfixes, the old message may be // more useful to users - static final Version UPDATE_LOG_VERSION = new Version(3,0,6); + static final Version UPDATE_LOG_VERSION = new Version(3,1,0); static final String UPDATE_LOG_CODE = UPDATE_LOG_VERSION.string(); static final int ANDROID_VERSION = android.os.Build.VERSION.SDK_INT; diff --git a/app/src/main/java/dnd/jon/spellbook/LocalizationUtils.java b/app/src/main/java/dnd/jon/spellbook/LocalizationUtils.java index be698112..cd78ff1a 100644 --- a/app/src/main/java/dnd/jon/spellbook/LocalizationUtils.java +++ b/app/src/main/java/dnd/jon/spellbook/LocalizationUtils.java @@ -1,15 +1,28 @@ package dnd.jon.spellbook; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; import android.os.Build; import android.os.LocaleList; +import androidx.annotation.NonNull; + +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; +import java.util.List; import java.util.Locale; import java.util.Map; public class LocalizationUtils { + private static final List supportedLanguages = new ArrayList<>(); + static { + supportedLanguages.add("en"); + supportedLanguages.add("pt"); + } + private static final Map tableLayoutIDs = new EnumMap(CasterClass.class) {{ put(CasterClass.ARTIFICER, R.layout.artificer_table_layout); put(CasterClass.BARD, R.layout.bard_table_layout); @@ -33,14 +46,39 @@ public class LocalizationUtils { put(CasterClass.WIZARD, R.string.wizard_spellcasting_info); }}; - static String getCurrentLanguage(){ + static Locale getLocale() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){ - return LocaleList.getDefault().get(0).getLanguage(); + return LocaleList.getDefault().get(0); } else{ - return Locale.getDefault().getLanguage(); + return Locale.getDefault(); + } + } + + static Locale defaultSpellLocale() { + final Locale locale = getLocale(); + final String language = locale.getLanguage(); + if (supportedLanguages.contains(language)) { + return locale; + } else { + return Locale.US; } } + static String getCurrentLanguage() { + return getLocale().getLanguage(); + } + + static @NonNull Context getLocalizedContext(Context context, Locale desiredLocale) { + Configuration conf = context.getResources().getConfiguration(); + conf = new Configuration(conf); + conf.setLocale(desiredLocale); + return context.createConfigurationContext(conf); + } + + static @NonNull Resources getLocalizedResources(Context context, Locale desiredLocale) { + return getLocalizedContext(context, desiredLocale).getResources(); + } + static CasterClass[] supportedClasses() { return CasterClass.values(); } static Source[] supportedSources() { return Source.values(); } static Source[] supportedCoreSourcebooks() { return Source.coreSourcebooks(); } diff --git a/app/src/main/java/dnd/jon/spellbook/MainActivity.java b/app/src/main/java/dnd/jon/spellbook/MainActivity.java index 1dfa95a2..3a48cb26 100755 --- a/app/src/main/java/dnd/jon/spellbook/MainActivity.java +++ b/app/src/main/java/dnd/jon/spellbook/MainActivity.java @@ -53,6 +53,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -228,7 +229,8 @@ protected void onCreate(final Bundle savedInstanceState) { setSupportActionBar(binding.toolbar); // Listen for preference changes - PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); // The DrawerLayout and the left navigation view drawerLayout = binding.drawerLayout; @@ -1247,8 +1249,8 @@ private void showUpdateDialog(boolean checkIfNecessary) { final boolean noCharacters = (characterNames == null) || characterNames.size() <= 0; final boolean toShow = !checkIfNecessary || !(prefs.contains(key) || noCharacters); if (toShow) { - final int titleID = string.update_03_00_06_title; - final int descriptionID = string.update_03_00_06_description; + final int titleID = string.update_03_01_00_title; + final int descriptionID = string.update_03_01_00_description; final Runnable onDismissAction = () -> { final SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(key, true).apply(); @@ -1330,6 +1332,9 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin } else if (key.equals(getString(string.spell_list_locations))) { updateBottomBarVisibility(); updateSpellListMenuVisibility(); + } else if (key.equals(getString(string.spell_language_key))) { + final Locale locale = new Locale(sharedPreferences.getString(key, getString(string.english_code))); + viewModel.updateSpellsForLocale(locale); } } diff --git a/app/src/main/java/dnd/jon/spellbook/SpellAdapter.java b/app/src/main/java/dnd/jon/spellbook/SpellAdapter.java index fce68f96..72399ca4 100755 --- a/app/src/main/java/dnd/jon/spellbook/SpellAdapter.java +++ b/app/src/main/java/dnd/jon/spellbook/SpellAdapter.java @@ -45,6 +45,7 @@ public SpellRowHolder(SpellRowBinding binding, SpellbookViewModel viewModel) { public void bind(Spell s) { spell = s; binding.setSpell(spell); + binding.setContext(viewModel.getSpellContext()); binding.executePendingBindings(); //Set the buttons to show the appropriate images @@ -91,7 +92,6 @@ public void bind(Spell s) { final SpellRowHolder srh = (SpellRowHolder) view.getTag(); final Spell spell = srh.getSpell(); this.viewModel.setCurrentSpell(spell); - System.out.println(spell.getName()); }; } diff --git a/app/src/main/java/dnd/jon/spellbook/SpellCodec.java b/app/src/main/java/dnd/jon/spellbook/SpellCodec.java index bb08e941..045a8c35 100755 --- a/app/src/main/java/dnd/jon/spellbook/SpellCodec.java +++ b/app/src/main/java/dnd/jon/spellbook/SpellCodec.java @@ -142,10 +142,12 @@ private Spell parseSpell(JSONObject json, SpellBuilder b, boolean useInternal) t return b.buildAndReset(); } - List parseSpellList(@Nullable JSONArray jsonArray, boolean useInternal) throws Exception { + // TODO: This is kinda gross - try to find a way not to need the useInternal + // It should be possible to just replace it with the locale + List parseSpellList(@Nullable JSONArray jsonArray, boolean useInternal, Locale locale) throws Exception { final List spells = new ArrayList<>(); - final SpellBuilder b = useInternal ? new SpellBuilder(context, Locale.US) : new SpellBuilder(context); + final SpellBuilder b = useInternal ? new SpellBuilder(context, Locale.US) : new SpellBuilder(context, locale); try { for (int i = 0; i < jsonArray.length(); i++) { @@ -159,8 +161,16 @@ List parseSpellList(@Nullable JSONArray jsonArray, boolean useInternal) t return spells; } + List parseSpellList(JSONArray jsonArray, Locale locale) throws Exception { + return parseSpellList(jsonArray, false, locale); + } + + List parseSpellList(JSONArray jsonArray, boolean useInternalParse) throws Exception { + return parseSpellList(jsonArray, useInternalParse, LocalizationUtils.getLocale()); + } + List parseSpellList(JSONArray jsonArray) throws Exception { - return parseSpellList(jsonArray, false); + return parseSpellList(jsonArray, false, LocalizationUtils.getLocale()); } JSONObject toJSON(Spell spell) throws JSONException { diff --git a/app/src/main/java/dnd/jon/spellbook/SpellWindowFragment.java b/app/src/main/java/dnd/jon/spellbook/SpellWindowFragment.java index b0b35381..ee6651e8 100644 --- a/app/src/main/java/dnd/jon/spellbook/SpellWindowFragment.java +++ b/app/src/main/java/dnd/jon/spellbook/SpellWindowFragment.java @@ -95,13 +95,15 @@ public View onCreateView(@NonNull LayoutInflater inflater, binding.setSpell(spell); //binding.getRoot().setVisibility(spell == null ? View.GONE : View.VISIBLE); binding.setUseExpanded(useExpanded); - binding.executePendingBindings(); binding.setTextSize(textSize); binding.setTextColor(textColor); + binding.setContext(viewModel.getSpellContext()); + binding.executePendingBindings(); viewModel.currentSpellFavoriteLD().observe(lifecycleOwner, binding.favoriteButton::set); viewModel.currentSpellPreparedLD().observe(lifecycleOwner, binding.preparedButton::set); viewModel.currentSpellKnownLD().observe(lifecycleOwner, binding.knownButton::set); + viewModel.currentSpellsContext().observe(lifecycleOwner, binding::setContext); // spellStatus.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { // @Override diff --git a/app/src/main/java/dnd/jon/spellbook/SpellbookUtils.java b/app/src/main/java/dnd/jon/spellbook/SpellbookUtils.java index 42f5af0e..d01512d1 100755 --- a/app/src/main/java/dnd/jon/spellbook/SpellbookUtils.java +++ b/app/src/main/java/dnd/jon/spellbook/SpellbookUtils.java @@ -3,6 +3,8 @@ import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Color; import android.view.LayoutInflater; import android.widget.Spinner; @@ -22,6 +24,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; diff --git a/app/src/main/java/dnd/jon/spellbook/SpellbookViewModel.java b/app/src/main/java/dnd/jon/spellbook/SpellbookViewModel.java index 1876151d..90ff2e4d 100644 --- a/app/src/main/java/dnd/jon/spellbook/SpellbookViewModel.java +++ b/app/src/main/java/dnd/jon/spellbook/SpellbookViewModel.java @@ -2,6 +2,8 @@ import android.app.Application; import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; import android.os.Build; import android.os.FileObserver; import android.util.Log; @@ -16,6 +18,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; +import androidx.preference.PreferenceManager; import org.json.JSONArray; import org.json.JSONException; @@ -25,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -74,8 +78,11 @@ public class SpellbookViewModel extends ViewModel implements Filterable { private final MutableLiveData currentSpellSlotStatusLD; private static List englishSpells = new ArrayList<>(); - private final List spells; + private List spells; private List currentSpellList; + private String spellsFilename; + private final MutableLiveData spellsContext; + private Locale spellsLocale; private final MutableLiveData> currentSpellsLD; private final MutableLiveData currentSpellFavoriteLD; private final MutableLiveData currentSpellPreparedLD; @@ -84,7 +91,7 @@ public class SpellbookViewModel extends ViewModel implements Filterable { private final MutableLiveData currentUseExpandedLD; private final MutableLiveData spellTableVisibleLD; - private final SpellCodec spellCodec; + private SpellCodec spellCodec; private static final List SORT_PROPERTY_IDS = Arrays.asList(BR.firstSortField, BR.firstSortReverse, BR.secondSortField, BR.secondSortReverse); @@ -95,13 +102,28 @@ private static LiveData distinctTransform(LiveData source, Function< public SpellbookViewModel(Application application) { this.application = application; + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application); + final String spellLanguageKey = application.getString(R.string.spell_language_key); + final String spellsLocaleString = sharedPreferences.getString(spellLanguageKey, null); + this.spellsLocale = spellsLocaleString == null ? LocalizationUtils.defaultSpellLocale() : new Locale(spellsLocaleString); + + // If we don't have an existing value for the spell language setting + // we set the default. + // TODO: Can we do this in the XML? It's default locale-dependent + if (spellsLocaleString == null) { + final SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(spellLanguageKey, this.spellsLocale.getLanguage()); + editor.apply(); + } + final Context spellsContext = LocalizationUtils.getLocalizedContext(application, this.spellsLocale); + this.spellsContext = new MutableLiveData<>(spellsContext); + this.spellCodec = new SpellCodec(spellsContext); + this.profilesDir = FilesystemUtils.createFileDirectory(application, PROFILES_DIR_NAME); this.statusesDir = FilesystemUtils.createFileDirectory(application, STATUSES_DIR_NAME); this.createdSourcesDir = FilesystemUtils.createFileDirectory(application, CREATED_SOURCES_DIR_NAME); this.createdSpellsDir = FilesystemUtils.createFileDirectory(application, CREATED_SPELLS_DIR_NAME); - this.spellCodec = new SpellCodec(application); - this.currentProfileLD = new MutableLiveData<>(); this.characterNamesLD = new MutableLiveData<>(); this.statusNamesLD = new MutableLiveData<>(); @@ -110,8 +132,8 @@ public SpellbookViewModel(Application application) { this.currentSpellFilterStatusLD = new MutableLiveData<>(); this.currentSortFilterStatusLD = new MutableLiveData<>(); this.currentSpellSlotStatusLD = new MutableLiveData<>(); - final String spellsFilename = application.getResources().getString(R.string.spells_filename); - this.spells = loadSpellsFromFile(spellsFilename, false); + this.spellsFilename = spellsContext.getResources().getString(R.string.spells_filename); + this.spells = loadSpellsFromFile(spellsFilename, this.spellsLocale); this.currentSpellList = new ArrayList<>(spells); this.currentSpellsLD = new MutableLiveData<>(spells); this.currentSpellLD = new MutableLiveData<>(); @@ -134,7 +156,7 @@ public SpellbookViewModel(Application application) { // If we don't already have the english spells, get them if (englishSpells.size() == 0) { - englishSpells = loadSpellsFromFile(ENGLISH_SPELLS_FILENAME, true); + englishSpells = loadSpellsFromFile(ENGLISH_SPELLS_FILENAME, Locale.US); } // Whenever a file is created or deleted in the profiles folder @@ -150,11 +172,34 @@ public SpellbookViewModel(Application application) { createdSpellsDirObserver.startWatching(); } - private List loadSpellsFromFile(String filename, boolean useInternalParse) { + void updateSpellsForLocale(Locale locale) { + this.spellsLocale = locale; + final Context context = LocalizationUtils.getLocalizedContext(this.getContext(), locale); + this.spellsContext.setValue(context); + final Resources resources = context.getResources(); + final String filename = resources.getString(R.string.spells_filename); + this.spells = loadSpellsFromFile(filename, locale); + this.spellCodec = new SpellCodec(context); + + // If we switch locales, we need to update the current spell + // to the version from the new locale + final Spell spell = currentSpell().getValue(); + if (spell != null) { + final int spellID = spell.getID(); + final Spell newSpell = this.spells.stream().filter(s -> s.getID() == spellID).findAny().orElse(null); + currentSpellLD.setValue(newSpell); + } + filter(); + + + } + + private List loadSpellsFromFile(String filename, Locale locale) { try { final JSONArray jsonArray = JSONUtils.loadJSONArrayFromAsset(application, filename); - final SpellCodec codec = new SpellCodec(application); - return codec.parseSpellList(jsonArray, useInternalParse); + final SpellCodec codec = new SpellCodec(LocalizationUtils.getLocalizedContext(application, locale)); + final boolean useInternalParse = locale == Locale.US; + return codec.parseSpellList(jsonArray, useInternalParse, locale); } catch (Exception e) { //TODO: Better error handling? e.printStackTrace(); @@ -200,6 +245,8 @@ void setCurrentSpell(Spell spell) { } List getAllSpells() { return spells; } + LiveData currentSpellsContext() { return spellsContext; } + Context getSpellContext() { return spellsContext.getValue(); } private String nameValidator(String name, int emptyItemID, int itemTypeID, List existingItems) { if (name.isEmpty()) { diff --git a/app/src/main/res/layout/spell_row.xml b/app/src/main/res/layout/spell_row.xml index 8d772368..3e1dee9b 100755 --- a/app/src/main/res/layout/spell_row.xml +++ b/app/src/main/res/layout/spell_row.xml @@ -5,6 +5,7 @@ + + @@ -277,7 +278,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:textSize="@{textSize}" - android:text="@string/higher_level" + android:text="@{DisplayUtils.higherLevelsPrompt(context)}" android:textStyle="bold" android:visibility="@{spell.higherLevel.isEmpty ? View.GONE : View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 993ab546..98a59bc3 100755 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -393,6 +393,8 @@ Esta atualização adiciona novamente os feitiços do Guia do Aventureiro Astral, que foram removidos por engano na v3.0.0.\n\nAlém disso, existem algumas pequenas correções de bugs. Atualização da versão 3.0.6 O livro de feitiços foi atualizado para incluir o feitiço do Guia do Mestre da Guilda para Ravnica.\n\nUm erro no conteúdo de Parede de Luz foi corrigido. + Atualização da versão 3.1 + Esta atualização adiciona a capacidade de alterar o idioma dos feitiços (atualmente inglês e português estão disponíveis). Renomear @@ -437,6 +439,10 @@ BAF Botão circular + + Soletrar linguagem + ${portuguese_code} + Imagem de fundo do livro diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 9c623ec1..cec2a2bd 100755 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -78,4 +78,14 @@ @string/both + + @string/english + @string/portuguese + + + + @string/english_code + @string/portuguese_code + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52f4166e..726ff3aa 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -394,6 +394,8 @@ This update re-adds spells from the Astral Adventurer\'s Guide, which were mistakenly removed in v3.0.0.\n\nAdditionally, there are some small bugfixes. Version 3.0.6 update The spellbook has been updated to include the spell from the Guildmaster\'s Guide to Ravnica.\n\nA mistake in the content of Wall of Light has been corrected. + Version 3.1 update + This update adds the ability to change the language of spells (currently English and Portuguese are available). Rename @@ -438,6 +440,15 @@ FAB Circular button + + spell_language + Spell language + English + en + Português + pt + ${english_code} + Book background image diff --git a/app/src/main/res/xml-sw600dp/settings_screen.xml b/app/src/main/res/xml-sw600dp/settings_screen.xml index 4994808b..a351151d 100644 --- a/app/src/main/res/xml-sw600dp/settings_screen.xml +++ b/app/src/main/res/xml-sw600dp/settings_screen.xml @@ -41,6 +41,15 @@ android:defaultValue="@string/bottom_navbar" /> + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings_screen.xml b/app/src/main/res/xml/settings_screen.xml index ebfe927b..aadbbbd7 100644 --- a/app/src/main/res/xml/settings_screen.xml +++ b/app/src/main/res/xml/settings_screen.xml @@ -50,6 +50,14 @@ android:defaultValue="@string/bottom_navbar" /> + + \ No newline at end of file