├── settings.gradle.kts ├── screenshots ├── Logo.png ├── Install.png └── Dashboard.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── http ├── tencent.http └── sina.http ├── src └── main │ ├── resources │ ├── icons │ │ └── logo.png │ └── META-INF │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ ├── kotlin │ └── com │ │ └── vermouthx │ │ └── stocker │ │ ├── enums │ │ ├── StockerMarketType.kt │ │ ├── StockerQuoteColorPattern.kt │ │ ├── StockerMarketIndex.kt │ │ └── StockerQuoteProvider.kt │ │ ├── entities │ │ ├── StockerSuggestion.kt │ │ └── StockerQuote.kt │ │ ├── StockerAppManager.kt │ │ ├── settings │ │ ├── StockerSettingState.kt │ │ └── StockerSetting.kt │ │ ├── actions │ │ ├── StockerStockSearchAction.kt │ │ ├── StockerStockManageAction.kt │ │ ├── StockerRefreshAction.kt │ │ ├── StockerResetAction.kt │ │ └── StockerStopAction.kt │ │ ├── activities │ │ └── StockerStartupActivity.kt │ │ ├── views │ │ ├── windows │ │ │ ├── StockerSimpleToolWindow.kt │ │ │ ├── StockerSettingWindow.kt │ │ │ └── StockerToolWindow.kt │ │ └── dialogs │ │ │ ├── StockerSuggestionDialog.kt │ │ │ └── StockerManagementDialog.kt │ │ ├── notifications │ │ └── StockerNotification.kt │ │ ├── utils │ │ ├── StockerQuoteHttpUtil.kt │ │ ├── StockerSuggestHttpUtil.kt │ │ └── StockerQuoteParser.kt │ │ └── StockerApp.kt │ └── java │ └── com │ └── vermouthx │ └── stocker │ ├── components │ ├── StockerTableModel.java │ ├── StockerDefaultTableCellRender.java │ └── StockerTableHeaderRender.java │ ├── utils │ ├── StockerTableModelUtil.java │ └── StockerActionUtil.java │ ├── enums │ └── StockerStockOperation.java │ ├── listeners │ ├── StockerQuoteReloadListener.java │ ├── StockerQuoteDeleteNotifier.java │ ├── StockerQuoteReloadNotifier.java │ ├── StockerQuoteDeleteListener.java │ ├── StockerQuoteUpdateNotifier.java │ └── StockerQuoteUpdateListener.java │ └── views │ └── StockerTableView.java ├── .gitignore ├── gradle.properties ├── README.md ├── azure-pipelines.yml ├── CHANGELOG.md ├── gradlew.bat ├── gradlew └── LICENSE /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "intellij-investor-dashboard" 2 | -------------------------------------------------------------------------------- /screenshots/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhiteVermouth/intellij-investor-dashboard/HEAD/screenshots/Logo.png -------------------------------------------------------------------------------- /screenshots/Install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhiteVermouth/intellij-investor-dashboard/HEAD/screenshots/Install.png -------------------------------------------------------------------------------- /screenshots/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhiteVermouth/intellij-investor-dashboard/HEAD/screenshots/Dashboard.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhiteVermouth/intellij-investor-dashboard/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /http/tencent.http: -------------------------------------------------------------------------------- 1 | ### Tencent 2 | GET https://qt.gtimg.cn/q=sh113031 3 | 4 | ### Tencent Suggest 5 | GET https://sqt.gtimg.cn/utf8/q=SH600 6 | -------------------------------------------------------------------------------- /src/main/resources/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhiteVermouth/intellij-investor-dashboard/HEAD/src/main/resources/icons/logo.png -------------------------------------------------------------------------------- /http/sina.http: -------------------------------------------------------------------------------- 1 | ### Sina 2 | GET https://hq.sinajs.cn/list=sh113031 3 | Referer: https://finance.sina.com.cn 4 | 5 | ### Sina Suggest 6 | GET https://suggest3.sinajs.cn/suggest/key=2400 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/enums/StockerMarketType.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.enums 2 | 3 | enum class StockerMarketType(val title: String) { 4 | AShare("CN"), 5 | HKStocks("HK"), 6 | USStocks("US"), 7 | Crypto("Crypto") 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/entities/StockerSuggestion.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.entities 2 | 3 | import com.vermouthx.stocker.enums.StockerMarketType 4 | 5 | data class StockerSuggestion(val code: String, val name: String, val market: StockerMarketType) 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/enums/StockerQuoteColorPattern.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.enums 2 | 3 | enum class StockerQuoteColorPattern(val title: String) { 4 | RED_UP_GREEN_DOWN("R.U.G.D. Mode"), 5 | GREEN_UP_RED_DOWN("G.U.R.D. Mode"), 6 | NONE("None") 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/enums/StockerMarketIndex.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.enums 2 | 3 | enum class StockerMarketIndex(val codes: List) { 4 | CN(listOf("SH000001", "SZ399001", "SZ399006")), 5 | HK(listOf("HSI")), 6 | US(listOf("DJI", "IXIC", "INX")), 7 | Crypto(listOf("BTCBTCUSD")) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/components/StockerTableModel.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.components; 2 | 3 | import javax.swing.table.DefaultTableModel; 4 | 5 | public class StockerTableModel extends DefaultTableModel { 6 | 7 | @Override 8 | public boolean isCellEditable(int row, int column) { 9 | return false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/StockerAppManager.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker 2 | 3 | import com.intellij.openapi.project.Project 4 | 5 | object StockerAppManager { 6 | val myApplicationMap: MutableMap = mutableMapOf() 7 | 8 | fun myApplication(project: Project?): StockerApp? { 9 | return myApplicationMap[project] 10 | } 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | Icon 6 | ._* 7 | .DocumentRevisions-V100 8 | .fseventsd 9 | .Spotlight-V100 10 | .TemporaryItems 11 | .Trashes 12 | .VolumeIcon.icns 13 | .com.apple.timemachine.donotpresent 14 | .AppleDB 15 | .AppleDesktop 16 | Network Trash Folder 17 | Temporary Items 18 | .apdisk 19 | .gradle 20 | build 21 | gradle-app.setting 22 | !gradle-wrapper.jar 23 | .gradletasknamecache 24 | .idea 25 | *.iml 26 | out 27 | gen 28 | .intellijPlatform 29 | .kotlin 30 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/utils/StockerTableModelUtil.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.utils; 2 | 3 | import javax.swing.table.DefaultTableModel; 4 | 5 | public final class StockerTableModelUtil { 6 | public static int existAt(DefaultTableModel tableModel, String code) { 7 | for (int i = 0; i < tableModel.getRowCount(); i++) { 8 | String c = tableModel.getValueAt(i, 0).toString(); 9 | if (code != null && code.equals(c)) { 10 | return i; 11 | } 12 | } 13 | return -1; 14 | } 15 | 16 | private StockerTableModelUtil() { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/enums/StockerStockOperation.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.enums; 2 | 3 | public enum StockerStockOperation { 4 | STOCK_ADD("Add"), 5 | STOCK_DELETE("Delete"); 6 | 7 | private final String operation; 8 | 9 | StockerStockOperation(String operation) { 10 | this.operation = operation; 11 | } 12 | 13 | public static StockerStockOperation mapOf(String des) { 14 | if ("Add".equals(des)) { 15 | return STOCK_ADD; 16 | } 17 | return STOCK_DELETE; 18 | } 19 | 20 | public String getOperation() { 21 | return operation; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/components/StockerDefaultTableCellRender.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.components; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.DefaultTableCellRenderer; 5 | import java.awt.*; 6 | 7 | public class StockerDefaultTableCellRender extends DefaultTableCellRenderer { 8 | 9 | @Override 10 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 11 | setHorizontalAlignment(SwingConstants.CENTER); 12 | return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.caching=true 3 | # IntelliJ Platform Artifacts Repositories 4 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 5 | pluginGroup=com.vermouthx 6 | pluginName=Stocker 7 | pluginVersion=1.11.1 8 | # Plugin Build Platform 9 | platformType=IC 10 | platformVersion=2024.1 11 | # Opt-out flag for bundling Kotlin standard library. 12 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 13 | kotlin.stdlib.default.dependency=false 14 | # Temporary workaround for Kotlin Compiler OutOfMemoryError -> https://jb.gg/intellij-platform-kotlin-oom 15 | kotlin.incremental.useClasspathSnapshot=false 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/settings/StockerSettingState.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.settings 2 | 3 | import com.vermouthx.stocker.enums.StockerQuoteColorPattern 4 | import com.vermouthx.stocker.enums.StockerQuoteProvider 5 | 6 | class StockerSettingState { 7 | var version: String = "" 8 | var refreshInterval: Long = 5 9 | var quoteProvider: StockerQuoteProvider = StockerQuoteProvider.SINA 10 | var quoteColorPattern: StockerQuoteColorPattern = StockerQuoteColorPattern.RED_UP_GREEN_DOWN 11 | var aShareList: MutableList = mutableListOf() 12 | var hkStocksList: MutableList = mutableListOf() 13 | var usStocksList: MutableList = mutableListOf() 14 | var cryptoList: MutableList = mutableListOf() 15 | } -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/listeners/StockerQuoteReloadListener.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.listeners; 2 | 3 | import com.vermouthx.stocker.views.StockerTableView; 4 | 5 | import javax.swing.table.DefaultTableModel; 6 | 7 | public class StockerQuoteReloadListener implements StockerQuoteReloadNotifier { 8 | private final StockerTableView myTableView; 9 | 10 | public StockerQuoteReloadListener(StockerTableView myTableView) { 11 | this.myTableView = myTableView; 12 | } 13 | 14 | @Override 15 | public void clear() { 16 | DefaultTableModel tableModel = myTableView.getTableModel(); 17 | synchronized (myTableView.getTableModel()) { 18 | // clear all table rows 19 | tableModel.setRowCount(0); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/entities/StockerQuote.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.entities 2 | 3 | data class StockerQuote( 4 | var code: String, 5 | var name: String, 6 | var current: Double, 7 | var opening: Double, 8 | var close: Double, 9 | var low: Double, 10 | var high: Double, 11 | var change: Double, 12 | var percentage: Double, 13 | var buys: Array = emptyArray(), 14 | var sells: Array = emptyArray(), 15 | var updateAt: String 16 | ) { 17 | override fun equals(other: Any?): Boolean { 18 | if (this === other) return true 19 | if (javaClass != other?.javaClass) return false 20 | 21 | other as StockerQuote 22 | 23 | return code == other.code 24 | } 25 | 26 | override fun hashCode(): Int { 27 | return code.hashCode() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/components/StockerTableHeaderRender.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.components; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.TableCellRenderer; 5 | import java.awt.*; 6 | 7 | public class StockerTableHeaderRender implements TableCellRenderer { 8 | 9 | private final TableCellRenderer renderer; 10 | 11 | public StockerTableHeaderRender(JTable table) { 12 | renderer = table.getTableHeader().getDefaultRenderer(); 13 | } 14 | 15 | @Override 16 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 17 | JLabel label = (JLabel) renderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 18 | label.setHorizontalAlignment(SwingConstants.CENTER); 19 | return label; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/actions/StockerStockSearchAction.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.actions 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.vermouthx.stocker.views.dialogs.StockerSuggestionDialog 7 | 8 | class StockerStockSearchAction : AnAction() { 9 | override fun update(e: AnActionEvent) { 10 | val project = e.project 11 | val presentation = e.presentation 12 | if (project == null) { 13 | presentation.isEnabled = false 14 | } 15 | } 16 | 17 | override fun actionPerformed(e: AnActionEvent) { 18 | StockerSuggestionDialog(e.project).show() 19 | } 20 | 21 | override fun getActionUpdateThread(): ActionUpdateThread { 22 | return ActionUpdateThread.BGT 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/actions/StockerStockManageAction.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.actions 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.vermouthx.stocker.views.dialogs.StockerManagementDialog 7 | 8 | class StockerStockManageAction : AnAction() { 9 | override fun update(e: AnActionEvent) { 10 | val project = e.project 11 | val presentation = e.presentation 12 | if (project == null) { 13 | presentation.isEnabled = false 14 | } 15 | } 16 | 17 | override fun actionPerformed(e: AnActionEvent) { 18 | val project = e.project 19 | StockerManagementDialog(project).show() 20 | } 21 | 22 | override fun getActionUpdateThread(): ActionUpdateThread { 23 | return ActionUpdateThread.BGT 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/actions/StockerRefreshAction.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.actions 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.vermouthx.stocker.StockerAppManager 7 | 8 | class StockerRefreshAction : AnAction() { 9 | 10 | override fun update(e: AnActionEvent) { 11 | val project = e.project 12 | val presentation = e.presentation 13 | if (project == null) { 14 | presentation.isEnabled = false 15 | } 16 | } 17 | 18 | override fun actionPerformed(e: AnActionEvent) { 19 | StockerAppManager.myApplicationMap[e.project]?.shutdownThenClear() 20 | StockerAppManager.myApplicationMap[e.project]?.schedule() 21 | } 22 | 23 | override fun getActionUpdateThread(): ActionUpdateThread { 24 | return ActionUpdateThread.BGT 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/listeners/StockerQuoteDeleteNotifier.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.listeners; 2 | 3 | import com.intellij.util.messages.Topic; 4 | 5 | public interface StockerQuoteDeleteNotifier { 6 | Topic STOCK_ALL_QUOTE_DELETE_TOPIC = Topic.create("StockAllQuoteDeleteTopic", StockerQuoteDeleteNotifier.class); 7 | Topic STOCK_CN_QUOTE_DELETE_TOPIC = Topic.create("StockCNQuoteDeleteTopic", StockerQuoteDeleteNotifier.class); 8 | Topic STOCK_HK_QUOTE_DELETE_TOPIC = Topic.create("StockHKQuoteDeleteTopic", StockerQuoteDeleteNotifier.class); 9 | Topic STOCK_US_QUOTE_DELETE_TOPIC = Topic.create("StockUSQuoteDeleteTopic", StockerQuoteDeleteNotifier.class); 10 | Topic CRYPTO_QUOTE_DELETE_TOPIC = Topic.create("CryptoQuoteDeleteTopic", StockerQuoteDeleteNotifier.class); 11 | 12 | void after(String code); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/listeners/StockerQuoteReloadNotifier.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.listeners; 2 | 3 | import com.intellij.util.messages.Topic; 4 | 5 | public interface StockerQuoteReloadNotifier { 6 | Topic STOCK_ALL_QUOTE_RELOAD_TOPIC = Topic.create("StockerAllQuoteReloadTopic", StockerQuoteReloadNotifier.class); 7 | Topic STOCK_CN_QUOTE_RELOAD_TOPIC = Topic.create("StockerCNQuoteReloadTopic", StockerQuoteReloadNotifier.class); 8 | Topic STOCK_HK_QUOTE_RELOAD_TOPIC = Topic.create("StockerHKQuoteReloadTopic", StockerQuoteReloadNotifier.class); 9 | Topic STOCK_US_QUOTE_RELOAD_TOPIC = Topic.create("StockerUSQuoteReloadTopic", StockerQuoteReloadNotifier.class); 10 | Topic STOCK_CRYPTO_QUOTE_RELOAD_TOPIC = Topic.create("StockerCryptoQuoteReloadTopic", StockerQuoteReloadNotifier.class); 11 | 12 | void clear(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/actions/StockerResetAction.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.actions 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.vermouthx.stocker.settings.StockerSetting 7 | 8 | class StockerResetAction : AnAction() { 9 | override fun update(e: AnActionEvent) { 10 | val project = e.project 11 | val presentation = e.presentation 12 | if (project == null) { 13 | presentation.isEnabled = false 14 | } 15 | } 16 | 17 | override fun actionPerformed(e: AnActionEvent) { 18 | val setting = StockerSetting.instance 19 | setting.aShareList.clear() 20 | setting.hkStocksList.clear() 21 | setting.usStocksList.clear() 22 | } 23 | 24 | override fun getActionUpdateThread(): ActionUpdateThread { 25 | return ActionUpdateThread.BGT 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/activities/StockerStartupActivity.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.activities 2 | 3 | import com.intellij.ide.plugins.PluginManagerCore 4 | import com.intellij.openapi.extensions.PluginId 5 | import com.intellij.openapi.project.DumbAware 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.startup.ProjectActivity 8 | import com.vermouthx.stocker.notifications.StockerNotification 9 | import com.vermouthx.stocker.settings.StockerSetting 10 | 11 | class StockerStartupActivity : ProjectActivity, DumbAware { 12 | 13 | private val setting = StockerSetting.instance 14 | private val pluginId = "com.vermouthx.intellij-investor-dashboard" 15 | 16 | override suspend fun execute(project: Project) { 17 | val currentVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version ?: "" 18 | if (setting.version != currentVersion) { 19 | setting.version = currentVersion 20 | StockerNotification.notifyInviteSupporter(project) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/listeners/StockerQuoteDeleteListener.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.listeners; 2 | 3 | import com.vermouthx.stocker.utils.StockerTableModelUtil; 4 | import com.vermouthx.stocker.views.StockerTableView; 5 | 6 | import javax.swing.table.DefaultTableModel; 7 | 8 | public class StockerQuoteDeleteListener implements StockerQuoteDeleteNotifier { 9 | 10 | private final StockerTableView myTableView; 11 | 12 | public StockerQuoteDeleteListener(StockerTableView myTableView) { 13 | this.myTableView = myTableView; 14 | } 15 | 16 | @Override 17 | public void after(String code) { 18 | synchronized (myTableView.getTableModel()) { 19 | DefaultTableModel tableModel = myTableView.getTableModel(); 20 | int rowIndex = StockerTableModelUtil.existAt(tableModel, code); 21 | if (rowIndex != -1) { 22 | tableModel.removeRow(rowIndex); 23 | tableModel.fireTableRowsDeleted(rowIndex, rowIndex); 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/actions/StockerStopAction.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.actions 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.vermouthx.stocker.StockerAppManager 7 | 8 | class StockerStopAction : AnAction() { 9 | override fun update(e: AnActionEvent) { 10 | val project = e.project 11 | val presentation = e.presentation 12 | if (project == null) { 13 | presentation.isEnabled = false 14 | } 15 | val myApplication = StockerAppManager.myApplication(project) 16 | if (myApplication?.isShutdown() == true) { 17 | presentation.isEnabled = false 18 | } 19 | } 20 | 21 | override fun actionPerformed(e: AnActionEvent) { 22 | val myApplication = StockerAppManager.myApplication(e.project) 23 | myApplication?.shutdown() 24 | } 25 | 26 | override fun getActionUpdateThread(): ActionUpdateThread { 27 | return ActionUpdateThread.BGT 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/listeners/StockerQuoteUpdateNotifier.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.listeners; 2 | 3 | import com.intellij.util.messages.Topic; 4 | import com.vermouthx.stocker.entities.StockerQuote; 5 | 6 | import java.util.List; 7 | 8 | public interface StockerQuoteUpdateNotifier { 9 | Topic STOCK_ALL_QUOTE_UPDATE_TOPIC = Topic.create("StockAllQuoteUpdateTopic", StockerQuoteUpdateNotifier.class); 10 | Topic STOCK_CN_QUOTE_UPDATE_TOPIC = Topic.create("StockCNQuoteUpdateTopic", StockerQuoteUpdateNotifier.class); 11 | Topic STOCK_HK_QUOTE_UPDATE_TOPIC = Topic.create("StockHKQuoteUpdateTopic", StockerQuoteUpdateNotifier.class); 12 | Topic STOCK_US_QUOTE_UPDATE_TOPIC = Topic.create("StockUSQuoteUpdateTopic", StockerQuoteUpdateNotifier.class); 13 | Topic CRYPTO_QUOTE_UPDATE_TOPIC = Topic.create("CryptoQuoteUpdateTopic", StockerQuoteUpdateNotifier.class); 14 | 15 | void syncQuotes(List quotes, int size); 16 | 17 | void syncIndices(List indices); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/enums/StockerQuoteProvider.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.enums 2 | 3 | enum class StockerQuoteProvider( 4 | val title: String, val host: String, val suggestHost: String, val providerPrefixMap: Map 5 | ) { 6 | /** 7 | * Sina API 8 | */ 9 | SINA( 10 | title = "Sina", 11 | host = "https://hq.sinajs.cn/list=", 12 | suggestHost = "https://suggest3.sinajs.cn/suggest/key=", 13 | providerPrefixMap = mapOf( 14 | StockerMarketType.AShare to "", 15 | StockerMarketType.HKStocks to "hk", 16 | StockerMarketType.USStocks to "gb_", 17 | StockerMarketType.Crypto to "btc_" 18 | ) 19 | ), 20 | 21 | /** 22 | * Tencent API 23 | */ 24 | TENCENT( 25 | title = "Tencent", 26 | host = "https://qt.gtimg.cn/q=", 27 | suggestHost = "https://smartbox.gtimg.cn/s3/?v=2&t=all&c=1&q=", 28 | providerPrefixMap = mapOf( 29 | StockerMarketType.AShare to "", 30 | StockerMarketType.HKStocks to "hk", 31 | StockerMarketType.USStocks to "us", 32 | ) 33 | ); 34 | 35 | fun fromTitle(title: String): StockerQuoteProvider { 36 | return when (title) { 37 | SINA.title -> SINA 38 | TENCENT.title -> TENCENT 39 | else -> SINA 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/views/windows/StockerSimpleToolWindow.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.views.windows 2 | 3 | import com.intellij.openapi.actionSystem.ActionManager 4 | import com.intellij.openapi.actionSystem.ActionPlaces 5 | import com.intellij.openapi.actionSystem.DefaultActionGroup 6 | import com.intellij.openapi.ui.SimpleToolWindowPanel 7 | import com.vermouthx.stocker.actions.StockerRefreshAction 8 | import com.vermouthx.stocker.actions.StockerStockManageAction 9 | import com.vermouthx.stocker.actions.StockerStockSearchAction 10 | import com.vermouthx.stocker.actions.StockerStopAction 11 | import com.vermouthx.stocker.views.StockerTableView 12 | 13 | class StockerSimpleToolWindow : SimpleToolWindowPanel(true) { 14 | var tableView: StockerTableView = StockerTableView() 15 | 16 | init { 17 | val actionManager = ActionManager.getInstance() 18 | val actionGroup = DefaultActionGroup( 19 | listOf(StockerRefreshAction::class.qualifiedName?.let { actionManager.getAction(it) }, 20 | StockerStopAction::class.qualifiedName?.let { actionManager.getAction(it) }, 21 | StockerStockManageAction::class.qualifiedName?.let { actionManager.getAction(it) }, 22 | StockerStockSearchAction::class.qualifiedName?.let { actionManager.getAction(it) }) 23 | ) 24 | val actionToolbar = actionManager.createActionToolbar(ActionPlaces.TOOLWINDOW_CONTENT, actionGroup, true) 25 | actionToolbar.targetComponent = tableView.component 26 | this.toolbar = actionToolbar.component 27 | setContent(tableView.component) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/notifications/StockerNotification.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.notifications 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.notification.Notification 5 | import com.intellij.notification.NotificationAction 6 | import com.intellij.notification.NotificationGroupManager 7 | import com.intellij.notification.NotificationType 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.util.IconLoader 10 | import org.intellij.lang.annotations.Language 11 | 12 | object StockerNotification { 13 | 14 | private const val DONATE_LINK = "https://www.buymeacoffee.com/nszihan" 15 | 16 | @Language("HTML") 17 | private val inviteSupporterMessage: String = """ 18 |

Your support helps me continue creating and improving the plugin.

19 | """.trimIndent() 20 | 21 | private const val NOTIFICATION_GROUP_ID = "Stocker" 22 | 23 | @JvmField 24 | val logoIcon = IconLoader.getIcon("/icons/logo.png", javaClass) 25 | 26 | fun notifyInviteSupporter(project: Project) { 27 | val notification = NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP_ID) 28 | .createNotification("Become Stocker Supporter", inviteSupporterMessage, NotificationType.INFORMATION) 29 | addNotificationActions(notification) 30 | notification.icon = logoIcon 31 | notification.notify(project) 32 | } 33 | 34 | private fun addNotificationActions(notification: Notification) { 35 | notification.addAction(NotificationAction.createSimple("Support Now") { 36 | BrowserUtil.browse(DONATE_LINK) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | icon
3 | Stocker 4 |

5 | 6 |

7 | Stocker is a JetBrains IDE extension dashboard for investors to track realtime stock market conditions. 8 |

9 |

10 | Build Status 11 | GitHub Release 12 | Marketplace Release 13 | Marketplace Downloads 14 |

15 |

16 | Dashboard 17 |

18 | 19 | ## Installation 20 | 21 | Search **Stocker** in `Plugin Marketplace` and click `Install`. 22 | 23 | ![Install](https://raw.githubusercontent.com/WhiteVermouth/intellij-investor-dashboard/master/screenshots/Install.png) 24 | 25 | ## Tutorial 26 | 27 | All instructions can be found at [here](https://vermouthx.com/2021/04/11/stocker). 28 | 29 | ## Licence 30 | 31 | [Apache-2.0 Licence](https://raw.githubusercontent.com/WhiteVermouth/intellij-investor-dashboard/master/LICENSE) 32 | 33 | ## Donation 34 | 35 | If you like this plugin, you can [buy me a cup of coffee](https://www.buymeacoffee.com/nszihan). Thank you! 36 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Gradle 2 | # Build your Java project and run tests with Gradle using a Gradle wrapper script. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/java 5 | 6 | trigger: 7 | branches: 8 | include: 9 | - master 10 | tags: 11 | include: 12 | - v1.* 13 | 14 | pool: 15 | vmImage: 'ubuntu-latest' 16 | 17 | variables: 18 | GRADLE_USER_HOME: $(Pipeline.Workspace)/.gradle 19 | 20 | steps: 21 | - task: Cache@2 22 | inputs: 23 | key: 'gradle | "$(Agent.OS)"' 24 | restoreKeys: gradle 25 | path: $(GRADLE_USER_HOME) 26 | displayName: Gradle build cache 27 | - task: Gradle@3 28 | inputs: 29 | workingDirectory: '' 30 | gradleWrapperFile: 'gradlew' 31 | gradleOptions: '-Xmx3072m' 32 | javaHomeOption: 'JDKVersion' 33 | jdkVersionOption: '1.17' 34 | jdkArchitectureOption: 'x64' 35 | publishJUnitResults: true 36 | testResultsFiles: '**/TEST-*.xml' 37 | tasks: 'buildPlugin' 38 | displayName: 'Build plugin' 39 | - task: GitHubRelease@1 40 | condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/')) 41 | inputs: 42 | gitHubConnection: 'WhiteVermouth' 43 | repositoryName: '$(Build.Repository.Name)' 44 | action: 'create' 45 | assets: 'build/distributions/*.zip' 46 | assetUploadMode: 'delete' 47 | tagSource: 'gitTag' 48 | target: '$(Build.SourceVersion)' 49 | displayName: 'Publish to GitHub Release' 50 | - task: Gradle@3 51 | condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/')) 52 | inputs: 53 | jdkVersionOption: '1.17' 54 | options: '-Djetbrains.token=$(jetbrains.token)' 55 | tasks: 'publishPlugin' 56 | displayName: 'Publish to JetBrains Plugin Repository' 57 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/listeners/StockerQuoteUpdateListener.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.listeners; 2 | 3 | import com.vermouthx.stocker.entities.StockerQuote; 4 | import com.vermouthx.stocker.utils.StockerTableModelUtil; 5 | import com.vermouthx.stocker.views.StockerTableView; 6 | 7 | import javax.swing.table.DefaultTableModel; 8 | import java.util.List; 9 | 10 | public class StockerQuoteUpdateListener implements StockerQuoteUpdateNotifier { 11 | private final StockerTableView myTableView; 12 | 13 | public StockerQuoteUpdateListener(StockerTableView myTableView) { 14 | this.myTableView = myTableView; 15 | } 16 | 17 | @Override 18 | public void syncQuotes(List quotes, int size) { 19 | DefaultTableModel tableModel = myTableView.getTableModel(); 20 | quotes.forEach(quote -> { 21 | synchronized (myTableView.getTableModel()) { 22 | int rowIndex = StockerTableModelUtil.existAt(tableModel, quote.getCode()); 23 | if (rowIndex != -1) { 24 | if (!tableModel.getValueAt(rowIndex, 1).equals(quote.getName())) { 25 | tableModel.setValueAt(quote.getName(), rowIndex, 1); 26 | tableModel.fireTableCellUpdated(rowIndex, 1); 27 | } 28 | if (!tableModel.getValueAt(rowIndex, 2).equals(quote.getCurrent())) { 29 | tableModel.setValueAt(quote.getCurrent(), rowIndex, 2); 30 | tableModel.fireTableCellUpdated(rowIndex, 2); 31 | } 32 | if (!tableModel.getValueAt(rowIndex, 3).equals(quote.getPercentage())) { 33 | tableModel.setValueAt(quote.getPercentage() + "%", rowIndex, 3); 34 | tableModel.fireTableCellUpdated(rowIndex, 3); 35 | } 36 | } else { 37 | if (quotes.size() == size) { 38 | tableModel.addRow(new Object[]{quote.getCode(), quote.getName(), quote.getCurrent(), quote.getPercentage() + "%"}); 39 | } 40 | } 41 | } 42 | }); 43 | } 44 | 45 | @Override 46 | public void syncIndices(List indices) { 47 | synchronized (myTableView) { 48 | myTableView.syncIndices(indices); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/views/windows/StockerSettingWindow.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.views.windows 2 | 3 | import com.intellij.openapi.options.BoundConfigurable 4 | import com.intellij.openapi.ui.DialogPanel 5 | import com.intellij.ui.dsl.builder.bindItem 6 | import com.intellij.ui.dsl.builder.panel 7 | import com.intellij.ui.dsl.builder.toMutableProperty 8 | import com.intellij.ui.dsl.builder.toNullableProperty 9 | import com.vermouthx.stocker.enums.StockerQuoteColorPattern 10 | import com.vermouthx.stocker.enums.StockerQuoteProvider 11 | import com.vermouthx.stocker.settings.StockerSetting 12 | 13 | class StockerSettingWindow : BoundConfigurable("Stocker") { 14 | 15 | private val setting = StockerSetting.instance 16 | 17 | private var colorPattern: StockerQuoteColorPattern = setting.quoteColorPattern 18 | private var quoteProviderTitle: String = setting.quoteProvider.title 19 | 20 | override fun createPanel(): DialogPanel { 21 | return panel { 22 | group("General") { 23 | row("Provider: ") { 24 | comboBox( 25 | StockerQuoteProvider.values() 26 | .map { it.title }).bindItem(::quoteProviderTitle.toNullableProperty()) 27 | } 28 | } 29 | 30 | group("Appearance") { 31 | buttonsGroup("Color Pattern: ") { 32 | row { 33 | radioButton("Red up and green down", StockerQuoteColorPattern.RED_UP_GREEN_DOWN) 34 | } 35 | row { 36 | radioButton("Green up and red down", StockerQuoteColorPattern.GREEN_UP_RED_DOWN) 37 | } 38 | row { 39 | radioButton("None", StockerQuoteColorPattern.NONE) 40 | } 41 | }.bind(::colorPattern.toMutableProperty(), StockerQuoteColorPattern::class.java) 42 | } 43 | 44 | onApply { 45 | setting.quoteProvider = setting.quoteProvider.fromTitle(quoteProviderTitle) 46 | setting.quoteColorPattern = colorPattern 47 | } 48 | onIsModified { 49 | quoteProviderTitle != setting.quoteProvider.title 50 | colorPattern != setting.quoteColorPattern 51 | } 52 | onReset { 53 | quoteProviderTitle = setting.quoteProvider.title 54 | colorPattern = setting.quoteColorPattern 55 | } 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.11.1 4 | 5 | - Fix IntelliJ 2024.2 series compatibility issues 6 | 7 | ## 1.11.0 8 | 9 | - Fix IntelliJ 2023.3 series compatibility issues 10 | 11 | ## 1.10.2 12 | 13 | - Fix compiler warnings 14 | 15 | ## 1.10.1 16 | 17 | - Add A-Share Convertible Bond support 18 | 19 | ## 1.10.0 20 | 21 | - Bring back SINA provider support 22 | 23 | ## 1.9.1 24 | 25 | - Fix three digits price accuracy issue 26 | 27 | ## 1.9.0 28 | 29 | - New management dialog: batch delete & reorder symbols 30 | 31 | ## 1.8.1 32 | 33 | - Fix compatibility issue 34 | 35 | ## 1.8.0 36 | 37 | - Support JetBrains 2022 EAP 38 | 39 | ## 1.7.0 40 | 41 | - Replace Sina API with Tencent API due to Sina API is closed 42 | - Crypto support is temporary removed since Sina API is no longer available 43 | 44 | ## 1.6.1 45 | 46 | - Support JetBrains 2021.3 series 47 | 48 | ## 1.6.0 49 | 50 | - Enhanced setting window UI 51 | - Enhanced search dialog UI 52 | - Enhanced management dialog UI 53 | 54 | ## 1.5.3 55 | 56 | - Fixed multiple projects compatibility [#12](https://github.com/WhiteVermouth/intellij-investor-dashboard/issues/12) 57 | - Fixed API compatibility 58 | 59 | ## 1.5.2 60 | 61 | - Support IntelliJ 2021.2 EAP 62 | 63 | ## 1.5.1 64 | 65 | - Fixed price accuracy [#11](https://github.com/WhiteVermouth/intellij-investor-dashboard/issues/11) 66 | 67 | ## 1.5.0 68 | 69 | - New action: Stop refresh 70 | - New pane: Crypto 71 | - Deprecated: Tencent API 72 | 73 | ## 1.4.4 74 | 75 | - Fix Long stock name wrapping 76 | - Fix search bar text change event 77 | 78 | ## 1.4.3 79 | 80 | - Fixed Android Studio compatibility 81 | - Fixed missed ETF in search results 82 | 83 | ## 1.4.2 84 | 85 | - Fix compatibility issue 86 | 87 | ## 1.4.1 88 | 89 | - Enhanced stock management dialogs 90 | 91 | ## 1.4.0 92 | 93 | - New Stock Add Dialog 94 | - New Stock Delete Dialog 95 | - Some enhancement and bug fix 96 | 97 | ## 1.3.7 98 | 99 | - Support JetBrains 2019 series 100 | 101 | ## 1.3.6 102 | 103 | - Add backward compatibility until 2020.1 104 | 105 | ## 1.3.5 106 | 107 | - Fixed compatibility issue 108 | 109 | ## 1.3.4 110 | 111 | - Support disable Red/Green color pattern 112 | 113 | ## 1.3.3 114 | 115 | - Bug fix 116 | 117 | ## 1.3.2 118 | 119 | - Bug fix 120 | 121 | ## 1.3.1 122 | 123 | - Add right-click popup menu to delete code(s) 124 | 125 | ## 1.3.0 126 | 127 | - Add index view 128 | 129 | ## 1.2.1 130 | 131 | - Enhanced UI 132 | - Bug fix 133 | 134 | ## 1.2.0 135 | 136 | - Add a tab: ALL 137 | - Enhanced UI 138 | 139 | ## 1.1.0 140 | 141 | - Adopt more distinct colors 142 | - Improve Last Update At datetime 143 | - Add a new quote provider: Tencent 144 | 145 | ## 1.0.0 146 | 147 | - Stocker: a stock quote dashboard 148 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.vermouthx.intellij-investor-dashboard 3 | Stocker 4 | Zihan Ma 5 | 6 | com.intellij.modules.platform 7 | 8 | 9 | 10 | 11 | 13 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 28 | 31 | 32 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/utils/StockerActionUtil.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.utils; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.ui.Messages; 6 | import com.intellij.util.messages.MessageBus; 7 | import com.vermouthx.stocker.entities.StockerSuggestion; 8 | import com.vermouthx.stocker.enums.StockerMarketType; 9 | import com.vermouthx.stocker.listeners.StockerQuoteDeleteNotifier; 10 | import com.vermouthx.stocker.settings.StockerSetting; 11 | 12 | public class StockerActionUtil { 13 | public static boolean addStock(StockerMarketType market, StockerSuggestion suggest, Project project) { 14 | StockerSetting setting = StockerSetting.Companion.getInstance(); 15 | String code = suggest.getCode(); 16 | String fullName = suggest.getName(); 17 | if (!setting.containsCode(code)) { 18 | if (StockerQuoteHttpUtil.INSTANCE.validateCode(market, setting.getQuoteProvider(), code)) { 19 | switch (market) { 20 | case AShare: 21 | return setting.getAShareList().add(code); 22 | case HKStocks: 23 | return setting.getHkStocksList().add(code); 24 | case USStocks: 25 | return setting.getUsStocksList().add(code); 26 | case Crypto: 27 | return setting.getCryptoList().add(code); 28 | } 29 | } else { 30 | String errMessage = fullName + " is not supported."; 31 | String errTitle = "Not Supported Stock"; 32 | Messages.showErrorDialog(project, errMessage, errTitle); 33 | return false; 34 | } 35 | } 36 | return false; 37 | } 38 | 39 | public static boolean removeStock(StockerMarketType market, StockerSuggestion suggest) { 40 | StockerSetting setting = StockerSetting.Companion.getInstance(); 41 | MessageBus messageBus = ApplicationManager.getApplication().getMessageBus(); 42 | setting.removeCode(market, suggest.getCode()); 43 | StockerQuoteDeleteNotifier publisher = null; 44 | switch (market) { 45 | case AShare: 46 | publisher = messageBus.syncPublisher(StockerQuoteDeleteNotifier.STOCK_CN_QUOTE_DELETE_TOPIC); 47 | break; 48 | case HKStocks: 49 | publisher = messageBus.syncPublisher(StockerQuoteDeleteNotifier.STOCK_HK_QUOTE_DELETE_TOPIC); 50 | break; 51 | case USStocks: 52 | publisher = messageBus.syncPublisher(StockerQuoteDeleteNotifier.STOCK_US_QUOTE_DELETE_TOPIC); 53 | break; 54 | case Crypto: 55 | publisher = messageBus.syncPublisher(StockerQuoteDeleteNotifier.CRYPTO_QUOTE_DELETE_TOPIC); 56 | 57 | } 58 | StockerQuoteDeleteNotifier publisherToAll = messageBus.syncPublisher(StockerQuoteDeleteNotifier.STOCK_ALL_QUOTE_DELETE_TOPIC); 59 | if (publisher != null) { 60 | publisherToAll.after(suggest.getCode()); 61 | publisher.after(suggest.getCode()); 62 | return true; 63 | } 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/settings/StockerSetting.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.settings 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.PersistentStateComponent 5 | import com.intellij.openapi.components.State 6 | import com.intellij.openapi.components.Storage 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.vermouthx.stocker.enums.StockerMarketType 9 | import com.vermouthx.stocker.enums.StockerQuoteColorPattern 10 | import com.vermouthx.stocker.enums.StockerQuoteProvider 11 | 12 | @State(name = "Stocker", storages = [Storage("stocker-config.xml")]) 13 | class StockerSetting : PersistentStateComponent { 14 | private var myState = StockerSettingState() 15 | 16 | private val log = Logger.getInstance(javaClass) 17 | 18 | companion object { 19 | val instance: StockerSetting 20 | get() = ApplicationManager.getApplication().getService(StockerSetting::class.java) 21 | } 22 | 23 | var version: String 24 | get() = myState.version 25 | set(value) { 26 | myState.version = value 27 | log.info("Stocker updated to $value") 28 | } 29 | 30 | var quoteProvider: StockerQuoteProvider 31 | get() = myState.quoteProvider 32 | set(value) { 33 | myState.quoteProvider = value 34 | log.info("Stocker quote provider switched to ${value.title}") 35 | } 36 | 37 | var quoteColorPattern: StockerQuoteColorPattern 38 | get() = myState.quoteColorPattern 39 | set(value) { 40 | myState.quoteColorPattern = value 41 | log.info("Stocker quote color pattern switched to ${value.title}") 42 | } 43 | 44 | var refreshInterval: Long 45 | get() = myState.refreshInterval 46 | set(value) { 47 | myState.refreshInterval = value 48 | log.info("Stocker refresh interval set to $value") 49 | } 50 | 51 | var aShareList: MutableList 52 | get() = myState.aShareList 53 | set(value) { 54 | myState.aShareList = value 55 | } 56 | 57 | var hkStocksList: MutableList 58 | get() = myState.hkStocksList 59 | set(value) { 60 | myState.hkStocksList = value 61 | } 62 | 63 | var usStocksList: MutableList 64 | get() = myState.usStocksList 65 | set(value) { 66 | myState.usStocksList = value 67 | } 68 | 69 | var cryptoList: MutableList 70 | get() = myState.cryptoList 71 | set(value) { 72 | myState.cryptoList = value 73 | } 74 | 75 | val allStockListSize: Int 76 | get() = aShareList.size + hkStocksList.size + usStocksList.size + cryptoList.size 77 | 78 | fun containsCode(code: String): Boolean { 79 | return aShareList.contains(code) || 80 | hkStocksList.contains(code) || 81 | usStocksList.contains(code) || 82 | cryptoList.contains(code) 83 | } 84 | 85 | fun marketOf(code: String): StockerMarketType? { 86 | if (aShareList.contains(code)) { 87 | return StockerMarketType.AShare 88 | } 89 | if (hkStocksList.contains(code)) { 90 | return StockerMarketType.HKStocks 91 | } 92 | if (usStocksList.contains(code)) { 93 | return StockerMarketType.USStocks 94 | } 95 | if (cryptoList.contains(code)) { 96 | return StockerMarketType.Crypto 97 | } 98 | return null 99 | } 100 | 101 | fun removeCode(market: StockerMarketType, code: String) { 102 | when (market) { 103 | StockerMarketType.AShare -> { 104 | synchronized(aShareList) { 105 | aShareList.remove(code) 106 | } 107 | } 108 | 109 | StockerMarketType.HKStocks -> { 110 | synchronized(hkStocksList) { 111 | hkStocksList.remove(code) 112 | } 113 | } 114 | 115 | StockerMarketType.USStocks -> { 116 | synchronized(usStocksList) { 117 | usStocksList.remove(code) 118 | } 119 | } 120 | 121 | StockerMarketType.Crypto -> { 122 | synchronized(cryptoList) { 123 | cryptoList.remove(code) 124 | } 125 | } 126 | } 127 | } 128 | 129 | override fun getState(): StockerSettingState { 130 | return myState 131 | } 132 | 133 | override fun loadState(state: StockerSettingState) { 134 | myState = state 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/utils/StockerQuoteHttpUtil.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.utils 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.vermouthx.stocker.entities.StockerQuote 5 | import com.vermouthx.stocker.enums.StockerMarketType 6 | import com.vermouthx.stocker.enums.StockerQuoteProvider 7 | import org.apache.http.client.config.RequestConfig 8 | import org.apache.http.client.methods.HttpGet 9 | import org.apache.http.impl.client.HttpClients 10 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager 11 | import org.apache.http.util.EntityUtils 12 | 13 | object StockerQuoteHttpUtil { 14 | 15 | private val log = Logger.getInstance(javaClass) 16 | 17 | private val httpClientPool = run { 18 | val connectionManager = PoolingHttpClientConnectionManager() 19 | connectionManager.maxTotal = 20 20 | val requestConfig = RequestConfig.custom().build() 21 | HttpClients.custom().setConnectionManager(connectionManager).setDefaultRequestConfig(requestConfig) 22 | .useSystemProperties().build() 23 | } 24 | 25 | fun get( 26 | marketType: StockerMarketType, quoteProvider: StockerQuoteProvider, codes: List 27 | ): List { 28 | if (codes.isEmpty()) { 29 | return emptyList() 30 | } 31 | val codesParam = when (quoteProvider) { 32 | StockerQuoteProvider.SINA -> { 33 | if (marketType == StockerMarketType.HKStocks) { 34 | codes.joinToString(",") { code -> 35 | "${quoteProvider.providerPrefixMap[marketType]}${code.uppercase()}" 36 | } 37 | } else { 38 | codes.joinToString(",") { code -> 39 | "${quoteProvider.providerPrefixMap[marketType]}${code.lowercase()}" 40 | } 41 | } 42 | } 43 | 44 | StockerQuoteProvider.TENCENT -> { 45 | if (marketType == StockerMarketType.HKStocks || marketType == StockerMarketType.USStocks) { 46 | codes.joinToString(",") { code -> 47 | "${quoteProvider.providerPrefixMap[marketType]}${code.uppercase()}" 48 | } 49 | } else { 50 | codes.joinToString(",") { code -> 51 | "${quoteProvider.providerPrefixMap[marketType]}${code.lowercase()}" 52 | } 53 | } 54 | } 55 | } 56 | 57 | val url = "${quoteProvider.host}${codesParam}" 58 | val httpGet = HttpGet(url) 59 | if (quoteProvider == StockerQuoteProvider.SINA) { 60 | httpGet.setHeader("Referer", "https://finance.sina.com.cn") // Sina API requires this header 61 | } 62 | return try { 63 | val response = httpClientPool.execute(httpGet) 64 | val responseText = EntityUtils.toString(response.entity, "UTF-8") 65 | StockerQuoteParser.parseQuoteResponse(quoteProvider, marketType, responseText) 66 | } catch (e: Exception) { 67 | log.warn(e) 68 | emptyList() 69 | } 70 | } 71 | 72 | fun validateCode( 73 | marketType: StockerMarketType, quoteProvider: StockerQuoteProvider, code: String 74 | ): Boolean { 75 | when (quoteProvider) { 76 | StockerQuoteProvider.SINA -> { 77 | val url = if (marketType == StockerMarketType.HKStocks) { 78 | "${quoteProvider.host}${quoteProvider.providerPrefixMap[marketType]}${code.uppercase()}" 79 | } else { 80 | "${quoteProvider.host}${quoteProvider.providerPrefixMap[marketType]}${code.lowercase()}" 81 | } 82 | val httpGet = HttpGet(url) 83 | httpGet.setHeader("Referer", "https://finance.sina.com.cn") // Sina API requires this header 84 | val response = httpClientPool.execute(httpGet) 85 | val responseText = EntityUtils.toString(response.entity, "UTF-8") 86 | val firstLine = responseText.split("\n")[0] 87 | val start = firstLine.indexOfFirst { c -> c == '"' } + 1 88 | val end = firstLine.indexOfLast { c -> c == '"' } 89 | if (start == end) { 90 | return false 91 | } 92 | return firstLine.subSequence(start, end).contains(",") 93 | } 94 | 95 | StockerQuoteProvider.TENCENT -> { 96 | val url = if (marketType == StockerMarketType.HKStocks || marketType == StockerMarketType.USStocks) { 97 | "${quoteProvider.host}${quoteProvider.providerPrefixMap[marketType]}${code.uppercase()}" 98 | } else { 99 | "${quoteProvider.host}${quoteProvider.providerPrefixMap[marketType]}${code.lowercase()}" 100 | } 101 | val httpGet = HttpGet(url) 102 | val response = httpClientPool.execute(httpGet) 103 | val responseText = EntityUtils.toString(response.entity, "UTF-8") 104 | return !responseText.startsWith("v_pv_none_match") 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/views/dialogs/StockerSuggestionDialog.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.views.dialogs 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.ui.DialogPanel 5 | import com.intellij.openapi.ui.DialogWrapper 6 | import com.intellij.ui.DocumentAdapter 7 | import com.intellij.ui.SearchTextField 8 | import com.intellij.ui.components.JBScrollPane 9 | import com.intellij.ui.dsl.builder.AlignX 10 | import com.intellij.ui.dsl.builder.panel 11 | import com.vermouthx.stocker.StockerAppManager 12 | import com.vermouthx.stocker.entities.StockerSuggestion 13 | import com.vermouthx.stocker.enums.StockerStockOperation 14 | import com.vermouthx.stocker.settings.StockerSetting 15 | import com.vermouthx.stocker.utils.StockerActionUtil 16 | import com.vermouthx.stocker.utils.StockerSuggestHttpUtil 17 | import java.awt.BorderLayout 18 | import java.awt.Dimension 19 | import java.util.concurrent.Executors 20 | import javax.swing.Action 21 | import javax.swing.BorderFactory 22 | import javax.swing.JButton 23 | import javax.swing.event.DocumentEvent 24 | 25 | class StockerSuggestionDialog(val project: Project?) : DialogWrapper(project) { 26 | 27 | private val service = Executors.newFixedThreadPool(1) 28 | private val setting = StockerSetting.instance 29 | 30 | private var suggestions: List = emptyList() 31 | 32 | init { 33 | title = "Search Stocks" 34 | init() 35 | } 36 | 37 | override fun createCenterPanel(): DialogPanel { 38 | val dialogPanel = DialogPanel(BorderLayout()) 39 | val searchTextField = SearchTextField(true) 40 | val scrollPane = JBScrollPane() 41 | 42 | searchTextField.addDocumentListener(object : DocumentAdapter() { 43 | override fun textChanged(e: DocumentEvent) { 44 | service.submit { 45 | val text = searchTextField.text.trim() 46 | if (text.isNotEmpty()) { 47 | suggestions = StockerSuggestHttpUtil.suggest(text, setting.quoteProvider) 48 | refreshScrollPane(scrollPane) 49 | } 50 | } 51 | } 52 | }) 53 | 54 | suggestions = StockerSuggestHttpUtil.suggest("600", setting.quoteProvider) 55 | refreshScrollPane(scrollPane) 56 | 57 | searchTextField.border = BorderFactory.createEmptyBorder(0, 0, 8, 0) 58 | dialogPanel.add(searchTextField, BorderLayout.NORTH) 59 | dialogPanel.add(scrollPane, BorderLayout.CENTER) 60 | dialogPanel.preferredSize = Dimension(300, 500) 61 | return dialogPanel 62 | } 63 | 64 | override fun createActions(): Array { 65 | return emptyArray() 66 | } 67 | 68 | private fun refreshScrollPane(scrollPane: JBScrollPane) { 69 | scrollPane.setViewportView( 70 | panel { 71 | suggestions.forEach { suggestion -> 72 | val actionButton = JButton() 73 | row { 74 | label(suggestion.code) 75 | label( 76 | if (suggestion.name.length <= 20) { 77 | suggestion.name 78 | } else { 79 | "${suggestion.name.substring(0, 20)}..." 80 | } 81 | ) 82 | if (StockerSetting.instance.containsCode(suggestion.code)) { 83 | actionButton.text = StockerStockOperation.STOCK_DELETE.operation 84 | } else { 85 | actionButton.text = StockerStockOperation.STOCK_ADD.operation 86 | } 87 | actionButton.addActionListener { 88 | val myApplication = StockerAppManager.myApplication(project) 89 | if (myApplication != null) { 90 | myApplication.shutdownThenClear() 91 | when (StockerStockOperation.mapOf(actionButton.text)) { 92 | StockerStockOperation.STOCK_ADD -> { 93 | StockerActionUtil.addStock(suggestion.market, suggestion, project) 94 | actionButton.text = StockerStockOperation.STOCK_DELETE.operation 95 | } 96 | 97 | StockerStockOperation.STOCK_DELETE -> { 98 | StockerActionUtil.removeStock(suggestion.market, suggestion) 99 | actionButton.text = StockerStockOperation.STOCK_ADD.operation 100 | } 101 | 102 | else -> { 103 | myApplication.schedule() 104 | return@addActionListener 105 | } 106 | } 107 | myApplication.schedule() 108 | } 109 | } 110 | cell(actionButton).align(AlignX.RIGHT) 111 | } 112 | separator() 113 | } 114 | }.withBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)) 115 | ) 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/utils/StockerSuggestHttpUtil.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.utils 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.vermouthx.stocker.entities.StockerSuggestion 5 | import com.vermouthx.stocker.enums.StockerMarketType 6 | import com.vermouthx.stocker.enums.StockerQuoteProvider 7 | import org.apache.commons.text.StringEscapeUtils 8 | import org.apache.http.client.config.RequestConfig 9 | import org.apache.http.client.methods.HttpGet 10 | import org.apache.http.impl.client.HttpClients 11 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager 12 | import org.apache.http.util.EntityUtils 13 | 14 | object StockerSuggestHttpUtil { 15 | 16 | private val log = Logger.getInstance(javaClass) 17 | 18 | private val httpClientPool = run { 19 | val connectionManager = PoolingHttpClientConnectionManager() 20 | connectionManager.maxTotal = 20 21 | val requestConfig = RequestConfig.custom().build() 22 | HttpClients.custom().setConnectionManager(connectionManager).setDefaultRequestConfig(requestConfig) 23 | .useSystemProperties().build() 24 | } 25 | 26 | fun suggest(key: String, provider: StockerQuoteProvider): List { 27 | val url = "${provider.suggestHost}$key" 28 | val httpGet = HttpGet(url) 29 | if (provider == StockerQuoteProvider.SINA) { 30 | httpGet.setHeader("Referer", "https://finance.sina.com.cn") // Sina API requires this header 31 | } 32 | return try { 33 | val response = httpClientPool.execute(httpGet) 34 | when (provider) { 35 | StockerQuoteProvider.SINA -> { 36 | val responseText = EntityUtils.toString(response.entity, "UTF-8") 37 | parseSinaSuggestion(responseText) 38 | } 39 | 40 | StockerQuoteProvider.TENCENT -> { 41 | val responseText = EntityUtils.toString(response.entity, "UTF-8") 42 | parseTencentSuggestion(responseText) 43 | } 44 | 45 | } 46 | } catch (e: Exception) { 47 | log.warn(e) 48 | emptyList() 49 | } 50 | } 51 | 52 | private fun parseSinaSuggestion(responseText: String): List { 53 | val result = mutableListOf() 54 | val regex = Regex("var suggestvalue=\"(.*?)\";") 55 | val matchResult = regex.find(responseText) 56 | val (_, snippetsText) = matchResult!!.groupValues 57 | if (snippetsText.isEmpty()) { 58 | return emptyList() 59 | } 60 | val snippets = snippetsText.split(";") 61 | for (snippet in snippets) { 62 | val columns = snippet.split(",") 63 | if (columns.size < 5) { 64 | continue 65 | } 66 | when (columns[1]) { 67 | "11" -> { 68 | if (columns[4].startsWith("S*ST")) { 69 | continue 70 | } 71 | result.add(StockerSuggestion(columns[3].uppercase(), columns[4], StockerMarketType.AShare)) 72 | } 73 | 74 | "22" -> { 75 | val code = columns[3].replace("of", "") 76 | when { 77 | code.startsWith("15") || code.startsWith("16") || code.startsWith("18") -> result.add( 78 | StockerSuggestion("SZ$code", columns[4], StockerMarketType.AShare) 79 | ) 80 | 81 | code.startsWith("50") || code.startsWith("51") -> result.add( 82 | StockerSuggestion( 83 | "SH$code", columns[4], StockerMarketType.AShare 84 | ) 85 | ) 86 | } 87 | } 88 | 89 | "31" -> result.add(StockerSuggestion(columns[3].uppercase(), columns[4], StockerMarketType.HKStocks)) 90 | "41" -> result.add(StockerSuggestion(columns[3].uppercase(), columns[4], StockerMarketType.USStocks)) 91 | "71" -> result.add(StockerSuggestion(columns[3].uppercase(), columns[4], StockerMarketType.Crypto)) 92 | "81" -> result.add(StockerSuggestion(columns[3].uppercase(), columns[4], StockerMarketType.AShare)) 93 | } 94 | } 95 | return result 96 | } 97 | 98 | private fun parseTencentSuggestion(responseText: String): List { 99 | if (responseText.isEmpty()) { 100 | return emptyList() 101 | } 102 | val result = mutableListOf() 103 | val snippets = responseText.replace("v_hint=\"", "").replace("\"", "").split("^") 104 | for (snippet in snippets) { 105 | val columns = snippet.split("~") 106 | if (columns.size < 3) { 107 | continue 108 | } 109 | val type = columns[0] 110 | val code = columns[1] 111 | val rawName = columns[2] 112 | val name = StringEscapeUtils.unescapeJava(rawName) 113 | when (type) { 114 | "sz", "sh" -> result.add(StockerSuggestion(type.uppercase() + code, name, StockerMarketType.AShare)) 115 | 116 | "hk" -> result.add(StockerSuggestion(code, name, StockerMarketType.HKStocks)) 117 | 118 | "us" -> result.add(StockerSuggestion(code.split(".")[0].uppercase(), name, StockerMarketType.USStocks)) 119 | } 120 | } 121 | return result 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/views/dialogs/StockerManagementDialog.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.views.dialogs 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.ui.DialogPanel 5 | import com.intellij.openapi.ui.DialogWrapper 6 | import com.intellij.ui.ToolbarDecorator 7 | import com.intellij.ui.components.JBList 8 | import com.intellij.ui.components.JBScrollPane 9 | import com.intellij.ui.components.JBTabbedPane 10 | import com.intellij.ui.dsl.builder.AlignX 11 | import com.intellij.ui.dsl.builder.AlignY 12 | import com.intellij.ui.dsl.builder.panel 13 | import com.vermouthx.stocker.StockerAppManager 14 | import com.vermouthx.stocker.entities.StockerQuote 15 | import com.vermouthx.stocker.enums.StockerMarketType 16 | import com.vermouthx.stocker.settings.StockerSetting 17 | import com.vermouthx.stocker.utils.StockerQuoteHttpUtil 18 | import java.awt.BorderLayout 19 | import java.awt.event.ActionEvent 20 | import javax.swing.* 21 | 22 | class StockerManagementDialog(val project: Project?) : DialogWrapper(project) { 23 | 24 | private val setting = StockerSetting.instance 25 | 26 | private val tabMap: MutableMap = mutableMapOf() 27 | 28 | private val currentSymbols: MutableMap> = mutableMapOf() 29 | 30 | private var currentMarketSelection: StockerMarketType = StockerMarketType.AShare 31 | 32 | init { 33 | title = "Manage Favorite Stocks" 34 | init() 35 | } 36 | 37 | override fun createCenterPanel(): DialogPanel { 38 | val tabbedPane = JBTabbedPane() 39 | tabbedPane.add("CN", createTabContent(0)) 40 | tabbedPane.add("HK", createTabContent(1)) 41 | tabbedPane.add("US", createTabContent(2)) 42 | // tabbedPane.add("Crypto", createTabContent(3)) 43 | tabbedPane.addChangeListener { 44 | currentMarketSelection = when (tabbedPane.selectedIndex) { 45 | 0 -> { 46 | StockerMarketType.AShare 47 | } 48 | 49 | 1 -> { 50 | StockerMarketType.HKStocks 51 | } 52 | 53 | 2 -> { 54 | StockerMarketType.USStocks 55 | } 56 | // 3 -> { 57 | // StockerMarketType.Crypto 58 | // } 59 | else -> return@addChangeListener 60 | } 61 | } 62 | 63 | val aShareListModel = DefaultListModel() 64 | aShareListModel.addAll( 65 | StockerQuoteHttpUtil.get( 66 | StockerMarketType.AShare, setting.quoteProvider, setting.aShareList 67 | ) 68 | ) 69 | currentSymbols[StockerMarketType.AShare] = aShareListModel 70 | tabMap[0]?.let { pane -> 71 | renderTabPane(pane, aShareListModel) 72 | } 73 | 74 | val hkStocksListModel = DefaultListModel() 75 | hkStocksListModel.addAll( 76 | StockerQuoteHttpUtil.get( 77 | StockerMarketType.HKStocks, setting.quoteProvider, setting.hkStocksList 78 | ) 79 | ) 80 | currentSymbols[StockerMarketType.HKStocks] = hkStocksListModel 81 | tabMap[1]?.let { pane -> 82 | renderTabPane(pane, hkStocksListModel) 83 | } 84 | 85 | val usStocksListModel = DefaultListModel() 86 | usStocksListModel.addAll( 87 | StockerQuoteHttpUtil.get( 88 | StockerMarketType.USStocks, setting.quoteProvider, setting.usStocksList 89 | ) 90 | ) 91 | currentSymbols[StockerMarketType.USStocks] = usStocksListModel 92 | tabMap[2]?.let { pane -> 93 | renderTabPane(pane, usStocksListModel) 94 | } 95 | 96 | tabbedPane.selectedIndex = 0 97 | return panel { 98 | row { 99 | cell(tabbedPane).align(AlignX.FILL) 100 | } 101 | }.withPreferredWidth(300) 102 | } 103 | 104 | override fun createActions(): Array { 105 | return arrayOf( 106 | object : OkAction() { 107 | override fun actionPerformed(e: ActionEvent?) { 108 | val myApplication = StockerAppManager.myApplication(project) 109 | if (myApplication != null) { 110 | myApplication.shutdownThenClear() 111 | currentSymbols[StockerMarketType.AShare]?.let { symbols -> 112 | setting.aShareList = symbols.elements().asSequence().map { it.code }.toMutableList() 113 | } 114 | currentSymbols[StockerMarketType.HKStocks]?.let { symbols -> 115 | setting.hkStocksList = symbols.elements().asSequence().map { it.code }.toMutableList() 116 | } 117 | currentSymbols[StockerMarketType.USStocks]?.let { symbols -> 118 | setting.usStocksList = symbols.elements().asSequence().map { it.code }.toMutableList() 119 | } 120 | myApplication.schedule() 121 | } 122 | super.actionPerformed(e) 123 | } 124 | }, cancelAction 125 | ) 126 | } 127 | 128 | private fun createTabContent(index: Int): JComponent { 129 | val pane = JPanel(BorderLayout()) 130 | tabMap[index] = pane 131 | return panel { 132 | row { 133 | cell(pane).align(AlignX.FILL).align(AlignY.FILL) 134 | } 135 | } 136 | } 137 | 138 | private fun renderTabPane(pane: JPanel, listModel: DefaultListModel) { 139 | val list = JBList(listModel) 140 | val decorator = ToolbarDecorator.createDecorator(list) 141 | val toolbarPane = decorator.createPanel() 142 | list.installCellRenderer { symbol -> 143 | panel { 144 | row { 145 | label(symbol.code).align(AlignX.LEFT) 146 | label( 147 | if (symbol.name.length <= 20) { 148 | symbol.name 149 | } else { 150 | "${symbol.name.substring(0, 20)}..." 151 | } 152 | ).align(AlignX.CENTER) 153 | } 154 | }.withBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16)) 155 | } 156 | val scrollPane = JBScrollPane(list) 157 | pane.add(toolbarPane, BorderLayout.NORTH) 158 | pane.add(scrollPane, BorderLayout.CENTER) 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/StockerApp.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.vermouthx.stocker.enums.StockerMarketIndex 5 | import com.vermouthx.stocker.enums.StockerMarketType 6 | import com.vermouthx.stocker.listeners.StockerQuoteReloadNotifier.* 7 | import com.vermouthx.stocker.listeners.StockerQuoteUpdateNotifier.* 8 | import com.vermouthx.stocker.settings.StockerSetting 9 | import com.vermouthx.stocker.utils.StockerQuoteHttpUtil 10 | import java.util.concurrent.Executors 11 | import java.util.concurrent.ScheduledExecutorService 12 | import java.util.concurrent.TimeUnit 13 | 14 | class StockerApp { 15 | 16 | private val setting = StockerSetting.instance 17 | private val messageBus = ApplicationManager.getApplication().messageBus 18 | 19 | private var scheduledExecutorService: ScheduledExecutorService = Executors.newScheduledThreadPool(4) 20 | 21 | private var scheduleInitialDelay: Long = 3 22 | private val schedulePeriod: Long = StockerSetting.instance.refreshInterval 23 | 24 | fun schedule() { 25 | if (scheduledExecutorService.isShutdown) { 26 | scheduledExecutorService = Executors.newScheduledThreadPool(4) 27 | scheduleInitialDelay = 0 28 | } 29 | scheduledExecutorService.scheduleAtFixedRate( 30 | createQuoteUpdateThread(StockerMarketType.AShare, setting.aShareList), 31 | scheduleInitialDelay, 32 | schedulePeriod, 33 | TimeUnit.SECONDS 34 | ) 35 | scheduledExecutorService.scheduleAtFixedRate( 36 | createQuoteUpdateThread(StockerMarketType.HKStocks, setting.hkStocksList), 37 | scheduleInitialDelay, 38 | schedulePeriod, 39 | TimeUnit.SECONDS 40 | ) 41 | scheduledExecutorService.scheduleAtFixedRate( 42 | createQuoteUpdateThread(StockerMarketType.USStocks, setting.usStocksList), 43 | scheduleInitialDelay, 44 | schedulePeriod, 45 | TimeUnit.SECONDS 46 | ) 47 | // scheduledExecutorService.scheduleAtFixedRate( 48 | // createQuoteUpdateThread(StockerMarketType.Crypto, setting.cryptoList), 49 | // scheduleInitialDelay, schedulePeriod, TimeUnit.SECONDS 50 | // ) 51 | scheduledExecutorService.scheduleAtFixedRate( 52 | createAllQuoteUpdateThread(), scheduleInitialDelay, schedulePeriod, TimeUnit.SECONDS 53 | ) 54 | } 55 | 56 | fun shutdown() { 57 | scheduledExecutorService.shutdown() 58 | } 59 | 60 | fun isShutdown(): Boolean { 61 | return scheduledExecutorService.isShutdown 62 | } 63 | 64 | private fun clear() { 65 | messageBus.syncPublisher(STOCK_ALL_QUOTE_RELOAD_TOPIC).clear() 66 | messageBus.syncPublisher(STOCK_CN_QUOTE_RELOAD_TOPIC).clear() 67 | messageBus.syncPublisher(STOCK_HK_QUOTE_RELOAD_TOPIC).clear() 68 | messageBus.syncPublisher(STOCK_US_QUOTE_RELOAD_TOPIC).clear() 69 | } 70 | 71 | fun shutdownThenClear() { 72 | shutdown() 73 | clear() 74 | } 75 | 76 | private fun createAllQuoteUpdateThread(): Runnable { 77 | return Runnable { 78 | val quoteProvider = setting.quoteProvider 79 | val allStockQuotes = listOf( 80 | StockerQuoteHttpUtil.get(StockerMarketType.AShare, quoteProvider, setting.aShareList), 81 | StockerQuoteHttpUtil.get(StockerMarketType.HKStocks, quoteProvider, setting.hkStocksList), 82 | StockerQuoteHttpUtil.get(StockerMarketType.USStocks, quoteProvider, setting.usStocksList), 83 | // StockerQuoteHttpUtil.get(StockerMarketType.Crypto, quoteProvider, setting.cryptoList) 84 | ).flatten() 85 | val allStockIndices = listOf( 86 | StockerQuoteHttpUtil.get(StockerMarketType.AShare, quoteProvider, StockerMarketIndex.CN.codes), 87 | StockerQuoteHttpUtil.get(StockerMarketType.HKStocks, quoteProvider, StockerMarketIndex.HK.codes), 88 | StockerQuoteHttpUtil.get(StockerMarketType.USStocks, quoteProvider, StockerMarketIndex.US.codes), 89 | // StockerQuoteHttpUtil.get(StockerMarketType.Crypto, quoteProvider, StockerMarketIndex.Crypto.codes) 90 | ).flatten() 91 | val publisher = messageBus.syncPublisher(STOCK_ALL_QUOTE_UPDATE_TOPIC) 92 | publisher.syncQuotes(allStockQuotes, setting.allStockListSize) 93 | publisher.syncIndices(allStockIndices) 94 | } 95 | } 96 | 97 | private fun createQuoteUpdateThread(marketType: StockerMarketType, stockCodeList: List): Runnable { 98 | return Runnable { 99 | refresh(marketType, stockCodeList) 100 | } 101 | } 102 | 103 | private fun refresh( 104 | marketType: StockerMarketType, stockCodeList: List 105 | ) { 106 | val quoteProvider = setting.quoteProvider 107 | val size = stockCodeList.size 108 | when (marketType) { 109 | StockerMarketType.AShare -> { 110 | val quotes = StockerQuoteHttpUtil.get(marketType, quoteProvider, stockCodeList) 111 | val indices = StockerQuoteHttpUtil.get(marketType, quoteProvider, StockerMarketIndex.CN.codes) 112 | val publisher = messageBus.syncPublisher(STOCK_CN_QUOTE_UPDATE_TOPIC) 113 | publisher.syncQuotes(quotes, size) 114 | publisher.syncIndices(indices) 115 | } 116 | 117 | StockerMarketType.HKStocks -> { 118 | val quotes = StockerQuoteHttpUtil.get(marketType, quoteProvider, stockCodeList) 119 | val indices = StockerQuoteHttpUtil.get(marketType, quoteProvider, StockerMarketIndex.HK.codes) 120 | val publisher = messageBus.syncPublisher(STOCK_HK_QUOTE_UPDATE_TOPIC) 121 | publisher.syncQuotes(quotes, size) 122 | publisher.syncIndices(indices) 123 | } 124 | 125 | StockerMarketType.USStocks -> { 126 | val quotes = StockerQuoteHttpUtil.get(marketType, quoteProvider, stockCodeList) 127 | val indices = StockerQuoteHttpUtil.get(marketType, quoteProvider, StockerMarketIndex.US.codes) 128 | val publisher = messageBus.syncPublisher(STOCK_US_QUOTE_UPDATE_TOPIC) 129 | publisher.syncQuotes(quotes, size) 130 | publisher.syncIndices(indices) 131 | } 132 | 133 | StockerMarketType.Crypto -> { 134 | val quotes = StockerQuoteHttpUtil.get(marketType, quoteProvider, stockCodeList) 135 | val indices = StockerQuoteHttpUtil.get(marketType, quoteProvider, StockerMarketIndex.Crypto.codes) 136 | val publisher = messageBus.syncPublisher(CRYPTO_QUOTE_UPDATE_TOPIC) 137 | publisher.syncQuotes(quotes, size) 138 | publisher.syncIndices(indices) 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/vermouthx/stocker/views/StockerTableView.java: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.views; 2 | 3 | import com.intellij.openapi.ui.ComboBox; 4 | import com.intellij.ui.JBColor; 5 | import com.intellij.ui.components.JBLabel; 6 | import com.intellij.ui.components.JBScrollPane; 7 | import com.intellij.ui.table.JBTable; 8 | import com.vermouthx.stocker.components.StockerDefaultTableCellRender; 9 | import com.vermouthx.stocker.components.StockerTableHeaderRender; 10 | import com.vermouthx.stocker.components.StockerTableModel; 11 | import com.vermouthx.stocker.entities.StockerQuote; 12 | import com.vermouthx.stocker.settings.StockerSetting; 13 | 14 | import javax.swing.*; 15 | import javax.swing.table.DefaultTableCellRenderer; 16 | import javax.swing.table.DefaultTableModel; 17 | import java.awt.*; 18 | import java.awt.event.MouseAdapter; 19 | import java.awt.event.MouseEvent; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | import java.util.Objects; 24 | 25 | public class StockerTableView { 26 | 27 | private JPanel mPane; 28 | private JScrollPane tbPane; 29 | private Color upColor; 30 | private Color downColor; 31 | private Color zeroColor; 32 | private JBTable tbBody; 33 | private StockerTableModel tbModel; 34 | 35 | private final ComboBox cbIndex = new ComboBox<>(); 36 | private final JBLabel lbIndexValue = new JBLabel("", SwingConstants.CENTER); 37 | private final JBLabel lbIndexExtent = new JBLabel("", SwingConstants.CENTER); 38 | private final JBLabel lbIndexPercent = new JBLabel("", SwingConstants.CENTER); 39 | private List indices = new ArrayList<>(); 40 | 41 | public StockerTableView() { 42 | syncColorPatternSetting(); 43 | initPane(); 44 | initTable(); 45 | } 46 | 47 | public void syncIndices(List indices) { 48 | this.indices = indices; 49 | if (cbIndex.getItemCount() == 0 && !indices.isEmpty()) { 50 | indices.forEach(i -> cbIndex.addItem(i.getName())); 51 | cbIndex.setSelectedIndex(0); 52 | } 53 | syncColorPatternSetting(); 54 | updateIndex(); 55 | } 56 | 57 | private void syncColorPatternSetting() { 58 | StockerSetting setting = StockerSetting.Companion.getInstance(); 59 | switch (setting.getQuoteColorPattern()) { 60 | case RED_UP_GREEN_DOWN: 61 | upColor = JBColor.RED; 62 | downColor = JBColor.GREEN; 63 | zeroColor = JBColor.GRAY; 64 | break; 65 | case GREEN_UP_RED_DOWN: 66 | upColor = JBColor.GREEN; 67 | downColor = JBColor.RED; 68 | zeroColor = JBColor.GRAY; 69 | break; 70 | default: 71 | upColor = JBColor.foreground(); 72 | downColor = JBColor.foreground(); 73 | zeroColor = JBColor.foreground(); 74 | break; 75 | } 76 | } 77 | 78 | private void updateIndex() { 79 | if (cbIndex.getSelectedIndex() != -1) { 80 | String name = Objects.requireNonNull(cbIndex.getSelectedItem()).toString(); 81 | for (StockerQuote index : indices) { 82 | if (index.getName().equals(name)) { 83 | lbIndexValue.setText(Double.toString(index.getCurrent())); 84 | lbIndexExtent.setText(Double.toString(index.getChange())); 85 | lbIndexPercent.setText(index.getPercentage() + "%"); 86 | double value = index.getPercentage(); 87 | if (value > 0) { 88 | lbIndexValue.setForeground(upColor); 89 | lbIndexExtent.setForeground(upColor); 90 | lbIndexPercent.setForeground(upColor); 91 | } else if (value < 0) { 92 | lbIndexValue.setForeground(downColor); 93 | lbIndexExtent.setForeground(downColor); 94 | lbIndexPercent.setForeground(downColor); 95 | } else { 96 | lbIndexValue.setForeground(zeroColor); 97 | lbIndexExtent.setForeground(zeroColor); 98 | lbIndexPercent.setForeground(zeroColor); 99 | } 100 | break; 101 | } 102 | } 103 | } 104 | } 105 | 106 | private void initPane() { 107 | tbPane = new JBScrollPane(); 108 | tbPane.setBorder(BorderFactory.createEmptyBorder()); 109 | JPanel iPane = new JPanel(new GridLayout(1, 4)); 110 | iPane.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border())); 111 | iPane.add(cbIndex); 112 | iPane.add(lbIndexValue); 113 | iPane.add(lbIndexExtent); 114 | iPane.add(lbIndexPercent); 115 | cbIndex.addItemListener(i -> updateIndex()); 116 | mPane = new JPanel(new BorderLayout()); 117 | mPane.add(tbPane, BorderLayout.CENTER); 118 | mPane.add(iPane, BorderLayout.SOUTH); 119 | } 120 | 121 | private static final String codeColumn = "Symbol"; 122 | private static final String nameColumn = "Name"; 123 | private static final String currentColumn = "Current"; 124 | private static final String percentColumn = "Change%"; 125 | 126 | private void initTable() { 127 | tbModel = new StockerTableModel(); 128 | tbBody = new JBTable(); 129 | tbBody.addMouseListener(new MouseAdapter() { 130 | @Override 131 | public void mouseReleased(MouseEvent e) { 132 | int row = tbBody.rowAtPoint(e.getPoint()); 133 | if (row >= 0 && row < tbBody.getRowCount()) { 134 | if (tbBody.getSelectedRows().length == 0 || Arrays.stream(tbBody.getSelectedRows()).noneMatch(p -> p == row)) { 135 | tbBody.setRowSelectionInterval(row, row); 136 | } 137 | } else { 138 | tbBody.clearSelection(); 139 | } 140 | } 141 | }); 142 | tbModel.setColumnIdentifiers(new String[]{codeColumn, nameColumn, currentColumn, percentColumn}); 143 | 144 | tbBody.setShowVerticalLines(false); 145 | tbBody.setModel(tbModel); 146 | 147 | tbBody.getTableHeader().setReorderingAllowed(false); 148 | tbBody.getTableHeader().setDefaultRenderer(new StockerTableHeaderRender(tbBody)); 149 | 150 | tbBody.getColumn(codeColumn).setCellRenderer(new StockerDefaultTableCellRender()); 151 | tbBody.getColumn(nameColumn).setCellRenderer(new StockerDefaultTableCellRender()); 152 | tbBody.getColumn(currentColumn).setCellRenderer(new StockerDefaultTableCellRender() { 153 | @Override 154 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 155 | syncColorPatternSetting(); 156 | setHorizontalAlignment(DefaultTableCellRenderer.CENTER); 157 | String percent = table.getValueAt(row, table.getColumn(percentColumn).getModelIndex()).toString(); 158 | Double v = Double.parseDouble(percent.substring(0, percent.indexOf("%"))); 159 | applyColorPatternToTable(v, this); 160 | return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 161 | } 162 | }); 163 | tbBody.getColumn(percentColumn).setCellRenderer(new StockerDefaultTableCellRender() { 164 | @Override 165 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 166 | syncColorPatternSetting(); 167 | setHorizontalAlignment(DefaultTableCellRenderer.CENTER); 168 | String percent = value.toString(); 169 | Double v = Double.parseDouble(percent.substring(0, percent.indexOf("%"))); 170 | applyColorPatternToTable(v, this); 171 | return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 172 | } 173 | }); 174 | tbPane.add(tbBody); 175 | tbPane.setViewportView(tbBody); 176 | } 177 | 178 | private void applyColorPatternToTable(Double value, DefaultTableCellRenderer renderer) { 179 | if (value > 0) { 180 | renderer.setForeground(upColor); 181 | } else if (value < 0) { 182 | renderer.setForeground(downColor); 183 | } else { 184 | renderer.setForeground(zeroColor); 185 | } 186 | } 187 | 188 | public JComponent getComponent() { 189 | return mPane; 190 | } 191 | 192 | public JBTable getTableBody() { 193 | return tbBody; 194 | } 195 | 196 | public DefaultTableModel getTableModel() { 197 | return tbModel; 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/utils/StockerQuoteParser.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.utils 2 | 3 | import com.vermouthx.stocker.entities.StockerQuote 4 | import com.vermouthx.stocker.enums.StockerMarketType 5 | import com.vermouthx.stocker.enums.StockerQuoteProvider 6 | import java.time.LocalDateTime 7 | import java.time.format.DateTimeFormatter 8 | import kotlin.math.roundToInt 9 | 10 | object StockerQuoteParser { 11 | 12 | private fun Double.twoDigits(): Double { 13 | return (this * 100.0).roundToInt() / 100.0 14 | } 15 | 16 | fun parseQuoteResponse( 17 | provider: StockerQuoteProvider, marketType: StockerMarketType, responseText: String 18 | ): List { 19 | return when (provider) { 20 | StockerQuoteProvider.SINA -> parseSinaQuoteResponse(marketType, responseText) 21 | StockerQuoteProvider.TENCENT -> parseTencentQuoteResponse(marketType, responseText) 22 | } 23 | } 24 | 25 | private fun parseSinaQuoteResponse(marketType: StockerMarketType, responseText: String): List { 26 | val regex = Regex("var hq_str_(\\w+?)=\"(.*?)\";") 27 | return responseText.split("\n").asSequence().filter { text -> text.isNotEmpty() }.map { text -> 28 | val matchResult = regex.find(text) 29 | val (_, code, quote) = matchResult!!.groupValues 30 | "${code},${quote}" 31 | }.map { text -> text.split(",") }.map { textArray -> 32 | when (marketType) { 33 | StockerMarketType.AShare -> { 34 | val code = textArray[0].uppercase() 35 | val name = textArray[1] 36 | val opening = textArray[2].toDouble() 37 | val close = textArray[3].toDouble() 38 | val current = textArray[4].toDouble() 39 | val high = textArray[5].toDouble() 40 | val low = textArray[6].toDouble() 41 | val change = (current - close).twoDigits() 42 | val percentage = ((current - close) / close * 100).twoDigits() 43 | val updateAt = textArray[31] + " " + textArray[32] 44 | StockerQuote( 45 | code = code, 46 | name = name, 47 | current = current, 48 | opening = opening, 49 | close = close, 50 | low = low, 51 | high = high, 52 | change = change, 53 | percentage = percentage, 54 | updateAt = updateAt 55 | ) 56 | } 57 | 58 | StockerMarketType.HKStocks -> { 59 | val code = textArray[0].substring(2).uppercase() 60 | val name = textArray[2] 61 | val opening = textArray[3].toDouble() 62 | val close = textArray[4].toDouble() 63 | val high = textArray[5].toDouble() 64 | val low = textArray[6].toDouble() 65 | val current = textArray[7].toDouble() 66 | val change = (current - close).twoDigits() 67 | val percentage = textArray[9].toDouble().twoDigits() 68 | val sourceFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm") 69 | val targetFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 70 | val datetime = LocalDateTime.parse(textArray[18] + " " + textArray[19], sourceFormatter) 71 | val updateAt = targetFormatter.format(datetime) 72 | StockerQuote( 73 | code = code, 74 | name = name, 75 | current = current, 76 | opening = opening, 77 | close = close, 78 | low = low, 79 | high = high, 80 | change = change, 81 | percentage = percentage, 82 | updateAt = updateAt 83 | ) 84 | } 85 | 86 | StockerMarketType.USStocks -> { 87 | val code = textArray[0].substring(3).uppercase() 88 | val name = textArray[1] 89 | val current = textArray[2].toDouble() 90 | val updateAt = textArray[4] 91 | val opening = textArray[6].toDouble() 92 | val high = textArray[7].toDouble() 93 | val low = textArray[8].toDouble() 94 | val close = textArray[27].toDouble() 95 | val change = (current - close).twoDigits() 96 | val percentage = textArray[3].toDouble().twoDigits() 97 | StockerQuote( 98 | code = code, 99 | name = name, 100 | current = current, 101 | opening = opening, 102 | close = close, 103 | low = low, 104 | high = high, 105 | change = change, 106 | percentage = percentage, 107 | updateAt = updateAt 108 | ) 109 | } 110 | 111 | StockerMarketType.Crypto -> { 112 | val code = textArray[0].substring(4).uppercase() 113 | val name = textArray[10] 114 | val current = textArray[9].toDouble() 115 | val low = textArray[8].toDouble() 116 | val high = textArray[7].toDouble() 117 | val opening = textArray[6].toDouble() 118 | val change = (current - opening).twoDigits() 119 | val percentage = ((current - opening) / opening * 100).twoDigits() 120 | val updateAt = "${textArray[12]} ${textArray[1]}" 121 | StockerQuote( 122 | code = code, 123 | name = name, 124 | current = current, 125 | opening = opening, 126 | close = current, 127 | low = low, 128 | high = high, 129 | change = change, 130 | percentage = percentage, 131 | updateAt = updateAt 132 | ) 133 | } 134 | } 135 | }.toList() 136 | } 137 | 138 | private fun parseTencentQuoteResponse(marketType: StockerMarketType, responseText: String): List { 139 | return responseText.split("\n").asSequence().filter { text -> text.isNotEmpty() }.map { text -> 140 | val code = when (marketType) { 141 | StockerMarketType.AShare -> text.subSequence(2, text.indexOfFirst { c -> c == '=' }) 142 | StockerMarketType.HKStocks, StockerMarketType.USStocks -> text.subSequence(4, 143 | text.indexOfFirst { c -> c == '=' }) 144 | 145 | StockerMarketType.Crypto -> "" 146 | } 147 | "$code~${text.subSequence(text.indexOfFirst { c -> c == '"' } + 1, text.indexOfLast { c -> c == '"' })}" 148 | }.map { text -> text.split("~") }.map { textArray -> 149 | val code = textArray[0].uppercase() 150 | val name = textArray[2] 151 | val opening = textArray[6].toDouble() 152 | val close = textArray[5].toDouble() 153 | val current = textArray[4].toDouble() 154 | val high = textArray[34].toDouble() 155 | val low = textArray[35].toDouble() 156 | val change = (current - close).twoDigits() 157 | val percentage = textArray[33].toDouble().twoDigits() 158 | val updateAt = when (marketType) { 159 | StockerMarketType.AShare -> { 160 | val sourceFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss") 161 | val targetFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 162 | val datetime = LocalDateTime.parse(textArray[31], sourceFormatter) 163 | targetFormatter.format(datetime) 164 | } 165 | 166 | StockerMarketType.HKStocks -> { 167 | val sourceFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss") 168 | val targetFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 169 | val datetime = LocalDateTime.parse(textArray[31], sourceFormatter) 170 | targetFormatter.format(datetime) 171 | } 172 | 173 | StockerMarketType.USStocks -> textArray[31] 174 | StockerMarketType.Crypto -> "" 175 | } 176 | StockerQuote( 177 | code = code, 178 | name = name, 179 | current = current, 180 | opening = opening, 181 | close = close, 182 | low = low, 183 | high = high, 184 | change = change, 185 | percentage = percentage, 186 | updateAt = updateAt 187 | ) 188 | }.toList() 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /src/main/kotlin/com/vermouthx/stocker/views/windows/StockerToolWindow.kt: -------------------------------------------------------------------------------- 1 | package com.vermouthx.stocker.views.windows 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.ui.JBMenuItem 7 | import com.intellij.openapi.ui.JBPopupMenu 8 | import com.intellij.openapi.ui.Messages 9 | import com.intellij.openapi.wm.ToolWindow 10 | import com.intellij.openapi.wm.ToolWindowFactory 11 | import com.intellij.ui.content.ContentFactory 12 | import com.vermouthx.stocker.StockerApp 13 | import com.vermouthx.stocker.StockerAppManager 14 | import com.vermouthx.stocker.enums.StockerMarketType 15 | import com.vermouthx.stocker.listeners.StockerQuoteDeleteListener 16 | import com.vermouthx.stocker.listeners.StockerQuoteDeleteNotifier.* 17 | import com.vermouthx.stocker.listeners.StockerQuoteReloadListener 18 | import com.vermouthx.stocker.listeners.StockerQuoteReloadNotifier.* 19 | import com.vermouthx.stocker.listeners.StockerQuoteUpdateListener 20 | import com.vermouthx.stocker.listeners.StockerQuoteUpdateNotifier.* 21 | import com.vermouthx.stocker.settings.StockerSetting 22 | 23 | class StockerToolWindow : ToolWindowFactory { 24 | 25 | private val messageBus = ApplicationManager.getApplication().messageBus 26 | 27 | private lateinit var allView: StockerSimpleToolWindow 28 | private lateinit var tabViewMap: Map 29 | private lateinit var myApplication: StockerApp 30 | 31 | private fun injectPopupMenu(project: Project?, window: StockerSimpleToolWindow?) { 32 | if (window != null) { 33 | val tbBody = window.tableView.tableBody 34 | val tbModel = window.tableView.tableModel 35 | val tbPopupMenu = JBPopupMenu() 36 | val tbPopupDeleteMenuItem = JBMenuItem("Delete", AllIcons.General.Remove) 37 | tbPopupDeleteMenuItem.addActionListener { 38 | if (tbBody.selectedRowCount == 0) { 39 | Messages.showErrorDialog( 40 | project, "You have not selected any stock symbol.", "Require Symbol Selection" 41 | ) 42 | return@addActionListener 43 | } 44 | val setting = StockerSetting.instance 45 | for (selectedRow in tbBody.selectedRows) { 46 | val code = tbModel.getValueAt(selectedRow, 0).toString() 47 | val market = setting.marketOf(code) 48 | if (market != null) { 49 | myApplication.shutdown() 50 | setting.removeCode(market, code) 51 | when (market) { 52 | StockerMarketType.AShare -> { 53 | val publisher = messageBus.syncPublisher(STOCK_CN_QUOTE_DELETE_TOPIC) 54 | publisher.after(code) 55 | } 56 | 57 | StockerMarketType.HKStocks -> { 58 | val publisher = messageBus.syncPublisher(STOCK_HK_QUOTE_DELETE_TOPIC) 59 | publisher.after(code) 60 | } 61 | 62 | StockerMarketType.USStocks -> { 63 | val publisher = messageBus.syncPublisher(STOCK_US_QUOTE_DELETE_TOPIC) 64 | publisher.after(code) 65 | } 66 | 67 | StockerMarketType.Crypto -> { 68 | val publisher = messageBus.syncPublisher(CRYPTO_QUOTE_DELETE_TOPIC) 69 | publisher.after(code) 70 | } 71 | } 72 | val publisher = messageBus.syncPublisher(STOCK_ALL_QUOTE_DELETE_TOPIC) 73 | publisher.after(code) 74 | myApplication.schedule() 75 | } 76 | } 77 | } 78 | tbPopupMenu.add(tbPopupDeleteMenuItem) 79 | tbBody.componentPopupMenu = tbPopupMenu 80 | } 81 | } 82 | 83 | override fun init(toolWindow: ToolWindow) { 84 | super.init(toolWindow) 85 | allView = StockerSimpleToolWindow() 86 | tabViewMap = mapOf( 87 | StockerMarketType.AShare to StockerSimpleToolWindow(), 88 | StockerMarketType.HKStocks to StockerSimpleToolWindow(), 89 | StockerMarketType.USStocks to StockerSimpleToolWindow(), 90 | StockerMarketType.Crypto to StockerSimpleToolWindow() 91 | ) 92 | myApplication = StockerApp() 93 | } 94 | 95 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 96 | val contentManager = toolWindow.contentManager 97 | val contentFactory = ContentFactory.getInstance() 98 | val allContent = 99 | contentFactory.createContent(allView.component, "ALL", false).also { injectPopupMenu(project, allView) } 100 | contentManager.addContent(allContent) 101 | val aShareContent = contentFactory.createContent( 102 | tabViewMap[StockerMarketType.AShare]?.component, StockerMarketType.AShare.title, false 103 | ).also { 104 | injectPopupMenu(project, tabViewMap[StockerMarketType.AShare]) 105 | } 106 | contentManager.addContent(aShareContent) 107 | val hkStocksContent = contentFactory.createContent( 108 | tabViewMap[StockerMarketType.HKStocks]?.component, StockerMarketType.HKStocks.title, false 109 | ).also { 110 | injectPopupMenu(project, tabViewMap[StockerMarketType.HKStocks]) 111 | } 112 | contentManager.addContent(hkStocksContent) 113 | val usStocksContent = contentFactory.createContent( 114 | tabViewMap[StockerMarketType.USStocks]?.component, StockerMarketType.USStocks.title, false 115 | ).also { 116 | injectPopupMenu(project, tabViewMap[StockerMarketType.USStocks]) 117 | } 118 | contentManager.addContent(usStocksContent) 119 | // val cryptoContent = contentFactory.createContent( 120 | // tabViewMap[StockerMarketType.Crypto]?.component, 121 | // StockerMarketType.Crypto.title, 122 | // false 123 | // ).also { 124 | // injectPopupMenu(project, tabViewMap[StockerMarketType.Crypto]) 125 | // } 126 | // contentManager.addContent(cryptoContent) 127 | this.subscribeMessage() 128 | StockerAppManager.myApplicationMap[project] = myApplication 129 | myApplication.schedule() 130 | } 131 | 132 | private fun subscribeMessage() { 133 | messageBus.connect().subscribe( 134 | STOCK_ALL_QUOTE_UPDATE_TOPIC, StockerQuoteUpdateListener(allView.tableView) 135 | ) 136 | messageBus.connect().subscribe( 137 | STOCK_ALL_QUOTE_DELETE_TOPIC, StockerQuoteDeleteListener(allView.tableView) 138 | ) 139 | messageBus.connect().subscribe( 140 | STOCK_ALL_QUOTE_RELOAD_TOPIC, StockerQuoteReloadListener(allView.tableView) 141 | ) 142 | tabViewMap.forEach { (market, myTableView) -> 143 | when (market) { 144 | StockerMarketType.AShare -> { 145 | messageBus.connect().subscribe( 146 | STOCK_CN_QUOTE_UPDATE_TOPIC, StockerQuoteUpdateListener( 147 | myTableView.tableView 148 | ) 149 | ) 150 | messageBus.connect().subscribe( 151 | STOCK_CN_QUOTE_DELETE_TOPIC, StockerQuoteDeleteListener( 152 | myTableView.tableView 153 | ) 154 | ) 155 | messageBus.connect().subscribe( 156 | STOCK_CN_QUOTE_RELOAD_TOPIC, StockerQuoteReloadListener(myTableView.tableView) 157 | ) 158 | } 159 | 160 | StockerMarketType.HKStocks -> { 161 | messageBus.connect().subscribe( 162 | STOCK_HK_QUOTE_UPDATE_TOPIC, StockerQuoteUpdateListener( 163 | myTableView.tableView 164 | ) 165 | ) 166 | messageBus.connect().subscribe( 167 | STOCK_HK_QUOTE_DELETE_TOPIC, StockerQuoteDeleteListener( 168 | myTableView.tableView 169 | ) 170 | ) 171 | messageBus.connect().subscribe( 172 | STOCK_HK_QUOTE_RELOAD_TOPIC, StockerQuoteReloadListener(myTableView.tableView) 173 | ) 174 | } 175 | 176 | StockerMarketType.USStocks -> { 177 | messageBus.connect().subscribe( 178 | STOCK_US_QUOTE_UPDATE_TOPIC, StockerQuoteUpdateListener( 179 | myTableView.tableView 180 | ) 181 | ) 182 | messageBus.connect().subscribe( 183 | STOCK_US_QUOTE_DELETE_TOPIC, StockerQuoteDeleteListener( 184 | myTableView.tableView 185 | ) 186 | ) 187 | messageBus.connect().subscribe( 188 | STOCK_US_QUOTE_RELOAD_TOPIC, StockerQuoteReloadListener( 189 | myTableView.tableView 190 | ) 191 | ) 192 | } 193 | 194 | StockerMarketType.Crypto -> { 195 | messageBus.connect().subscribe( 196 | CRYPTO_QUOTE_UPDATE_TOPIC, StockerQuoteUpdateListener( 197 | myTableView.tableView 198 | ) 199 | ) 200 | messageBus.connect().subscribe( 201 | CRYPTO_QUOTE_DELETE_TOPIC, StockerQuoteDeleteListener( 202 | myTableView.tableView 203 | ) 204 | ) 205 | messageBus.connect().subscribe( 206 | STOCK_CRYPTO_QUOTE_RELOAD_TOPIC, StockerQuoteReloadListener( 207 | myTableView.tableView 208 | ) 209 | ) 210 | } 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------