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);