diff --git a/app/build.gradle b/app/build.gradle
index f9a12a36..a6ae4339 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
applicationId "org.qp.android"
minSdk 26
targetSdk 34
- versionCode 202503
- versionName "3.25.3"
+ versionCode 202504
+ versionName "3.25.4"
resourceConfigurations += ['en', 'ru']
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5612caaa..9b73f13e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
+
diff --git a/app/src/main/java/org/qp/android/model/repository/LocalGame.java b/app/src/main/java/org/qp/android/model/repository/LocalGame.java
index d0693534..f3ad4cec 100644
--- a/app/src/main/java/org/qp/android/model/repository/LocalGame.java
+++ b/app/src/main/java/org/qp/android/model/repository/LocalGame.java
@@ -1,5 +1,6 @@
package org.qp.android.model.repository;
+import static org.qp.android.helpers.utils.DirUtil.MOD_DIR_NAME;
import static org.qp.android.helpers.utils.FileUtil.documentWrap;
import static org.qp.android.helpers.utils.FileUtil.findOrCreateFile;
import static org.qp.android.helpers.utils.FileUtil.forceCreateFile;
@@ -149,6 +150,20 @@ public boolean searchAndWriteFileInfo(DocumentFile rootDir, GameData data) {
}
});
+ var modDir = fromRelPath(context, MOD_DIR_NAME, rootDir);
+ if (isWritableDir(context, modDir)) {
+ var modFiles = modDir.listFiles();
+ if (modFiles != null || modFiles.length != 0) {
+ Arrays.stream(modFiles).forEach(d -> {
+ var dirExtension = documentWrap(d).getExtension();
+ var lcName = dirExtension.toLowerCase(Locale.ROOT);
+ if (lcName.endsWith("qsp") || lcName.endsWith("gam")) {
+ gameFiles.add(d.getUri());
+ }
+ });
+ }
+ }
+
if (gameFiles.isEmpty()) {
var allFiles = DocumentFileUtils.search(
rootDir,
diff --git a/app/src/main/java/org/qp/android/model/service/HtmlProcessor.java b/app/src/main/java/org/qp/android/model/service/HtmlProcessor.java
index 989f02eb..56b891f0 100644
--- a/app/src/main/java/org/qp/android/model/service/HtmlProcessor.java
+++ b/app/src/main/java/org/qp/android/model/service/HtmlProcessor.java
@@ -1,7 +1,6 @@
package org.qp.android.model.service;
import static org.qp.android.helpers.utils.Base64Util.encodeBase64;
-import static org.qp.android.helpers.utils.FileUtil.fromRelPath;
import static org.qp.android.helpers.utils.StringUtil.isNotEmpty;
import static org.qp.android.helpers.utils.StringUtil.isNullOrEmpty;
@@ -17,7 +16,6 @@
import org.qp.android.ui.settings.SettingsController;
import java.util.ArrayList;
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
@@ -176,64 +174,35 @@ private String lineBreaksInHTML(@NonNull String s) {
private void handleImagesInHtml(@NonNull Context context,
@NonNull Element documentBody) {
- var dynBlackList = new ArrayList();
- documentBody.select("a").forEach(element -> {
- if (element.attr("href").contains("exec:")) {
- dynBlackList.add(element.select("img").attr("src"));
- }
- });
+ if (controller.isUseFullscreenImages) {
+ var dynBlackList = new ArrayList();
+ documentBody.select("a").forEach(element -> {
+ if (element.attr("href").contains("exec:")) {
+ dynBlackList.add(element.select("img").attr("src"));
+ }
+ });
- documentBody.select("img").forEach(img -> {
- if (controller.isUseFullscreenImages) {
+ documentBody.select("img").forEach(img -> {
if (!dynBlackList.contains(img.attr("src"))) {
- img.attr("onclick" , "img.onClickImage(this.src);");
+ img.attr("onclick", "img.onClickImage(this.src);");
}
- }
+ });
+ }
+
+ documentBody.select("img").forEach(img -> {
if (controller.isUseAutoWidth && controller.isUseAutoHeight) {
img.attr("style", "display: inline; height: auto; max-width: 100%;");
- }
- if (!controller.isUseAutoWidth) {
- shouldChangeWidth(context, img).thenAccept(aBoolean -> {
- if (!aBoolean) return;
+ } else {
+ if (!controller.isUseAutoWidth) {
img.attr("style" , "max-width:" + controller.customWidthImage+";");
- });
- } else if (!controller.isUseAutoHeight) {
- shouldChangeHeight(context, img).thenAccept(aBoolean -> {
- if (!aBoolean) return;
- img.attr("style" , "max-height:" + controller.customHeightImage+";");
- });
+ }
+ if (!controller.isUseAutoHeight) {
+ img.attr("style" , "max-height:" + controller.customHeightImage+";");
+ }
}
});
}
- private CompletableFuture shouldChangeWidth(Context context,
- Element img) {
- var relPath = img.attr("src");
- return CompletableFuture
- .supplyAsync(() -> fromRelPath(context , relPath , curGameDir), executors)
- .thenApply(imageFile -> {
- if (imageFile == null) return false;
- var drawable = imageProvider.getDrawableFromPath(context , imageFile.getUri());
- if (drawable == null) return false;
- var widthPix = context.getResources().getDisplayMetrics().widthPixels;
- return drawable.getIntrinsicWidth() < widthPix;
- });
- }
-
- private CompletableFuture shouldChangeHeight(Context context,
- Element img) {
- var relPath = img.attr("src");
- return CompletableFuture
- .supplyAsync(() -> fromRelPath(context , relPath , curGameDir), executors)
- .thenApply(imageFile -> {
- if (imageFile == null) return false;
- var drawable = imageProvider.getDrawableFromPath(context , imageFile.getUri());
- if (drawable == null) return false;
- var heightPix = context.getResources().getDisplayMetrics().heightPixels;
- return drawable.getIntrinsicHeight() < heightPix;
- });
- }
-
private void handleVideosInHtml(Element documentBody) {
var videoElement = documentBody.select("video");
videoElement.attr("style", "max-width:100%;");
diff --git a/app/src/main/java/org/qp/android/ui/game/GameItemRecycler.java b/app/src/main/java/org/qp/android/ui/game/GameItemAdapter.java
similarity index 89%
rename from app/src/main/java/org/qp/android/ui/game/GameItemRecycler.java
rename to app/src/main/java/org/qp/android/ui/game/GameItemAdapter.java
index cae0d4eb..dbb653bb 100644
--- a/app/src/main/java/org/qp/android/ui/game/GameItemRecycler.java
+++ b/app/src/main/java/org/qp/android/ui/game/GameItemAdapter.java
@@ -21,7 +21,7 @@
import java.util.List;
import java.util.Objects;
-public class GameItemRecycler extends RecyclerView.Adapter {
+public class GameItemAdapter extends RecyclerView.Adapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK =
new DiffUtil.ItemCallback<>() {
@@ -62,15 +62,15 @@ public void submitList(List gameData) {
@NonNull
@Override
- public GameItemRecycler.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
- int viewType) {
+ public GameItemAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+ int viewType) {
var inflater = LayoutInflater.from(parent.getContext());
var listGameItemBinding = ListGameItemBinding.inflate(inflater, parent, false);
- return new GameItemRecycler.ViewHolder(listGameItemBinding);
+ return new GameItemAdapter.ViewHolder(listGameItemBinding);
}
@Override
- public void onBindViewHolder(@NonNull GameItemRecycler.ViewHolder holder, int position) {
+ public void onBindViewHolder(@NonNull GameItemAdapter.ViewHolder holder, int position) {
var qpListItem = getItem(position);
final var itemImage = holder.listGameItemBinding.itemIcon;
diff --git a/app/src/main/java/org/qp/android/ui/game/GameMainFragment.java b/app/src/main/java/org/qp/android/ui/game/GameMainFragment.java
index af6ff2f8..1e301c9e 100644
--- a/app/src/main/java/org/qp/android/ui/game/GameMainFragment.java
+++ b/app/src/main/java/org/qp/android/ui/game/GameMainFragment.java
@@ -24,6 +24,7 @@
public class GameMainFragment extends Fragment {
+ private final GameItemAdapter adapter = new GameItemAdapter();
private GameViewModel viewModel;
private ConstraintLayout layoutTop;
private WebView mainDescView;
@@ -76,7 +77,7 @@ public void onClickImage(String src) {
}
}, "img");
if (viewModel.getSettingsController().isUseAutoscroll) {
- mainDescView.postDelayed(onScroll, 300);
+ mainDescView.post(onScroll);
}
viewModel.getMainDescObserver().observe(getViewLifecycleOwner(), desc ->
mainDescView.loadDataWithBaseURL(
@@ -90,13 +91,20 @@ public void onClickImage(String src) {
actionsView = gameMainBinding.actions;
var manager = (LinearLayoutManager) actionsView.getLayoutManager();
var dividerItemDecoration = new DividerItemDecoration(
- actionsView.getContext(),
- manager.getOrientation());
+ actionsView.getContext(), manager.getOrientation());
actionsView.addItemDecoration(dividerItemDecoration);
actionsView.setOverScrollMode(View.OVER_SCROLL_NEVER);
- viewModel.getActionObserver().observe(getViewLifecycleOwner(), actions -> {
+ actionsView.setBackgroundColor(viewModel.getBackgroundColor());
+ actionsView.setAdapter(adapter);
+
+ viewModel.actsListLiveData.observe(getViewLifecycleOwner(), listItems -> {
actionsView.setBackgroundColor(viewModel.getBackgroundColor());
- actionsView.setAdapter(actions);
+ adapter.typeface = viewModel.getSettingsController().getTypeface();
+ adapter.textSize = viewModel.getFontSize();
+ adapter.textColor = viewModel.getTextColor();
+ adapter.linkTextColor = viewModel.getLinkColor();
+ adapter.backgroundColor = viewModel.getBackgroundColor();
+ adapter.submitList(listItems);
});
// Settings
@@ -118,6 +126,7 @@ public void onClickImage(String src) {
actionsView.setBackgroundColor(viewModel.getBackgroundColor());
gameMainBinding.getRoot().refreshDrawableState();
});
+
return gameMainBinding.getRoot();
}
diff --git a/app/src/main/java/org/qp/android/ui/game/GameObjectFragment.java b/app/src/main/java/org/qp/android/ui/game/GameObjectFragment.java
index c46157fc..25f0e1b6 100644
--- a/app/src/main/java/org/qp/android/ui/game/GameObjectFragment.java
+++ b/app/src/main/java/org/qp/android/ui/game/GameObjectFragment.java
@@ -18,6 +18,7 @@
public class GameObjectFragment extends Fragment {
+ private final GameItemAdapter adapter = new GameItemAdapter();
private FragmentRecyclerBinding recyclerBinding;
private GameViewModel viewModel;
private RecyclerView objectView;
@@ -38,9 +39,17 @@ public View onCreateView(@NonNull LayoutInflater inflater,
manager.getOrientation());
objectView.addItemDecoration(dividerItemDecoration);
objectView.setOverScrollMode(View.OVER_SCROLL_NEVER);
- viewModel.getObjectsObserver().observe(getViewLifecycleOwner(), gameItemRecycler -> {
+ objectView.setBackgroundColor(viewModel.getBackgroundColor());
+ objectView.setAdapter(adapter);
+
+ viewModel.objsListLiveData.observe(getViewLifecycleOwner(), listItems -> {
objectView.setBackgroundColor(viewModel.getBackgroundColor());
- recyclerBinding.shareRecyclerView.setAdapter(gameItemRecycler);
+ adapter.typeface = viewModel.getSettingsController().getTypeface();
+ adapter.textSize = viewModel.getFontSize();
+ adapter.textColor = viewModel.getTextColor();
+ adapter.linkTextColor = viewModel.getLinkColor();
+ adapter.backgroundColor = viewModel.getBackgroundColor();
+ adapter.submitList(listItems);
});
// Settings
@@ -48,6 +57,7 @@ public View onCreateView(@NonNull LayoutInflater inflater,
objectView.setBackgroundColor(viewModel.getBackgroundColor());
recyclerBinding.getRoot().refreshDrawableState();
});
+
return recyclerBinding.getRoot();
}
diff --git a/app/src/main/java/org/qp/android/ui/game/GameViewModel.java b/app/src/main/java/org/qp/android/ui/game/GameViewModel.java
index 06b0bf38..a6d9d7ce 100644
--- a/app/src/main/java/org/qp/android/ui/game/GameViewModel.java
+++ b/app/src/main/java/org/qp/android/ui/game/GameViewModel.java
@@ -6,7 +6,7 @@
import static org.qp.android.helpers.utils.ColorUtil.getHexColor;
import static org.qp.android.helpers.utils.FileUtil.findOrCreateFolder;
import static org.qp.android.helpers.utils.FileUtil.fromRelPath;
-import static org.qp.android.helpers.utils.FileUtil.isWritableDir;
+import static org.qp.android.helpers.utils.FileUtil.isWritableFile;
import static org.qp.android.helpers.utils.PathUtil.getExtension;
import static org.qp.android.helpers.utils.StringUtil.isNotEmptyOrBlank;
import static org.qp.android.helpers.utils.ThreadUtil.assertNonUiThread;
@@ -61,11 +61,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
public class GameViewModel extends AndroidViewModel implements GameInterface {
@@ -89,12 +85,11 @@ public class GameViewModel extends AndroidViewModel implements GameInterface {
""";
private static final String PAGE_BODY_TEMPLATE = "REPLACETEXT";
private final QuestopiaApplication questopiaApplication;
- private final ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private final MutableLiveData controllerObserver = new MutableLiveData<>();
private final MutableLiveData mainDescLiveData = new MutableLiveData<>();
private final MutableLiveData varsDescLiveData = new MutableLiveData<>();
- private final MutableLiveData actionsListLiveData = new MutableLiveData<>();
- private final MutableLiveData objectsListLiveData = new MutableLiveData<>();
+ public final MutableLiveData> actsListLiveData = new MutableLiveData<>();
+ public final MutableLiveData> objsListLiveData = new MutableLiveData<>();
private final Handler counterHandler = new Handler();
public ObservableBoolean isActionVisible = new ObservableBoolean();
public MutableLiveData outputTextObserver = new MutableLiveData<>();
@@ -236,20 +231,6 @@ public LiveData getVarsDescObserver() {
return varsDescLiveData;
}
- public LiveData getObjectsObserver() {
- if (objectsListLiveData.getValue() == null) {
- refreshObjectsRecycler();
- }
- return objectsListLiveData;
- }
-
- public LiveData getActionObserver() {
- if (actionsListLiveData.getValue() == null) {
- refreshActionsRecycler();
- }
- return actionsListLiveData;
- }
-
public Optional getCurGameDir() {
if (gameDirUri == null) return Optional.empty();
return Optional.ofNullable(DocumentFileCompat.fromUri(getApplication(), gameDirUri));
@@ -375,39 +356,33 @@ public void updatePageTemplate() {
}
private void refreshMainDesc() {
- CompletableFuture
- .supplyAsync(() -> getHtml(getLibGameState().mainDesc), service)
- .thenAcceptAsync(libMainDesc -> {
- var dirtyHTML = pageTemplate.replace("REPLACETEXT", libMainDesc);
- var cleanHTML = "";
- if (getSettingsController().isImageDisabled) {
- cleanHTML = getHtmlProcessor().getCleanHtmlRemMedia(dirtyHTML);
- } else {
- cleanHTML = getHtmlProcessor().getCleanHtmlAndMedia(getApplication(), dirtyHTML);
- }
- if (!cleanHTML.isBlank()) {
- getGameActivity().warnUser(GameActivity.TAB_MAIN_DESC_AND_ACTIONS);
- }
- mainDescLiveData.postValue(cleanHTML);
- }, service);
+ var libMainDesc = getHtml(getLibGameState().mainDesc);
+ var dirtyHTML = pageTemplate.replace("REPLACETEXT", libMainDesc);
+ var cleanHTML = "";
+ if (getSettingsController().isImageDisabled) {
+ cleanHTML = getHtmlProcessor().getCleanHtmlRemMedia(dirtyHTML);
+ } else {
+ cleanHTML = getHtmlProcessor().getCleanHtmlAndMedia(getApplication(), dirtyHTML);
+ }
+ if (!cleanHTML.isBlank()) {
+ getGameActivity().warnUser(GameActivity.TAB_MAIN_DESC_AND_ACTIONS);
+ }
+ mainDescLiveData.postValue(cleanHTML);
}
private void refreshVarsDesc() {
- CompletableFuture
- .supplyAsync(() -> getHtml(getLibGameState().varsDesc), service)
- .thenAcceptAsync(libVarsDesc -> {
- var dirtyHTML = pageTemplate.replace("REPLACETEXT", libVarsDesc);
- var cleanHTML = "";
- if (getSettingsController().isImageDisabled) {
- cleanHTML = getHtmlProcessor().getCleanHtmlRemMedia(dirtyHTML);
- } else {
- cleanHTML = getHtmlProcessor().getCleanHtmlAndMedia(getApplication(), dirtyHTML);
- }
- if (!cleanHTML.isBlank()) {
- getGameActivity().warnUser(GameActivity.TAB_VARS_DESC);
- }
- varsDescLiveData.postValue(cleanHTML);
- }, service);
+ final var libVarsDesc = getHtml(getLibGameState().varsDesc);
+ final var dirtyHTML = pageTemplate.replace("REPLACETEXT", libVarsDesc);
+ var cleanHTML = "";
+ if (getSettingsController().isImageDisabled) {
+ cleanHTML = getHtmlProcessor().getCleanHtmlRemMedia(dirtyHTML);
+ } else {
+ cleanHTML = getHtmlProcessor().getCleanHtmlAndMedia(getApplication(), dirtyHTML);
+ }
+ if (!cleanHTML.isBlank()) {
+ getGameActivity().warnUser(GameActivity.TAB_VARS_DESC);
+ }
+ varsDescLiveData.postValue(cleanHTML);
}
public void onActionClicked(int index) {
@@ -415,35 +390,10 @@ public void onActionClicked(int index) {
}
private void refreshActionsRecycler() {
- CompletableFuture
- .supplyAsync(() -> {
- var list = new ArrayList<>(getLibGameState().actionsList);
- var dir = DocumentFileCompat.fromUri(getApplication(), gameDirUri);
- if (!isWritableDir(getApplication(), dir)) return null;
- list.stream()
- .parallel()
- .map(item -> isNotEmptyOrBlank(item.image())
- ? String.valueOf(fromRelPath(getApplication(), item.image(), dir))
- : ""
- )
- .collect(Collectors.toUnmodifiableList());
- return list;
- }, service)
- .thenApplyAsync(list -> {
- var actionsRecycler = new GameItemRecycler();
- actionsRecycler.typeface = getSettingsController().getTypeface();
- actionsRecycler.textSize = getFontSize();
- actionsRecycler.textColor = getTextColor();
- actionsRecycler.linkTextColor = getLinkColor();
- actionsRecycler.backgroundColor = getBackgroundColor();
- actionsRecycler.submitList(list);
- return actionsRecycler;
- })
- .thenAcceptAsync(actionsRecycler -> {
- actionsListLiveData.postValue(actionsRecycler);
- int count = actionsRecycler.getItemCount();
- isActionVisible.set(showActions && count > 0);
- }, service);
+ var listItems = getLibGameState().actionsList;
+ var listSize = listItems.size();
+ isActionVisible.set(showActions && listSize > 0);
+ actsListLiveData.postValue(listItems);
}
public void onObjectClicked(int index) {
@@ -451,32 +401,8 @@ public void onObjectClicked(int index) {
}
private void refreshObjectsRecycler() {
- CompletableFuture
- .supplyAsync(() -> {
- var list = new ArrayList<>(getLibGameState().objectsList);
- var dir = DocumentFileCompat.fromUri(getApplication(), gameDirUri);
- if (!isWritableDir(getApplication(), dir)) return null;
- list.stream()
- .parallel()
- .map(item -> isNotEmptyOrBlank(item.image())
- ? String.valueOf(fromRelPath(getApplication(), item.image(), dir))
- : ""
- )
- .collect(Collectors.toUnmodifiableList());
- return list;
- }, service)
- .thenApplyAsync(list -> {
- getGameActivity().warnUser(GameActivity.TAB_OBJECTS);
- var objectsRecycler = new GameItemRecycler();
- objectsRecycler.typeface = getSettingsController().getTypeface();
- objectsRecycler.textSize = getFontSize();
- objectsRecycler.textColor = getTextColor();
- objectsRecycler.linkTextColor = getLinkColor();
- objectsRecycler.backgroundColor = getBackgroundColor();
- objectsRecycler.submitList(list);
- return objectsRecycler;
- })
- .thenAcceptAsync(objectsListLiveData::postValue, service);
+ getGameActivity().warnUser(GameActivity.TAB_OBJECTS);
+ objsListLiveData.postValue(getLibGameState().objectsList);
}
public void setCallback() {
@@ -729,6 +655,27 @@ public WebResourceResponse shouldInterceptRequest(WebView view,
try {
if (uri.getPath() == null) throw new NullPointerException();
var imageFile = fromRelPath(getGameActivity(), uri.getPath(), rootDir);
+ if (!isWritableFile(getApplication(), imageFile)) {
+ var pathElement = uri.getPath().split("/");
+ var corrPath = new StringBuilder("/");
+ var files = rootDir.listFiles();
+ for (var path : pathElement) {
+ for (var file : files) {
+ var name = file.getName();
+ if (!isNotEmptyOrBlank(name)) continue;
+ if (name.equalsIgnoreCase(path)) {
+ if (getExtension(name) != null) {
+ corrPath.append("/").append(name);
+ } else {
+ corrPath.append("/").append(name).append("/");
+ }
+ files = file.listFiles();
+ }
+ }
+ }
+ var corrFilePath = corrPath.toString().replace("//", "/");
+ imageFile = fromRelPath(getApplication(), corrFilePath, rootDir);
+ }
var extension = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getExtension(imageFile));
var in = getApplication().getContentResolver().openInputStream(imageFile.getUri());
return new WebResourceResponse(extension, null, in);