diff --git a/android/src/mindustry/android/AndroidLauncher.java b/android/src/mindustry/android/AndroidLauncher.java index 3d40d5136f..4c8a1ff7e1 100644 --- a/android/src/mindustry/android/AndroidLauncher.java +++ b/android/src/mindustry/android/AndroidLauncher.java @@ -5,20 +5,22 @@ import android.app.*; import android.content.*; import android.content.pm.*; import android.net.*; -import android.os.Build.*; import android.os.*; +import android.os.Build.*; import android.telephony.*; import arc.*; import arc.backend.android.*; import arc.files.*; -import arc.func.*; import arc.scene.ui.layout.*; +import arc.struct.*; import arc.util.*; import dalvik.system.*; import mindustry.*; import mindustry.game.Saves.*; import mindustry.io.*; import mindustry.net.*; +import mindustry.ui.*; +import mindustry.ui.FileChooser.*; import mindustry.ui.dialogs.*; import java.io.*; @@ -30,7 +32,7 @@ import static mindustry.Vars.*; public class AndroidLauncher extends AndroidApplication{ public static final int PERMISSION_REQUEST_CODE = 1; boolean doubleScaleTablets = true; - FileChooser chooser; + FileChooserDialog chooser; Runnable permCallback; @Override @@ -97,8 +99,7 @@ public class AndroidLauncher extends AndroidApplication{ } }; }catch(SecurityException e){ - //`setReadOnly` to jar file in `/sdcard/Android/data/...` does not work on some Android 14 device - //But in `/data/...` it works + //`setReadOnly` to jar file in `/sdcard/Android/data/...` does not work on some Android 14 devices, but in `/data/...`, it does if(Build.VERSION.SDK_INT < VERSION_CODES.O_MR1){ throw e; @@ -127,27 +128,34 @@ public class AndroidLauncher extends AndroidApplication{ } @Override - public void showFileChooser(boolean open, String title, String extension, Cons cons){ - showFileChooser(open, title, cons, extension); - } - - void showFileChooser(boolean open, String title, Cons cons, String... extensions){ + public void showFileChooser(FileChooserParams params){ try{ - String extension = extensions[0]; + String extension = params.extensions[0]; if(VERSION.SDK_INT >= VERSION_CODES.Q){ - Intent intent = new Intent(open ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_CREATE_DOCUMENT); + Intent intent = new Intent(params.open ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(extension.equals("zip") && !open && extensions.length == 1 ? "application/zip" : "*/*"); - intent.putExtra(Intent.EXTRA_TITLE, "export." + extension); + intent.setType(extension.equals("zip") && !params.open && params.extensions.length == 1 ? "application/zip" : "*/*"); + intent.putExtra(Intent.EXTRA_TITLE, params.fileName); + if(params.allowMultiple){ + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } addResultListener(i -> startActivityForResult(intent, i), (code, in) -> { if(code == Activity.RESULT_OK && in != null && in.getData() != null){ - Uri uri = in.getData(); + Uri[] uris; + if(in.getClipData() != null){ + uris = new Uri[in.getClipData().getItemCount()]; + for(int i = 0; i < uris.length; i++){ + uris[i] = in.getClipData().getItemAt(i).getUri(); + } + }else{ + uris = new Uri[]{in.getData()}; + } - if(uri.getPath().contains("(invalid)")) return; + if(uris.length == 0 || uris[0].getPath().contains("(invalid)")) return; - Core.app.post(() -> Core.app.post(() -> cons.get(new Fi(uri.getPath()){ + Fi[] files = Seq.with(uris).map(uri -> new Fi(uri.getPath()){ @Override public InputStream read(){ try{ @@ -165,18 +173,14 @@ public class AndroidLauncher extends AndroidApplication{ throw new ArcRuntimeException(e); } } - }))); + }).toArray(Fi.class); + + Core.app.post(() -> Core.app.post(() -> params.handleChooseResult(files))); } }); }else if(VERSION.SDK_INT >= VERSION_CODES.M && !(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)){ - chooser = new FileChooser(title, file -> Structs.contains(extensions, file.extension().toLowerCase()), open, file -> { - if(!open){ - cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension)); - }else{ - cons.get(file); - } - }); + chooser = FileChooser.createFallbackFileChooser(params); ArrayList perms = new ArrayList<>(); if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){ @@ -187,22 +191,13 @@ public class AndroidLauncher extends AndroidApplication{ } requestPermissions(perms.toArray(new String[0]), PERMISSION_REQUEST_CODE); }else{ - if(open){ - new FileChooser(title, file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show(); - }else{ - super.showFileChooser(open, "@open", extension, cons); - } + FileChooser.showFallbackFileChooser(params); } }catch(Throwable error){ Core.app.post(() -> Vars.ui.showException(error)); } } - @Override - public void showMultiFileChooser(Cons cons, String... extensions){ - showFileChooser(true, "@open", cons, extensions); - } - @Override public void beginForceLandscape(){ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); @@ -235,7 +230,6 @@ public class AndroidLauncher extends AndroidApplication{ Log.err("Failed to delete cached folder", t); } - //move to internal storage if there's no file indicating that it moved if(!Core.files.local("files_moved").exists()){ Log.info("Moving files to external storage..."); diff --git a/core/assets-raw/fontgen/config.json b/core/assets-raw/fontgen/config.json index 614df2a56c..bb01e1c650 100644 --- a/core/assets-raw/fontgen/config.json +++ b/core/assets-raw/fontgen/config.json @@ -308,12 +308,6 @@ "discord-logo-white" ] }, - { - "uid": "dd6df3b754e8d0dbf09d9b24088f0442", - "css": "box", - "code": 59422, - "src": "iconic" - }, { "uid": "f08ba9f0e8f9b426a92efcaa59c1e932", "css": "reddit-alien", @@ -1203,6 +1197,66 @@ "css": "sitemap", "code": 61672, "src": "fontawesome" + }, + { + "uid": "2c413e78faf1d6631fd7b094d14c2253", + "css": "cloud", + "code": 59441, + "src": "fontawesome" + }, + { + "uid": "de2fc7a5c986ab8c622f63455d7cf814", + "css": "upload-cloud", + "code": 61678, + "src": "fontawesome" + }, + { + "uid": "f5999a012fc3752386635ec02a858447", + "css": "download-cloud", + "code": 61677, + "src": "fontawesome" + }, + { + "uid": "e7a4824e79002c2d8ce3047b2da8b5a2", + "css": "rain", + "code": 59442, + "src": "typicons" + }, + { + "uid": "ac10a3a3077eab1b9d66509eb25fb9b4", + "css": "cloud-sun", + "code": 59448, + "src": "typicons" + }, + { + "uid": "6f200a5a449259c51cd0ccf56b4c1e93", + "css": "drizzle", + "code": 59452, + "src": "typicons" + }, + { + "uid": "3cac7625657b10cf706e7f332749c912", + "css": "cloud-flash", + "code": 59456, + "src": "typicons" + }, + { + "uid": "364cc69fb3edb8fb21caa6a2c0489589", + "css": "cloud-wind", + "code": 59457, + "src": "typicons" + }, + { + "uid": "cc05df515bebe11df3ada0a5910a8f6d", + "css": "box", + "code": 59422, + "src": "entypo" + }, + { + "uid": "d5f287ad8e3a2db8f808bc09980ec3dc", + "css": "chart-alt", + "code": 59459, + "src": "typicons" } ] -} \ No newline at end of file +} diff --git a/core/assets/fonts/font.woff b/core/assets/fonts/font.woff index 2a04426c75..451589cedf 100644 Binary files a/core/assets/fonts/font.woff and b/core/assets/fonts/font.woff differ diff --git a/core/assets/fonts/icon.ttf b/core/assets/fonts/icon.ttf index 36a0681abf..064557da3b 100644 Binary files a/core/assets/fonts/icon.ttf and b/core/assets/fonts/icon.ttf differ diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index fb3bd22af9..d751d7f177 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -3,13 +3,11 @@ package mindustry.core; import arc.*; import arc.assets.*; import arc.audio.*; -import arc.graphics.*; import arc.graphics.g2d.*; import arc.input.*; import arc.math.*; import arc.scene.style.*; import arc.scene.ui.*; -import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import mindustry.*; @@ -24,7 +22,6 @@ import mindustry.game.Objectives.*; import mindustry.game.Saves.*; import mindustry.game.Teams.*; import mindustry.gen.*; -import mindustry.graphics.*; import mindustry.input.*; import mindustry.io.*; import mindustry.io.SaveIO.*; @@ -113,39 +110,6 @@ public class Control implements ApplicationListener, Loadable{ input.checkUnit(); }); - Events.on(WorldLoadEvent.class, e -> app.post(() -> { - var errored = state.data.getContent().select(c -> c.errored); - if(!errored.isEmpty()){ - new BaseDialog(bundle.get("mod.erroredcontent").replace("[red]", "")){{ - setFillParent(true); - setTitleColor(Color.scarlet); - cont.margin(15); - cont.row(); - cont.add("@mod.errors.map").wrap().growX().center().labelAlign(Align.center); - cont.row(); - cont.pane(d -> { - d.left().marginLeft(15f); - for(var asset : errored){ - d.add(asset.path).color(Pal.accent).left().padRight(10).growX().row(); - - d.table(t -> { - t.left(); - t.image(Tex.whiteui).width(3f).growY().color(Color.lightGray).padRight(8f).padTop(2f).padBottom(2f); - - t.add(asset.warnings.toString("\n\n")).color(Color.lightGray).labelAlign(Align.left, Align.left).left().growX().wrap().padTop(5f).row(); - }).padBottom(10f).grow().row(); - } - }).width(Math.min(graphics.getWidth() / Scl.scl(1f) * 0.9f, 700f)); - - buttons.button("@quit", Icon.exit, () -> { - hide(); - ui.paused.runExitSave(false); - }).size(210, 64); - buttons.button("@continue", Icon.warning, this::hide).size(210, 64); - }}.show(); - } - })); - Events.on(ResetEvent.class, event -> { player.reset(); toBePlaced.clear(); diff --git a/core/src/mindustry/core/Platform.java b/core/src/mindustry/core/Platform.java index b617768205..cf86077496 100644 --- a/core/src/mindustry/core/Platform.java +++ b/core/src/mindustry/core/Platform.java @@ -2,22 +2,18 @@ package mindustry.core; import arc.*; import arc.files.*; -import arc.func.*; import arc.math.*; import arc.struct.*; -import arc.util.*; import arc.util.serialization.*; import mindustry.mod.*; import mindustry.net.*; import mindustry.net.Net.*; import mindustry.type.*; -import mindustry.ui.dialogs.*; +import mindustry.ui.FileChooser.*; import rhino.*; import java.net.*; -import static mindustry.Vars.*; - public interface Platform{ /** Dynamically creates a class loader for a jar file. This loader must be child-first. */ @@ -106,68 +102,9 @@ public interface Platform{ default void shareFile(Fi file){ } - default void export(String name, String extension, FileWriter writer){ - if(!ios){ - platform.showFileChooser(false, extension, file -> { - ui.loadAnd(() -> { - try{ - writer.write(file); - }catch(Throwable e){ - ui.showException(e); - Log.err(e); - } - }); - }); - }else{ - ui.loadAnd(() -> { - try{ - Fi result = Core.files.local(name + "." + extension); - writer.write(result); - platform.shareFile(result); - }catch(Throwable e){ - ui.showException(e); - Log.err(e); - } - }); - } - } - - /** - * Show a file chooser. - * @param cons Selection listener - * @param open Whether to open or save files - * @param extension File extension to filter - * @param title The title of the native dialog - */ - default void showFileChooser(boolean open, String title, String extension, Cons cons){ - defaultFileDialog(open, title, extension, cons); - } - - /** - * Show a file chooser for multiple file types. - * @param cons Selection listener - * @param extensions File extensions to filter - */ - default void showMultiFileChooser(Cons cons, String... extensions){ - defaultMultiFileChooser(cons, extensions); - } - - default void showFileChooser(boolean open, String extension, Cons cons){ - showFileChooser(open, open ? "@open": "@save", extension, cons); - } - - static void defaultFileDialog(boolean open, String title, String extension, Cons cons){ - new FileChooser(title, file -> file.extEquals(extension), open, file -> { - if(!open){ - cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension)); - }else{ - cons.get(file); - } - }).show(); - } - - static void defaultMultiFileChooser(Cons cons, String... extensions){ - new FileChooser("@open", file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show(); + /** Do not call directly; use the builder pattern in {@link mindustry.ui.FileChooser}. */ + default void showFileChooser(FileChooserParams params){ + throw new IllegalArgumentException("Not implemented on this platform!"); } /** Hide the app. Android only. */ diff --git a/core/src/mindustry/editor/MapEditorDialog.java b/core/src/mindustry/editor/MapEditorDialog.java index 1dfc7ad0d4..dd3cb57b86 100644 --- a/core/src/mindustry/editor/MapEditorDialog.java +++ b/core/src/mindustry/editor/MapEditorDialog.java @@ -94,7 +94,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ t.button("@editor.import", Icon.download, () -> createDialog("@editor.import", "@editor.importmap", "@editor.importmap.description", Icon.download, (Runnable)loadDialog::show, "@editor.importfile", "@editor.importfile.description", Icon.file, (Runnable)() -> - platform.showFileChooser(true, mapExtension, file -> ui.loadAnd(() -> { + FileChooser.open(mapExtension).submit(file -> ui.loadAnd(() -> { maps.tryCatchMapError(() -> { if(MapIO.isImage(file)){ ui.showInfo("@editor.errorimage"); @@ -105,7 +105,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ })), "@editor.importimage", "@editor.importimage.description", Icon.fileImage, (Runnable)() -> - platform.showFileChooser(true, "png", file -> + FileChooser.open("png").submit(file -> ui.loadAnd(() -> { try{ Pixmap pixmap = new Pixmap(file); @@ -124,9 +124,9 @@ public class MapEditorDialog extends Dialog implements Disposable{ t.button("@editor.export", Icon.upload, () -> createDialog("@editor.export", "@editor.exportfile", "@editor.exportfile.description", Icon.file, - (Runnable)() -> platform.export(editor.tags.get("name", "unknown"), mapExtension, file -> MapIO.writeMap(file, editor.createMap(file))), + (Runnable)() -> FileChooser.export(editor.tags.get("name", "unknown"), mapExtension, file -> MapIO.writeMap(file, editor.createMap(file))), "@editor.exportimage", "@editor.exportimage.description", Icon.fileImage, - (Runnable)() -> platform.export(editor.tags.get("name", "unknown"), "png", file -> { + (Runnable)() -> FileChooser.export(editor.tags.get("name", "unknown"), "png", file -> { Pixmap out = MapIO.writeImage(editor.tiles()); file.writePng(out); out.dispose(); diff --git a/core/src/mindustry/editor/data/MapAssetsDialog.java b/core/src/mindustry/editor/data/MapAssetsDialog.java index 76263c01d0..73edd2fcfd 100644 --- a/core/src/mindustry/editor/data/MapAssetsDialog.java +++ b/core/src/mindustry/editor/data/MapAssetsDialog.java @@ -16,12 +16,8 @@ import static mindustry.Vars.*; /* TODO: -• warn about critical errors for content that prevent content loading • dividers for content types -• weather icon in font? -• remove planets from list of content that can be parsed to prevent confusion? • drop down for import/export zip, clear all -• allow multi file selection when importing */ public class MapAssetsDialog extends BaseDialog{ private static final TextureRegionDrawable[] typeIcons = { diff --git a/core/src/mindustry/editor/data/MapAudioView.java b/core/src/mindustry/editor/data/MapAudioView.java index 5b245ebb04..81d77b33a0 100644 --- a/core/src/mindustry/editor/data/MapAudioView.java +++ b/core/src/mindustry/editor/data/MapAudioView.java @@ -71,6 +71,8 @@ public class MapAudioView implements AssetView{ m.setPosition(value); } }); + //only ogg can seek + slider.touchable = asset.path.endsWith(".ogg") ? Touchable.enabled : Touchable.disabled; slider.visible(() -> { boolean valid = lastPlaying == m && m.isPlaying(); if(valid){ @@ -88,25 +90,7 @@ public class MapAudioView implements AssetView{ } }).size(mobile ? 390f : 450f, h).margin(10f); - list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> { - if(ios){ - try{ - Fi out = tmpDirectory.child(Strings.getFileName(asset.path)); - file.copyTo(out); - platform.shareFile(out); - }catch(Exception e){ - ui.showException(e); - } - }else{ - platform.showFileChooser(false, Strings.getFileExtension(asset.path), result -> { - try{ - file.copyTo(result); - }catch(Exception e){ - ui.showException(e); - } - }); - } - }).size(h).disabled(file == null); + list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> FileChooser.export(asset.name, Strings.getFileExtension(asset.path), file::copyTo)).size(h).disabled(file == null); list.button(Icon.trash, Styles.graySquarei, iconMed, () -> { ui.showConfirm("@asset.delete.confirm", () -> { @@ -140,32 +124,35 @@ public class MapAudioView implements AssetView{ public void buildButtons(MapAssetsDialog diag, Table buttons){ buttons.button("@add", Icon.add, () -> { - platform.showMultiFileChooser(result -> { - try{ - //path and name are the same here; there's no path context. - String name = result.nameWithoutExtension(); - String path = name; - var other = state.data.getAssets(type).find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name))); - if(other != null){ - ui.showErrorMessage(Core.bundle.format("asset.exists", other.name + " (" + other.path + ")")); - return; + FileChooser.open("ogg", "mp3").submitMulti(result -> { + var assets = state.data.getAssets(type); + + for(Fi file : result){ + try{ + //path and name are the same here; there's no path context. + String name = file.nameWithoutExtension(); + String path = name; + var other = state.data.getAssets(type).find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name))); + if(other != null){ + ui.showErrorMessage(Core.bundle.format("asset.exists", other.name + " (" + other.path + ")")); + return; + } + + DataAsset asset = (type == DataAssetType.music ? new MusicAsset() : new SoundAsset()); + asset.setPath(path); + asset.updateData(file.readBytes()); + + assets.add(asset); + }catch(Exception e){ + ui.showException(e); } - - var assets = state.data.getAssets(type); - - DataAsset asset = (type == DataAssetType.music ? new MusicAsset() : new SoundAsset()); - asset.setPath(path); - asset.updateData(result.readBytes()); - - assets.add(asset); - assets.sort(); - state.data.reloadAudio(); - diag.rebuild(); - lastPlaying = null; - }catch(Exception e){ - ui.showException(e); } - }, "ogg", "mp3"); + + assets.sort(); + state.data.reloadAudio(); + diag.rebuild(); + lastPlaying = null; + }); }).size(190f, 64f); } diff --git a/core/src/mindustry/editor/data/MapBundlesView.java b/core/src/mindustry/editor/data/MapBundlesView.java index 4673e10ad0..d75edbc1b8 100644 --- a/core/src/mindustry/editor/data/MapBundlesView.java +++ b/core/src/mindustry/editor/data/MapBundlesView.java @@ -69,23 +69,7 @@ public class MapBundlesView implements AssetView{ }).size(h).disabled(file == null); list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> { - if(ios){ - try{ - Fi out = tmpDirectory.child(Strings.getFileName(bundle.path)); - file.copyTo(out); - platform.shareFile(out); - }catch(Exception e){ - ui.showException(e); - } - }else{ - platform.showFileChooser(false, "properties", out -> { - try{ - file.copyTo(out); - }catch(Exception e){ - ui.showException(e); - } - }); - } + FileChooser.export(bundle.name, "properties", file::copyTo); }).size(h).disabled(file == null); list.button(Icon.trash, Styles.graySquarei, iconMed, () -> { @@ -140,10 +124,12 @@ public class MapBundlesView implements AssetView{ handler.get("bundle.properties", Core.app.getClipboardText()); }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null); t.row(); - t.button("@schematic.importfile", Icon.download, style, () -> platform.showMultiFileChooser(file -> { + t.button("@schematic.importfile", Icon.download, style, () -> FileChooser.open("properties", "txt").submitMulti(files -> { dialog.hide(); - handler.get(file.name(), file.readString()); - }, "txt", "bundle")).marginLeft(12f); + for(var file : files){ + handler.get(file.name(), file.readString()); + } + })).marginLeft(12f); t.row(); }); }); diff --git a/core/src/mindustry/editor/data/MapContentView.java b/core/src/mindustry/editor/data/MapContentView.java index fe9ee78bc1..abe4b8ee6e 100644 --- a/core/src/mindustry/editor/data/MapContentView.java +++ b/core/src/mindustry/editor/data/MapContentView.java @@ -1,7 +1,6 @@ package mindustry.editor.data; import arc.*; -import arc.files.*; import arc.scene.event.*; import arc.scene.style.*; import arc.scene.ui.layout.*; @@ -24,7 +23,7 @@ public class MapContentView implements AssetView{ ContentType.unit, Icon.units, ContentType.block, Icon.distribution, ContentType.planet, Icon.planet, - ContentType.weather, Icon.power, //TODO cloud icon + ContentType.weather, Icon.drizzle, ContentType.status, Icon.power ); @@ -44,7 +43,14 @@ public class MapContentView implements AssetView{ dialog.cont.top().pane(p -> { p.top(); - for(var warning : content.warnings){ + var warns = content.warnings; + if(content.errored){ + warns = new Seq<>(); + warns.add(Iconc.warning + " This content has critical errors, and cannot be used until the errors are fixed."); + warns.addAll(content.warnings); + } + + for(var warning : warns){ p.table(Styles.grayPanel, in -> { in.add(warning.replaceAll("\t", " "), Styles.monoLabel).grow().wrap(); }).margin(6f).growX().pad(3f).row(); @@ -80,23 +86,7 @@ public class MapContentView implements AssetView{ }).size(h); list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> { - if(ios){ - try{ - Fi out = tmpDirectory.child(Strings.getFileName(content.path)); - out.writeString(content.data); - platform.shareFile(out); - }catch(Exception e){ - ui.showException(e); - } - }else{ - platform.showFileChooser(false, "properties", out -> { - try{ - out.writeString(content.data); - }catch(Exception e){ - ui.showException(e); - } - }); - } + FileChooser.export(content.name, "json", file -> file.writeString(content.data)); }).size(h); list.button(Icon.trash, Styles.graySquarei, iconMed, () -> { @@ -158,11 +148,11 @@ public class MapContentView implements AssetView{ choose.buttons.button("@cancel", Icon.cancel, choose::hide).size(170f, 64f); - choose.buttons.button("@asset.content.import.file", Icon.fileText, () -> platform.showMultiFileChooser(file -> { + choose.buttons.button("@asset.content.import.file", Icon.fileText, () -> FileChooser.open("json", "json5", "hjson").submit(file -> { choose.hide(); addContent(selected[0], nameField.getText(), file.readString()); diag.rebuild(); - }, "json", "hjson", "json5")).size(200f, 64f).disabled(b -> nameField.getText().isEmpty() || invalid[0]); + })).size(200f, 64f).disabled(b -> nameField.getText().isEmpty() || invalid[0]); choose.buttons.button("@asset.content.import.clipboard", Icon.copy, () -> { String text = Core.app.getClipboardText(); diff --git a/core/src/mindustry/editor/data/MapImagesView.java b/core/src/mindustry/editor/data/MapImagesView.java index 1bb73a5011..b1388eb014 100644 --- a/core/src/mindustry/editor/data/MapImagesView.java +++ b/core/src/mindustry/editor/data/MapImagesView.java @@ -30,40 +30,43 @@ public class MapImagesView implements AssetView{ public void buildButtons(MapAssetsDialog diag, Table buttons){ buttons.button("@add", Icon.add, () -> { - platform.showFileChooser(true, "png", result -> { - try{ - Pixmap pix = new Pixmap(result); - int width = pix.width; - int height = pix.height; - Pixmaps.bleed(pix); - byte[] bytes = PixmapIO.writePngBytes(pix); - pix.dispose(); + FileChooser.open("png", "jpeg", "jpg").submitMulti(results -> { + var images = getImages(); - if(width > DataPatcher.maxImageSize || height > DataPatcher.maxImageSize){ - ui.showErrorMessage(Core.bundle.format("asset.image.toolarge", width, height, DataPatcher.maxImageSize, DataPatcher.maxImageSize)); - return; + for(var result : results){ + try{ + Pixmap pix = new Pixmap(result); + int width = pix.width; + int height = pix.height; + Pixmaps.bleed(pix); + byte[] bytes = PixmapIO.writePngBytes(pix); + pix.dispose(); + + if(width > DataPatcher.maxImageSize || height > DataPatcher.maxImageSize){ + ui.showErrorMessage(Core.bundle.format("asset.image.toolarge", width, height, DataPatcher.maxImageSize, DataPatcher.maxImageSize)); + return; + } + + //path and name are the same here; there's no path context. + String name = result.nameWithoutExtension(); + String path = name; + var other = images.find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name))); + if(other != null){ + ui.showErrorMessage(Core.bundle.format("asset.image.exists", other.name + " (" + other.path + ")")); + return; + } + + byte[] hash = assetCache.add(bytes); + + images.add(new ImageAsset(path, hash)); + }catch(Exception e){ + ui.showException(e); } - - var images = getImages(); - - //path and name are the same here; there's no path context. - String name = result.nameWithoutExtension(); - String path = name; - var other = images.find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name))); - if(other != null){ - ui.showErrorMessage(Core.bundle.format("asset.image.exists", other.name + " (" + other.path + ")")); - return; - } - - byte[] hash = assetCache.add(bytes); - - images.add(new ImageAsset(path, hash)); - images.sort(); - state.data.reloadImages(images); - diag.rebuild(); - }catch(Exception e){ - ui.showException(e); } + + images.sort(); + state.data.reloadImages(images); + diag.rebuild(); }); }).size(190f, 64f); @@ -74,7 +77,7 @@ public class MapImagesView implements AssetView{ p.table(Tex.button, t -> { TextButtonStyle style = Styles.flatt; t.defaults().size(280f, 60f).left(); - t.button("@asset.image.importzip", Icon.download, style, () -> platform.showFileChooser(true, "zip", file -> { + t.button("@asset.image.importzip", Icon.download, style, () -> FileChooser.open("zip").submit(file -> { //Android doesn't allow accessing the file contents outside the callback; other platforms either copy it already, or don't have dumb permission issues Fi targetFile; @@ -171,21 +174,17 @@ public class MapImagesView implements AssetView{ } }); })).marginLeft(12f).row(); - t.button("@asset.image.exportzip", Icon.upload, style, () -> platform.showFileChooser(false, "zip", file -> { + t.button("@asset.image.exportzip", Icon.upload, style, () -> FileChooser.export("images", "zip", file -> { dialog.hide(); - try{ - try(OutputStream fos = file.write(false, 4096); ZipOutputStream zos = new ZipOutputStream(fos)){ - for(var image : getImages()){ - Fi cacheFile = image.getCacheFile(); - if(cacheFile == null) continue; + try(OutputStream fos = file.write(false, 4096); ZipOutputStream zos = new ZipOutputStream(fos)){ + for(var image : getImages()){ + Fi cacheFile = image.getCacheFile(); + if(cacheFile == null) continue; - zos.putNextEntry(new ZipEntry(image.path + ".png")); - zos.write(cacheFile.readBytes()); - zos.closeEntry(); - } + zos.putNextEntry(new ZipEntry(image.path + ".png")); + zos.write(cacheFile.readBytes()); + zos.closeEntry(); } - }catch(Throwable e){ - ui.showException(e); } })).marginLeft(12f).row(); t.button("@asset.image.clearall", Icon.trash, style, () -> { @@ -279,13 +278,7 @@ public class MapImagesView implements AssetView{ }); b.button(Icon.info, istyle, showInfo); b.button(Icon.export, istyle, () -> { - platform.showFileChooser(false, "png", out -> { - try{ - image.getCacheFileNoNull().copyTo(out); - }catch(Throwable e){ - ui.showException(e); - } - }); + FileChooser.export(image.name, "png", out -> image.getCacheFileNoNull().copyTo(out)); }); b.button(Icon.trash, istyle, () -> { ui.showConfirm("@asset.image.delete.confirm", () -> { diff --git a/core/src/mindustry/editor/data/MapPatchesView.java b/core/src/mindustry/editor/data/MapPatchesView.java index 5ec44c1068..5f260a0489 100644 --- a/core/src/mindustry/editor/data/MapPatchesView.java +++ b/core/src/mindustry/editor/data/MapPatchesView.java @@ -18,7 +18,10 @@ public class MapPatchesView implements AssetView{ @Override public void buildButtons(MapAssetsDialog diag, Table buttons){ - buttons.button("@add", Icon.add, () -> showImport(this::addPatch)).size(190f, 64f); + buttons.button("@add", Icon.add, () -> showImport(s -> { + addPatch(s); + diag.rebuild(); + })).size(190f, 64f); } @Override @@ -106,10 +109,11 @@ public class MapPatchesView implements AssetView{ handler.get(Core.app.getClipboardText()); }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null); t.row(); - t.button("@schematic.importfile", Icon.download, style, () -> platform.showMultiFileChooser(file -> { + //currently doesn't allow multi import because it could be replacing exactly 1 patch + t.button("@schematic.importfile", Icon.download, style, () -> FileChooser.open("json", "hjson", "json5").submit(file -> { dialog.hide(); handler.get(file.readString()); - }, "json", "hjson", "json5")).marginLeft(12f); + })).marginLeft(12f); t.row(); }); }); diff --git a/core/src/mindustry/mod/DataPatcher.java b/core/src/mindustry/mod/DataPatcher.java index ddf58c79c3..b31fd8512e 100644 --- a/core/src/mindustry/mod/DataPatcher.java +++ b/core/src/mindustry/mod/DataPatcher.java @@ -28,7 +28,7 @@ import java.util.*; /** The current implementation is awful. Consider it a proof of concept. */ @SuppressWarnings("unchecked") public class DataPatcher{ - public static final int maxImageSize = 1024; + public static final int maxImageSize = 2000; public static final int patchFormatVersion = 2; private static boolean needsArrayFix = false; @@ -109,7 +109,9 @@ public class DataPatcher{ set.error = false; try{ - JsonValue value = parser.getJson().fromJson(null, Jval.read(set.patch).toString(Jformat.plain)); + Object someValue = parser.getJson().fromJson(null, Jval.read(set.patch).toString(Jformat.plain)); + if(!(someValue instanceof JsonValue value)) throw new SerializationException("Patch must be a JSON object."); + if(Vars.state.rules.planet != null && value.has("requiredPlanets")){ JsonValue req = value.get("requiredPlanets"); value.remove("requiredPlanets"); @@ -136,6 +138,7 @@ public class DataPatcher{ }catch(Exception e){ set.error = true; + set.name = ""; set.warnings.add(Strings.getSimpleMessage(e)); currentlyApplyingPatch = null; @@ -146,7 +149,6 @@ public class DataPatcher{ if(!content.isEmpty()){ content.sort(); - ContentLoader preLoad = Vars.content.copy(); dpMod.erroredContent.clear(); for(var asset : content){ @@ -169,7 +171,6 @@ public class DataPatcher{ needsArrayFix = true; } - //TODO: what to do when errors happen in general? try{ //this binds the content but does not load it entirely asset.content = parser.parse(dpMod, asset.name, asset.data, file, asset.type); @@ -178,10 +179,11 @@ public class DataPatcher{ asset.warnings.add(Strings.getFinalMessage(e)); asset.errored = true; - //TODO: this warning is not very useful, nor is markError in general. What should be done here? - if(current != Vars.content.getLastAdded() && Vars.content.getLastAdded() != null){ + var lastAdded = Vars.content.getLastAdded(); + if(current != lastAdded && lastAdded != null){ + Vars.content.remove(lastAdded); //markError should log it already - parser.markError(Vars.content.getLastAdded(), dpMod, file, e); + parser.markError(lastAdded, dpMod, file, e); }else{ Log.err("Error loading content: " + asset.path, e); } @@ -207,6 +209,7 @@ public class DataPatcher{ try{ cont.init(); }catch(Throwable t){ + Vars.content.remove(cont); if(cont.minfo.asset != null) cont.minfo.asset.errored = true; parser.markError(cont, dpMod, cont.minfo.sourceFile, t); } @@ -216,6 +219,7 @@ public class DataPatcher{ try{ cont.postInit(); }catch(Throwable t){ + Vars.content.remove(cont); if(cont.minfo.asset != null) cont.minfo.asset.errored = true; parser.markError(cont, dpMod, cont.minfo.sourceFile, t); } @@ -227,24 +231,18 @@ public class DataPatcher{ cont.loadIcon(); cont.load(); if(cont.minfo.asset != null && cont instanceof UnlockableContent u){ - if(!u.uiIcon.found()){ + if(!u.uiIcon.found() && u.getContentType() != ContentType.planet && u.getContentType() != ContentType.weather){ cont.minfo.asset.warnings.add("[" + u.name.substring(u.minfo.mod.name.length() + 1) + "] Could not find an icon. Ensure that you have an image named '" + u.name + "' loaded. Remember that imported images always have the 'dp-' prefix automatically applied."); } } }catch(Throwable t){ + Vars.content.remove(cont); if(cont.minfo.asset != null) cont.minfo.asset.errored = true; parser.markError(cont, dpMod, cont.minfo.sourceFile, t); } } } - if(all.contains(Content::hasErrored)){ - Log.err("Errors were encountered loading content. Content will be unloaded."); - callContentRemove(); - Vars.content = preLoad; - needsArrayFix = false; - } - if(reloadContentWorld) fixContentArrays(); } diff --git a/core/src/mindustry/mod/data/ContentAsset.java b/core/src/mindustry/mod/data/ContentAsset.java index 236b2e8ab5..33bd2302aa 100644 --- a/core/src/mindustry/mod/data/ContentAsset.java +++ b/core/src/mindustry/mod/data/ContentAsset.java @@ -8,8 +8,8 @@ import mindustry.ctype.*; import java.io.*; public class ContentAsset extends DataAsset{ - //Note: sectors can't be loaded at the moment - public static final ContentType[] loadableContent = {ContentType.item, ContentType.block, ContentType.liquid, ContentType.status, ContentType.unit, ContentType.weather, ContentType.planet}; + //Note: sectors and planets can't be loaded at the moment, as custom campaigns aren't functional to begin with, and adding them would cause confusion + public static final ContentType[] loadableContent = {ContentType.item, ContentType.block, ContentType.liquid, ContentType.status, ContentType.unit, ContentType.weather}; /** Content type to be parsed as. */ public ContentType type = ContentType.unit; diff --git a/core/src/mindustry/ui/FileChooser.java b/core/src/mindustry/ui/FileChooser.java new file mode 100644 index 0000000000..473eee7fc7 --- /dev/null +++ b/core/src/mindustry/ui/FileChooser.java @@ -0,0 +1,142 @@ +package mindustry.ui; + +import arc.*; +import arc.files.*; +import arc.func.*; +import arc.util.*; +import mindustry.*; +import mindustry.core.Platform.*; +import mindustry.ui.dialogs.*; + +import static mindustry.Vars.*; + +public class FileChooser{ + + public static void export(String name, String extension, FileWriter writer){ + if(!ios){ + FileChooser.save(extension).name(name).submit(file -> { + ui.loadAnd(() -> { + try{ + writer.write(file); + }catch(Throwable e){ + ui.showException(e); + Log.err(e); + } + }); + }); + }else{ + ui.loadAnd(() -> { + try{ + Fi result = Core.files.local(name + "." + extension); + writer.write(result); + platform.shareFile(result); + }catch(Throwable e){ + ui.showException(e); + Log.err(e); + } + }); + } + } + + public static FileChooserParams open(String... extensions){ + return new FileChooserParams().open(true).extensions(extensions); + } + + public static FileChooserParams save(String... extensions){ + return new FileChooserParams().open(false).extensions(extensions); + } + + public static FileChooserDialog createFallbackFileChooser(FileChooserParams params){ + return new FileChooserDialog(params.title, file -> Structs.contains(params.extensions, file::extEquals), params.open, file -> { + if(!params.open){ + params.handleChooseResult(file.parent().child(file.nameWithoutExtension() + "." + params.extensions[0])); + }else{ + params.handleChooseResult(file); + } + }); + } + + public static void showFallbackFileChooser(FileChooserParams params){ + createFallbackFileChooser(params).show(); + } + + public static class FileChooserParams{ + public boolean open; + public boolean allowMultiple; + public String title; + public String fileName; + public String[] extensions; + public @Nullable Cons handler; + public @Nullable Cons multipleHandler; + + private void checkParams(){ + if(extensions == null || extensions.length == 0) throw new IllegalArgumentException("Extension types must be defined."); + + if(title == null){ + title = open ? Core.bundle.get("open") : Core.bundle.get("save"); + }else if(title.startsWith("@")){ + title = Core.bundle.get(title.substring(1)); + } + + if(fileName == null){ + fileName = "file." + extensions[0]; + } + } + + /** Submits the request to handle a single file. */ + public void submit(Cons handler){ + checkParams(); + this.handler = handler; + Vars.platform.showFileChooser(this); + } + + /** Submits the request to handle multiple files. */ + public void submitMulti(Cons multipleHandler){ + checkParams(); + if(!open) throw new IllegalArgumentException("Saving in a file chooser with multiple choices does not make sense."); + + this.multipleHandler = multipleHandler; + this.allowMultiple = true; + Vars.platform.showFileChooser(this); + } + + /** Submits the request to handle multiple files as a loop that iterates through the resulting files and invokes the callback repeatedly. */ + public void submitMultiLoop(Cons multipleHandler){ + submitMulti(files -> { + for(var f : files){ + multipleHandler.get(f); + } + }); + } + + public FileChooserParams name(String fileName){ + this.fileName = fileName; + return this; + } + + public FileChooserParams extensions(String... extensions){ + this.extensions = extensions; + return this; + } + + public FileChooserParams open(boolean open){ + this.open = open; + return this; + } + + public FileChooserParams title(String title){ + this.title = title; + return this; + } + + public void handleChooseResult(Fi... files){ + if(files.length == 0) return; + + if(handler != null){ + handler.get(files[0]); + }else if(multipleHandler != null){ + multipleHandler.get(files); + } + } + } +} diff --git a/core/src/mindustry/ui/dialogs/CanvasEditDialog.java b/core/src/mindustry/ui/dialogs/CanvasEditDialog.java index 1ff852b782..292a5e8a98 100644 --- a/core/src/mindustry/ui/dialogs/CanvasEditDialog.java +++ b/core/src/mindustry/ui/dialogs/CanvasEditDialog.java @@ -43,9 +43,9 @@ public class CanvasEditDialog extends BaseDialog{ addCloseButton(160f); - buttons.button("@import", Icon.image, () -> platform.showFileChooser(true, "png", this::importFrom)); + buttons.button("@import", Icon.image, () -> FileChooser.open("png", "jpeg").submit(this::importFrom)); - buttons.button("@export", Icon.export, () -> platform.showFileChooser(false, "png", this::exportTo)); + buttons.button("@export", Icon.export, () -> FileChooser.export("canvas", "png", this::exportTo)); hidden(() -> { save(); diff --git a/core/src/mindustry/ui/dialogs/EditorMapsDialog.java b/core/src/mindustry/ui/dialogs/EditorMapsDialog.java index 15af17fc44..017381f263 100644 --- a/core/src/mindustry/ui/dialogs/EditorMapsDialog.java +++ b/core/src/mindustry/ui/dialogs/EditorMapsDialog.java @@ -1,6 +1,7 @@ package mindustry.ui.dialogs; import arc.*; +import arc.files.*; import arc.graphics.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; @@ -40,48 +41,49 @@ public class EditorMapsDialog extends MapListDialog{ }).size(210f, 64f); buttons.button("@editor.importmap", Icon.upload, () -> { - platform.showFileChooser(true, mapExtension, file -> { + FileChooser.open(mapExtension).submitMulti(files -> { ui.loadAnd(() -> { - maps.tryCatchMapError(() -> { - if(MapIO.isImage(file)){ - ui.showErrorMessage("@editor.errorimage"); - return; - } + for(Fi file : files){ + maps.tryCatchMapError(() -> { + if(MapIO.isImage(file)){ + ui.showErrorMessage("@editor.errorimage"); + return; + } - Map map = MapIO.createMap(file, true); + Map map = MapIO.createMap(file, true); - //when you attempt to import a save, it will have no name, so generate one - String name = map.tags.get("name", () -> { - String result = "unknown"; - int number = 0; - while(maps.byName(result + number++) != null) ; - return result + number; - }); - - //this will never actually get called, but it remains just in case - if(name == null){ - ui.showErrorMessage("@editor.errorname"); - return; - } - - Map conflict = maps.all().find(m -> m.name().equalsIgnoreCase(name)); - - if(conflict != null && !conflict.custom){ - ui.showInfo(Core.bundle.format("editor.import.exists", name)); - }else if(conflict != null){ - ui.showConfirm("@confirm", Core.bundle.format("editor.overwrite.confirm", map.name()), () -> { - maps.tryCatchMapError(() -> { - maps.removeMap(conflict); - maps.importMap(map.file); - setup(); - }); + //when you attempt to import a save, it will have no name, so generate one + String name = map.tags.get("name", () -> { + String result = "unknown"; + int number = 0; + while(maps.byName(result + number++) != null) ; + return result + number; }); - }else{ - maps.importMap(map.file); - setup(); - } - }); + //this will never actually get called, but it remains just in case + if(name == null){ + ui.showErrorMessage("@editor.errorname"); + return; + } + + Map conflict = maps.all().find(m -> m.name().equalsIgnoreCase(name)); + + if(conflict != null && !conflict.custom){ + ui.showInfo(Core.bundle.format("editor.import.exists", name)); + }else if(conflict != null){ + ui.showConfirm("@confirm", Core.bundle.format("editor.overwrite.confirm", map.name()), () -> { + maps.tryCatchMapError(() -> { + maps.removeMap(conflict); + maps.importMap(map.file); + setup(); + }); + }); + }else{ + maps.importMap(map.file); + setup(); + } + }); + } }); }); }).size(210f, 64f); diff --git a/core/src/mindustry/ui/dialogs/FileChooser.java b/core/src/mindustry/ui/dialogs/FileChooserDialog.java similarity index 98% rename from core/src/mindustry/ui/dialogs/FileChooser.java rename to core/src/mindustry/ui/dialogs/FileChooserDialog.java index 6a33696fdc..4fa65154c1 100644 --- a/core/src/mindustry/ui/dialogs/FileChooser.java +++ b/core/src/mindustry/ui/dialogs/FileChooserDialog.java @@ -16,7 +16,7 @@ import mindustry.ui.*; import java.util.*; -public class FileChooser extends BaseDialog{ +public class FileChooserDialog extends BaseDialog{ private static final Fi homeDirectory = Core.files.absolute(Core.files.getExternalStoragePath()); static Fi lastDirectory = Core.files.absolute(Core.settings.getString("lastDirectory", homeDirectory.absolutePath())); @@ -30,7 +30,7 @@ public class FileChooser extends BaseDialog{ private Cons selectListener; private boolean open; - public FileChooser(String title, Boolf filter, boolean open, Cons result){ + public FileChooserDialog(String title, Boolf filter, boolean open, Cons result){ super(title); setFillParent(true); this.open = open; diff --git a/core/src/mindustry/ui/dialogs/LoadDialog.java b/core/src/mindustry/ui/dialogs/LoadDialog.java index f85b337052..48e906b3bc 100644 --- a/core/src/mindustry/ui/dialogs/LoadDialog.java +++ b/core/src/mindustry/ui/dialogs/LoadDialog.java @@ -1,6 +1,7 @@ package mindustry.ui.dialogs; import arc.*; +import arc.files.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.scene.style.*; @@ -139,7 +140,7 @@ public class LoadDialog extends BaseDialog{ if(slot.hasExternalAssets() && !slot.isBeingPlayed()){ ui.showInfo("@save.export.needsload"); }else{ - platform.export("save-" + slot.getName(), saveExtension, slot::exportFile); + FileChooser.export(slot.getName(), saveExtension, slot::exportFile); } }).right(); @@ -196,24 +197,27 @@ public class LoadDialog extends BaseDialog{ public void addSetup(){ buttons.button("@save.import", Icon.add, () -> { - platform.showFileChooser(true, saveExtension, file -> { - if(SaveIO.isSaveValid(file)){ - var meta = SaveIO.getMeta(file); + FileChooser.open("msav").submitMulti(files -> { + for(Fi file : files){ + if(SaveIO.isSaveValid(file)){ + var meta = SaveIO.getMeta(file); - if(meta.rules.sector != null){ - ui.showErrorMessage("@save.nocampaign"); - }else{ - try{ - control.saves.importSave(file); - rebuild(); - }catch(IOException e){ - e.printStackTrace(); - ui.showException("@save.import.fail", e); + if(meta.rules.sector != null){ + ui.showErrorMessage("@save.nocampaign"); + }else{ + try{ + control.saves.importSave(file); + }catch(Exception e){ + Log.err(e); + ui.showException("@save.import.fail", e); + } } + }else{ + ui.showErrorMessage("@save.import.invalid"); } - }else{ - ui.showErrorMessage("@save.import.invalid"); } + + rebuild(); }); }).fillX().margin(10f); } diff --git a/core/src/mindustry/ui/dialogs/ModsDialog.java b/core/src/mindustry/ui/dialogs/ModsDialog.java index f14b7c2458..4efb7f361c 100644 --- a/core/src/mindustry/ui/dialogs/ModsDialog.java +++ b/core/src/mindustry/ui/dialogs/ModsDialog.java @@ -191,15 +191,18 @@ public class ModsDialog extends BaseDialog{ t.button("@mod.import.file", Icon.file, bstyle, () -> { dialog.hide(); - platform.showMultiFileChooser(file -> { - try{ - mods.importMod(file); - setup(); - }catch(Exception e){ - ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("writable dex") ? "@error.moddex" : "", e); - Log.err(e); + FileChooser.open("zip", "jar").submitMulti(files -> { + for(var file : files){ + try{ + mods.importMod(file); + }catch(Exception e){ + ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("writable dex") ? "@error.moddex" : "", e); + Log.err(e); + } } - }, "zip", "jar"); + + setup(); + }); }).margin(12f); t.row(); diff --git a/core/src/mindustry/ui/dialogs/SchematicsDialog.java b/core/src/mindustry/ui/dialogs/SchematicsDialog.java index 1f3ccd390e..4b4acc7f37 100644 --- a/core/src/mindustry/ui/dialogs/SchematicsDialog.java +++ b/core/src/mindustry/ui/dialogs/SchematicsDialog.java @@ -1,6 +1,7 @@ package mindustry.ui.dialogs; import arc.*; +import arc.files.*; import arc.func.*; import arc.graphics.*; import arc.graphics.Texture.*; @@ -241,19 +242,28 @@ public class SchematicsDialog extends BaseDialog{ } }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith(schematicBaseStart)); t.row(); - t.button("@schematic.importfile", Icon.download, style, () -> platform.showFileChooser(true, schematicExtension, file -> { + t.button("@schematic.importfile", Icon.download, style, () -> FileChooser.open(schematicExtension).submitMulti(files -> { dialog.hide(); - try{ - Schematic s = Schematics.read(file); - s.removeSteamID(); - schematics.add(s); - setup(); - showInfo(s); - checkTags(s); - }catch(Exception e){ - ui.showException(e); + Schematic last = null; + + for(Fi file : files){ + try{ + Schematic s = Schematics.read(file); + s.removeSteamID(); + schematics.add(s); + checkTags(s); + last = s; + }catch(Exception e){ + ui.showException(e); + } } + + if(last != null){ + showInfo(last); + } + + setup(); })).marginLeft(12f); t.row(); if(steam){ @@ -290,7 +300,7 @@ public class SchematicsDialog extends BaseDialog{ t.row(); t.button("@schematic.exportfile", Icon.export, style, () -> { dialog.hide(); - platform.export(s.name(), schematicExtension, file -> Schematics.write(s, file)); + FileChooser.export(s.name(), schematicExtension, file -> Schematics.write(s, file)); }).marginLeft(12f); }); }); diff --git a/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java b/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java index d9fd8017e6..60ae162d82 100644 --- a/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java +++ b/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java @@ -249,30 +249,12 @@ public class SettingsMenuDialog extends BaseDialog{ t.row(); t.button("@data.export", Icon.upload, style, () -> { - if(ios){ - Fi file = Core.files.local("mindustry-data-export.zip"); - try{ - exportData(file); - }catch(Exception e){ - ui.showException(e); - } - platform.shareFile(file); - }else{ - platform.showFileChooser(false, "zip", file -> { - try{ - exportData(file); - ui.showInfo("@data.exported"); - }catch(Exception e){ - e.printStackTrace(); - ui.showException(e); - } - }); - } + FileChooser.export("mindustry-data-export", "zip", this::exportData); }).marginLeft(4); t.row(); - t.button("@data.import", Icon.download, style, () -> ui.showConfirm("@confirm", "@data.import.confirm", () -> platform.showFileChooser(true, "zip", file -> { + t.button("@data.import", Icon.download, style, () -> ui.showConfirm("@confirm", "@data.import.confirm", () -> FileChooser.open("zip").submit(file -> { try{ importData(file); control.saves.resetSave(); @@ -281,7 +263,7 @@ public class SettingsMenuDialog extends BaseDialog{ }catch(IllegalArgumentException e){ ui.showErrorMessage("@data.invalid"); }catch(Exception e){ - e.printStackTrace(); + Log.err(e); if(e.getMessage() == null || !e.getMessage().contains("too short")){ ui.showException(e); }else{ @@ -301,20 +283,7 @@ public class SettingsMenuDialog extends BaseDialog{ if(settings.getDataDirectory().child("crashes").list().length == 0 && !settings.getDataDirectory().child("last_log.txt").exists()){ ui.showInfo("@crash.none"); }else{ - if(ios){ - Fi logs = tmpDirectory.child("logs.txt"); - logs.writeString(getLogs()); - platform.shareFile(logs); - }else{ - platform.showFileChooser(false, "txt", file -> { - try{ - file.writeBytes(getLogs().getBytes(Strings.utf8)); - app.post(() -> ui.showInfo("@crash.exported")); - }catch(Throwable e){ - ui.showException(e); - } - }); - } + FileChooser.export("logs", "txt", file -> file.writeString(getLogs())); } }).marginLeft(4); }); diff --git a/core/src/mindustry/ui/fragments/ConsoleFragment.java b/core/src/mindustry/ui/fragments/ConsoleFragment.java index 07e24fe2b6..19f5a4454e 100644 --- a/core/src/mindustry/ui/fragments/ConsoleFragment.java +++ b/core/src/mindustry/ui/fragments/ConsoleFragment.java @@ -122,7 +122,7 @@ public class ConsoleFragment extends Table{ button(Icon.downOpen, Styles.cleari, () -> scrollPos = Mathf.clamp(scrollPos - 1, 0, Math.max(0, messages.size))).disabled(b -> scrollPos <= 0).size(s).padLeft(4f); - button(Icon.fileText, Styles.cleari, () -> platform.showFileChooser(true, "js", file -> { + button(Icon.fileText, Styles.cleari, () -> FileChooser.open("js").submit(file -> { try{ mods.getScripts().runConsole(file.readString()); }catch(Exception e){ diff --git a/desktop/src/mindustry/desktop/DesktopLauncher.java b/desktop/src/mindustry/desktop/DesktopLauncher.java index 9982b9be91..6b23a4353c 100644 --- a/desktop/src/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/mindustry/desktop/DesktopLauncher.java @@ -8,7 +8,6 @@ import arc.discord.*; import arc.discord.DiscordRPC.*; import arc.filedialogs.*; import arc.files.*; -import arc.func.*; import arc.math.*; import arc.profiling.*; import arc.struct.*; @@ -27,6 +26,8 @@ import mindustry.net.*; import mindustry.net.Net.*; import mindustry.service.*; import mindustry.type.*; +import mindustry.ui.*; +import mindustry.ui.FileChooser.*; import mindustry.ui.dialogs.*; import java.io.*; @@ -327,30 +328,11 @@ public class DesktopLauncher extends ClientLauncher{ } @Override - public void showFileChooser(boolean open, String title, String extension, Cons cons){ - showNativeFileChooser(title, open, cons, extension); - } - - @Override - public void showMultiFileChooser(Cons cons, String... extensions){ - showNativeFileChooser("@open", true, cons, extensions); - } - - void showNativeFileChooser(String title, boolean open, Cons cons, String... shownExtensions){ - String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'"); - - //this should never happen unless someone is being dumb with the parameters - String[] ext = shownExtensions == null || shownExtensions.length == 0 ? new String[]{""} : shownExtensions; - - if(OS.isLinux){ - showZenity(open, formatted, shownExtensions, cons, () -> Platform.defaultFileDialog(open, title, ext[0], cons)); - return; - } - - //native file dialog + public void showFileChooser(FileChooserParams params){ Threads.daemon(() -> { try{ FileDialogs.loadNatives(); + var ext = params.extensions; String result; String[] patterns = new String[ext.length]; @@ -359,14 +341,14 @@ public class DesktopLauncher extends ClientLauncher{ } //on MacOS, .msav is not properly recognized until I put garbage into the array? - if(patterns.length == 1 && OS.isMac && open){ + if(patterns.length == 1 && OS.isMac && params.open){ patterns = new String[]{"", "*." + ext[0]}; } - if(open){ - result = FileDialogs.openFileDialog(formatted, FileChooser.getLastDirectory().absolutePath(), patterns, "." + ext[0] + " files", false); + if(params.open){ + result = FileDialogs.openFileDialog(params.title, FileChooserDialog.getLastDirectory().absolutePath() + "/", patterns, "." + ext[0] + " files", params.allowMultiple); }else{ - result = FileDialogs.saveFileDialog(formatted, FileChooser.getLastDirectory().child("file." + ext[0]).absolutePath(), patterns, "." + ext[0] + " files"); + result = FileDialogs.saveFileDialog(params.title, FileChooserDialog.getLastDirectory().child(params.fileName).absolutePath(), patterns, "." + ext[0] + " files"); } if(result == null) return; @@ -378,86 +360,32 @@ public class DesktopLauncher extends ClientLauncher{ //cancelled selection, ignore result if(result.isEmpty() || result.equals("\n")) return; if(result.endsWith("\n")) result = result.substring(0, result.length() - 1); - if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\""); - Fi file = Core.files.absolute(result); + Fi[] resultFiles = Seq.with(result.split("\\|")).map(Core.files::absolute).toArray(Fi.class); + + if(result.isEmpty()) return; + Core.app.post(() -> { - FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent()); + FileChooserDialog.setLastDirectory(resultFiles[0].isDirectory() ? resultFiles[0] : resultFiles[0].parent()); - if(!open){ - cons.get(file.parent().child(file.nameWithoutExtension() + "." + ext[0])); + if(!params.open){ + Fi single = resultFiles[0]; + //fix extension to match filters + if(!Structs.contains(params.extensions, single::extEquals)){ + single = single.parent().child(single.nameWithoutExtension() + "." + ext[0]); + } + params.handleChooseResult(single); }else{ - cons.get(file); + params.handleChooseResult(resultFiles); } }); }catch(Throwable error){ - Log.err("Failure to execute native file chooser", error); - Core.app.post(() -> { - if(ext.length > 1){ - showMultiFileChooser(cons, ext); - }else{ - Platform.defaultFileDialog(open, formatted, ext[0], cons); - } - }); + Log.err("Failed to execute native file chooser", error); + Core.app.post(() -> FileChooser.showFallbackFileChooser(params)); } }); } - - /** attempt to use the native file picker with zenity, or runs the fallback Runnable if the operation fails */ - static void showZenity(boolean open, String title, String[] extensions, Cons cons, Runnable fallback){ - Threads.daemon(() -> { - try{ - String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'"); - - String last = FileChooser.getLastDirectory().absolutePath(); - if(!last.endsWith("/")) last += "/"; - - //zenity doesn't support filtering by extension - Seq args = Seq.with("zenity", - "--file-selection", - "--title=" + formatted, - "--filename=" + last, - "--confirm-overwrite", - "--file-filter=" + Seq.with(extensions).toString(" ", s -> "*." + s), - "--file-filter=All files | *" //allow anything if the user wants - ); - - if(!open){ - args.add("--save"); - } - - String result = OS.exec(args.toArray(String.class)); - //first line. - if(result.length() > 1 && result.contains("\n")){ - result = result.split("\n")[0]; - } - - //cancelled selection, ignore result - if(result.isEmpty() || result.equals("\n")) return; - - if(result.endsWith("\n")) result = result.substring(0, result.length() - 1); - if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\""); - - Fi file = Core.files.absolute(result); - Core.app.post(() -> { - FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent()); - - if(!open){ - cons.get(file.parent().child(file.nameWithoutExtension() + "." + extensions[0])); - }else{ - cons.get(file); - } - }); - }catch(Exception e){ - Log.err(e); - Log.warn("zenity not found, using non-native file dialog. Consider installing `zenity` for native file dialogs."); - Core.app.post(fallback); - } - }); - } - - @Override public Seq getWorkshopContent(Class type){ return !steam ? super.getWorkshopContent(type) : SVars.workshop.getWorkshopFiles(type); diff --git a/gradle.properties b/gradle.properties index c1bee84ab8..a701ca8f75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,4 +26,4 @@ org.gradle.caching=true org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 android.enableR8.fullMode=false -archash=aaa6d755e7 +archash=62887bdcae diff --git a/ios/src/mindustry/ios/IOSLauncher.java b/ios/src/mindustry/ios/IOSLauncher.java index ad377c8397..6aa59234a4 100644 --- a/ios/src/mindustry/ios/IOSLauncher.java +++ b/ios/src/mindustry/ios/IOSLauncher.java @@ -4,8 +4,8 @@ import arc.*; import arc.Input.*; import arc.backend.robovm.*; import arc.files.*; -import arc.func.*; import arc.scene.ui.layout.*; +import arc.struct.*; import arc.util.*; import arc.util.io.*; import mindustry.*; @@ -13,6 +13,7 @@ import mindustry.game.EventType.*; import mindustry.game.Saves.*; import mindustry.io.*; import mindustry.net.*; +import mindustry.ui.FileChooser.*; import mindustry.ui.*; import org.robovm.apple.coregraphics.*; import org.robovm.apple.foundation.*; @@ -44,16 +45,16 @@ public class IOSLauncher extends IOSApplication.Delegate{ return new IOSApplication(new ClientLauncher(){ @Override - public void showFileChooser(boolean open, String titleIgn, String extension, Cons cons){ - if(!open){ //when exporting, just share it. + public void showFileChooser(FileChooserParams params){ + if(!params.open){ //when exporting, just share it. //ask for export name Core.input.getTextInput(new TextInput(){{ title = Core.bundle.get("filename"); accepted = name -> { try{ //write result - Fi result = tmpDirectory.child(name + "." + extension); - cons.get(result); + Fi result = tmpDirectory.child(name + "." + params.extensions[0]); + params.handleChooseResult(result); //import the document shareFile(result); @@ -80,33 +81,35 @@ public class IOSLauncher extends IOSApplication.Delegate{ @Override public void didPickDocumentsAtURLs(UIDocumentBrowserViewController controller, NSArray documentURLs){ - if(documentURLs.size() < 1) return; + if(documentURLs.isEmpty()) return; - NSURL url = documentURLs.first(); + Seq results = new Seq<>(); NSFileCoordinator coord = new NSFileCoordinator(null); - url.startAccessingSecurityScopedResource(); - try{ - coord.coordinateReadingItem(url, NSFileCoordinatorReadingOptions.ForUploading, result -> { - - Fi src = Core.files.absolute(result.getAbsoluteURL().getPath()); - Fi dst = Core.files.absolute(getDocumentsDirectory()).child(src.name()); - src.copyTo(dst); - - Core.app.post(() -> { - try{ - cons.get(dst); - }catch(Throwable t){ - ui.showException(t); - } + for(NSURL url : documentURLs){ + url.startAccessingSecurityScopedResource(); + try{ + coord.coordinateReadingItem(url, NSFileCoordinatorReadingOptions.ForUploading, result -> { + Fi src = Core.files.absolute(result.getAbsoluteURL().getPath()); + Fi dst = Core.files.absolute(getDocumentsDirectory()).child(src.name()); + src.copyTo(dst); + results.add(dst); }); - }); - }catch(Throwable e){ - ui.showException(e); + }catch(Throwable e){ + ui.showException(e); + } + + url.stopAccessingSecurityScopedResource(); + + cont.dismissViewController(true, () -> {}); } - url.stopAccessingSecurityScopedResource(); - - cont.dismissViewController(true, () -> {}); + Core.app.post(() -> { + try{ + params.handleChooseResult(results.toArray(Fi.class)); + }catch(Throwable t){ + ui.showException(t); + } + }); } @Override @@ -137,12 +140,6 @@ public class IOSLauncher extends IOSApplication.Delegate{ UIApplication.getSharedApplication().getKeyWindow().getRootViewController().presentViewController(cont, true, () -> {}); } - - @Override - public void showMultiFileChooser(Cons cons, String... extensions){ - showFileChooser(true, extensions[0], cons); - } - @Override public Context getScriptContext(){ Context context = Context.getCurrentContext();