diff --git a/app/build.gradle b/app/build.gradle index 6aa569d65..7ba6d5f92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,6 +48,7 @@ dependencies { api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8' api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8' + api("com.github.weisj:jsvg:1.7.0") api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar") api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar") api 'org.bouncycastle:bcprov-jdk18on:1.79' diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java index b4aacdc87..bd6a7de3e 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java @@ -16,9 +16,7 @@ import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.resources.AppImages; -import io.xpipe.app.resources.AppResources; -import io.xpipe.app.resources.SystemIcons; +import io.xpipe.app.resources.*; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorageSyncHandler; import io.xpipe.app.terminal.TerminalLauncherManager; @@ -95,6 +93,9 @@ public class BaseMode extends OperationMode { XPipeDistributionType.init(); AppPrefs.setLocalDefaultsIfNeeded(); }, + () -> { + SystemIconManager.init(); + }, () -> { shellLoaded.await(); AppMainWindow.loadingText("loadingGit"); diff --git a/app/src/main/java/io/xpipe/app/resources/ContainerAutoSystemIcon.java b/app/src/main/java/io/xpipe/app/resources/ContainerAutoSystemIcon.java deleted file mode 100644 index 02a6f7d45..000000000 --- a/app/src/main/java/io/xpipe/app/resources/ContainerAutoSystemIcon.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.xpipe.app.resources; - -import io.xpipe.app.ext.ContainerImageStore; -import io.xpipe.core.process.ShellControl; -import io.xpipe.core.store.DataStore; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -import java.util.function.Predicate; - -@Value -@EqualsAndHashCode(callSuper = true) -public class ContainerAutoSystemIcon extends SystemIcon { - - Predicate imageCheck; - - public ContainerAutoSystemIcon(String iconName, String displayName, Predicate imageCheck) { - super(iconName, displayName); - this.imageCheck = imageCheck; - } - - @Override - public boolean isApplicable(ShellControl sc) { - var source = sc.getSourceStore(); - if (source.isEmpty()) { - return false; - } - - return isApplicable(source.get()); - } - - @Override - public boolean isApplicable(DataStore store) { - if (!(store instanceof ContainerImageStore containerImageStore)) { - return false; - } - - if (containerImageStore.getImageName() == null) { - return false; - } - - return imageCheck.test(containerImageStore.getImageName()); - } -} diff --git a/app/src/main/java/io/xpipe/app/resources/FileAutoSystemIcon.java b/app/src/main/java/io/xpipe/app/resources/FileAutoSystemIcon.java deleted file mode 100644 index 3fa43cf65..000000000 --- a/app/src/main/java/io/xpipe/app/resources/FileAutoSystemIcon.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.xpipe.app.resources; - -import io.xpipe.core.process.OsType; -import io.xpipe.core.process.ShellControl; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -@Value -@EqualsAndHashCode(callSuper = true) -public class FileAutoSystemIcon extends SystemIcon { - - OsType.Any osType; - String file; - - public FileAutoSystemIcon(String iconName, String displayName, OsType.Any osType, String file) { - super(iconName, displayName); - this.osType = osType; - this.file = file; - } - - @Override - public boolean isApplicable(ShellControl sc) throws Exception { - if (sc.getOsType() != osType) { - return false; - } - - var abs = sc.getShellDialect().evaluateExpression(sc, file).readStdoutIfPossible(); - if (abs.isEmpty()) { - return false; - } - - return sc.getShellDialect().createFileExistsCommand(sc, abs.get()).executeAndCheck() - || sc.getShellDialect().directoryExists(sc, abs.get()).executeAndCheck(); - } -} diff --git a/app/src/main/java/io/xpipe/app/resources/ShellAutoSystemIcon.java b/app/src/main/java/io/xpipe/app/resources/ShellAutoSystemIcon.java deleted file mode 100644 index 427747985..000000000 --- a/app/src/main/java/io/xpipe/app/resources/ShellAutoSystemIcon.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.app.resources; - -import io.xpipe.core.process.ShellControl; -import io.xpipe.core.util.FailableFunction; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -@Value -@EqualsAndHashCode(callSuper = true) -public class ShellAutoSystemIcon extends SystemIcon { - - FailableFunction applicable; - - public ShellAutoSystemIcon( - String iconName, String displayName, FailableFunction applicable) { - super(iconName, displayName); - this.applicable = applicable; - } - - @Override - public boolean isApplicable(ShellControl sc) throws Exception { - return applicable.apply(sc); - } -} diff --git a/app/src/main/java/io/xpipe/app/resources/SystemIcon.java b/app/src/main/java/io/xpipe/app/resources/SystemIcon.java index 7c87333df..11aba24c5 100644 --- a/app/src/main/java/io/xpipe/app/resources/SystemIcon.java +++ b/app/src/main/java/io/xpipe/app/resources/SystemIcon.java @@ -7,17 +7,9 @@ import lombok.Value; import lombok.experimental.NonFinal; @Value -@NonFinal public class SystemIcon { + SystemIconSource source; String iconName; String displayName; - - public boolean isApplicable(ShellControl sc) throws Exception { - return false; - } - - public boolean isApplicable(DataStore store) { - return false; - } } diff --git a/app/src/main/java/io/xpipe/app/resources/SystemIconCache.java b/app/src/main/java/io/xpipe/app/resources/SystemIconCache.java new file mode 100644 index 000000000..47d003bf3 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/resources/SystemIconCache.java @@ -0,0 +1,90 @@ +package io.xpipe.app.resources; + +import com.github.weisj.jsvg.SVGDocument; +import com.github.weisj.jsvg.geometry.size.FloatSize; +import com.github.weisj.jsvg.parser.SVGLoader; +import io.xpipe.app.core.AppProperties; +import org.apache.commons.io.FilenameUtils; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.*; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.List; + +public class SystemIconCache { + + private static final Path DIRECTORY = AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("raster"); + private static final int[] sizes = new int[]{16, 24, 40, 80}; + + public static void init() throws Exception { + Files.createDirectories(DIRECTORY); + walkTree(Path.of("C:\\Projects\\xpipe\\xpipex\\img\\selfhst"),"selfhst"); + } + + public static void walkTree(Path dir, String source) throws Exception { + var target = DIRECTORY.resolve(source); + Files.createDirectories(target); + + var files = Files.walk(dir, FileVisitOption.FOLLOW_LINKS).toList(); + for (var file : files) { + if (file.getFileName().toString().endsWith(".svg")) { + rasterize(file, source); + } + } + } + + private static void rasterize(Path path, String source) throws Exception { + var name = FilenameUtils.getBaseName(path.getFileName().toString()); + var dark = !name.endsWith("-light"); + var md5Name = name.replaceFirst("-light$", "") + (dark ? "-dark" : ""); + var bytes = Files.readAllBytes(path); + var md = MessageDigest.getInstance("MD5"); + md.update(bytes); + var digest = md.digest(); + var dir = DIRECTORY.resolve(source); + var md5File = dir.resolve(md5Name); + if (Files.exists(md5File) && Arrays.equals(Files.readAllBytes(md5File), digest)) { + return; + } else { + Files.write(md5File, digest); + } + + for (var size : sizes) { + rasterize(path, source, size); + } + } + + private static void rasterize(Path path, String source, int px) throws IOException { + var name = FilenameUtils.getBaseName(path.getFileName().toString()); + var dark = !name.endsWith("-light"); + var fixedName = name.replaceFirst("-light$", ""); + rasterize(path, source,fixedName, px, dark); + } + + private static void rasterize(Path path, String source, String name, int px, boolean dark) throws IOException { + SVGLoader loader = new SVGLoader(); + URL svgUrl = path.toUri().toURL(); + SVGDocument svgDocument = loader.load(svgUrl); + if (svgDocument == null) { + return; + } + + BufferedImage image = new BufferedImage(px, px, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + svgDocument.render(null, g); + g.dispose(); + + var dir = DIRECTORY.resolve(source); + Files.createDirectories(dir); + var out = dir.resolve(name + "-" + px + (dark ? "-dark" : "") + ".png"); + ImageIO.write(image,"png", out.toFile()); + } +} diff --git a/app/src/main/java/io/xpipe/app/resources/SystemIconManager.java b/app/src/main/java/io/xpipe/app/resources/SystemIconManager.java new file mode 100644 index 000000000..74ec055d3 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/resources/SystemIconManager.java @@ -0,0 +1,39 @@ +package io.xpipe.app.resources; + +import com.github.weisj.jsvg.SVGDocument; +import com.github.weisj.jsvg.geometry.size.FloatSize; +import com.github.weisj.jsvg.parser.SVGLoader; +import io.xpipe.app.core.AppProperties; +import org.apache.commons.io.FilenameUtils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.List; + +public class SystemIconManager { + + private static final Path DIRECTORY = AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("pool"); + + private static final List SOURCES = java.util.List.of( + new SystemIconSource.GitRepository("https://github.com/selfhst/icons", "selfhst")); + + public static void init() throws Exception { + Files.createDirectories(DIRECTORY); + for (var source : SOURCES) { + source.init(); + SystemIconCache.walkTree(source.getPath(), source.getId()); + } + } + + public static Path getPoolPath() { + return AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("pool"); + } +} diff --git a/app/src/main/java/io/xpipe/app/resources/SystemIconSource.java b/app/src/main/java/io/xpipe/app/resources/SystemIconSource.java new file mode 100644 index 000000000..44ee801a0 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/resources/SystemIconSource.java @@ -0,0 +1,42 @@ +package io.xpipe.app.resources; + +import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; +import lombok.Value; + +import java.nio.file.Files; +import java.nio.file.Path; + +public interface SystemIconSource { + + @Value + static class GitRepository implements SystemIconSource{ + + String remote; + String id; + + @Override + public void init() throws Exception { + try (var sc = ProcessControlProvider.get().createLocalProcessControl(true).start()) { + var dir = SystemIconManager.getPoolPath().resolve(id); + if (!Files.exists(dir)) { + sc.command(CommandBuilder.of().add("git", "clone").addQuoted(remote).addFile(dir.toString())).execute(); + } else { + sc.command(CommandBuilder.of().add("git", "pull")).withWorkingDirectory(dir.toString()).execute(); + } + } + } + + @Override + public Path getPath() { + return SystemIconManager.getPoolPath().resolve(id); + } + } + + void init() throws Exception; + + String getId(); + + Path getPath(); +} diff --git a/app/src/main/java/io/xpipe/app/resources/SystemIcons.java b/app/src/main/java/io/xpipe/app/resources/SystemIcons.java index ca52e97bf..5e5e5e153 100644 --- a/app/src/main/java/io/xpipe/app/resources/SystemIcons.java +++ b/app/src/main/java/io/xpipe/app/resources/SystemIcons.java @@ -15,26 +15,6 @@ import java.util.Optional; public class SystemIcons { - private static final List AUTO_SYSTEM_ICONS = List.of( - new SystemIcon("opnsense", "opnsense") { - @Override - public boolean isApplicable(DataStore store) { - return store instanceof StatefulDataStore statefulDataStore - && statefulDataStore.getState() instanceof SystemState systemState - && systemState.getShellDialect() == ShellDialects.OPNSENSE; - } - }, - new SystemIcon("pfsense", "pfsense") { - @Override - public boolean isApplicable(DataStore store) { - return store instanceof StatefulDataStore statefulDataStore - && statefulDataStore.getState() instanceof SystemState systemState - && systemState.getShellDialect() == ShellDialects.PFSENSE; - } - }, - new ContainerAutoSystemIcon("file-browser", "file browser", name -> name.contains("filebrowser")), - new FileAutoSystemIcon("syncthing", "syncthing", OsType.LINUX, "~/.local/state/syncthing")); - private static final List SYSTEM_ICONS = new ArrayList<>(); private static boolean loaded = false; @@ -42,28 +22,6 @@ public class SystemIcons { if (SYSTEM_ICONS.size() > 0) { return; } - - SYSTEM_ICONS.addAll(AUTO_SYSTEM_ICONS); - AppResources.with(AppResources.XPIPE_MODULE, "img/system", path -> { - try (var stream = Files.list(path)) { - var all = stream.toList(); - for (Path file : all) { - var name = FilenameUtils.getBaseName(file.getFileName().toString()); - if (name.contains("-dark") || name.contains("-16") || name.contains("-24")) { - continue; - } - var base = name.replaceAll("-40", ""); - if (AUTO_SYSTEM_ICONS.stream() - .anyMatch(autoSystemIcon -> - autoSystemIcon.getIconName().equals(base))) { - continue; - } - var displayName = base.replaceAll("-", " "); - SYSTEM_ICONS.add(new SystemIcon(base, displayName)); - } - } - }); - SYSTEM_ICONS.sort(Comparator.comparing(systemIcon -> systemIcon.getIconName())); } public static synchronized void load() { @@ -88,28 +46,6 @@ public class SystemIcons { return Optional.empty(); } - public static Optional detectForSystem(ShellControl sc) throws Exception { - for (var autoSystemIcon : AUTO_SYSTEM_ICONS) { - if (autoSystemIcon.isApplicable(sc)) { - return Optional.of(autoSystemIcon); - } - } - return Optional.empty(); - } - - public static Optional detectForStore(DataStore store) { - if (store == null) { - return Optional.empty(); - } - - for (var autoSystemIcon : AUTO_SYSTEM_ICONS) { - if (autoSystemIcon.isApplicable(store)) { - return Optional.of(autoSystemIcon); - } - } - return Optional.empty(); - } - public static List getSystemIcons() { return SYSTEM_ICONS; } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index 6c2b73ddf..e2482a0d1 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -156,7 +156,6 @@ public class DataStoreEntry extends StorageElement { var validity = storeFromNode == null ? Validity.LOAD_FAILED : store.isComplete() ? Validity.COMPLETE : Validity.INCOMPLETE; - var icon = SystemIcons.detectForStore(store); var entry = new DataStoreEntry( null, uuid, @@ -174,7 +173,7 @@ public class DataStoreEntry extends StorageElement { null, null, null, - icon.map(systemIcon -> systemIcon.getIconName()).orElse(null)); + null); return entry; } @@ -190,22 +189,6 @@ public class DataStoreEntry extends StorageElement { return "app:system/" + icon + ".svg"; } - void refreshIcon() { - if (icon != null && SystemIcons.getForId(icon).isEmpty()) { - icon = null; - return; - } - - if (icon != null) { - return; - } - - var icon = SystemIcons.detectForStore(store); - if (icon.isPresent()) { - setIcon(icon.get().getIconName(), true); - } - } - public static Optional fromDirectory(Path dir) throws Exception { ObjectMapper mapper = JacksonMapper.getDefault(); @@ -416,7 +399,6 @@ public class DataStoreEntry extends StorageElement { this.storePersistentState = value; this.storePersistentStateNode = JacksonMapper.getDefault().valueToTree(value); if (changed) { - refreshIcon(); notifyUpdate(false, true); } } diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java index 0d2dff588..ca735bb2b 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -203,8 +203,6 @@ public class StandardStorage extends DataStorage { if (e.getCategoryUuid() != null && e.getCategoryUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID)) { e.setCategoryUuid(DEFAULT_CATEGORY_UUID); } - - e.refreshIcon(); }); } } catch (IOException ex) { diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index a54dd846a..f272d68be 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -78,6 +78,7 @@ open module io.xpipe.app { requires com.shinyhut.vernacular; requires org.kordamp.ikonli.core; requires jdk.httpserver; + requires com.github.weisj.jsvg; // Required runtime modules requires jdk.charsets; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java index 4015a554e..1283e3f94 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java @@ -28,15 +28,6 @@ public interface ShellStoreProvider extends DataStoreProvider { var replacement = ProcessControlProvider.get().replace(entry.ref()); ShellStore store = replacement.getStore().asNeeded(); var control = ScriptStore.controlWithDefaultScripts(store.tempControl()); - control.onInit(sc -> { - if (entry.getStorePersistentState() instanceof SystemState systemState - && systemState.getShellDialect() == null) { - var found = SystemIcons.detectForSystem(sc); - if (found.isPresent()) { - entry.setIcon(found.get().getIconName(), false); - } - } - }); TerminalLauncher.open( replacement.get(), DataStorage.get().getStoreEntryDisplayName(replacement.get()),