diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c6e85c..793284ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ # AMII Changelog +## [0.13.0] + +### Added + +- The ability to view information about the source of a meme on click. +- `Show Previous Meme`: an action that shows the previously shown meme. + ## [0.12.3] ### Fixed diff --git a/README.md b/README.md index 0d8607be..a81fbe32 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ waifu, husbando, and/or favorite character(s)!

- [Status](#status) - [Minimal Mode](#minimal-mode) - [Discreet Mode](#discreet-mode) + - [Info On Click](#info-on-click) + - [Show Previous Meme](#show-previous-meme) - [Offline Mode](#offline-mode) - [Clear Memes](#clear-memes) - [Rider Support](#rider-support) @@ -273,7 +275,7 @@ your status bar. ## Minimal Mode -MIKU can be pretty chatty some times, especially if you are trying to figure out how to get your integration test to +MIKU can be pretty chatty sometimes, especially if you are trying to figure out how to get your integration test to work. With `Minimal Mode` you have the ability to tell MIKU to only react to events that are different. So when your tests fail a bunch of times, you will only see one failure reaction. However, whenever you break your build, or your tests pass, you'll get a notification then. @@ -291,6 +293,25 @@ and MIKU will re-appear and resume their duties as your virtual companion. This plugin is also integrated with [The Doki Theme](https://github.com/doki-theme/doki-theme-jetbrains#discreet-mode), for the ultimate shame hiding experience. Enabling/disabling `Discreet Mode` in The Doki Theme will enable/disable `Discrete Mode` for this plugin. +## Info On Click + +Curious about the source of a reaction supplied by MIKU? +This feature is enabled by default, and you have the ability to configure it via the settings menu, or even in the information notification. +Just click inside the active meme, and you will get a notification about the source in the lower right-hand corner. +I have tried to tag as many assets as possible with accurate information. +However, there are some assets that I do not know the source for, sorry in advance if you wanted to know the anime! + +**Note**: Clicking on a meme, changes the dismissal mode. [Please see this documentation for more information.](#dismissal) + +![Info on click](./readmeAssets/info_on_click.gif) + +## Show Previous Meme + +Just in case you missed something, you now have the ability to tell MIKU, to show their previous reaction. +Whether you missed you chance to [show info on click](#info-on-click) or you just want to see the reaction again. +The `Show Previous Meme` action is accessible via +Tools > AMII Options > Show Previous Meme + ## Offline Mode If you ever find yourself coding without any internet, don't worry friend, you can take MIKU with you. All interactions diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 24f07636..df23d93c 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -1,5 +1,7 @@ ### Added +- The ability to view information about the source of a meme on click. +- `Show Previous Meme`: an action that shows the previously shown meme. - `Discreet Mode`, when enabled MIKU will clear and not show _any_ anime in the IDE. Also, the mood in the status bar will temporarily hide as well. - Added 2021.3 Build support. Plugin only supports 2020.3+ builds now. diff --git a/gradle.properties b/gradle.properties index c5a0fe3c..dc789e86 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ pluginGroup = io.unthrottled pluginName_ = Anime Memes -pluginVersion = 0.12.3 +pluginVersion = 0.13.0 pluginSinceBuild = 203.7148.57 pluginUntilBuild = 213.* # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl @@ -15,7 +15,7 @@ platformVersion = 2021.2 platformDownloadSources = true # Plugin Dependencies -> https://www.jetbrains.org/intellij/sdk/docs/basics/plugin_structure/plugin_dependencies.html # Example: platformPlugins = com.intellij.java,com.jetbrains.php:203.4449.22 -platformPlugins = io.acari.DDLCTheme:20.0.0 +platformPlugins = io.acari.DDLCTheme:21.0.0 idePath= #idePath=/home/alex/.local/share/JetBrains/Toolbox/apps/AndroidStudio/ch-0/203.7321754 diff --git a/readmeAssets/info_on_click.gif b/readmeAssets/info_on_click.gif new file mode 100644 index 00000000..fe226bdc Binary files /dev/null and b/readmeAssets/info_on_click.gif differ diff --git a/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.form b/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.form index 7b9a003d..f4960adb 100644 --- a/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.form +++ b/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.form @@ -90,7 +90,7 @@ - + @@ -100,7 +100,7 @@ - + @@ -108,9 +108,17 @@ - + + + + + + + + + diff --git a/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.java b/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.java index 41a25ba8..4c730252 100644 --- a/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.java +++ b/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.java @@ -111,6 +111,7 @@ public class PluginSettingsUI implements SearchableConfigurable, Configurable.No private JSpinner maxHeightSpinner; private JSpinner maxWidthSpinner; private JCheckBox discreetModeCheckBox; + private JCheckBox infoOnClickCheckBox; private PreferredCharacterPanel characterModel; private PreferredCharacterPanel blacklistedCharacterModel; private JBTable exitCodeTable; @@ -343,6 +344,12 @@ private Optional getFilePath(String asset) { updateDimensionCapComponents(); pluginSettingsModel.setCapDimensions(enableDimensionCappingCheckBox.isSelected()); }); + + infoOnClickCheckBox.setSelected(initialSettings.getInfoOnClick()); + infoOnClickCheckBox.addActionListener(e -> + pluginSettingsModel.setInfoOnClick(infoOnClickCheckBox.isSelected())); + + SpinnerNumberModel maxMemeHeightSpinnerModel = new SpinnerNumberModel( config.getMaxMemeHeight(), -1, @@ -647,6 +654,7 @@ public void apply() { config.setMinimalMode(pluginSettingsModel.getMinimalMode()); config.setDiscreetMode(pluginSettingsModel.getDiscreetMode()); config.setCapDimensions(pluginSettingsModel.getCapDimensions()); + config.setInfoOnClick(pluginSettingsModel.getInfoOnClick()); config.setMaxMemeHeight(pluginSettingsModel.getMaxMemeHeight()); config.setMaxMemeWidth(pluginSettingsModel.getMaxMemeWidth()); ApplicationManager.getApplication().getMessageBus().syncPublisher( diff --git a/src/main/kotlin/io/unthrottled/amii/actions/PreviousMemeAction.kt b/src/main/kotlin/io/unthrottled/amii/actions/PreviousMemeAction.kt new file mode 100644 index 00000000..2c0c9ef6 --- /dev/null +++ b/src/main/kotlin/io/unthrottled/amii/actions/PreviousMemeAction.kt @@ -0,0 +1,28 @@ +package io.unthrottled.amii.actions + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAware +import io.unthrottled.amii.memes.memeService +import io.unthrottled.amii.tools.AlarmDebouncer +import io.unthrottled.amii.tools.Logging + +class PreviousMemeAction : AnAction(), DumbAware, Logging, Disposable { + + companion object { + private const val DEMAND_DELAY = 250 + } + + private val debouncer = AlarmDebouncer(DEMAND_DELAY) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project!! + debouncer.debounce { + project.memeService().displayLastMeme() + } + } + + override fun dispose() { + } +} diff --git a/src/main/kotlin/io/unthrottled/amii/assets/AssetModels.kt b/src/main/kotlin/io/unthrottled/amii/assets/AssetModels.kt index 8a6af35f..c2e0ce92 100644 --- a/src/main/kotlin/io/unthrottled/amii/assets/AssetModels.kt +++ b/src/main/kotlin/io/unthrottled/amii/assets/AssetModels.kt @@ -26,7 +26,7 @@ enum class MemeAssetCategory(val value: Int) { ; companion object { - private val mappedMemeAssetCategories = values().map { it.value to it }.toMap() + private val mappedMemeAssetCategories = values().associateBy { it.value } fun fromValue(value: Int): MemeAssetCategory = mappedMemeAssetCategories[value] ?: MOTIVATION diff --git a/src/main/kotlin/io/unthrottled/amii/config/Config.kt b/src/main/kotlin/io/unthrottled/amii/config/Config.kt index 099c4146..d9b94423 100644 --- a/src/main/kotlin/io/unthrottled/amii/config/Config.kt +++ b/src/main/kotlin/io/unthrottled/amii/config/Config.kt @@ -1,5 +1,6 @@ package io.unthrottled.amii.config +import com.intellij.openapi.application.Application import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.State @@ -14,6 +15,8 @@ import io.unthrottled.amii.listeners.OK_EXIT_CODE import io.unthrottled.amii.memes.PanelDismissalOptions import io.unthrottled.amii.tools.lt +fun Application.getConfig(): Config = this.getService(Config::class.java) + @State( name = "Plugin-Config", storages = [Storage("AMII.xml")] @@ -41,6 +44,7 @@ class Config : PersistentStateComponent, Cloneable { var memeVolume: Int = DEFAULT_VOLUME_LEVEL var soundEnabled = true var discreetMode = false + var infoOnClick = true var discreetModeConfig = "{}" var memeDisplayModeValue: String = PanelDismissalOptions.TIMED.toString() var memeDisplayAnchorValue: String = NotificationAnchor.TOP_RIGHT.toString() diff --git a/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt b/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt index 2fb17196..7c0adcf2 100644 --- a/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt +++ b/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt @@ -25,6 +25,7 @@ data class ConfigSettingsModel( var minimalMode: Boolean, var discreetMode: Boolean, var capDimensions: Boolean, + var infoOnClick: Boolean, var maxMemeWidth: Int, var maxMemeHeight: Int, ) { @@ -62,6 +63,7 @@ object PluginSettings { minimalMode = Config.instance.minimalMode, discreetMode = Config.instance.discreetMode, capDimensions = Config.instance.capDimensions, + infoOnClick = Config.instance.infoOnClick, maxMemeWidth = Config.instance.maxMemeWidth, maxMemeHeight = Config.instance.maxMemeHeight, ) diff --git a/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt b/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt index 4979349e..b09867c9 100644 --- a/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt +++ b/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt @@ -2,10 +2,12 @@ package io.unthrottled.amii.memes import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project import com.intellij.util.messages.Topic import io.unthrottled.amii.assets.AudibleContent import io.unthrottled.amii.assets.VisualMemeContent import io.unthrottled.amii.config.Config +import io.unthrottled.amii.config.getConfig import io.unthrottled.amii.config.ui.NotificationAnchor import io.unthrottled.amii.events.UserEvent import io.unthrottled.amii.memes.player.MemePlayer @@ -33,24 +35,41 @@ interface MemeLifecycleListener { // user triggered event fun onDismiss() {} + fun onClick() {} + fun onRemoval() {} fun onDisplay() {} } +@Suppress("LongParameterList") class Meme( private val memePlayer: MemePlayer?, private val memePanel: MemePanel, val userEvent: UserEvent, private val comparator: (Meme) -> Comparison, val metadata: Map, + private val project: Project, + val visualMemeContent: VisualMemeContent, ) : Disposable { + fun clone(): Meme = + Meme( + memePlayer, + memePanel.clone(), + userEvent, + comparator, + metadata, + project, + visualMemeContent + ) + class Builder( private val visualMemeContent: VisualMemeContent, private val audibleContent: AudibleContent?, private val userEvent: UserEvent, private val rootPane: JLayeredPane, + private val project: Project, ) { private var notificationMode = Config.instance.notificationMode private var notificationAnchor = Config.instance.notificationAnchor @@ -106,6 +125,8 @@ class Meme( userEvent, memeComparator, metaData, + project, + visualMemeContent, ) } } @@ -129,6 +150,12 @@ class Meme( } } + override fun onClick() { + if (ApplicationManager.getApplication().getConfig().infoOnClick) { + project.memeInfoService().displayInfo(visualMemeContent) + } + } + override fun onDismiss() { listeners.forEach { it.onDismiss() } } diff --git a/src/main/kotlin/io/unthrottled/amii/memes/MemeInfoService.kt b/src/main/kotlin/io/unthrottled/amii/memes/MemeInfoService.kt new file mode 100644 index 00000000..332abfea --- /dev/null +++ b/src/main/kotlin/io/unthrottled/amii/memes/MemeInfoService.kt @@ -0,0 +1,98 @@ +package io.unthrottled.amii.memes + +import com.intellij.ide.BrowserUtil +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationListener +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import icons.AMIIIcons +import io.unthrottled.amii.assets.VisualEntityRepository +import io.unthrottled.amii.assets.VisualMemeContent +import io.unthrottled.amii.config.getConfig +import io.unthrottled.amii.tools.PluginMessageBundle +import io.unthrottled.amii.tools.toOptional +import org.intellij.lang.annotations.Language +import java.net.URLEncoder + +fun Project.memeInfoService(): MemeInfoService = this.getService(MemeInfoService::class.java) + +class MemeInfoService(private val project: Project) { + + private val notificationGroup = + NotificationGroupManager.getInstance() + .getNotificationGroup("AMII Info") + + fun stopShowing() { + ApplicationManager.getApplication().getConfig().infoOnClick = false + } + + fun displayInfo(visualMemeContent: VisualMemeContent) { + val stopShowingAction = object : NotificationAction(PluginMessageBundle.message("amii.meme.info.stop")) { + override fun actionPerformed(e: AnActionEvent, notification: Notification) { + stopShowing() + notification.expire() + } + } + val visualAssetEntity = VisualEntityRepository.instance.visualAssetEntities[visualMemeContent.id] ?: return + visualAssetEntity.toOptional() + .filter { + it.characters.none { character -> + character.name.contains("Unknown ", ignoreCase = true) + } && + it.characters.isNotEmpty() + } + .map { + val animeShown = visualAssetEntity.characters + .map { it.anime } + .distinct() + .map { it.name } + val characters = visualAssetEntity.characters.map { it.name } + val characterPluralization = if (characters.size > 1) "s" else "" + + @Language("HTML") + val content = """
+ | Anime: ${animeShown.joinToString(", ")}
+ | Character$characterPluralization: ${characters.joinToString(", ")} + |
""".trimMargin() + + notificationGroup.createNotification( + content, + NotificationType.INFORMATION + ).addAction( + object : NotificationAction(PluginMessageBundle.message("amii.meme.info.search")) { + override fun actionPerformed(e: AnActionEvent, notification: Notification) { + val queryString = URLEncoder.encode( + "${animeShown.joinToString(" ") { "\"$it\"" }} ${characters.joinToString(" ")}", + Charsets.UTF_8 + ) + val queryParameters = "q=$queryString&oq=$queryString" + val searchUrl = "https://google.com/search?$queryParameters" + BrowserUtil.browse(searchUrl) + } + } + ) + .addAction( + stopShowingAction + ) + }.orElseGet { + @Language("HTML") + val lulDunno = """ + |${PluginMessageBundle.message("amii.meme.info.dunno")}
+ |¯\_(ツ)_/¯ + """.trimMargin() + notificationGroup.createNotification( + lulDunno, + NotificationType.INFORMATION + ) + .addAction(stopShowingAction) + } + .setIcon(AMIIIcons.PLUGIN_ICON) + .setTitle(PluginMessageBundle.message("amii.meme.info.title")) + .setListener(NotificationListener.UrlOpeningListener(false)) + .notify(project) + } +} diff --git a/src/main/kotlin/io/unthrottled/amii/memes/MemePanel.kt b/src/main/kotlin/io/unthrottled/amii/memes/MemePanel.kt index 8f433aaf..2ab4bbca 100644 --- a/src/main/kotlin/io/unthrottled/amii/memes/MemePanel.kt +++ b/src/main/kotlin/io/unthrottled/amii/memes/MemePanel.kt @@ -87,6 +87,9 @@ class MemePanel( private val memePanelSettings: MemePanelSettings, ) : HwFacadeJPanel(), Disposable, Logging { + fun clone(): MemePanel = + MemePanel(rootPane, visualMeme, memePlayer, memePanelSettings) + companion object { val PANEL_LAYER: Int = JBLayeredPane.DRAG_LAYER private const val TOTAL_FRAMES = 8 @@ -113,7 +116,7 @@ class MemePanel( private val fadeoutAlarm = Alarm() private val invulnerabilityAlarm = Alarm() - private val mouseListener: AWTEventListener = createMouseLister() + private val mouseListener: AWTEventListener = createMouseListener() private val memeDisplay: JComponent init { @@ -156,7 +159,7 @@ class MemePanel( ) } - private fun createMouseLister(): AWTEventListener { + private fun createMouseListener(): AWTEventListener { var clickedInside = false return AWTEventListener { event -> if (invulnerable) return@AWTEventListener @@ -176,6 +179,7 @@ class MemePanel( } else if (wasInside) { fadeoutAlarm.cancelAllRequests() clickedInside = true + this.lifecycleListener.onClick() } } } else if ( diff --git a/src/main/kotlin/io/unthrottled/amii/memes/MemeService.kt b/src/main/kotlin/io/unthrottled/amii/memes/MemeService.kt index 17ca9c3e..df7196d7 100644 --- a/src/main/kotlin/io/unthrottled/amii/memes/MemeService.kt +++ b/src/main/kotlin/io/unthrottled/amii/memes/MemeService.kt @@ -20,6 +20,12 @@ fun Project.memeService(): MemeService = this.getService(MemeService::class.java class MemeService(private val project: Project) { + fun displayLastMeme() { + val memeToShow = previousMeme ?: return + + displayMeme(memeToShow.clone()) + } + fun createMeme( userEvent: UserEvent, memeAssetCategory: MemeAssetCategory, @@ -52,7 +58,8 @@ class MemeService(private val project: Project) { memeAssets.visualMemeContent, memeAssets.audibleMemeContent, userEvent, - rootPane + rootPane, + project ) ) } @@ -73,28 +80,34 @@ class MemeService(private val project: Project) { )?.layeredPane private var displayedMeme: Meme? = null + private var previousMeme: Meme? = null private fun attemptToDisplayMeme(meme: Meme) { val currentlyDisplayedMeme = displayedMeme val comparison = currentlyDisplayedMeme?.compareTo(meme) ?: Comparison.UNKNOWN if (comparison == Comparison.GREATER || comparison == Comparison.UNKNOWN) { currentlyDisplayedMeme?.dismiss() - - // be paranoid about existing memes - // hanging around for some reason https://github.com/ani-memes/AMII/issues/108 - getRootPane().toOptional().ifPresent { dismissAllMemesInPane(it) } - - showMeme(meme) + previousMeme = currentlyDisplayedMeme ?: previousMeme + displayMeme(meme) } else { meme.dispose() } } + private fun displayMeme(meme: Meme) { + // be paranoid about existing memes + // hanging around for some reason https://github.com/ani-memes/AMII/issues/108 + getRootPane().toOptional().ifPresent { dismissAllMemesInPane(it) } + + showMeme(meme) + } + private fun showMeme(meme: Meme) { displayedMeme = meme meme.addListener( object : MemeLifecycleListener { override fun onRemoval() { displayedMeme = null + previousMeme = meme } } ) diff --git a/src/main/kotlin/io/unthrottled/amii/onboarding/UpdateNotification.kt b/src/main/kotlin/io/unthrottled/amii/onboarding/UpdateNotification.kt index 68d0490a..cfee613a 100644 --- a/src/main/kotlin/io/unthrottled/amii/onboarding/UpdateNotification.kt +++ b/src/main/kotlin/io/unthrottled/amii/onboarding/UpdateNotification.kt @@ -23,6 +23,8 @@ private fun buildUpdateMessage(updateAsset: String): String = What's New?
  • Added Integrated Discreet Mode
  • +
  • Click Meme to show info!
  • +
  • Show Previous Meme action.
  • 2021.3 Build Support
  • Handling vertical overflow better in settings UI
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7cdb11de..9a96c2ee 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -21,8 +21,10 @@ /> + + @@ -95,6 +97,12 @@ description="Displays AMII's Settings"> + + + Plugins" and search for "Anime Memes" settings.general.modes.discreet-mode=羞耻设置 +amii.meme.info.title=Meme Information +amii.meme.info.search=Search +amii.meme.info.dunno=Sorry, I do not know the source of this asset. +amii.meme.info.stop=Stop Showing Info +settings.general.misc.show-info=Info On Click