This commit is contained in:
crschnick
2025-01-24 19:31:05 +00:00
parent b98722a5a7
commit fa5f08dd74
22 changed files with 297 additions and 230 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ dependencies {
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'
api 'org.bouncycastle:bcprov-jdk18on:1.80'
api 'info.picocli:picocli:4.7.6'
api ('org.kohsuke:github-api:1.326') {
exclude group: 'org.apache.commons', module: 'commons-lang3'
@@ -2,8 +2,9 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.resources.SystemIcon;
import io.xpipe.app.icon.SystemIcon;
import io.xpipe.app.icon.SystemIconManager;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -14,24 +15,21 @@ import javafx.scene.layout.Region;
import atlantafx.base.theme.Tweaks;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.*;
import static atlantafx.base.theme.Styles.TEXT_SMALL;
public class StoreIconChoiceComp extends SimpleComp {
private final Property<SystemIcon> selected;
private final List<SystemIcon> icons;
private final Set<SystemIcon> icons;
private final int columns;
private final SimpleStringProperty filter;
private final Runnable doubleClick;
public StoreIconChoiceComp(
Property<SystemIcon> selected,
List<SystemIcon> icons,
Set<SystemIcon> icons,
int columns,
SimpleStringProperty filter,
Runnable doubleClick) {
@@ -73,9 +71,9 @@ public class StoreIconChoiceComp extends SimpleComp {
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
var displayedIcons = filterString == null || filterString.isBlank() || filterString.length() < 2
? icons
? icons.stream().sorted(Comparator.<SystemIcon, String>comparing(systemIcon -> systemIcon.getId())).toList()
: icons.stream()
.filter(icon -> containsString(icon.getDisplayName(), filterString))
.filter(icon -> containsString(icon.getId(), filterString))
.toList();
var data = partitionList(displayedIcons, columns);
@@ -137,8 +135,8 @@ public class StoreIconChoiceComp extends SimpleComp {
return;
}
root.setText(icon.getDisplayName());
image.set("app:system/" + icon.getIconName() + ".svg");
root.setText(icon.getId());
image.set(SystemIconManager.getIconFile(icon));
setGraphic(root);
}
}
@@ -1,8 +1,8 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.resources.SystemIcon;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.icon.SystemIcon;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.Hyperlinks;
@@ -41,7 +41,7 @@ public class StoreIconChoiceDialog {
.grow(false, true);
var modal = ModalOverlay.of(
"chooseCustomIcon",
new StoreIconChoiceComp(selected, SystemIcons.getSystemIcons(), 5, filterText, () -> {
new StoreIconChoiceComp(selected, SystemIconManager.getIcons(), 5, filterText, () -> {
finish();
})
.prefWidth(600));
@@ -63,7 +63,7 @@ public class StoreIconChoiceDialog {
}
private void finish() {
entry.setIcon(selected.get() != null ? selected.getValue().getIconName() : null, true);
entry.setIcon(selected.get() != null ? selected.getValue().getSource().getId() + "/" + selected.getValue().getId() : null, true);
overlay.close();
}
}
@@ -13,6 +13,7 @@ import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
@@ -93,9 +94,6 @@ public class BaseMode extends OperationMode {
XPipeDistributionType.init();
AppPrefs.setLocalDefaultsIfNeeded();
},
() -> {
SystemIconManager.init();
},
() -> {
shellLoaded.await();
AppMainWindow.loadingText("loadingGit");
@@ -135,8 +133,9 @@ public class BaseMode extends OperationMode {
() -> {
PlatformInit.init(true);
AppImages.init();
SystemIcons.init();
imagesLoaded.countDown();
SystemIconManager.loadSources();
SystemIconManager.loadImages();
},
() -> {
BrowserIconManager.loadIfNecessary();
@@ -0,0 +1,10 @@
package io.xpipe.app.icon;
import lombok.Value;
@Value
public class SystemIcon {
SystemIconSource source;
String id;
}
@@ -0,0 +1,95 @@
package io.xpipe.app.icon;
import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.attributes.ViewBox;
import com.github.weisj.jsvg.parser.SVGLoader;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Map;
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 Path getDirectory(SystemIconSource source) {
var target = DIRECTORY.resolve(source.getId());
return target;
}
public static void buildCache(Map<SystemIconSource, SystemIconSourceData> all) {
try {
for (var e : all.entrySet()) {
var target = DIRECTORY.resolve(e.getKey().getId());
Files.createDirectories(target);
for (var icon : e.getValue().getIcons()) {
if (refreshChecksum(icon.getFile(),target,icon.getName(), icon.isDark())) {
continue;
}
rasterizeSizes(icon.getFile(),target,icon.getName(), icon.isDark());
}
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
private static boolean refreshChecksum(Path source, Path dir, String name, boolean dark) throws Exception {
var md5Name = name + (dark ? "-dark" : "") + ".md5";
var bytes = Files.readAllBytes(source);
var md = MessageDigest.getInstance("MD5");
md.update(bytes);
var digest = md.digest();
var md5File = dir.resolve(md5Name);
if (Files.exists(md5File) && Arrays.equals(Files.readAllBytes(md5File), digest)) {
return true;
} else {
Files.write(md5File, digest);
return false;
}
}
private static boolean rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
try {
for (var size : sizes) {
rasterize(path, dir, name, dark, size);
}
return true;
} catch (Exception ex) {
if (ex instanceof IOException) {
throw ex;
}
ErrorEvent.fromThrowable(ex).omit().expected().handle();
return false;
}
}
private static void rasterize(Path path, Path dir, String name, boolean dark, int px) 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((Component) null, g, new ViewBox(0, 0, px, px));
g.dispose();
var out = dir.resolve(name + "-" + px + (dark ? "-dark" : "") + ".png");
ImageIO.write(image,"png", out.toFile());
}
}
@@ -0,0 +1,68 @@
package io.xpipe.app.icon;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.resources.AppImages;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class SystemIconManager {
private static final Path DIRECTORY = AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("pool");
private static final List<SystemIconSource> SOURCES = java.util.List.of(
new SystemIconSource.GitRepository("https://github.com/selfhst/icons", "selfhst"));
private static final Map<SystemIconSource, SystemIconSourceData> LOADED = new HashMap<>();
private static final Set<SystemIcon> ICONS = new HashSet<>();
public static Set<SystemIcon> getIcons() {
return ICONS;
}
public static String getIconFile(SystemIcon icon) {
return "icons/" + icon.getSource().getId() + "/" + icon.getId() + ".svg";
}
public static void loadSources() throws Exception {
Files.createDirectories(DIRECTORY);
LOADED.clear();
for (var source : SOURCES) {
LOADED.put(source,SystemIconSourceData.of(source));
}
ICONS.clear();
SystemIconCache.buildCache(LOADED);
LOADED.forEach((source, systemIconSourceData) -> {
systemIconSourceData.getIcons().forEach(systemIconSourceFile -> {
var icon = new SystemIcon(source, systemIconSourceFile.getName());
ICONS.add(icon);
});
});
}
public static void loadImages() {
try {
for (var source : SOURCES) {
AppImages.loadRasterImages(SystemIconCache.getDirectory(source), "icons/" + source.getId());
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
public static void reload() throws Exception {
Files.createDirectories(DIRECTORY);
for (var source : SOURCES) {
source.refresh();
}
loadSources();
}
public static Path getPoolPath() {
return AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("pool");
}
}
@@ -1,7 +1,6 @@
package io.xpipe.app.resources;
package io.xpipe.app.icon;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.CommandBuilder;
import lombok.Value;
@@ -17,7 +16,7 @@ public interface SystemIconSource {
String id;
@Override
public void init() throws Exception {
public void refresh() throws Exception {
try (var sc = ProcessControlProvider.get().createLocalProcessControl(true).start()) {
var dir = SystemIconManager.getPoolPath().resolve(id);
if (!Files.exists(dir)) {
@@ -34,7 +33,7 @@ public interface SystemIconSource {
}
}
void init() throws Exception;
void refresh() throws Exception;
String getId();
@@ -0,0 +1,56 @@
package io.xpipe.app.icon;
import io.xpipe.app.issue.ErrorEvent;
import lombok.Value;
import org.apache.commons.io.FilenameUtils;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@Value
public class SystemIconSourceData {
Path directory;
List<SystemIconSourceFile> icons;
public static SystemIconSourceData of(SystemIconSource source) throws IOException {
var target = source.getPath();
var list = new ArrayList<SystemIconSourceFile>();
walkTree(source, target, list);
return new SystemIconSourceData(target, list);
}
private static void walkTree(SystemIconSource source, Path dir, List<SystemIconSourceFile> sourceFiles) {
try {
if (!Files.isDirectory(dir)) {
return;
}
var files = Files.walk(dir, FileVisitOption.FOLLOW_LINKS).toList();
for (var file : files) {
if (file.getFileName().toString().endsWith(".svg")) {
var name = FilenameUtils.getBaseName(file.getFileName().toString());
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
if (name.endsWith("-light") || name.endsWith("-dark")) {
var s = new SystemIconSourceFile(source, cleanedName, file, name.endsWith("-dark"));
sourceFiles.add(s);
continue;
}
var bothVariants = Files.exists(file.getParent().resolve(cleanedName + "-light.svg")) && Files.exists(
file.getParent().resolve(cleanedName + "-dark.svg"));
if (!bothVariants) {
var s = new SystemIconSourceFile(source, cleanedName, file, name.endsWith("-dark"));
sourceFiles.add(s);
}
}
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
}
@@ -0,0 +1,14 @@
package io.xpipe.app.icon;
import lombok.Value;
import java.nio.file.Path;
@Value
public class SystemIconSourceFile {
SystemIconSource source;
String name;
Path file;
boolean dark;
}
@@ -75,6 +75,24 @@ public class AppImages {
TrackEvent.trace("Loaded images in " + module + ":" + dir + " in " + elapsed.toMillis() + " ms");
}
public static void loadRasterImages(Path directory, String prefix) throws IOException {
Files.walkFileTree(directory, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
var relativeFileName = FilenameUtils.separatorsToUnix(
directory.relativize(file).toString());
var key = prefix + "/" + relativeFileName;
if (images.containsKey(key)) {
return FileVisitResult.CONTINUE;
}
images.put(key, loadImage(file));
return FileVisitResult.CONTINUE;
}
});
}
public static String svgImage(String file) {
if (file == null) {
return "";
@@ -95,6 +113,10 @@ public class AppImages {
return false;
}
if (images.containsKey(file)) {
return true;
}
var key = file.contains(":") ? file : "app:" + file;
return images.containsKey(key);
}
@@ -113,6 +135,10 @@ public class AppImages {
return DEFAULT_IMAGE;
}
if (images.containsKey(file)) {
return images.get(file);
}
var key = file.contains(":") ? file : "app:" + file;
if (images.containsKey(key)) {
@@ -1,15 +0,0 @@
package io.xpipe.app.resources;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.DataStore;
import lombok.Value;
import lombok.experimental.NonFinal;
@Value
public class SystemIcon {
SystemIconSource source;
String iconName;
String displayName;
}
@@ -1,90 +0,0 @@
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());
}
}
@@ -1,39 +0,0 @@
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<SystemIconSource> 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");
}
}
@@ -1,52 +0,0 @@
package io.xpipe.app.resources;
import io.xpipe.core.process.*;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StatefulDataStore;
import org.apache.commons.io.FilenameUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class SystemIcons {
private static final List<SystemIcon> SYSTEM_ICONS = new ArrayList<>();
private static boolean loaded = false;
public static synchronized void init() {
if (SYSTEM_ICONS.size() > 0) {
return;
}
}
public static synchronized void load() {
if (loaded) {
return;
}
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "img/system", true, false);
loaded = true;
}
public static Optional<SystemIcon> getForId(String id) {
if (id == null) {
return Optional.empty();
}
for (SystemIcon systemIcon : SYSTEM_ICONS) {
if (systemIcon.getIconName().equals(id)) {
return Optional.of(systemIcon);
}
}
return Optional.empty();
}
public static List<SystemIcon> getSystemIcons() {
return SYSTEM_ICONS;
}
}
@@ -4,7 +4,6 @@ import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.ext.UserScopeStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.*;
import io.xpipe.core.util.JacksonMapper;
@@ -186,7 +185,7 @@ public class DataStoreEntry extends StorageElement {
return getProvider().getDisplayIconFileName(getStore());
}
return "app:system/" + icon + ".svg";
return "icons/" + icon + ".svg";
}
public static Optional<DataStoreEntry> fromDirectory(Path dir) throws Exception {
+1
View File
@@ -40,6 +40,7 @@ open module io.xpipe.app {
exports io.xpipe.app.core.window;
exports io.xpipe.app.resources;
exports io.xpipe.app.comp;
exports io.xpipe.app.icon;
requires com.sun.jna;
requires com.sun.jna.platform;
+1 -1
View File
@@ -14,7 +14,7 @@ buildscript {
plugins {
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
id 'org.gradlex.extra-java-module-info' version '1.9' apply false
id("com.diffplug.spotless") version "6.25.0" apply false
id("com.diffplug.spotless") version "7.0.2" apply false
}
allprojects { subproject ->
+3 -3
View File
@@ -1,9 +1,9 @@
plugins {
id 'org.beryx.jlink' version '3.1.1'
id "org.asciidoctor.jvm.convert" version "4.0.3"
id 'org.jreleaser' version '1.15.0'
id("com.netflix.nebula.ospackage") version "11.10.0"
id "org.asciidoctor.jvm.convert" version "4.0.4"
id 'org.jreleaser' version '1.16.0'
id("com.netflix.nebula.ospackage") version "11.10.1"
id 'org.gradle.crypto.checksum' version '1.4.0'
id 'signing'
}
+1 -1
View File
@@ -1,4 +1,4 @@
name=Bouncy Castle Crypto Package For Java
version=1.79
version=1.80
license=MIT License
link=https://github.com/bcgit/bc-java
@@ -7,12 +7,10 @@ import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreSection;
import io.xpipe.app.comp.store.SystemStateComp;
import io.xpipe.app.ext.*;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.util.ShellStoreFormat;
import io.xpipe.core.process.SystemState;
import io.xpipe.ext.base.script.ScriptStore;
import javafx.beans.property.BooleanProperty;
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME