diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bed0c91..0ffe9cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ # AMII Changelog ## [Unreleased] + ### Added +- The ability for MIKU to continuously give you a stream of AniMemes (Silence Breaker feature). ### Changed diff --git a/README.md b/README.md index 863d1a0c..46337e21 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,15 @@ Programs that exit with: are part of the default allowed exit codes, MIKU will not react to these (but can if you want to). +### Silence Breaker + +So you've been working diligently building your code, but not using any features of your IDE. +Such as building, testing, or running your project. +Well MIKU likes to remind you every so often that they exist. + +You can specify how long you can go without seeing a meme. +After that, MIKU will give you one! + ### Logs Do you work on a project that takes a billion years for the application to start? diff --git a/gradle.properties b/gradle.properties index 0ae45335..9b5343d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ pluginGroup = io.unthrottled pluginName_ = AMII -pluginVersion = 0.3.1 +pluginVersion = 0.4.0 pluginSinceBuild = 201.4515.24 pluginUntilBuild = 203.* # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl 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 ea505c4d..ce9e9bc0 100644 --- a/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.form +++ b/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.form @@ -3,7 +3,7 @@ - + @@ -459,204 +459,241 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - + - - - + - + - - - - - - - - - + - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 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 cf5d2131..c9cb13ff 100644 --- a/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.java +++ b/src/main/java/io/unthrottled/amii/config/ui/PluginSettingsUI.java @@ -34,6 +34,7 @@ import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JRadioButton; +import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.JTabbedPane; @@ -53,6 +54,7 @@ import static io.unthrottled.amii.events.UserEvents.IDLE; import static io.unthrottled.amii.events.UserEvents.LOGS; import static io.unthrottled.amii.events.UserEvents.PROCESS; +import static io.unthrottled.amii.events.UserEvents.SILENCE; import static io.unthrottled.amii.events.UserEvents.STARTUP; import static io.unthrottled.amii.events.UserEvents.TASK; import static io.unthrottled.amii.events.UserEvents.TEST; @@ -95,6 +97,9 @@ public class PluginSettingsUI implements SearchableConfigurable, Configurable.No private JTextPane generalLinks; private JPanel idleAnchorPanel; private JTabbedPane tabbedPane1; + private JScrollPane eventsPane; + private JSpinner silenceSpinner; + private JCheckBox permitBreaksInSilenceCheckBox; private PreferredCharacterPanel characterModel; private PreferredCharacterPanel blacklistedCharacterModel; private JBTable exitCodeTable; @@ -285,8 +290,7 @@ public boolean isCellEditable(Integer info) { 1 ); eventsBeforeFrustrationSpinner.setModel(frustrationSpinnerModel); - eventsBeforeFrustrationSpinner.addChangeListener(e -> pluginSettingsModel.setEventsBeforFrustration(frustrationSpinnerModel.getNumber().intValue())); - + eventsBeforeFrustrationSpinner.addChangeListener(e -> pluginSettingsModel.setEventsBeforeFrustration(frustrationSpinnerModel.getNumber().intValue())); soundEnabled.addActionListener(e -> volumeSlider.setEnabled(soundEnabled.isSelected())); volumeSlider.setForeground(UIUtil.getContextHelpForeground()); @@ -307,6 +311,19 @@ public boolean isCellEditable(Integer info) { idleTimeoutSpinner.setModel(idleSpinnerModel); idleTimeoutSpinner.addChangeListener(e -> pluginSettingsModel.setIdleTimeOutInMinutes(idleSpinnerModel.getNumber().intValue())); + SpinnerNumberModel silenceSpinnerModel = new SpinnerNumberModel( + config.getSilenceTimeoutInMinutes(), + 1, + Integer.MAX_VALUE, + 1 + ); + silenceSpinner.setModel(silenceSpinnerModel); + silenceSpinner.addChangeListener(e -> pluginSettingsModel.setSilenceTimeOutInMinutes(silenceSpinnerModel.getNumber().intValue())); + + permitBreaksInSilenceCheckBox.addActionListener(e -> { + updateIdleComponents(); + updateEventPreference(SILENCE.getValue(), permitBreaksInSilenceCheckBox.isSelected()); + }); idleEnabled.addActionListener(e -> { updateIdleComponents(); updateEventPreference(IDLE.getValue(), idleEnabled.isSelected()); @@ -364,6 +381,9 @@ private void updateLogComponents() { private void updateIdleComponents() { idleTimeoutSpinner.setEnabled(idleEnabled.isSelected()); } + private void updateSilenceComponents() { + silenceSpinner.setEnabled(permitBreaksInSilenceCheckBox.isSelected()); + } private void updateFrustrationComponents() { frustrationProbabilitySlider.setEnabled(allowFrustrationCheckBox.isSelected()); @@ -398,6 +418,7 @@ private void initFromState() { preferOther.setSelected(isGenderSelected(Gender.OTHER.getValue())); idleEnabled.setSelected(isEventEnabled(IDLE.getValue())); + permitBreaksInSilenceCheckBox.setSelected(isEventEnabled(SILENCE.getValue())); updateIdleComponents(); watchLogs.setSelected(isEventEnabled(LOGS.getValue())); updateLogComponents(); @@ -469,7 +490,8 @@ public void apply() { config.setLogSearchIgnoreCase(pluginSettingsModel.getLogSearchIgnoreCase()); config.setShowMood(pluginSettingsModel.getShowMood()); config.setIdleTimeoutInMinutes(pluginSettingsModel.getIdleTimeOutInMinutes()); - config.setEventsBeforeFrustration(pluginSettingsModel.getEventsBeforFrustration()); + config.setSilenceTimeoutInMinutes(pluginSettingsModel.getSilenceTimeOutInMinutes()); + config.setEventsBeforeFrustration(pluginSettingsModel.getEventsBeforeFrustration()); ApplicationManager.getApplication().getMessageBus().syncPublisher( ConfigListener.Companion.getCONFIG_TOPIC() ).pluginConfigUpdated(config); diff --git a/src/main/kotlin/io/unthrottled/amii/PluginMaster.kt b/src/main/kotlin/io/unthrottled/amii/PluginMaster.kt index 2abed12b..d5071eb0 100644 --- a/src/main/kotlin/io/unthrottled/amii/PluginMaster.kt +++ b/src/main/kotlin/io/unthrottled/amii/PluginMaster.kt @@ -11,6 +11,7 @@ import io.unthrottled.amii.assets.CharacterContentManager import io.unthrottled.amii.assets.Status import io.unthrottled.amii.assets.VisualContentManager import io.unthrottled.amii.listeners.IdleEventListener +import io.unthrottled.amii.listeners.SilenceListener import io.unthrottled.amii.onboarding.UpdateNotification import io.unthrottled.amii.onboarding.UserOnBoarding import io.unthrottled.amii.platform.LifeCycleManager @@ -83,8 +84,10 @@ internal data class ProjectListeners( ) : Disposable { private val idleEventListener = IdleEventListener(project) + private val silenceListener = SilenceListener(project) override fun dispose() { idleEventListener.dispose() + silenceListener.dispose() } } diff --git a/src/main/kotlin/io/unthrottled/amii/config/Config.kt b/src/main/kotlin/io/unthrottled/amii/config/Config.kt index d007cbf9..89c68ef7 100644 --- a/src/main/kotlin/io/unthrottled/amii/config/Config.kt +++ b/src/main/kotlin/io/unthrottled/amii/config/Config.kt @@ -25,6 +25,7 @@ class Config : PersistentStateComponent, Cloneable { get() = ServiceManager.getService(Config::class.java) const val DEFAULT_DELIMITER = "," const val DEFAULT_IDLE_TIMEOUT_IN_MINUTES: Long = 5L + const val DEFAULT_SILENCE_TIMEOUT_IN_MINUTES: Long = 10L const val DEFAULT_MEME_INVULNERABLE_DURATION: Int = 3 const val DEFAULT_TIMED_MEME_DISPLAY_DURATION: Int = 40 const val DEFAULT_EVENTS_BEFORE_FRUSTRATION: Int = 5 @@ -48,6 +49,7 @@ class Config : PersistentStateComponent, Cloneable { FORCE_KILLED_EXIT_CODE ).joinToString(DEFAULT_DELIMITER) var idleTimeoutInMinutes = DEFAULT_IDLE_TIMEOUT_IN_MINUTES + var silenceTimeoutInMinutes = DEFAULT_SILENCE_TIMEOUT_IN_MINUTES var allowFrustration = true var eventsBeforeFrustration = DEFAULT_EVENTS_BEFORE_FRUSTRATION var probabilityOfFrustration = DEFAULT_FRUSTRATION_PROBABILITY diff --git a/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt b/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt index 13058818..b5eead6e 100644 --- a/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt +++ b/src/main/kotlin/io/unthrottled/amii/config/PluginSettings.kt @@ -5,6 +5,7 @@ import java.net.URI data class ConfigSettingsModel( var allowedExitCodes: String, var idleTimeOutInMinutes: Long, + var silenceTimeOutInMinutes: Long, var memeDisplayAnchorValue: String, var idleMemeDisplayAnchorValue: String, var memeDisplayModeValue: String, @@ -18,7 +19,7 @@ data class ConfigSettingsModel( var logKeyword: String, var logSearchIgnoreCase: Boolean, var showMood: Boolean, - var eventsBeforFrustration: Int, + var eventsBeforeFrustration: Int, ) { fun duplicate(): ConfigSettingsModel = copy() } @@ -34,6 +35,7 @@ object PluginSettings { fun getInitialConfigSettingsModel() = ConfigSettingsModel( Config.instance.allowedExitCodes, Config.instance.idleTimeoutInMinutes, + Config.instance.silenceTimeoutInMinutes, Config.instance.memeDisplayAnchorValue, Config.instance.memeDisplayModeValue, Config.instance.idleMemeDisplayAnchorValue, diff --git a/src/main/kotlin/io/unthrottled/amii/core/MIKU.kt b/src/main/kotlin/io/unthrottled/amii/core/MIKU.kt index bb818884..b99624a9 100644 --- a/src/main/kotlin/io/unthrottled/amii/core/MIKU.kt +++ b/src/main/kotlin/io/unthrottled/amii/core/MIKU.kt @@ -140,6 +140,7 @@ class MIKU : private fun reactToEvent(userEvent: UserEvent, emotionalState: Mood) { when (userEvent.type) { in USER_TRIGGERED_EVENTS -> taskPersonalityCore.processUserEvent(userEvent, emotionalState) + UserEvents.SILENCE, UserEvents.ON_DEMAND -> onDemandPersonalityCore.processUserEvent(userEvent, emotionalState) UserEvents.IDLE, UserEvents.RETURN, -> idlePersonalityCore.processUserEvent(userEvent, emotionalState) diff --git a/src/main/kotlin/io/unthrottled/amii/core/personality/OnDemandPersonalityCore.kt b/src/main/kotlin/io/unthrottled/amii/core/personality/OnDemandPersonalityCore.kt index 673bb97b..b8549196 100644 --- a/src/main/kotlin/io/unthrottled/amii/core/personality/OnDemandPersonalityCore.kt +++ b/src/main/kotlin/io/unthrottled/amii/core/personality/OnDemandPersonalityCore.kt @@ -1,8 +1,10 @@ package io.unthrottled.amii.core.personality import io.unthrottled.amii.assets.MemeAssetCategory +import io.unthrottled.amii.config.Config import io.unthrottled.amii.core.personality.emotions.Mood import io.unthrottled.amii.events.UserEvent +import io.unthrottled.amii.events.UserEvents import io.unthrottled.amii.memes.Comparison import io.unthrottled.amii.memes.memeService @@ -13,13 +15,20 @@ class OnDemandPersonalityCore : PersonalityCore { mood: Mood ) { userEvent.project.memeService() - .createMeme( + .createMemeFromCategories( userEvent, - MemeAssetCategory.MOTIVATION, + MemeAssetCategory.HAPPY, + MemeAssetCategory.CELEBRATION, + MemeAssetCategory.ALERT, ) { - it.withComparator { - Comparison.GREATER - }.build() + it + .withSound( + if (userEvent.type == UserEvents.SILENCE) false + else Config.instance.soundEnabled + ) + .withComparator { + Comparison.GREATER + }.build() } } } diff --git a/src/main/kotlin/io/unthrottled/amii/events/UserEvents.kt b/src/main/kotlin/io/unthrottled/amii/events/UserEvents.kt index 61fa820a..bbe83064 100644 --- a/src/main/kotlin/io/unthrottled/amii/events/UserEvents.kt +++ b/src/main/kotlin/io/unthrottled/amii/events/UserEvents.kt @@ -16,6 +16,7 @@ enum class UserEvents(val value: Int) { TASK(1 shl 5), TEST(1 shl 6), RELAX(1 shl 7), + SILENCE(1 shl 8), } enum class UserEventCategory { diff --git a/src/main/kotlin/io/unthrottled/amii/listeners/SilenceListener.kt b/src/main/kotlin/io/unthrottled/amii/listeners/SilenceListener.kt new file mode 100644 index 00000000..4d1a2c43 --- /dev/null +++ b/src/main/kotlin/io/unthrottled/amii/listeners/SilenceListener.kt @@ -0,0 +1,83 @@ +package io.unthrottled.amii.listeners + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.util.Alarm +import io.unthrottled.amii.config.Config +import io.unthrottled.amii.config.ConfigListener +import io.unthrottled.amii.events.EVENT_TOPIC +import io.unthrottled.amii.events.UserEvent +import io.unthrottled.amii.events.UserEventCategory +import io.unthrottled.amii.events.UserEventListener +import io.unthrottled.amii.events.UserEvents +import io.unthrottled.amii.tools.PluginMessageBundle +import java.util.concurrent.TimeUnit + +class SilenceListener(private val project: Project) : Runnable, UserEventListener, Disposable { + private val messageBus = ApplicationManager.getApplication().messageBus.connect() + private val log = Logger.getInstance(this::class.java) + private val silenceAlarm = Alarm() + + init { + val self = this + messageBus.subscribe(EVENT_TOPIC, this) + messageBus.subscribe( + ConfigListener.CONFIG_TOPIC, + ConfigListener { newPluginState -> + silenceAlarm.cancelAllRequests() + silenceAlarm.addRequest( + self, + TimeUnit.MILLISECONDS.convert( + newPluginState.silenceTimeoutInMinutes, + TimeUnit.MINUTES + ).toInt() + ) + } + ) + scheduleSilenceAlert() + } + + private fun scheduleSilenceAlert() { + silenceAlarm.addRequest( + this, + TimeUnit.MILLISECONDS.convert( + getCurrentTimoutInMinutes(), + TimeUnit.MINUTES + ).toInt() + ) + } + + private fun getCurrentTimoutInMinutes(): Long = + Config.instance.silenceTimeoutInMinutes + + override fun dispose() { + messageBus.dispose() + silenceAlarm.dispose() + } + + override fun run() { + log.debug("Observed silence timeout") + ApplicationManager.getApplication().messageBus + .syncPublisher(EVENT_TOPIC) + .onDispatch( + UserEvent( + UserEvents.SILENCE, + UserEventCategory.NEUTRAL, + PluginMessageBundle.message("user.event.silence.name"), + project + ) + ) + } + + override fun onDispatch(userEvent: UserEvent) { + when (userEvent.type) { + UserEvents.IDLE -> silenceAlarm.cancelAllRequests() + else -> { + silenceAlarm.cancelAllRequests() + scheduleSilenceAlert() + } + } + } +} diff --git a/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt b/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt index dc051168..4979349e 100644 --- a/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt +++ b/src/main/kotlin/io/unthrottled/amii/memes/Meme.kt @@ -54,6 +54,7 @@ class Meme( ) { private var notificationMode = Config.instance.notificationMode private var notificationAnchor = Config.instance.notificationAnchor + private var soundEnabled = Config.instance.soundEnabled private var memeDisplayInvulnerabilityDuration = Config.instance.memeDisplayInvulnerabilityDuration private var memeDisplayTimedDuration = Config.instance.memeDisplayTimedDuration private var memeComparator: (Meme) -> Comparison = { Comparison.EQUAL } @@ -69,6 +70,11 @@ class Meme( return this } + fun withSound(newSoundOption: Boolean): Builder { + soundEnabled = newSoundOption + return this + } + fun withMetaData(newMetaData: Map): Builder { metaData = newMetaData return this @@ -81,6 +87,7 @@ class Meme( fun build(): Meme { val memePlayer = audibleContent.toOptional() + .filter { soundEnabled } .map { MemePlayerFactory.createPlayer(it) } .orElse(null) return Meme( diff --git a/src/main/kotlin/io/unthrottled/amii/memes/player/Mp3Player.kt b/src/main/kotlin/io/unthrottled/amii/memes/player/Mp3Player.kt deleted file mode 100644 index fc7874c3..00000000 --- a/src/main/kotlin/io/unthrottled/amii/memes/player/Mp3Player.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.unthrottled.amii.memes.player - -import io.unthrottled.amii.assets.AudibleContent -import javazoom.jl.player.FactoryRegistry -import javazoom.jl.player.advanced.AdvancedPlayer -import javazoom.jl.player.advanced.PlaybackEvent -import javazoom.jl.player.advanced.PlaybackListener -import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader -import java.nio.file.Files -import java.nio.file.Paths -import javax.sound.sampled.AudioFileFormat - -// todo: post mvp: volume adjustment -class Mp3Player( - private val audibleAssetContent: AudibleContent -) : MemePlayer { - - val player: AdvancedPlayer - - init { - val audioDevice = FactoryRegistry.systemRegistry().createAudioDevice() - player = Files.newInputStream(Paths.get(audibleAssetContent.filePath)) - .use { audioInputStream -> - AdvancedPlayer(audioInputStream, audioDevice) - } - player.playBackListener = object : PlaybackListener() { - override fun playbackFinished(evt: PlaybackEvent?) { - evt?.source?.close() - player.close() - } - } - } - - override val duration: Long - get() { - val baseFileFormat: AudioFileFormat = MpegAudioFileReader().getAudioFileFormat( - audibleAssetContent.filePath.toURL() - ) - val duration = baseFileFormat.properties()["duration"] as Long? - return duration ?: MemePlayer.NO_LENGTH - } - - override fun play() { - player.play() - } - - override fun stop() { - player.close() - } -} diff --git a/src/main/kotlin/io/unthrottled/amii/onboarding/UserOnBoarding.kt b/src/main/kotlin/io/unthrottled/amii/onboarding/UserOnBoarding.kt index 358e6c98..0f6b14eb 100644 --- a/src/main/kotlin/io/unthrottled/amii/onboarding/UserOnBoarding.kt +++ b/src/main/kotlin/io/unthrottled/amii/onboarding/UserOnBoarding.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.startup.StartupManager import io.unthrottled.amii.config.Config import io.unthrottled.amii.config.Constants.PLUGIN_ID +import io.unthrottled.amii.events.UserEvents import io.unthrottled.amii.platform.UpdateAssetsListener import io.unthrottled.amii.tools.toOptional import java.util.Optional @@ -14,6 +15,10 @@ import java.util.UUID object UserOnBoarding { + private val addedEvents = setOf( + UserEvents.SILENCE + ).map { it.value } + fun attemptToPerformNewUpdateActions(project: Project) { getNewVersion().ifPresent { newVersion -> Config.instance.version = newVersion @@ -29,6 +34,12 @@ object UserOnBoarding { if (Config.instance.userId.isEmpty()) { Config.instance.userId = UUID.randomUUID().toString() } + + // Add new events for user + Config.instance.enabledEvents = addedEvents.stream() + .reduce(Config.instance.enabledEvents) { accum, newEventToAdd -> + accum or newEventToAdd + } } private fun getNewVersion() = diff --git a/src/main/resources/messages/AMII.properties b/src/main/resources/messages/AMII.properties index 09158f60..579a9ae5 100644 --- a/src/main/resources/messages/AMII.properties +++ b/src/main/resources/messages/AMII.properties @@ -59,3 +59,8 @@ actions.sync.start.title=Starting Asset Sync actions.sync.start.message=Fetching list of assets from the remote repository. miku.startup.error.body=For full functionality, please try restarting your IDE. Please submit an issue if it persists. miku.startup.error.title=Unable to fully initialize! +user.event.silence.name=Silence Events +settings.events.silence.name=Silence +settings.events.general.title=General Events +settings.events.silence.label=Duration before event (minutes) +settings.events.silence.enabled=Permit breaks in silence diff --git a/src/main/resources/messages/AMII_zh.properties b/src/main/resources/messages/AMII_zh.properties index 62dda90b..ead01091 100644 --- a/src/main/resources/messages/AMII_zh.properties +++ b/src/main/resources/messages/AMII_zh.properties @@ -52,3 +52,8 @@ actions.sync.start.title=Starting Asset Sync actions.sync.start.message=Fetching list of assets from the remote repository. miku.startup.error.body=For full functionality, please try restarting your IDE. Please submit an issue if it persists. miku.startup.error.title=Unable to fully initialize! +user.event.silence.name=Silence Events +settings.events.silence.name=Silence +settings.events.general.title=General Events +settings.events.silence.label=Duration before event (minutes) +settings.events.silence.enabled=Permit breaks in silence