From fc5d5ec8e25050b4e3dd77cb60d17abc9d413e05 Mon Sep 17 00:00:00 2001 From: konggdev Date: Tue, 7 Jul 2026 01:39:27 +0200 Subject: [PATCH] New style UI --- .../strikemaps/data/helper/FileHelper.java | 49 +++- .../implementation/PointSelectionOverlay.java | 4 + .../implementation/MapLibreGLJSRenderer.java | 6 +- .../MapLibreNativeRenderer.java | 8 +- .../strikemaps/map/style/MapStyle.java | 2 +- .../ui/element/item/GenericItem.java | 29 ++- .../ui/factory/AlertDialogFactory.java | 242 +++++++++++++++++- .../fragment/dialog/NewStyleBottomSheet.java | 148 +++++++++++ .../dialog/StyleDetailsBottomSheet.java | 151 +++++++++++ .../layout/FragmentLayoutControls.java | 2 +- .../popup/FragmentMapChangePopup.java | 43 +++- app/src/main/res/layout/dialog_new_style.xml | 51 ++++ .../main/res/layout/dialog_style_details.xml | 239 +++++++++++++++++ 13 files changed, 951 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/NewStyleBottomSheet.java create mode 100644 app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/StyleDetailsBottomSheet.java create mode 100644 app/src/main/res/layout/dialog_new_style.xml create mode 100644 app/src/main/res/layout/dialog_style_details.xml diff --git a/app/src/main/java/eu/konggdev/strikemaps/data/helper/FileHelper.java b/app/src/main/java/eu/konggdev/strikemaps/data/helper/FileHelper.java index 6d323d0..2ce6cc8 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/data/helper/FileHelper.java +++ b/app/src/main/java/eu/konggdev/strikemaps/data/helper/FileHelper.java @@ -26,7 +26,6 @@ public final class FileHelper { public static String loadStringFromUserFile(String filePath) { File file = new File(filePath); - try (FileInputStream fis = new FileInputStream(file)) { int size = fis.available(); byte[] buffer = new byte[size]; @@ -38,6 +37,39 @@ public final class FileHelper { } } + public static void writeUserFile(String path, String fileName, String content, AppController app) throws IOException { + try { + File userDirectory = new File(app.getActivity().getExternalFilesDir(null), path); + + if (!userDirectory.exists() && !userDirectory.mkdirs()) { + app.logcat("Failed to create directory: " + userDirectory.getAbsolutePath()); + return; + } + + File file = new File(userDirectory, fileName); + + try (FileOutputStream fos = new FileOutputStream(file); + OutputStreamWriter writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { + + writer.write(content); + writer.flush(); + } + + } catch (IOException e) { + throw e; + } + } + + public static boolean deleteFile(String filePath) { + File file = new File(filePath); + + if (!file.exists() || !file.isFile()) { + return false; + } + + return file.delete(); + } + public static String[] getAssetFiles(String path, String fileExt, AppController app) { AssetManager assetManager = app.getActivity().getAssets(); try { @@ -77,9 +109,20 @@ public final class FileHelper { } } + public static boolean userFileExists(String path, String fileName, AppController app) { + File userDirectory = new File(app.getActivity().getExternalFilesDir(null), path); + + if (!userDirectory.exists() || !userDirectory.isDirectory()) { + return false; + } + + File file = new File(userDirectory, fileName); + + return file.exists() && file.isFile(); + } + public static String[] getUserFiles(String path, String fileExt, AppController app) { - String packageName = app.getActivity().getPackageName(); - File userDirectory = new File(Environment.getExternalStorageDirectory(), "Android/data/" + packageName + "/" + path); + File userDirectory = new File(app.getActivity().getExternalFilesDir(null), path); if (!userDirectory.exists() || !userDirectory.isDirectory()) return new String[0]; diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java index 62ec2de..0f79db1 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java @@ -1,9 +1,13 @@ package eu.konggdev.strikemaps.map.overlay.implementation; import com.fasterxml.jackson.databind.JsonNode; +import eu.konggdev.strikemaps.app.AppController; +import eu.konggdev.strikemaps.map.MapComponent; import eu.konggdev.strikemaps.map.overlay.MapOverlay; public class PointSelectionOverlay implements MapOverlay { + AppController app; + MapComponent map; @Override public JsonNode makePatch() { return null; diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java index b54d011..7e311cb 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java @@ -72,11 +72,13 @@ public class MapLibreGLJSRenderer implements MapRenderer { //Sources ObjectNode sources = mapper.createObjectNode(); - style.sources.forEach((k, v) -> sources.set(k, mapper.valueToTree(v))); + if (style.sources != null) + style.sources.forEach((k, v) -> sources.set(k, mapper.valueToTree(v))); //Layers ArrayNode layers = mapper.createArrayNode(); - layers.addAll((ArrayNode) style.layerDefinitions); + if (style.layerDefinitions != null) + layers.addAll((ArrayNode) style.layerDefinitions); //Set all to root root.set("sources", sources); diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java index 7d8c7ca..a1d0160 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java @@ -56,11 +56,13 @@ public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback { //Sources ObjectNode sources = mapper.createObjectNode(); - style.sources.forEach((k, v) -> sources.set(k, mapper.valueToTree(v))); + if (style.sources != null) + style.sources.forEach((k, v) -> sources.set(k, mapper.valueToTree(v))); //Layers ArrayNode layers = mapper.createArrayNode(); - layers.addAll((ArrayNode) style.layerDefinitions); + if (style.layerDefinitions != null) + layers.addAll((ArrayNode) style.layerDefinitions); //Set all to root root.set("sources", sources); @@ -126,7 +128,7 @@ public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback { public void onMapReady(@NonNull MapLibreMap maplibreMap) { this.map = maplibreMap; - controller.setStyle(MapStyle.fromJsonFile(UserPrefsHelper.startupMapStyle(app.getPrefs()), app)); + controller.setStyle(MapStyle.fromFile(UserPrefsHelper.startupMapStyle(app.getPrefs()), app)); //I have my own implementation of attribution that credits MapLibre among others, it's not as bad as it looks :) map.getUiSettings().setLogoEnabled(false); diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java b/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java index 8dad321..0af52ba 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java @@ -23,7 +23,7 @@ public class MapStyle { public ArrayNode layerDefinitions; // "layers" array //FIXME - public static MapStyle fromJsonFile(String filename, AppController app) { + public static MapStyle fromFile(String filename, AppController app) { String styleContents; if (filename.startsWith("/storage")) styleContents = FileHelper.loadStringFromUserFile(filename); else styleContents = FileHelper.loadStringFromAssetFile(filename, app); diff --git a/app/src/main/java/eu/konggdev/strikemaps/ui/element/item/GenericItem.java b/app/src/main/java/eu/konggdev/strikemaps/ui/element/item/GenericItem.java index 5c77772..7227f83 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/ui/element/item/GenericItem.java +++ b/app/src/main/java/eu/konggdev/strikemaps/ui/element/item/GenericItem.java @@ -15,6 +15,7 @@ public class GenericItem implements UIItem { @NonNull public String name; public Bitmap image; public Runnable onClick; + public Runnable onLongClick; boolean hasImage; public GenericItem(String refName) { @@ -26,6 +27,12 @@ public class GenericItem implements UIItem { this.onClick = onClick; hasImage = false; } + public GenericItem(String refName, Runnable onClick, Runnable onLongClick) { + this.name = refName; + this.onClick = onClick; + this.onLongClick = onLongClick; + hasImage = false; + } public GenericItem(String refName, Bitmap refImage) { this.name = refName; this.image = refImage; @@ -38,18 +45,34 @@ public class GenericItem implements UIItem { hasImage = true; } - public final static GenericItem fromStyle(MapStyle style, MapComponent map) { + public GenericItem(String refName, Bitmap refImage, Runnable onClick, Runnable onLongClick) { + this.name = refName; + this.image = refImage; + this.onClick = onClick; + this.onLongClick = onLongClick; + hasImage = true; + } + + public final static GenericItem fromStyle(MapStyle style, MapComponent map, Runnable onClick) { if(style == null) return new GenericItem("Unknown"); if(style.icon != null) - return new GenericItem(style.name, style.icon, () -> map.setStyle(style)); - return new GenericItem(style.name, () -> map.setStyle(style)); + return new GenericItem(style.name, style.icon, onClick); + return new GenericItem(style.name, onClick); } + public final static GenericItem fromStyle(MapStyle style, MapComponent map, Runnable onClick, Runnable onLongClick) { + if(style == null) return new GenericItem("Unknown"); + if(style.icon != null) + return new GenericItem(style.name, style.icon, onClick, onLongClick); + return new GenericItem(style.name, onClick, onLongClick); + } + public View makeView(UIComponent spawner) { View v = spawner.inflateUi(R.layout.item_generic); //FIXME: These shouldn't be casted like that! ((TextView) v.findViewById(R.id.name)).setText(name); if(image != null) ((ImageButton) v.findViewById(R.id.image)).setImageBitmap(image); if(onClick != null) v.findViewById(R.id.image).setOnClickListener(click(onClick)); + if(onLongClick != null) v.findViewById(R.id.image).setOnLongClickListener(longClick(onLongClick)); return v; } diff --git a/app/src/main/java/eu/konggdev/strikemaps/ui/factory/AlertDialogFactory.java b/app/src/main/java/eu/konggdev/strikemaps/ui/factory/AlertDialogFactory.java index 5913794..5ec5849 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/ui/factory/AlertDialogFactory.java +++ b/app/src/main/java/eu/konggdev/strikemaps/ui/factory/AlertDialogFactory.java @@ -1,22 +1,256 @@ package eu.konggdev.strikemaps.ui.factory; import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; import android.view.View; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.Toast; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import eu.konggdev.strikemaps.app.AppController; +import eu.konggdev.strikemaps.app.util.JsonPatcher; +import eu.konggdev.strikemaps.data.helper.FileHelper; +import eu.konggdev.strikemaps.map.MapComponent; +import eu.konggdev.strikemaps.map.style.MapStyle; +import eu.konggdev.strikemaps.ui.UIComponent; +import eu.konggdev.strikemaps.ui.element.item.GenericItem; import eu.konggdev.strikemaps.ui.element.item.PreviewItem; +import eu.konggdev.strikemaps.ui.fragment.popup.FragmentMapChangePopup; import org.maplibre.geojson.Feature; +import java.io.IOException; +import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.function.Consumer; +import static androidx.core.content.ContextCompat.getSystemService; + //FIXME: Move Item functions into specific classes for specific types - e.g. StyleItem public final class AlertDialogFactory { + public static AlertDialog copyBuiltInStyle(AppController app, MapComponent map, UIComponent ui, FragmentMapChangePopup mapChangePopup) { + //TODO: Use an UI element thats supposed to be vertical, instead of GenericItem + List styles = Arrays.asList(FileHelper.getAssetFiles("bundled/style", ".style.json", app)); + + LinearLayout container = new LinearLayout(app.getActivity()); + container.setOrientation(LinearLayout.VERTICAL); + + for (String style : styles) { + View itemView = GenericItem + .fromStyle( + MapStyle.fromFile(style, app), + map, + () -> ui.alert(AlertDialogFactory.createStyle(app, FileHelper.loadStringFromAssetFile(style, app), mapChangePopup)) + ) + .makeView(ui); + + container.addView(itemView); + } + + ScrollView scrollView = new ScrollView(app.getActivity()); + scrollView.addView(container); + + AlertDialog dialog = new AlertDialog.Builder(app.getActivity()) + .setTitle("Copy from") + .setView(scrollView) + .setNegativeButton("Cancel", null) + .create(); + + return dialog; + } + + public static AlertDialog createStyle(AppController app, String baseStyleContents, FragmentMapChangePopup mapChangePopup) { + final EditText nameInput = new EditText(app.getActivity()); + nameInput.setHint("Name"); + + final CheckBox inferFileName = new CheckBox(app.getActivity()); + inferFileName.setText("Infer filename automatically"); + inferFileName.setChecked(true); + + final EditText fileInput = new EditText(app.getActivity()); + fileInput.setHint("Filename"); + + final TextView inferedFileName = new TextView(app.getActivity()); + inferedFileName.setPadding(0, 2, 0, 0); + LinearLayout container = new LinearLayout(app.getActivity()); + container.setOrientation(LinearLayout.VERTICAL); + + int padding = (int) (20 * app.getActivity().getResources().getDisplayMetrics().density); + container.setPadding(padding, padding, padding, 0); + + container.addView(nameInput); + container.addView(inferFileName); + container.addView(fileInput); + container.addView(inferedFileName); + + inferFileName.setOnCheckedChangeListener((buttonView, isChecked) -> { + fileInput.setVisibility(isChecked ? View.GONE : View.VISIBLE); + inferedFileName.setVisibility(isChecked ? View.VISIBLE : View.GONE); + }); + + fileInput.setVisibility(inferFileName.isChecked() ? View.GONE : View.VISIBLE); + inferedFileName.setVisibility(inferFileName.isChecked() ? View.VISIBLE : View.GONE); + + nameInput.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String nameText = ""; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + if (!s.isEmpty() && s != null) { + nameText = "File name will be: " + s.toString().toLowerCase(Locale.ROOT) + ".style.json"; + } + } else if (s != null) { + nameText = "File name will be: " + s.toString().toLowerCase(Locale.ROOT) + ".style.json"; + } + + inferedFileName.setText(nameText); + } + @Override + public void afterTextChanged(Editable s) {} + }); + + AlertDialog dialog = new AlertDialog.Builder(app.getActivity()) + .setTitle("Create") + .setView(container) + .setPositiveButton("Create", null) + .setNegativeButton("Cancel", (d, w) -> d.dismiss()) + .create(); + + dialog.setOnShowListener(d -> { + Button createButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + + createButton.setOnClickListener(v -> { + ObjectMapper mapper = new ObjectMapper(); + boolean nameEntryRequired = true; + String styleContentName = ""; + if (baseStyleContents != null) { + try { + JsonNode root = mapper.readTree(baseStyleContents); + if (!root.path("name").asText().isEmpty()) { + nameEntryRequired = false; //We can take the name from the style + styleContentName = root.path("name").asText(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + String name = nameInput.getText().toString().trim(); + + if (name.isEmpty() && nameEntryRequired) { + nameInput.setError("Name required"); + return; + } else if (name.isEmpty() && !nameEntryRequired) { + name = styleContentName; + } + + assert !name.isEmpty(); + + String fileName; + if (inferFileName.isChecked()) { + fileName = name.toLowerCase(Locale.ROOT) + ".style.json"; + } else { + fileName = fileInput.getText().toString(); + if (fileName.isEmpty()) { + fileInput.setError("File name required"); + return; + } + + if (!fileName.endsWith(".style.json")) { + fileInput.setError("File must end with .style.json"); + return; + } + } + + try { + JsonNode root; + if(baseStyleContents != null) { + if(!baseStyleContents.isEmpty()) { + root = mapper.readTree(baseStyleContents); + } else { + root = mapper.createObjectNode(); + } + } else { + root = mapper.createObjectNode(); + } + + if (!root.path("name").asText().isEmpty()) { + ObjectNode node = mapper.createObjectNode(); + node.put("name", name); + root = JsonPatcher.patch(root, node); + } + + if (FileHelper.userFileExists("style", fileName, app)) { + app.getUi().alert(askUserOverwriteFile(app, fileName, "style", mapper.writeValueAsString(root), dialog)); + } else { + FileHelper.writeUserFile("style", fileName, mapper.writeValueAsString(root), app); + } + dialog.dismiss(); + } catch (Exception e) { + Toast.makeText(app.getActivity(), "Failed to create", Toast.LENGTH_SHORT).show(); + e.printStackTrace(); + } + + mapChangePopup.reloadStyles(); + }); + }); + + return dialog; + } + + public static AlertDialog askUserOverwriteFile(AppController app, String fileName, String path, String content) { + return new AlertDialog.Builder(app.getActivity()) + .setMessage("Style of filename " + fileName + " already exists, do you wish to overwrite it?") + .setPositiveButton("Yes", (dialog, which) -> { + try { + FileHelper.writeUserFile(path, fileName, content, app); + } catch (Exception e) { + e.printStackTrace(); + } + }) + .setNegativeButton("No", null) + .create(); + } + + public static AlertDialog askUserOverwriteFile(AppController app, String fileName, String path, String content, AlertDialog originDialog) { + + return new AlertDialog.Builder(app.getActivity()) + .setMessage("Style of filename: " + fileName + " already exists, do you wish to overwrite it?") + .setPositiveButton("Yes", (dialog, which) -> { + try { + FileHelper.writeUserFile(path, fileName, content, app); + } catch (IOException e) { + e.printStackTrace(); + } + + if (originDialog != null) { + originDialog.dismiss(); + } + dialog.dismiss(); + }) + .setNegativeButton("No", (dialog, which) -> { + if (originDialog != null) { + originDialog.dismiss(); + } + dialog.dismiss(); + }) + .create(); + } + public static AlertDialog pointSelector(AppController app, List features, Consumer callback) { LinearLayout layout = new LinearLayout(app.getActivity()); layout.setOrientation(LinearLayout.VERTICAL); @@ -32,7 +266,7 @@ public final class AlertDialogFactory { for (Feature feature : features) { View itemView = PreviewItem.fromFeature(feature).makeView(app.getUi(), v -> { dialog.dismiss(); - new android.os.Handler(android.os.Looper.getMainLooper()) + new Handler(Looper.getMainLooper()) .post(() -> callback.accept(feature)); }); layout.addView(itemView); diff --git a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/NewStyleBottomSheet.java b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/NewStyleBottomSheet.java new file mode 100644 index 0000000..a0b9d27 --- /dev/null +++ b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/NewStyleBottomSheet.java @@ -0,0 +1,148 @@ +package eu.konggdev.strikemaps.ui.fragment.dialog; + +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.OpenableColumns; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import androidx.annotation.UiContext; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import eu.konggdev.strikemaps.R; +import eu.konggdev.strikemaps.app.AppController; +import eu.konggdev.strikemaps.data.helper.FileHelper; +import eu.konggdev.strikemaps.map.MapComponent; +import eu.konggdev.strikemaps.ui.UIComponent; +import eu.konggdev.strikemaps.ui.factory.AlertDialogFactory; +import eu.konggdev.strikemaps.ui.fragment.popup.FragmentMapChangePopup; + +import java.io.*; +import java.util.stream.Collectors; + +public class NewStyleBottomSheet extends BottomSheetDialogFragment { + private final String styleBase = "{\"name\":\"None\"}"; + + @NonNull AppController app; + @NonNull MapComponent map; + @NonNull UIComponent ui; + @NonNull + FragmentMapChangePopup mapChangePopup; + + + private final ActivityResultLauncher importLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == android.app.Activity.RESULT_OK + && result.getData() != null) { + + Uri uri = result.getData().getData(); + + try (InputStream in = requireContext() + .getContentResolver() + .openInputStream(uri)) { + + try (Cursor cursor = requireContext() + .getContentResolver() + .query(uri, null, null, null, null)) { + + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (nameIndex >= 0) { + String fileName = cursor.getString(nameIndex); + + BufferedReader reader = new BufferedReader( + new InputStreamReader(in) + ); + + StringBuilder contentBuilder = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + contentBuilder.append(line).append(System.lineSeparator()); + } + reader.close(); + String content = contentBuilder.toString(); + + if (fileName != null && !fileName.endsWith(".style.json")) { + app.getUi().alert( + AlertDialogFactory.createStyle( + app, + content, + mapChangePopup + ) + ); + + return; + } else { + if (!FileHelper.userFileExists("style", fileName, app)) { + FileHelper.writeUserFile("style", fileName, content, app); + } else { + app.getUi().alert(AlertDialogFactory.askUserOverwriteFile(app, fileName, "style", content)); + } + + mapChangePopup.reloadStyles(); + return; + } + } + } + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + } + ); + + private void showImportDialog() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("application/json"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + importLauncher.launch(intent); + } + + public NewStyleBottomSheet(AppController app, MapComponent map, UIComponent ui, FragmentMapChangePopup mapChangePopup) { + this.app = app; + this.map = map; + this.ui = ui; + this.mapChangePopup = mapChangePopup; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.dialog_new_style, container, false); + + Button builtInBtn = view.findViewById(R.id.buttonFromBuiltInStyle); + Button fileBtn = view.findViewById(R.id.buttonFromFile); + Button emptyBtn = view.findViewById(R.id.buttonCreateEmpty); + + builtInBtn.setOnClickListener(v -> { + app.getUi().alert(AlertDialogFactory.copyBuiltInStyle(app, map, ui, mapChangePopup)); + }); + + fileBtn.setOnClickListener(v -> { + showImportDialog(); + }); + + emptyBtn.setOnClickListener(v -> { + app.getUi().alert(AlertDialogFactory.createStyle(app, styleBase, mapChangePopup)); + }); + + return view; + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/StyleDetailsBottomSheet.java b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/StyleDetailsBottomSheet.java new file mode 100644 index 0000000..4f52d04 --- /dev/null +++ b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/dialog/StyleDetailsBottomSheet.java @@ -0,0 +1,151 @@ +package eu.konggdev.strikemaps.ui.fragment.dialog; + +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.constraintlayout.widget.ConstraintLayout; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import eu.konggdev.strikemaps.R; +import eu.konggdev.strikemaps.app.AppController; +import eu.konggdev.strikemaps.data.helper.FileHelper; +import eu.konggdev.strikemaps.map.MapComponent; +import eu.konggdev.strikemaps.map.style.MapStyle; +import eu.konggdev.strikemaps.ui.UIComponent; +import eu.konggdev.strikemaps.ui.factory.AlertDialogFactory; +import eu.konggdev.strikemaps.ui.fragment.popup.FragmentMapChangePopup; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Objects; + +import static android.media.MediaExtractor.MetricsConstants.MIME_TYPE; + +public class StyleDetailsBottomSheet extends BottomSheetDialogFragment { + @NonNull + AppController app; + @NonNull + MapComponent map; + @NonNull + UIComponent ui; + @NonNull + FragmentMapChangePopup mapChangePopup; + + private final MapStyle style; + private final String stylePath; + + private final ActivityResultLauncher exportLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + + if (result.getResultCode() == android.app.Activity.RESULT_OK + && result.getData() != null) { + + Uri uri = result.getData().getData(); + + try (OutputStream out = + requireContext() + .getContentResolver() + .openOutputStream(uri)) { + + out.write(getContents().getBytes()); + + } catch (IOException e) { + e.printStackTrace(); + } + } + } + ); + + //Action definitions + //*// + void deleteStyle() { + boolean deleted = FileHelper.deleteFile(stylePath); + if (!deleted) { + Toast.makeText(requireContext(), "Failed deleting style", Toast.LENGTH_SHORT).show(); + return; + } + + mapChangePopup.reloadStyles(); + dismiss(); + } + + + private void showExportDialog(String fileName) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.setType("application/json"); + intent.putExtra(Intent.EXTRA_TITLE, fileName); + + exportLauncher.launch(intent); + } + //*// + + String getContents() { + if (stylePath.startsWith("/storage")) return FileHelper.loadStringFromUserFile(stylePath); + else return FileHelper.loadStringFromAssetFile(stylePath, app); + } + + public StyleDetailsBottomSheet(AppController app, MapComponent map, UIComponent ui, FragmentMapChangePopup mapChangePopup, MapStyle style, String stylePath) { + this.app = app; + this.map = map; + this.ui = ui; + this.mapChangePopup = mapChangePopup; + this.style = style; + this.stylePath = stylePath; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.dialog_style_details, container, false); + + TextView styleNameView = view.findViewById(R.id.styleName); + TextView fileNameView = view.findViewById(R.id.fileName); + TextView styleTypeView = view.findViewById(R.id.styleType); + + ConstraintLayout editButtonLayout = view.findViewById(R.id.editButton); + ConstraintLayout copyButtonLayout = view.findViewById(R.id.copyButton); + ConstraintLayout exportButtonLayout = view.findViewById(R.id.exportButton); + ConstraintLayout deleteButtonLayout = view.findViewById(R.id.deleteButton); + ConstraintLayout closeButtonLayout = view.findViewById(R.id.closeButton); + + + styleNameView.setText(style.name); + + String[] pathSplit = stylePath.split("/"); + String fileName = pathSplit[pathSplit.length - 1]; + + fileNameView.setText(fileName); + + if (Objects.equals(pathSplit[0], "bundled") && pathSplit.length > 1) { + styleTypeView.setText("Built-In Style"); + deleteButtonLayout.setVisibility(View.GONE); + } else { + styleTypeView.setText("User Style"); + } + + + editButtonLayout.setOnClickListener(v -> Toast.makeText(requireContext(), "Editor not implemented yet\nWait for release", Toast.LENGTH_SHORT).show()); + copyButtonLayout.setOnClickListener(v -> ui.alert(AlertDialogFactory.createStyle(app, getContents(), mapChangePopup))); + exportButtonLayout.setOnClickListener(v -> showExportDialog(fileName)); + deleteButtonLayout.setOnClickListener(v -> deleteStyle()); + closeButtonLayout.setOnClickListener(v -> dismiss()); + + return view; + } +} diff --git a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/layout/FragmentLayoutControls.java b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/layout/FragmentLayoutControls.java index 776dbda..6200045 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/layout/FragmentLayoutControls.java +++ b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/layout/FragmentLayoutControls.java @@ -52,7 +52,7 @@ public class FragmentLayoutControls extends Fragment implements Layout { .setTitle(app.getActivity().getString(R.string.attribution_title)) .setMessage(app.getActivity().getString(R.string.shipped_attribution)) .setPositiveButton("OK", null).show(); - } + } //*// public FragmentLayoutControls(AppController app, Integer region) { diff --git a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java index 98d3d27..433d172 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java +++ b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java @@ -1,5 +1,7 @@ package eu.konggdev.strikemaps.ui.fragment.popup; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.os.Bundle; import android.view.View; @@ -14,6 +16,8 @@ import eu.konggdev.strikemaps.map.MapComponent; import eu.konggdev.strikemaps.map.style.MapStyle; import eu.konggdev.strikemaps.ui.UIComponent; +import eu.konggdev.strikemaps.ui.fragment.dialog.NewStyleBottomSheet; +import eu.konggdev.strikemaps.ui.fragment.dialog.StyleDetailsBottomSheet; import eu.konggdev.strikemaps.ui.element.item.GenericItem; import java.util.ArrayList; @@ -28,6 +32,19 @@ public class FragmentMapChangePopup extends Fragment implements Popup { private final Integer region; + private View view; + + // Action definitions + //*// + void newStyleFlow() { + new NewStyleBottomSheet(app, map, ui, this).show(app.getActivity().getSupportFragmentManager(), "NewStyleBottomSheet"); + } + + void styleDetails(MapStyle style, String stylePath) { + new StyleDetailsBottomSheet(app, map, ui, this, style, stylePath).show(app.getActivity().getSupportFragmentManager(), "StyleDetailsBottomSheet"); + } + //*// + public FragmentMapChangePopup(AppController app, Integer region) { super(R.layout.popup_map_change); this.app = app; @@ -46,16 +63,30 @@ public class FragmentMapChangePopup extends Fragment implements Popup { return this; } + public void reloadStyles() { + List stylePaths = new ArrayList<>(); + stylePaths.addAll(Arrays.asList(FileHelper.getAssetFiles("bundled/style", ".style.json", app))); + stylePaths.addAll(Arrays.asList(FileHelper.getUserFiles("style", ".style.json", app))); + LinearLayout stylesLayout = view.findViewById(R.id.stylesLayout); + stylesLayout.removeAllViews(); + for (String stylePath : stylePaths) { + MapStyle parsedStyle = MapStyle.fromFile(stylePath, app); + stylesLayout.addView(GenericItem.fromStyle(parsedStyle, map, + () -> map.setStyle(parsedStyle), + () -> this.styleDetails(parsedStyle, stylePath)).makeView(ui)); + } + Bitmap addNewIcon = BitmapFactory.decodeResource(app.getActivity().getResources(), android.R.drawable.ic_menu_add); + stylesLayout.addView(new GenericItem("", + addNewIcon, + this::newStyleFlow).makeView(ui)); + } + @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { //FIXME setupButton(view, R.id.closeButton, click(() -> ui.getCurrentScreen().closePopup())); setupDragHandle(view, view, () -> ui.getCurrentScreen().closePopup()); - List stylePaths = new ArrayList<>(); - stylePaths.addAll(Arrays.asList(FileHelper.getAssetFiles("bundled/style", ".style.json", app))); - stylePaths.addAll(Arrays.asList(FileHelper.getUserFiles("style", ".style.json", app))); - LinearLayout stylesLayout = view.findViewById(R.id.stylesLayout); - for (String style : stylePaths) - stylesLayout.addView(GenericItem.fromStyle(MapStyle.fromJsonFile(style, app), map).makeView(ui)); + this.view = view; + reloadStyles(); } } diff --git a/app/src/main/res/layout/dialog_new_style.xml b/app/src/main/res/layout/dialog_new_style.xml new file mode 100644 index 0000000..58b7dce --- /dev/null +++ b/app/src/main/res/layout/dialog_new_style.xml @@ -0,0 +1,51 @@ + + + + + + + +