Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,10 @@ jobs:
- name: Build Plugin
run: ./gradlew buildPlugin

- name: Verify Plugin Compatibility Range
run: |
if grep -R 'until-build=' build/resources/main/META-INF/plugin.xml build/tmp/patchPluginXml/plugin.xml; then
echo "Plugin XML must not define an upper until-build cap."
exit 1
fi

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ out/

# Gradle
.gradle/
.gradle-user-home/
build/
classes/
.kotlin/

./club_members/

Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

# AMII Changelog

## [Unreleased] - 2026-05-05

### Added

- Custom asset browser in Settings: asynchronous scanning, selectable editor pane, and improved asset list rendering.
- Project lifecycle registry to prevent duplicate listener registration across project opens.

### Changed

- Make asset downloads and asset-definition updates non-blocking on the UI thread.
- Ensure meme display and related services run on the EDT safely; added defensive invokes.
- Simplified REST/Asset API calls to avoid unnecessary thread-hopping.
- Refactored `PluginSettingsUI` to add parsing and bitmask helpers and reduce duplicated logic.
- Improved GIF handling with proper reader disposal and bounded cache size.

### Fixed

- Avoid synchronous network or file I/O on the EDT that could hang the IDE.
- Prevent duplicate project listener registration and related resource leaks.
- Various UI stability and threading fixes.

## [1.5.0]

### Added
Expand Down
11 changes: 8 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType

plugins {
id("java") // Java support
Expand Down Expand Up @@ -31,7 +32,7 @@ dependencies {
implementation("com.googlecode.soundlibs:mp3spi:1.9.5.4")
implementation("io.sentry:sentry:6.28.0")
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("io.mockk:mockk:1.14.9")
compileOnly(files("lib/instrumented-doki-theme-jetbrains-88.5-1.11.0.jar"))
testImplementation(libs.junit)
testImplementation(libs.opentest4j)
Expand Down Expand Up @@ -66,7 +67,9 @@ intellijPlatform {

ideaVersion {
sinceBuild = providers.gradleProperty("pluginSinceBuild")
untilBuild = providers.gradleProperty("pluginUntilBuild")
providers.gradleProperty("pluginUntilBuild").orNull?.takeIf { it.isNotBlank() }?.let {
untilBuild = it
}
}
}

Expand All @@ -86,7 +89,9 @@ intellijPlatform {

pluginVerification {
ides {
recommended()
create(IntelliJPlatformType.IntellijIdeaUltimate, "2025.1")
create(IntelliJPlatformType.IntellijIdeaUltimate, "2026.1")
create(IntelliJPlatformType.Rider, "2026.1")
}
}
}
Expand Down
13 changes: 11 additions & 2 deletions docs/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
### Added
## Unreleased (2026-05-05)

- 2024.3π Build Support
Highlights

- Non-blocking asset management: asset downloads and updates no longer run synchronously on the UI thread.
- New custom asset browser in Settings with an editor pane and asynchronous scanning.
- Improved robustness: safer EDT handling for meme display and services, retrying listener subscriptions.
- Added regression tests for asset managers and project lifecycle.

Build Support

- 2025.2 Build Support
Comment on lines +10 to +12
7 changes: 3 additions & 4 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html

pluginGroup=io.unthrottled
pluginVersion=1.6.0
pluginVersion=1.6.1
pluginSinceBuild=251
pluginUntilBuild=251.*

# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IU
platformVersion = 2025.1
platformVersion = 2026.1

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
Expand All @@ -16,7 +15,7 @@ platformPlugins = Dart:251.23774.318,io.flutter:85.2.4
platformBundledPlugins = NodeJS

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 8.13
gradleVersion = 9.0.0

# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
kotlin.stdlib.default.dependency = false
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ junit = "4.13.2"
opentest4j = "1.3.0"

# plugins
intelliJPlatform = "2.5.0"
kotlin = "2.1.20"
intelliJPlatform = "2.14.0"
kotlin = "2.3.21"

[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" }

[plugins]
intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<children>
<grid id="20abc" binding="ayyLmao" layout-manager="GridBagLayout">
<constraints>
<grid row="3" column="0" row-span="1" col-span="6" vsize-policy="3" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
<grid row="3" column="0" row-span="1" col-span="6" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
Expand Down
154 changes: 136 additions & 18 deletions src/main/java/io/unthrottled/amii/config/ui/CustomMemeList.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.intellij.openapi.ui.TextComponentAccessor;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.components.JBList;
import io.unthrottled.amii.assets.AssetFetchOptions;
import io.unthrottled.amii.assets.LocalVisualContentManager;
import io.unthrottled.amii.assets.MemeAsset;
Expand All @@ -25,13 +26,27 @@
import io.unthrottled.amii.tools.PluginMessageBundle;
import org.jetbrains.annotations.NotNull;

import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.Component;
import java.net.URI;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class CustomMemeList {
private static final Logger logger = Logger.getInstance(CustomMemeList.class);
Expand All @@ -45,14 +60,18 @@ public class CustomMemeList {
private JCheckBox onlyShowUntaggedItemsCheckBox;
private JCheckBox allowSuggestiveContentCheckBox;
private JCheckBox onlyUseCustomAssetsCheckBox;
private final DefaultListModel<VisualAssetRepresentation> assetListModel = new DefaultListModel<>();
private final AtomicInteger scanGeneration = new AtomicInteger();
private JBList<VisualAssetRepresentation> assetList;
private JPanel assetEditorPanel;

public CustomMemeList(
Consumer<MemeAsset> onTest,
ConfigSettingsModel pluginSettingsModel
) {
this.onTest = onTest;
this.pluginSettingsModel = pluginSettingsModel;
ayyLmao.setLayout(new BoxLayout(ayyLmao, BoxLayout.PAGE_AXIS));
initializeAssetBrowser();
onlyShowUntaggedItemsCheckBox.addActionListener(a ->
populateDirectory(textFieldWithBrowseButton.getText()));
allowSuggestiveContentCheckBox.addActionListener(a -> {
Expand All @@ -69,43 +88,112 @@ public CustomMemeList(

private void populateDirectory(String workingDirectory) {
if (workingDirectory.isBlank()) {
clearAssetBrowser();
return;
}

removePreExistingStuff();
showLoadingState();
int currentGeneration = scanGeneration.incrementAndGet();
boolean includeLewds = this.pluginSettingsModel.getAllowLewds();
boolean onlyShowUntaggedItems = onlyShowUntaggedItemsCheckBox.isSelected();

ApplicationManager.getApplication().executeOnPooledThread(() -> {
Set<VisualAssetRepresentation> visualAssetRepresentations = LocalVisualContentManager.supplyAllVisualAssetDefinitionsFromWorkingDirectory(
new AssetFetchOptions(
workingDirectory,
this.pluginSettingsModel.getAllowLewds()
includeLewds
)
);
VisualEntityRepository.Companion.getInstance().refreshLocalAssets();

// this makes it run on the Dialog's separate
// AWT Thread
SwingUtilities.invokeLater(()->{
visualAssetRepresentations.stream()
if (currentGeneration != scanGeneration.get()) {
return;
}

List<VisualAssetRepresentation> assets = visualAssetRepresentations.stream()
.filter(rep ->
!onlyShowUntaggedItemsCheckBox.isSelected() ||
!onlyShowUntaggedItems ||
rep.getCat().isEmpty()
)
.forEach(visualAssetRepresentation -> {
CustomMemePanel customMemePanel = new CustomMemePanel(
this.onTest,
visualAssetRepresentation
);
ayyLmao.add(customMemePanel.getComponent());
});
.sorted(Comparator.comparing(VisualAssetRepresentation::getPath))
.collect(Collectors.toList());
updateAssetList(assets);
});
});
}

private void removePreExistingStuff() {
while (ayyLmao.getComponentCount() > 0) {
ayyLmao.remove(0);
private void initializeAssetBrowser() {
ayyLmao.setLayout(new BorderLayout());

assetList = new JBList<>(assetListModel);
assetList.setCellRenderer(new AssetListCellRenderer());
assetList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
assetList.addListSelectionListener(event -> {
if (!event.getValueIsAdjusting()) {
showSelectedAsset(assetList.getSelectedValue());
}
});

assetEditorPanel = new JPanel(new BorderLayout());
assetEditorPanel.add(createPlaceholder("Select a custom asset to edit."), BorderLayout.CENTER);

JSplitPane assetBrowser = new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
new JScrollPane(assetList),
assetEditorPanel
);
assetBrowser.setResizeWeight(0.25);
ayyLmao.add(assetBrowser, BorderLayout.CENTER);
}

private void clearAssetBrowser() {
scanGeneration.incrementAndGet();
assetListModel.clear();
showSelectedAsset(null);
}

private void showLoadingState() {
assetListModel.clear();
showPlaceholder("Scanning custom assets...");
}

private void updateAssetList(List<VisualAssetRepresentation> assets) {
assetListModel.clear();
assets.forEach(assetListModel::addElement);
if (assets.isEmpty()) {
showPlaceholder("No custom GIF assets found.");
} else {
assetList.setSelectedIndex(0);
}
}

private void showSelectedAsset(VisualAssetRepresentation asset) {
assetEditorPanel.removeAll();
if (asset == null) {
assetEditorPanel.add(createPlaceholder("Select a custom asset to edit."), BorderLayout.CENTER);
} else {
CustomMemePanel customMemePanel = new CustomMemePanel(
this.onTest,
asset
);
assetEditorPanel.add(customMemePanel.getComponent(), BorderLayout.CENTER);
}
assetEditorPanel.revalidate();
assetEditorPanel.repaint();
}

private void showPlaceholder(String message) {
assetEditorPanel.removeAll();
assetEditorPanel.add(createPlaceholder(message), BorderLayout.CENTER);
assetEditorPanel.revalidate();
assetEditorPanel.repaint();
}

private JComponent createPlaceholder(String message) {
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JLabel(message), BorderLayout.CENTER);
return panel;
}

public void setPluginSettingsModel(ConfigSettingsModel pluginSettingsModel) {
Expand Down Expand Up @@ -176,4 +264,34 @@ public void load() {
loaded = true;
}
}

private static class AssetListCellRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList<?> list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus
) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof VisualAssetRepresentation asset) {
label.setText(getDisplayName(asset));
label.setToolTipText(asset.getPath());
}
return label;
}

private String getDisplayName(VisualAssetRepresentation asset) {
try {
return Paths.get(URI.create(asset.getPath())).getFileName().toString();
} catch (RuntimeException ignored) {
try {
return Paths.get(asset.getPath()).getFileName().toString();
} catch (RuntimeException ignoredAgain) {
return asset.getPath();
}
}
}
}
}
Loading
Loading