diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf1c1f..0b919d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ ## [Unreleased] +## [1.11.0] - 2026-02-27 + +### Added + +- **Dashboard Hub Banner** - Non-intrusive top banner in the Dashboard encouraging users to connect to CodeClocker Hub: + - Shown when no API key is configured + - Shown when subscription has expired, encouraging renewal to keep syncing data + - Hidden when user has an active Hub subscription + - Clickable links to connect or renew directly from the banner + +## [1.10.0] - 2026-02-22 + ### Added - **Project Timeline Heatmap** - Gantt-style heatmap in the Dashboard showing when each project was worked on over time: @@ -213,7 +225,9 @@ - Support IntelliJ Platform 2024.3.5 -[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.9.0...HEAD +[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.11.0...HEAD +[1.11.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.10.0...v1.11.0 +[1.10.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.9.0...v1.10.0 [1.9.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.8.0...v1.9.0 [1.8.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.7.0...v1.8.0 [1.7.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.6.0...v1.7.0 diff --git a/CLAUDE.md b/CLAUDE.md index b4f2b42..bac82c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,3 +75,8 @@ CodeClocker is an IntelliJ IDEA plugin that tracks coding time and activity. It ## Code Style - Only add comments for complex logic; avoid commenting obvious code + +## Release Process + +1. Update `pluginVersion` in `gradle.properties` +2. Add release note in `CHANGELOG.md` diff --git a/gradle.properties b/gradle.properties index 26d0d1c..a64ff63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.codeclocker pluginName = CodeClocker pluginRepositoryUrl = https://github.com/codeclocker/codeclocker-intellij-plugin # SemVer format -> https://semver.org -pluginVersion = 1.9.0 +pluginVersion = 1.11.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 252 diff --git a/src/main/java/com/codeclocker/plugin/intellij/dashboard/DashboardPanel.java b/src/main/java/com/codeclocker/plugin/intellij/dashboard/DashboardPanel.java index bdd398b..e7e8bf8 100644 --- a/src/main/java/com/codeclocker/plugin/intellij/dashboard/DashboardPanel.java +++ b/src/main/java/com/codeclocker/plugin/intellij/dashboard/DashboardPanel.java @@ -1,5 +1,11 @@ package com.codeclocker.plugin.intellij.dashboard; +import static com.codeclocker.plugin.intellij.HubHost.HUB_UI_HOST; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import com.codeclocker.plugin.intellij.apikey.ApiKeyLifecycle; +import com.codeclocker.plugin.intellij.apikey.ApiKeyPersistence; +import com.codeclocker.plugin.intellij.apikey.EnterApiKeyAction; import com.codeclocker.plugin.intellij.dashboard.DashboardDataService.DashboardData; import com.codeclocker.plugin.intellij.dashboard.DashboardDataService.ProjectBreakdownEntry; import com.codeclocker.plugin.intellij.dashboard.DashboardDataService.ProjectTimelineData; @@ -13,6 +19,7 @@ import com.codeclocker.plugin.intellij.dashboard.ui.StreakCardPanel; import com.codeclocker.plugin.intellij.dashboard.ui.TimePeriodSelectorPanel; import com.intellij.icons.AllIcons; +import com.intellij.ide.BrowserUtil; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionToolbar; @@ -20,14 +27,17 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.ui.HyperlinkLabel; import com.intellij.ui.JBColor; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.ui.JBUI; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.FlowLayout; import java.awt.GridLayout; import java.util.List; import javax.swing.BoxLayout; +import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.event.AncestorEvent; import javax.swing.event.AncestorListener; @@ -51,10 +61,18 @@ public class DashboardPanel extends JPanel implements Disposable { private final ActivityTimelinePanel activityTimeline; private final ProjectTimelineGanttPanel projectTimelinePanel; private final AllProjectsPanel allProjectsPanel; + private final JPanel infoBanner; + private final JLabel bannerMessageLabel; + private final HyperlinkLabel bannerLink; public DashboardPanel() { setLayout(new BorderLayout()); + // Hub connection banner + this.bannerMessageLabel = new JLabel(); + this.bannerLink = new HyperlinkLabel(); + this.infoBanner = createInfoBanner(); + // Header: period selector + refresh button JPanel headerPanel = new JPanel(new BorderLayout()); periodSelector = new TimePeriodSelectorPanel(this::onPeriodChanged); @@ -73,7 +91,11 @@ public void actionPerformed(@NotNull AnActionEvent e) { toolbar.setTargetComponent(this); headerPanel.add(toolbar.getComponent(), BorderLayout.EAST); - add(headerPanel, BorderLayout.NORTH); + JPanel topPanel = new JPanel(); + topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); + topPanel.add(infoBanner); + topPanel.add(headerPanel); + add(topPanel, BorderLayout.NORTH); // Scrollable content JPanel contentPanel = new JPanel(); @@ -162,6 +184,8 @@ private void refreshData() { linesAddedCard.setLoading(true); linesRemovedCard.setLoading(true); + updateInfoBanner(); + ApplicationManager.getApplication() .executeOnPooledThread( () -> { @@ -239,6 +263,65 @@ private String formatNumber(long value) { return String.valueOf(value); } + private JPanel createInfoBanner() { + JPanel banner = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 4)); + banner.setBackground(new JBColor(new Color(255, 248, 225), new Color(62, 53, 32))); + banner.setBorder( + JBUI.Borders.customLine( + new JBColor(new Color(255, 183, 77), new Color(139, 105, 20)), 0, 0, 1, 0)); + + JLabel infoIcon = new JLabel(AllIcons.General.Information); + banner.add(infoIcon); + banner.add(bannerMessageLabel); + banner.add(bannerLink); + + bannerLink.addHyperlinkListener( + e -> { + if (javax.swing.event.HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) { + boolean hasApiKey = isNotBlank(ApiKeyPersistence.getApiKey()); + boolean subscriptionExpired = ApiKeyLifecycle.isActivityDataStoppedBeingCollected(); + + if (hasApiKey && subscriptionExpired) { + BrowserUtil.browse(HUB_UI_HOST + "/payment"); + } else { + EnterApiKeyAction.showAction(); + } + } + }); + + banner.setVisible(false); + return banner; + } + + private void updateInfoBanner() { + ApplicationManager.getApplication() + .invokeLater( + () -> { + try { + boolean hasApiKey = isNotBlank(ApiKeyPersistence.getApiKey()); + boolean subscriptionExpired = ApiKeyLifecycle.isActivityDataStoppedBeingCollected(); + + if (hasApiKey && !subscriptionExpired) { + infoBanner.setVisible(false); + return; + } + + if (hasApiKey) { + bannerMessageLabel.setText( + "Your subscription has expired. New activity data is no longer being synced."); + bannerLink.setHyperlinkText("Renew subscription to keep your data safe"); + } else { + bannerMessageLabel.setText("Your coding data is stored locally."); + bannerLink.setHyperlinkText("Connect to Hub to save your data forever"); + } + + infoBanner.setVisible(true); + } catch (Exception e) { + infoBanner.setVisible(false); + } + }); + } + @Override public void dispose() {} }