├── .gitignore ├── resources ├── 1.jpg ├── 2.png ├── icons │ └── icon_main_window.svg └── META-INF │ ├── pluginIcon.svg │ └── plugin.xml ├── src ├── com │ └── dengzii │ │ └── plugin │ │ └── fund │ │ ├── api │ │ ├── Subscriber.java │ │ ├── RequestTask.java │ │ ├── FundApiEnum.java │ │ ├── PollTask.java │ │ ├── StockApi.java │ │ ├── bean │ │ │ ├── FundCompanyBean.java │ │ │ ├── NetValueBean.java │ │ │ ├── FundBean.java │ │ │ └── StockUpdateBean.java │ │ ├── SubScribeSource.java │ │ ├── FundApi.java │ │ ├── SinaFundApi.java │ │ ├── AbstractPollTask.java │ │ ├── SinaStockApi.java │ │ └── TianTianFundApi.java │ │ ├── tools │ │ ├── Helper.kt │ │ ├── EditorHintUtils.kt │ │ ├── ui │ │ │ ├── XMenuBar.kt │ │ │ ├── XMenu.kt │ │ │ ├── RGBColor.kt │ │ │ ├── ComponentExtensions.kt │ │ │ ├── PopMenuUtils.kt │ │ │ ├── ActionToolBarUtils.kt │ │ │ ├── TableColumnModelDecorator.kt │ │ │ ├── ColumnInfo.kt │ │ │ ├── XDialog.kt │ │ │ └── TableAdapter.kt │ │ ├── NotificationUtils.kt │ │ └── PersistentConfig.kt │ │ ├── utils │ │ ├── Logger.kt │ │ ├── GsonUtils.kt │ │ └── async.kt │ │ ├── design │ │ ├── FundPanelForm.java │ │ ├── StockPanelForm.java │ │ ├── SettingsForm.java │ │ ├── EditStockForm.java │ │ ├── EditFundGroupForm.java │ │ ├── FundPanelForm.form │ │ ├── StockPanelForm.form │ │ ├── FundDetailForm.java │ │ ├── FundDetailForm.form │ │ ├── EditStockForm.form │ │ ├── SettingsForm.form │ │ └── EditFundGroupForm.form │ │ ├── conf │ │ ├── FundTheme.kt │ │ ├── FundColConfig.kt │ │ └── StockColConfig.kt │ │ ├── ui │ │ ├── ToolWindowPanel.kt │ │ ├── fund │ │ │ ├── NetValueHistoryColInfo.kt │ │ │ ├── FundCodeColInfo.kt │ │ │ ├── FundDetailDialog.kt │ │ │ ├── EditFundGroupListDialog.kt │ │ │ └── FundPanel.kt │ │ ├── ColoredColInfo.kt │ │ ├── stock │ │ │ ├── EditStockDialog.kt │ │ │ └── StockPanel.kt │ │ ├── MainToolWindowFactory.kt │ │ └── LineChart.kt │ │ ├── model │ │ ├── FundGroup.kt │ │ ├── UserStockModel.kt │ │ └── UserFundModel.kt │ │ ├── PluginConfigurable.kt │ │ ├── PluginConfig.kt │ │ └── http │ │ └── Http.kt └── icons │ └── PluginIcons.kt ├── README.md └── test └── com └── dengzii └── plugin └── fund ├── api ├── SinaStockApiTest.java └── TianTianFundApiTest.java └── http └── HttpTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /out 3 | 4 | *.class 5 | *.iml -------------------------------------------------------------------------------- /resources/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzii/FundAssistant/HEAD/resources/1.jpg -------------------------------------------------------------------------------- /resources/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzii/FundAssistant/HEAD/resources/2.png -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/Subscriber.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | /** 4 | * @author https://github.com/dengzii/ 5 | */ 6 | public interface Subscriber { 7 | void onUpdate(T result); 8 | } 9 | -------------------------------------------------------------------------------- /src/icons/PluginIcons.kt: -------------------------------------------------------------------------------- 1 | package icons 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | 5 | object PluginIcons { 6 | @JvmField 7 | val PluginIcon = IconLoader.getIcon("/icons/icon_main_window.svg", PluginIcons::class.java) 8 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/RequestTask.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | /** 4 | * @author https://github.com/dengzii 5 | */ 6 | public abstract class RequestTask extends AbstractPollTask { 7 | 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/Helper.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | 5 | fun invokeLater(action: () -> Unit) { 6 | ApplicationManager.getApplication().invokeLater(action) 7 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/EditorHintUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools 2 | 3 | object EditorHintUtils { 4 | 5 | fun showError() { 6 | 7 | } 8 | 9 | fun showQuestion() { 10 | 11 | } 12 | 13 | fun showInfo() { 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/FundApiEnum.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | /** 4 | * @author https://github.com/dengzii/ 5 | */ 6 | public enum FundApiEnum { 7 | Sina("新浪"), 8 | TianTian("天天基金"); 9 | 10 | FundApiEnum(String s) { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.utils 2 | 3 | /** 4 | * @author https://github.com/dengzii/ 5 | */ 6 | object Logger { 7 | 8 | @JvmStatic 9 | fun log(tag: String = "", log: String) { 10 | println("$tag: $log") 11 | } 12 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/PollTask.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | /** 4 | * @author https://github.com/dengzii/ 5 | */ 6 | public interface PollTask { 7 | void start(long durationMilliSec); 8 | 9 | void stop(); 10 | 11 | boolean isStopped(); 12 | } 13 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/FundPanelForm.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.design; 2 | 3 | import javax.swing.*; 4 | 5 | /** 6 | * @author https://github.com/dengzii/ 7 | */ 8 | public class FundPanelForm { 9 | public JPanel contentPanel; 10 | public JTable tableFund; 11 | } 12 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/StockPanelForm.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.design; 2 | 3 | import javax.swing.*; 4 | 5 | /** 6 | * @author https://github.com/dengzii/ 7 | */ 8 | public class StockPanelForm { 9 | protected JPanel contentPanel; 10 | protected JTable tableStock; 11 | } 12 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/StockApi.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import com.dengzii.plugin.fund.api.bean.StockUpdateBean; 4 | 5 | import java.util.List; 6 | 7 | public interface StockApi { 8 | AbstractPollTask> subscribeUpdate(List stockList); 9 | } 10 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/bean/FundCompanyBean.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api.bean; 2 | 3 | /** 4 | * http://fund.eastmoney.com/js/jjjz_gs.js 5 | * 6 | * @author https://github.com/dengzii/ 7 | */ 8 | public class FundCompanyBean { 9 | private String code; 10 | private String name; 11 | } 12 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/SubScribeSource.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | /** 4 | * @author https://github.com/dengzii/ 5 | */ 6 | public interface SubScribeSource { 7 | void subscribe(Subscriber subscriber); 8 | 9 | void unsubscribe(Subscriber subscriber); 10 | 11 | void unsubscribeAll(); 12 | } 13 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/conf/FundTheme.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.conf 2 | 3 | /** 4 | * @author https://github.com/dengzii/ 5 | */ 6 | enum class FundTheme( 7 | val rise: String, 8 | val fall: String 9 | ) { 10 | Default("#A61000", "#007D1C"), 11 | Conceal("#5D1B14", "#0F471B"), 12 | ConcealPlus("#FFFFFF", "#FFFFFF"); 13 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/ToolWindowPanel.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.wm.ToolWindow 5 | 6 | /** 7 | * @author https://github.com/dengzii/ 8 | */ 9 | interface ToolWindowPanel { 10 | fun onCreate(project: Project, toolWindow: ToolWindow) 11 | fun onInit(toolWindow: ToolWindow) 12 | fun onWindowActive() 13 | fun onWindowHide() 14 | fun onWindowShow() 15 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/SettingsForm.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.design; 2 | 3 | import javax.swing.*; 4 | 5 | /** 6 | * @author https://github.com/dengzii/ 7 | */ 8 | public class SettingsForm { 9 | public JPanel contentPanel; 10 | public JComboBox comboBoxTheme; 11 | public JButton buttonExport; 12 | public JComboBox comboBoxApi; 13 | public JComboBox comboBoxDuration; 14 | public JPanel panelColConf; 15 | } 16 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/XMenuBar.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import javax.swing.JMenuBar 4 | 5 | class XMenuBar(private val block: XMenuBar.() -> Unit) : JMenuBar() { 6 | 7 | init { 8 | block.invoke(this) 9 | } 10 | 11 | operator fun invoke(): XMenuBar { 12 | block.invoke(this) 13 | return this 14 | } 15 | 16 | fun menu(title: String, block: XMenu.() -> Unit) { 17 | val menu = XMenu(title) 18 | block(menu) 19 | add(menu) 20 | } 21 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/model/FundGroup.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.model 2 | 3 | import com.dengzii.plugin.fund.utils.GsonUtils 4 | import com.google.gson.reflect.TypeToken 5 | 6 | /** 7 | * @author https://github.com/dengzii/ 8 | */ 9 | class FundGroup { 10 | 11 | var groupName = "default-group" 12 | var fundList = mutableMapOf() 13 | 14 | fun clone(): FundGroup { 15 | val t = object : TypeToken() {}.type 16 | return GsonUtils.fromJson(GsonUtils.toJson(this), t) 17 | } 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FundAssistant 2 | 3 | 基金小助手, 支持 JetBrains 家所有基于 IDEA 的 IDE. 4 | 5 | 6 | 7 | ## 功能 8 | 9 | --- 10 | - [x] 基金搜索: 名称, 拼音, 拼音简写 11 | - [x] 基金列表: 排序, 基本字段, 涨跌染色标记 12 | - [x] 基金列表编辑: 通过搜索批量添加, 删除 13 | - [x] 基金轮询更新: 定时请求刷新基金数据 14 | - [x] 基金主题: 三种主题可选, 隐蔽模式 15 | - [x] 基金自定义列: 选择你想要关注的字段 16 | - [ ] 基金收益: 收益, 收益率, 持仓 17 | - [ ] 基金详情: 时段净值走势图, 持仓信息, 基金状态 18 | - [ ] 基金预警: 当日净值变化监控警报 19 | - [ ] 基金分组 20 | - [ ] 基金列表导出 21 | 22 | --- 23 | - [x] 股票 24 | - [ ] 股票走势图 -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/EditStockForm.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.design; 2 | 3 | import com.dengzii.plugin.fund.tools.ui.XDialog; 4 | 5 | import javax.swing.*; 6 | 7 | public class EditStockForm extends XDialog { 8 | protected JPanel contentPanel; 9 | protected JButton buttonOk; 10 | protected JButton buttonCancel; 11 | protected JTable table; 12 | protected JTextField textFieldSearch; 13 | 14 | public EditStockForm(String title) { 15 | super(title); 16 | setContentPane(contentPanel); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/utils/GsonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.utils 2 | 3 | import com.google.gson.GsonBuilder 4 | import java.lang.reflect.Type 5 | 6 | /** 7 | * @author https://github.com/dengzii 8 | */ 9 | object GsonUtils { 10 | @JvmStatic 11 | private val gson = GsonBuilder().serializeNulls().setLenient().create() 12 | 13 | @JvmStatic 14 | fun fromJson(json: String, type: Type): T { 15 | return gson.fromJson(json, type) 16 | } 17 | 18 | fun toJson(t: Any): String { 19 | return gson.toJson(t) 20 | } 21 | } -------------------------------------------------------------------------------- /resources/icons/icon_main_window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/EditFundGroupForm.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.design; 2 | 3 | import com.dengzii.plugin.fund.tools.ui.XDialog; 4 | 5 | import javax.swing.*; 6 | 7 | /** 8 | * @author https://github.com/dengzii/ 9 | */ 10 | public class EditFundGroupForm extends XDialog { 11 | 12 | protected JPanel contentPanel; 13 | protected JButton buttonApply; 14 | protected JButton buttonCancel; 15 | protected JList listSelected; 16 | protected JTextField textFieldSearch; 17 | protected JList listSearch; 18 | protected JTextField textFieldGroupName; 19 | 20 | public EditFundGroupForm(String title){ 21 | super(title); 22 | super.setContentPane(contentPanel); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/fund/NetValueHistoryColInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui.fund 2 | 3 | import com.dengzii.plugin.fund.api.bean.NetValueBean 4 | import com.dengzii.plugin.fund.tools.ui.ColumnInfo 5 | import com.dengzii.plugin.fund.ui.LineChart 6 | import java.awt.Component 7 | import javax.swing.JLabel 8 | 9 | 10 | class NetValueHistoryColInfo(name: String) : ColumnInfo(name) { 11 | 12 | override val columnClass = List::class.java 13 | 14 | override fun getRendererComponent(item: Any?, row: Int, col: Int): Component { 15 | val s = item as? List<*> ?: return JLabel("-") 16 | val chart = LineChart() 17 | chart.setData(s.filterIsInstance().map { it.netValue.toDouble() }.asReversed()) 18 | return chart 19 | } 20 | } -------------------------------------------------------------------------------- /test/com/dengzii/plugin/fund/api/SinaStockApiTest.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import com.dengzii.plugin.fund.api.bean.StockUpdateBean; 4 | import junit.framework.TestCase; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | public class SinaStockApiTest extends TestCase { 10 | 11 | public void testSubscribeUpdate() throws InterruptedException { 12 | AbstractPollTask> s = new SinaStockApi().subscribeUpdate(Arrays.asList("sz002157", "sz000001")); 13 | s.subscribe(result -> { 14 | for (StockUpdateBean stockUpdateBean : result) { 15 | System.out.println(stockUpdateBean.toString()); 16 | } 17 | }); 18 | s.start(2000); 19 | Thread.sleep(30000); 20 | } 21 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/utils/async.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.utils 2 | 3 | import java.util.concurrent.Executors 4 | 5 | /** 6 | * @author https://github.com/dengzii/ 7 | */ 8 | private val executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2) 9 | 10 | fun async(block: () -> T): AsyncTask { 11 | return AsyncTask(block) 12 | } 13 | 14 | class AsyncTask(private val task: () -> T) : Runnable { 15 | 16 | private lateinit var callback: (T) -> Unit 17 | 18 | override fun run() { 19 | val result = task() 20 | // invokeLater { 21 | callback(result) 22 | // } 23 | } 24 | 25 | fun callback(callback: (T) -> Unit) { 26 | this.callback = callback 27 | executor.submit(this) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/FundApi.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import com.dengzii.plugin.fund.api.bean.FundBean; 4 | import com.dengzii.plugin.fund.api.bean.NetValueBean; 5 | 6 | import java.io.IOException; 7 | import java.util.List; 8 | 9 | /** 10 | * @author https://github.com/dengzii/ 11 | */ 12 | public interface FundApi { 13 | List getFundList() throws IOException, InterruptedException; 14 | 15 | FundBean getFundNewestDetail(String fundCode); 16 | 17 | NetValueBean getNewestNetValue(String fundCode); 18 | 19 | AbstractPollTask> updateFundList(List fundBeans); 20 | 21 | List getNetValueHistory3Month(String fundCode); 22 | 23 | List getNetValueHistory(String fundCode, int page, int pageSize); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /test/com/dengzii/plugin/fund/http/HttpTest.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.http; 2 | 3 | import junit.framework.TestCase; 4 | import org.apache.http.impl.execchain.RequestAbortedException; 5 | import org.junit.Assert; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * @author https://github.com/dengzii/ 11 | */ 12 | public class HttpTest extends TestCase { 13 | 14 | public void testGet() { 15 | 16 | Http h = new Http(); 17 | try { 18 | String res = h.get("http://fund.eastmoney.com/js/fundcode_search.js"); 19 | System.out.println(res); 20 | Assert.assertNotNull(res); 21 | } catch (InterruptedException| RequestAbortedException e) { 22 | // ignore 23 | } catch (IOException e) { 24 | e.printStackTrace(); 25 | } 26 | } 27 | 28 | public void testPost() { 29 | } 30 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/FundPanelForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/StockPanelForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/model/UserStockModel.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.model 2 | 3 | import com.dengzii.plugin.fund.api.bean.StockUpdateBean 4 | 5 | class UserStockModel(val stock: StockUpdateBean) { 6 | 7 | var buyPrice: Double = 0.0 8 | 9 | fun update(u: StockUpdateBean) { 10 | stock.name = u.name 11 | stock.code = u.code 12 | stock.time = u.time 13 | stock.date = u.date 14 | stock.totalTransactionAmount = u.totalTransactionAmount 15 | stock.dealShares = u.dealShares 16 | stock.todayLowPrice = u.todayLowPrice 17 | stock.todayHighPrice = u.todayHighPrice 18 | stock.priceByOne = u.priceByOne 19 | stock.priceByTwo = u.priceByTwo 20 | stock.unitPriceByOne = u.unitPriceByOne 21 | stock.unitPriceByTwo = u.unitPriceByTwo 22 | stock.unitPriceSaleOne = u.unitPriceSaleOne 23 | stock.unitPriceSaleTwo = u.unitPriceSaleTwo 24 | stock.yesterdayClosingPrice = u.yesterdayClosingPrice 25 | stock.todayOpeningPrice = u.todayOpeningPrice 26 | stock.currentPrice = u.currentPrice 27 | } 28 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/XMenu.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import javax.swing.JMenu 4 | import javax.swing.JMenuItem 5 | 6 | /** 7 | *
 8 |  * author : dengzi
 9 |  * e-mail : dengzii@foxmail.com
10 |  * github : https://github.com/dengzii
11 |  * time   : 2019/11/23
12 |  * desc   :
13 |  * 
14 | */ 15 | class XMenu(name: String?, private val block: XMenu.() -> Unit) : JMenu(name) { 16 | 17 | constructor(name: String?) : this(name, {}) 18 | 19 | operator fun invoke(): XMenu { 20 | block(this) 21 | return this 22 | } 23 | 24 | fun addItem(title: String?, onItemClick: OnItemClick) { 25 | val menuItem = JMenuItem(title) 26 | menuItem.onClick { 27 | onItemClick.onItemClick() 28 | } 29 | add(menuItem) 30 | } 31 | 32 | fun item(title: String, onClick: () -> Unit) { 33 | addItem(title, object : OnItemClick { 34 | override fun onItemClick() { 35 | onClick() 36 | } 37 | }) 38 | } 39 | 40 | interface OnItemClick { 41 | fun onItemClick() 42 | } 43 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/SinaFundApi.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import com.dengzii.plugin.fund.api.bean.FundBean; 4 | import com.dengzii.plugin.fund.api.bean.NetValueBean; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | /** 10 | * @author https://github.com/dengzii/ 11 | */ 12 | public class SinaFundApi implements FundApi { 13 | @Override 14 | public List getFundList() { 15 | return Collections.emptyList(); 16 | } 17 | 18 | @Override 19 | public FundBean getFundNewestDetail(String fundCode) { 20 | return null; 21 | } 22 | 23 | @Override 24 | public NetValueBean getNewestNetValue(String fundCode) { 25 | return null; 26 | } 27 | 28 | @Override 29 | public AbstractPollTask> updateFundList(List fundBeans) { 30 | return null; 31 | } 32 | 33 | @Override 34 | public List getNetValueHistory3Month(String fundCode) { 35 | return null; 36 | } 37 | 38 | @Override 39 | public List getNetValueHistory(String fundCode, int page, int pageSize) { 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/RGBColor.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import java.awt.Color 4 | 5 | class RGBColor private constructor() { 6 | 7 | private var r: Int = 0 8 | private var g: Int = 0 9 | private var b: Int = 0 10 | 11 | companion object { 12 | fun get(c: String): RGBColor { 13 | var s = c.removePrefix("#") 14 | if (s.length != 6){ 15 | s = "FFFFFF" 16 | } 17 | return RGBColor().apply { 18 | r = s.substring(0..1).conv() 19 | g = s.substring(2..3).conv() 20 | b = s.substring(4..5).conv() 21 | } 22 | } 23 | 24 | fun get(r: Int, g: Int, b: Int): RGBColor { 25 | return RGBColor().apply { 26 | this.r = r 27 | this.b = b 28 | this.g = g 29 | } 30 | } 31 | } 32 | 33 | fun getHSL(): Color { 34 | val hsb = FloatArray(3) 35 | Color.RGBtoHSB(r, g, b, hsb) 36 | return Color.getHSBColor(hsb[0], hsb[1], hsb[2]) 37 | } 38 | 39 | private fun String.conv(): Int { 40 | return Integer.parseInt(this, 16) 41 | } 42 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/FundDetailForm.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.design; 2 | 3 | import com.dengzii.plugin.fund.tools.ui.XDialog; 4 | import com.dengzii.plugin.fund.ui.fund.FundDetailDialog; 5 | import com.dengzii.plugin.fund.ui.LineChart; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.util.ArrayList; 10 | 11 | /** 12 | * @author https://github.com/dengzii/ 13 | */ 14 | public class FundDetailForm extends XDialog { 15 | 16 | protected JPanel contentPanel; 17 | protected JTextField tfCount; 18 | protected JPanel container; 19 | 20 | public static void main(String[] args) { 21 | FundDetailDialog f = new FundDetailDialog(); 22 | LineChart l = new LineChart(); 23 | ArrayList a = new ArrayList(); 24 | a.add(-1.20); 25 | a.add(-1.30); 26 | a.add(-1.40); 27 | a.add(-1.10); 28 | a.add(-0.2); 29 | a.add(1.40); 30 | a.add(1.50); 31 | a.add(1.20); 32 | a.add(1.50); 33 | a.add(1.90); 34 | a.add(1.70); 35 | a.add(1.50); 36 | a.add(1.60); 37 | a.add(1.8); 38 | a.add(1.40); 39 | l.setData(a); 40 | f.contentPanel.add(l, BorderLayout.CENTER); 41 | f.packAndShow(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/fund/FundCodeColInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui.fund 2 | 3 | import com.dengzii.plugin.fund.tools.ui.ColumnInfo 4 | import com.dengzii.plugin.fund.tools.ui.onClick 5 | import com.intellij.ui.components.JBLabel 6 | import java.awt.Color 7 | import java.awt.Component 8 | import java.awt.event.MouseEvent 9 | 10 | /** 11 | * @author https://github.com/dengzii/ 12 | */ 13 | class FundCodeColInfo( 14 | private val name: String = "Fund Code", 15 | private val onCellClick: (MouseEvent?) -> Unit 16 | ) : ColumnInfo(name, true) { 17 | 18 | override val columnClass = String::class.java 19 | 20 | override fun getRendererComponent(item: Any?, row: Int, col: Int): Component { 21 | return item?.let { getComponent(it.toString()) } ?: super.getRendererComponent(item, row, col) 22 | } 23 | 24 | override fun getEditComponent(item: Any?, row: Int, col: Int): Component { 25 | return item?.let { getComponent(it.toString()) } ?: super.getRendererComponent(item, row, col) 26 | } 27 | 28 | private fun getComponent(text: String): Component { 29 | return JBLabel(text).apply { 30 | // val hsb = FloatArray(3) 31 | // Color.RGBtoHSB(62, 143, 94, hsb) 32 | // foreground = Color.getHSBColor(hsb[0], hsb[1], hsb[2]) 33 | onClick(onCellClick) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/ComponentExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import java.awt.Component 4 | import java.awt.event.MouseAdapter 5 | import java.awt.event.MouseEvent 6 | 7 | 8 | /** 9 | * Add right mouse button clicked listener to component. 10 | * 11 | * @param action The click event callback. 12 | */ 13 | inline fun Component.onRightMouseButtonClicked(crossinline action: (MouseEvent) -> Unit) { 14 | onMouseButtonClicked(MouseEvent.BUTTON3, action) 15 | } 16 | 17 | 18 | /** 19 | * Add mouse pressed listener. 20 | * 21 | * @param action The click event callback. 22 | */ 23 | inline fun Component.onClick(crossinline action: (MouseEvent?) -> Unit){ 24 | addMouseListener(object : MouseAdapter() { 25 | override fun mousePressed(e: MouseEvent?) { 26 | super.mousePressed(e) 27 | action(e) 28 | } 29 | }) 30 | } 31 | 32 | /** 33 | * Add a listener to component when specified [button] clicked. 34 | * 35 | * @param button The mouse button code. 36 | * @param action The click event callback. 37 | */ 38 | inline fun Component.onMouseButtonClicked(button: Int, crossinline action: (MouseEvent) -> Unit) { 39 | addMouseListener(object : MouseAdapter() { 40 | override fun mouseClicked(e: MouseEvent?) { 41 | super.mouseClicked(e) 42 | if (e?.button == button) { 43 | action(e) 44 | } 45 | } 46 | }) 47 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/ColoredColInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui 2 | 3 | import com.dengzii.plugin.fund.PluginConfig 4 | import com.dengzii.plugin.fund.tools.ui.ColumnInfo 5 | import com.dengzii.plugin.fund.tools.ui.RGBColor 6 | import com.intellij.ui.components.JBLabel 7 | import java.awt.Component 8 | 9 | /** 10 | * @author https://github.com/dengzii/ 11 | */ 12 | class ColoredColInfo( 13 | private val name: String 14 | ) : ColumnInfo(name, true) { 15 | 16 | 17 | private val colorRise by lazy { 18 | RGBColor.get(PluginConfig.fundTheme.rise).getHSL() 19 | } 20 | private val colorFall by lazy { 21 | RGBColor.get(PluginConfig.fundTheme.fall).getHSL() 22 | } 23 | 24 | override val columnClass = String::class.java 25 | 26 | override fun getRendererComponent(item: Any?, row: Int, col: Int): Component { 27 | return item?.let { getComponent(it.toString()) } ?: super.getRendererComponent(item, row, col) 28 | } 29 | 30 | override fun getEditComponent(item: Any?, row: Int, col: Int): Component { 31 | return item?.let { getComponent(it.toString()) } ?: super.getRendererComponent(item, row, col) 32 | } 33 | 34 | private fun getComponent(text: String): Component { 35 | val s1 = text.replace("%", "").replace(",", "").toDouble() 36 | val color = if (s1 > 0) colorRise else colorFall 37 | return JBLabel(text).apply { 38 | foreground = color 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/PopMenuUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import com.intellij.openapi.ui.JBMenuItem 4 | import com.intellij.openapi.ui.JBPopupMenu 5 | import java.awt.Component 6 | import java.awt.event.MouseEvent 7 | 8 | /** 9 | *
10 |  * author : dengzi
11 |  * e-mail : dengzii@foxmail.com
12 |  * github : https://github.com/dengzii
13 |  * time   : 2020/1/16
14 |  * desc   :
15 |  * 
16 | */ 17 | object PopMenuUtils { 18 | 19 | /** 20 | * Return a [JBPopupMenu] instance with [menu]. 21 | * 22 | * @param menu The menu item and click event callback pair. 23 | */ 24 | fun create(menu: Map Unit?>): JBPopupMenu { 25 | 26 | val popMenu = JBPopupMenu() 27 | menu.forEach { (k, v) -> 28 | if (k.isBlank()) { 29 | popMenu.addSeparator() 30 | return@forEach 31 | } 32 | val item = JBMenuItem(k) 33 | item.addActionListener { 34 | v.invoke() 35 | } 36 | popMenu.add(item) 37 | } 38 | return popMenu 39 | } 40 | 41 | /** 42 | * Show a [JBPopupMenu] that location depends on [event]'s x, y. 43 | * 44 | * @param event The mouse event. 45 | * @param menu The menu item and click event callback pair. 46 | */ 47 | fun show(event: MouseEvent, menu: Map Unit?>) { 48 | create(menu).show(event.source as Component, event.x, event.y) 49 | } 50 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/model/UserFundModel.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.model 2 | 3 | import com.dengzii.plugin.fund.api.bean.FundBean 4 | import kotlin.random.Random 5 | 6 | /** 7 | * @author https://github.com/dengzii/ 8 | */ 9 | class UserFundModel() { 10 | 11 | lateinit var fundBean: FundBean 12 | var growthRateReckon = 0f 13 | var buyingPrice = 0f 14 | var totalYield = 0f 15 | var totalGains = 0f 16 | var holdingShare = 0f 17 | var netValueReckon = 0f 18 | var gainsReckon = 0f 19 | var updatedAt = "" 20 | 21 | var addDate = 0L 22 | var gainsAfterAdded = 0f 23 | var gainsWeek = 0f 24 | var gainsMonth = 0f 25 | 26 | constructor(fundBean: FundBean) : this() { 27 | this.fundBean = fundBean 28 | } 29 | 30 | fun updateFund(updated: FundBean) { 31 | fundBean.cutOffDate = updated.cutOffDate 32 | fundBean.growthRateReckon = updated.growthRateReckon 33 | fundBean.netValue = updated.netValue 34 | fundBean.updateTime = updated.updateTime 35 | fundBean.netValueReckon = updated.netValueReckon 36 | } 37 | 38 | fun rand() { 39 | this.fundBean = FundBean().apply { 40 | fundName = "诺安成长混合" 41 | fundCode = "2300042" 42 | } 43 | val r = Random(System.nanoTime()) 44 | this.buyingPrice = r.nextInt(1, 7) * 1.34f 45 | this.gainsReckon = r.nextFloat() 46 | this.totalGains = r.nextFloat() * r.nextInt(-500, 1000) 47 | this.holdingShare = r.nextInt(100, 200) * 1.32f 48 | this.totalYield = r.nextFloat() * r.nextInt(-30, 30) 49 | this.updatedAt = "2-26 15:51" 50 | this.growthRateReckon = r.nextFloat() * 5 51 | this.netValueReckon = r.nextFloat() * 5 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/FundDetailForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/fund/FundDetailDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui.fund 2 | 3 | import com.dengzii.plugin.fund.api.TianTianFundApi 4 | import com.dengzii.plugin.fund.design.FundDetailForm 5 | import com.dengzii.plugin.fund.ui.LineChart 6 | import com.dengzii.plugin.fund.utils.async 7 | import java.awt.BorderLayout 8 | import java.awt.event.KeyAdapter 9 | import java.awt.event.KeyEvent 10 | import kotlin.random.Random 11 | 12 | /** 13 | * @author https://github.com/dengzii/ 14 | */ 15 | class FundDetailDialog : FundDetailForm() { 16 | 17 | private var pre = 4.5 18 | private val p1 = LineChart() 19 | 20 | init { 21 | persistDialogState = false 22 | container.add(p1, BorderLayout.CENTER) 23 | contentPane = contentPanel 24 | tfCount.addKeyListener(object : KeyAdapter() { 25 | override fun keyPressed(e: KeyEvent?) { 26 | super.keyPressed(e) 27 | if (e?.keyCode == KeyEvent.VK_ENTER) { 28 | // random(50) 29 | 30 | async { 31 | TianTianFundApi().getNetValueHistory(tfCount.text, 1,30) 32 | }.callback { 33 | val ntv = it.map { b -> b.netValue.toDouble() }.asReversed() 34 | println(ntv.size) 35 | p1.setData(ntv) 36 | p1.invalidate() 37 | p1.repaint() 38 | } 39 | } 40 | } 41 | }) 42 | 43 | } 44 | 45 | private fun random(size: Int) { 46 | val data = mutableListOf() 47 | for (i in 1..size) { 48 | val rate = Random(System.nanoTime()).nextInt(-5, 5).toDouble() / 100.0 49 | pre += pre * rate 50 | data.add(pre) 51 | } 52 | p1.setData(data) 53 | p1.invalidate() 54 | p1.repaint() 55 | pre = 3.0 56 | } 57 | 58 | override fun onOpened() { 59 | super.onOpened() 60 | setBounds(400, 200, 900, 600) 61 | } 62 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/bean/NetValueBean.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api.bean; 2 | 3 | /** 4 | * @author https://github.com/dengzii/ 5 | */ 6 | public class NetValueBean { 7 | private String date; 8 | private float netValue; 9 | private float growthRate; 10 | private String subscribeStatus; 11 | private String redemptionStatus; 12 | private String dividend; 13 | 14 | public String getDate() { 15 | return date; 16 | } 17 | 18 | public void setDate(String date) { 19 | this.date = date; 20 | } 21 | 22 | public float getNetValue() { 23 | return netValue; 24 | } 25 | 26 | public void setNetValue(float netValue) { 27 | this.netValue = netValue; 28 | } 29 | 30 | public float getGrowthRate() { 31 | return growthRate; 32 | } 33 | 34 | public void setGrowthRate(float growthRate) { 35 | this.growthRate = growthRate; 36 | } 37 | 38 | public String getSubscribeStatus() { 39 | return subscribeStatus; 40 | } 41 | 42 | public void setSubscribeStatus(String subscribeStatus) { 43 | this.subscribeStatus = subscribeStatus; 44 | } 45 | 46 | public String getRedemptionStatus() { 47 | return redemptionStatus; 48 | } 49 | 50 | public void setRedemptionStatus(String redemptionStatus) { 51 | this.redemptionStatus = redemptionStatus; 52 | } 53 | 54 | public String getDividend() { 55 | return dividend; 56 | } 57 | 58 | public void setDividend(String dividend) { 59 | this.dividend = dividend; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "NetValueBean{" + 65 | "date='" + date + '\'' + 66 | ", netValue=" + netValue + 67 | ", growthRate=" + growthRate + 68 | ", subscribeStatus='" + subscribeStatus + '\'' + 69 | ", redemptionStatus='" + redemptionStatus + '\'' + 70 | ", dividend='" + dividend + '\'' + 71 | '}'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/stock/EditStockDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui.stock 2 | 3 | import com.dengzii.plugin.fund.api.bean.StockUpdateBean 4 | import com.dengzii.plugin.fund.design.EditStockForm 5 | import com.dengzii.plugin.fund.model.UserStockModel 6 | import com.dengzii.plugin.fund.tools.ui.ColumnInfo 7 | import com.dengzii.plugin.fund.tools.ui.TableAdapter 8 | import com.dengzii.plugin.fund.tools.ui.onClick 9 | 10 | class EditStockDialog( 11 | val origin: MutableMap, 12 | val callback: (Map) -> Unit 13 | ) : EditStockForm("编辑股票列表") { 14 | 15 | private val tableData = mutableListOf>() 16 | private val columnInfos = mutableListOf>() 17 | private val tableAdapter = TableAdapter(tableData, columnInfos) 18 | 19 | companion object { 20 | 21 | fun show(s: Map, callback: (Map) -> Unit) { 22 | val dialog = EditStockDialog(s.toMutableMap(), callback) 23 | dialog.packAndShow() 24 | } 25 | } 26 | 27 | override fun onOpened() { 28 | super.onOpened() 29 | columnInfos.add(ColumnInfo.new("代码", false)) 30 | columnInfos.add(ColumnInfo.new("名称", false)) 31 | columnInfos.add(ColumnInfo.new("成本价", false)) 32 | columnInfos.add(ColumnInfo.new("持有数量", false)) 33 | tableAdapter.setup(table) 34 | tableAdapter.fireTableStructureChanged() 35 | 36 | textFieldSearch.text = origin.keys.joinToString { it } 37 | buttonCancel.onClick { 38 | hideAndDispose() 39 | } 40 | buttonOk.onClick { 41 | val s = textFieldSearch.text.split(",").filter { it.isNotEmpty() } 42 | origin.clear() 43 | s.forEach { 44 | val code = it.trim() 45 | // if (!origin.containsKey(code)) { 46 | origin[code] = UserStockModel(StockUpdateBean()) 47 | // } 48 | } 49 | callback.invoke(origin) 50 | hideAndDispose() 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.dengzii.plugin.fund 3 | Fund Assistant 4 | 1.2.2 5 | dengzi 6 | 7 | A quick and easy way to view fund trends 9 |
10 | Feature 11 |
12 | [x] Fund Search: by name, pinyin, pinyin abbr.
13 | [x] Fund List: sort, basically fields, fall/rise color.
14 | [x] List Edit: batch add by search, delete.
15 | [x] Poll Update: poll fund newest net value.
16 | [x] Ui Theme: three themes, stealth mode.
17 | [x] Custom Column: choose fields your concerned.
18 | [ ] Gains: gains, yield, hold share.
19 | [ ] Detail: time period net value chart, fund status, and etc.
20 | [ ] Alert: trading day today's net value alert.
21 | [ ] Fund Group
22 | [ ] Fund List Export
23 | -
24 | [x] Stock
25 | 
26 |
27 | 功能 28 |
29 | [x] 基金搜索: 名称, 拼音, 拼音简写
30 | [x] 基金列表: 排序, 基本字段, 涨跌染色标记
31 | [x] 基金列表编辑: 通过搜索批量添加, 删除
32 | [x] 基金轮询更新: 定时请求刷新基金数据
33 | [x] 基金主题: 三种主题可选, 隐蔽模式
34 | [x] 基金自定义列: 选择你想要关注的字段
35 | [ ] 基金收益: 收益, 收益率, 持仓
36 | [ ] 基金详情: 时段净值走势图, 持仓信息, 基金状态
37 | [ ] 基金预警: 当日净值变化监控警报
38 | [ ] 基金分组
39 | [ ] 基金列表导出
40 | -
41 | [x] 股票
42 | 
43 | 44 |
45 | GitHub 46 | ]]>
47 | 48 | 51 | update: fund search, press enter to search. 52 | ]]> 53 | 54 | 55 | 56 | com.intellij.modules.platform 57 | 58 | 59 | 67 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
-------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/MainToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui 2 | 3 | import com.dengzii.plugin.fund.PluginConfig 4 | import com.dengzii.plugin.fund.api.TianTianFundApi 5 | import com.dengzii.plugin.fund.api.bean.FundBean 6 | import com.dengzii.plugin.fund.tools.NotificationUtils 7 | import com.dengzii.plugin.fund.tools.invokeLater 8 | import com.dengzii.plugin.fund.ui.fund.FundPanel 9 | import com.dengzii.plugin.fund.ui.stock.StockPanel 10 | import com.dengzii.plugin.fund.utils.async 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.wm.ToolWindow 13 | import com.intellij.openapi.wm.ToolWindowFactory 14 | import com.jetbrains.rd.util.string.print 15 | 16 | /** 17 | * @author https://github.com/dengzii/ 18 | */ 19 | class MainToolWindowFactory : ToolWindowFactory { 20 | 21 | private val panels = mutableListOf() 22 | 23 | override fun init(toolWindow: ToolWindow) { 24 | panels.forEach { 25 | it.onInit(toolWindow) 26 | } 27 | } 28 | 29 | override fun shouldBeAvailable(project: Project) = true 30 | 31 | // override fun isApplicable(project: Project) = true 32 | 33 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 34 | if (panels.isEmpty()) { 35 | panels.add(FundPanel()) 36 | panels.add(StockPanel()) 37 | } 38 | async { 39 | try { 40 | TianTianFundApi().fundList 41 | } catch (e: Exception) { 42 | e.printStackTrace() 43 | invokeLater { 44 | NotificationUtils.showError("请求基金列表接口出错了: ${e.message}", "Found Assistant") 45 | } 46 | emptyList() 47 | } 48 | }.callback { 49 | PluginConfig.allFund = it 50 | } 51 | 52 | toolWindow.activate { 53 | panels.forEach { 54 | it.onWindowActive() 55 | } 56 | } 57 | toolWindow.hide { 58 | panels.forEach { 59 | it.onWindowHide() 60 | } 61 | } 62 | toolWindow.show { 63 | panels.forEach { 64 | it.onWindowShow() 65 | } 66 | } 67 | panels.forEach { 68 | it.onCreate(project, toolWindow) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /test/com/dengzii/plugin/fund/api/TianTianFundApiTest.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import com.dengzii.plugin.fund.api.bean.FundBean; 4 | import com.dengzii.plugin.fund.api.bean.NetValueBean; 5 | import junit.framework.TestCase; 6 | 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * @author https://github.com/dengzii/ 13 | */ 14 | public class TianTianFundApiTest extends TestCase { 15 | 16 | public void testGetNetValueHistory3Month() { 17 | 18 | TianTianFundApi api = new TianTianFundApi(); 19 | List netValueBeans = api.getNetValueHistory3Month("000235"); 20 | for (NetValueBean netValueBean : netValueBeans) { 21 | System.out.println(netValueBean); 22 | } 23 | } 24 | 25 | public void testGetAllFunds() { 26 | 27 | TianTianFundApi api = new TianTianFundApi(); 28 | List allFunds = null; 29 | try { 30 | allFunds = api.getFundList(); 31 | } catch (IOException | InterruptedException e) { 32 | e.printStackTrace(); 33 | } 34 | System.out.println(allFunds); 35 | } 36 | 37 | public void testGetNewestNetValue() { 38 | 39 | TianTianFundApi api = new TianTianFundApi(); 40 | System.out.println(api.getNewestNetValue("000235")); 41 | 42 | } 43 | 44 | public void testTestGetNetValueHistory3Month() { 45 | } 46 | 47 | public void testGetNetValueHistory() { 48 | } 49 | 50 | public void testGetFundNewestDetail() { 51 | 52 | TianTianFundApi api = new TianTianFundApi(); 53 | System.out.println(api.getFundNewestDetail("160220")); 54 | } 55 | 56 | public void testUpdateFundList() throws InterruptedException { 57 | 58 | List fundBeans = new ArrayList<>(); 59 | fundBeans.add(new FundBean("160220")); 60 | fundBeans.add(new FundBean("000001")); 61 | fundBeans.add(new FundBean("000002")); 62 | 63 | TianTianFundApi api = new TianTianFundApi(); 64 | AbstractPollTask> pollTask = api.updateFundList(fundBeans); 65 | pollTask.subscribe(result -> { 66 | for (FundBean fundBean : result) { 67 | System.out.println(fundBean); 68 | } 69 | }); 70 | 71 | pollTask.start(500); 72 | Thread.sleep(20000); 73 | pollTask.stop(); 74 | Thread.sleep(10000); 75 | } 76 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/ActionToolBarUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.actionSystem.CheckedActionGroup 6 | import com.intellij.openapi.actionSystem.impl.ActionButton 7 | import com.intellij.openapi.actionSystem.impl.PresentationFactory 8 | import com.intellij.ui.AnActionButton 9 | import com.intellij.ui.AnActionButton.AnActionButtonWrapper 10 | import com.intellij.ui.AnActionButton.CheckedAnActionButton 11 | import com.intellij.util.ui.JBUI 12 | import java.awt.Dimension 13 | import java.awt.FlowLayout 14 | import javax.swing.Icon 15 | import javax.swing.JComponent 16 | import javax.swing.JPanel 17 | 18 | object ActionToolBarUtils { 19 | 20 | fun create(place: String, action: List): JComponent { 21 | 22 | val panel = JPanel() 23 | panel.layout = FlowLayout().also { 24 | it.alignment = FlowLayout.LEFT 25 | it.hgap = 0 26 | it.vgap = 0 27 | } 28 | val factory = PresentationFactory() 29 | action.forEach { 30 | val presentation = factory.getPresentation(it).also { p -> 31 | p.icon = it.icon 32 | p.isEnabled = it.isEnabled 33 | p.description = it.desc 34 | } 35 | val bt = ActionButton(it, presentation, place, Dimension(22, 24)) 36 | bt.setIconInsets(JBUI.insets(0)) 37 | panel.add(bt) 38 | } 39 | return panel 40 | } 41 | 42 | fun createActionButton(hint: String, icon: Icon, block: (AnActionEvent) -> Unit): AnActionButton { 43 | val action = object:AnAction(){ 44 | override fun actionPerformed(p0: AnActionEvent) { 45 | block(p0) 46 | } 47 | } 48 | val presentation = action.templatePresentation 49 | presentation.icon = icon 50 | presentation.description = hint 51 | presentation.text = hint 52 | val button = AnActionButtonWrapper(presentation, action) 53 | button.shortcut = action.shortcutSet 54 | return button 55 | } 56 | 57 | class Action( 58 | var icon: Icon, 59 | var isEnabled: Boolean = true, 60 | var desc: String = "", 61 | var action: () -> Unit 62 | ) : AnAction() { 63 | 64 | override fun actionPerformed(p0: AnActionEvent) { 65 | action.invoke() 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/PluginConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund 2 | 3 | import com.dengzii.plugin.fund.conf.FundColConfig 4 | import com.dengzii.plugin.fund.conf.FundTheme 5 | import com.dengzii.plugin.fund.design.SettingsForm 6 | import com.intellij.openapi.options.SearchableConfigurable 7 | import java.awt.Dimension 8 | import javax.swing.JCheckBox 9 | import javax.swing.JComponent 10 | 11 | /** 12 | * @author https://github.com/dengzii/ 13 | */ 14 | class PluginConfigurable : SearchableConfigurable { 15 | 16 | private lateinit var form: SettingsForm 17 | private val colCheckBox = mutableListOf() 18 | 19 | companion object { 20 | private var listener: (() -> Unit)? = null 21 | 22 | fun setConfigChangeListener(listener: (() -> Unit)?) { 23 | this.listener = listener 24 | } 25 | } 26 | 27 | override fun createComponent(): JComponent? { 28 | form = SettingsForm() 29 | colCheckBox.clear() 30 | 31 | form.comboBoxTheme.selectedIndex = FundTheme.values().indexOf(PluginConfig.fundTheme) 32 | form.comboBoxDuration.selectedIndex = PluginConfig.fundRefreshDuration / 30 - 1 33 | 34 | val defaultCol = FundColConfig() 35 | val colConfig = PluginConfig.fundColConfig 36 | FundColConfig.Col.values().forEach { 37 | val cb = JCheckBox(it.getName()) 38 | cb.isSelected = colConfig.columns.contains(it) 39 | cb.isEnabled = defaultCol.columns.contains(it) 40 | colCheckBox.add(cb) 41 | form.panelColConf.add(cb) 42 | } 43 | val s = Dimension(form.contentPanel.width, form.contentPanel.height) 44 | form.panelColConf.preferredSize = s 45 | 46 | return form.contentPanel 47 | } 48 | 49 | override fun isModified(): Boolean { 50 | return true 51 | } 52 | 53 | override fun apply() { 54 | val enabledCol = mutableListOf() 55 | colCheckBox.forEach { 56 | if (it.isSelected) { 57 | enabledCol.add(FundColConfig.Col.getByName(it.text)) 58 | } 59 | } 60 | PluginConfig.fundColConfig = FundColConfig(enabledCol) 61 | PluginConfig.fundTheme = FundTheme.values()[form.comboBoxTheme.selectedIndex] 62 | PluginConfig.fundRefreshDuration = form.comboBoxDuration.selectedItem?.toString()?.toInt() ?: 20 63 | listener?.invoke() 64 | } 65 | 66 | override fun getDisplayName(): String { 67 | return "Fund Assistant" 68 | } 69 | 70 | override fun getId(): String { 71 | return "preferences.fund" 72 | } 73 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/TableColumnModelDecorator.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import java.util.* 4 | import javax.swing.ListSelectionModel 5 | import javax.swing.event.TableColumnModelListener 6 | import javax.swing.table.TableColumn 7 | import javax.swing.table.TableColumnModel 8 | 9 | open class TableColumnModelDecorator(private val wrapper: TableColumnModel) : TableColumnModel { 10 | 11 | override fun addColumn(aColumn: TableColumn?) { 12 | wrapper.addColumn(aColumn) 13 | } 14 | 15 | override fun removeColumn(column: TableColumn?) { 16 | wrapper.removeColumn(column) 17 | } 18 | 19 | override fun moveColumn(columnIndex: Int, newIndex: Int) { 20 | wrapper.moveColumn(columnIndex, newIndex) 21 | } 22 | 23 | override fun setColumnMargin(newMargin: Int) { 24 | wrapper.columnMargin = newMargin 25 | } 26 | 27 | override fun getColumnCount(): Int { 28 | return wrapper.columnCount 29 | } 30 | 31 | override fun getColumns(): Enumeration { 32 | return wrapper.columns 33 | } 34 | 35 | override fun getColumnIndex(columnIdentifier: Any?): Int { 36 | return wrapper.getColumnIndex(columnIdentifier) 37 | } 38 | 39 | override fun getColumn(columnIndex: Int): TableColumn { 40 | return wrapper.getColumn(columnIndex) 41 | } 42 | 43 | override fun getColumnMargin(): Int { 44 | return wrapper.columnMargin 45 | } 46 | 47 | override fun getColumnIndexAtX(xPosition: Int): Int { 48 | return wrapper.getColumnIndexAtX(xPosition) 49 | } 50 | 51 | override fun getTotalColumnWidth(): Int { 52 | return wrapper.totalColumnWidth 53 | } 54 | 55 | override fun setColumnSelectionAllowed(flag: Boolean) { 56 | wrapper.columnSelectionAllowed = flag 57 | } 58 | 59 | override fun getColumnSelectionAllowed(): Boolean { 60 | return wrapper.columnSelectionAllowed 61 | } 62 | 63 | override fun getSelectedColumns(): IntArray { 64 | return wrapper.selectedColumns 65 | } 66 | 67 | override fun getSelectedColumnCount(): Int { 68 | return wrapper.selectedColumnCount 69 | } 70 | 71 | override fun setSelectionModel(newModel: ListSelectionModel?) { 72 | wrapper.selectionModel = newModel 73 | } 74 | 75 | override fun getSelectionModel(): ListSelectionModel { 76 | return wrapper.selectionModel 77 | } 78 | 79 | override fun addColumnModelListener(x: TableColumnModelListener?) { 80 | wrapper.addColumnModelListener(x) 81 | } 82 | 83 | override fun removeColumnModelListener(x: TableColumnModelListener?) { 84 | wrapper.removeColumnModelListener(x) 85 | } 86 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/PluginConfig.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund 2 | 3 | import com.dengzii.plugin.fund.api.bean.FundBean 4 | import com.dengzii.plugin.fund.conf.FundColConfig 5 | import com.dengzii.plugin.fund.conf.FundTheme 6 | import com.dengzii.plugin.fund.conf.StockColConfig 7 | import com.dengzii.plugin.fund.model.FundGroup 8 | import com.dengzii.plugin.fund.model.UserStockModel 9 | import com.dengzii.plugin.fund.tools.PersistentConfig 10 | import com.dengzii.plugin.fund.utils.GsonUtils 11 | import com.google.gson.reflect.TypeToken 12 | import com.intellij.ide.util.PropertiesComponent 13 | import java.lang.reflect.Type 14 | 15 | /** 16 | * @author https://github.com/dengzii/ 17 | */ 18 | object PluginConfig : PersistentConfig() { 19 | 20 | var stockList 21 | get() = load("StockList", mapOf(), 22 | object : TypeToken>() {}.type 23 | ) 24 | set(value) = save("StockList", value) 25 | 26 | var stockColConfig 27 | get() = load("StockColConfig", StockColConfig(), object : TypeToken() {}.type) 28 | set(value) = save("StockColConfig", value) 29 | 30 | var fundColConfig 31 | get() = load("FundColConfig", FundColConfig(), object : TypeToken() {}.type) 32 | set(value) = save("FundColConfig", value) 33 | 34 | var fundTheme 35 | get() = load("FundTheme", FundTheme.Default, object : TypeToken() {}.type) 36 | set(value) = save("FundTheme", value) 37 | 38 | var fundRefreshDuration: Int 39 | get() = PropertiesComponent.getInstance().getValue("FundRefreshDuration", "60").toInt() 40 | set(value) = PropertiesComponent.getInstance().setValue("FundRefreshDuration", value.toString()) 41 | 42 | 43 | var fundGroup: Map 44 | get() = load("FundGroups", mapOf(), object : TypeToken>() {}.type) 45 | set(value) = save("FundGroups", value) 46 | 47 | var allFund: List 48 | get() = load("AllFunds", listOf(), object : TypeToken>() {}.type) 49 | set(value) = save("AllFunds", value) 50 | 51 | 52 | private fun load(key: String, default: T, type: Type): T { 53 | val v = PropertiesComponent.getInstance().getValue(key) 54 | if (v.isNullOrBlank()) { 55 | return default 56 | } 57 | return try { 58 | GsonUtils.fromJson(v, type) 59 | } catch (e: Throwable) { 60 | save(key, default!!) 61 | default 62 | } 63 | } 64 | 65 | private fun save(key: String, value: Any) { 66 | PropertiesComponent.getInstance().setValue(key, GsonUtils.toJson(value)) 67 | } 68 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/conf/FundColConfig.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.conf 2 | 3 | import com.dengzii.plugin.fund.model.UserFundModel 4 | import java.text.DecimalFormat 5 | 6 | /** 7 | * @author https://github.com/dengzii/ 8 | */ 9 | class FundColConfig(val columns: List) { 10 | 11 | constructor() : this( 12 | mutableListOf( 13 | Col.FundName, 14 | Col.FundCode, 15 | Col.CurrentNetValue, 16 | Col.NetValueReckon, 17 | Col.GrowthRateReckon, 18 | Col.UpdateTime, 19 | Col.Last30DayNetValue, 20 | // Col.BuyingPrice, 21 | // Col.TotalYield, 22 | // Col.TotalGains, 23 | // Col.HoldingShare, 24 | // Col.GainsReckon 25 | ) 26 | ) 27 | 28 | 29 | enum class Col(private val s: String) { 30 | FundCode("代码"), 31 | FundName("名称"), 32 | CurrentNetValue("当前净值"), 33 | NetValueReckon("估算净值"), 34 | GrowthRateReckon("估算浮动"), 35 | Last30DayNetValue("30日净值"), 36 | BuyingPrice("买入净值"), 37 | TotalYield("收益率"), 38 | TotalGains("总收益"), 39 | HoldingShare("持有份额"), 40 | GainsReckon("估算收益"), 41 | UpdateTime("更新时间"); 42 | 43 | companion object { 44 | private val format = DecimalFormat("###,###.##") 45 | private val formatPercent = DecimalFormat("###,###.##%") 46 | val valueMap = values().associateBy { it.s } 47 | 48 | fun getByName(s: String): Col { 49 | return valueMap[s]!! 50 | } 51 | } 52 | 53 | fun getName(): String { 54 | return s 55 | } 56 | 57 | fun getAttr(model: UserFundModel): Any { 58 | return when (this) { 59 | FundCode -> model.fundBean.fundCode 60 | FundName -> model.fundBean.fundName 61 | CurrentNetValue -> model.fundBean.netValue 62 | UpdateTime -> model.fundBean.updateTime ?: "-" 63 | GrowthRateReckon -> model.fundBean.growthRateReckon.formatPercent() 64 | NetValueReckon -> model.fundBean.netValueReckon.format() 65 | HoldingShare -> model.holdingShare.format() 66 | BuyingPrice -> model.buyingPrice.format() 67 | TotalYield -> model.totalYield.formatPercent() 68 | TotalGains -> model.totalGains.format() 69 | GainsReckon -> model.gainsReckon.format() 70 | Last30DayNetValue -> model.fundBean.last30DayNetValue ?: "-" 71 | } 72 | } 73 | 74 | private fun Float.format() = format.format(this) 75 | private fun Float.formatPercent() = formatPercent.format(this / 100) 76 | } 77 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools 2 | 3 | import com.intellij.notification.Notification 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.notification.Notifications 6 | import com.intellij.openapi.Disposable 7 | import com.intellij.openapi.application.ApplicationManager 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.util.Disposer 10 | import com.intellij.util.Alarm 11 | 12 | /** 13 | *
14 |  * author : dengzi
15 |  * e-mail : dengzii@foxmail.com
16 |  * github : https://github.com/dengzii
17 |  * time   : 2020/11/18
18 |  * desc   : Utils about Notification.
19 | 
* 20 | */ 21 | object NotificationUtils { 22 | 23 | var defaultTitle = "Notification" 24 | var defaultGroupId = "Untitled_Group" 25 | 26 | fun getInfo(msg: String, title: String = defaultTitle, groupId: String = defaultGroupId): Notification { 27 | return Notification(groupId, null, NotificationType.INFORMATION).also { 28 | it.setContent(msg) 29 | it.setTitle(title) 30 | Notifications.Bus.notify(it, null) 31 | } 32 | } 33 | 34 | fun getError(msg: String, title: String = defaultTitle, groupId: String = defaultGroupId): Notification { 35 | return Notification(groupId, null, NotificationType.ERROR).also { 36 | it.setContent(msg) 37 | it.setTitle(title) 38 | it.isImportant = true 39 | Notifications.Bus.notify(it, null) 40 | } 41 | } 42 | 43 | fun getWarning(msg: String, title: String = defaultTitle, groupId: String = defaultGroupId): Notification { 44 | return Notification(groupId, null, NotificationType.WARNING).also { 45 | it.setContent(msg) 46 | it.setTitle(title) 47 | Notifications.Bus.notify(it, null) 48 | } 49 | } 50 | 51 | fun showInfo(msg: String, title: String = defaultTitle): Notification { 52 | return getInfo(msg, title).show() 53 | } 54 | 55 | fun showError(msg: String, title: String = defaultTitle): Notification { 56 | return getError(msg, title).show() 57 | } 58 | 59 | fun showWarning(msg: String, title: String = defaultTitle): Notification { 60 | return getWarning(msg, title).show() 61 | } 62 | 63 | fun Notification.show(expireMillis: Long? = null, project: Project? = null): Notification { 64 | Notifications.Bus.notify(this, project) 65 | if (expireMillis != null) { 66 | val alarm = Alarm(((project ?: ApplicationManager.getApplication()) as Disposable)) 67 | alarm.addRequest({ 68 | expire() 69 | Disposer.dispose(alarm) 70 | }, expireMillis) 71 | } 72 | return this 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/ColumnInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import com.intellij.ui.components.JBLabel 4 | import com.intellij.ui.components.JBTextField 5 | import java.awt.Component 6 | 7 | 8 | /** 9 | * The model of the column of [JTable]. 10 | * 11 | * @author https://github.com/dengzii 12 | */ 13 | open class ColumnInfo(val colName: String) { 14 | 15 | private var editable: Boolean = false 16 | 17 | open val columnClass: Class<*> get() = String::class.java 18 | 19 | open var columnWidth: Int? = null 20 | 21 | constructor(colName: String, editable: Boolean) : this(colName) { 22 | this.editable = editable 23 | } 24 | 25 | companion object { 26 | fun of(vararg columns: String): List> { 27 | return columns.map { ColumnInfo(it) } 28 | } 29 | 30 | fun new(colName: String, editable: Boolean): ColumnInfo { 31 | return ColumnInfo(colName, editable) 32 | } 33 | } 34 | 35 | /** 36 | * Return the value of column when edit finish, if this column is editable. 37 | * 38 | * @param component The edit component. 39 | * @param oldValue The old value before edit. 40 | * @return A new value. 41 | */ 42 | open fun getEditorValue(component: Component, oldValue: Item?, row: Int, col: Int): Item? { 43 | return null 44 | } 45 | 46 | /** 47 | * Whether this column editable. 48 | * @param item The item value. 49 | * @return True is editable, otherwise not. 50 | */ 51 | open fun isCellEditable(item: Item?): Boolean { 52 | return editable 53 | } 54 | 55 | /** 56 | * Return a [Component] use for display item value. 57 | * @param item The item value. 58 | * @return The component. 59 | */ 60 | open fun getRendererComponent(item: Item?, row: Int, col: Int): Component { 61 | return JBLabel(item?.toString().orEmpty()) 62 | } 63 | 64 | /** 65 | * Return a [Component] use for edit this column. 66 | * 67 | * Working just when [isCellEditable] returns true. 68 | * 69 | * @param item The item value. 70 | * @return The component. 71 | */ 72 | open fun getEditComponent(item: Item?, row: Int, col: Int): Component { 73 | return JBTextField(item?.toString().orEmpty()) 74 | } 75 | 76 | override fun equals(other: Any?): Boolean { 77 | return if (this === other) { 78 | true 79 | } else if (other != null && this.javaClass == other.javaClass) { 80 | val that = other as ColumnInfo<*> 81 | colName == that.colName 82 | } else { 83 | false 84 | } 85 | } 86 | 87 | override fun hashCode(): Int { 88 | return colName.hashCode() 89 | } 90 | 91 | override fun toString(): String { 92 | return colName 93 | } 94 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/conf/StockColConfig.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.conf 2 | 3 | import com.dengzii.plugin.fund.model.UserStockModel 4 | import java.text.DecimalFormat 5 | 6 | /** 7 | * @author https://github.com/dengzii/ 8 | */ 9 | class StockColConfig(val columns: List) { 10 | 11 | constructor() : this( 12 | mutableListOf( 13 | Col.StockCode, 14 | Col.StockName, 15 | Col.CurrentPrice, 16 | Col.DayHighPrice, 17 | Col.DayLowPrice, 18 | Col.TodayOpenPrice, 19 | Col.YesterdayClosePrice, 20 | Col.UpdateDate, 21 | Col.UpdateTime, 22 | ) 23 | ) 24 | 25 | enum class Col(private val s: String) { 26 | StockCode("代码"), 27 | StockName("名称"), 28 | CurrentPrice("当前价格"), 29 | DayHighPrice("今日最高价"), 30 | DayLowPrice("今日最低价"), 31 | TodayOpenPrice("今日开盘价"), 32 | YesterdayClosePrice("昨日收盘价"), 33 | PriceByOne("买二"), 34 | DealShares("成交量"), 35 | TotalDealAmount("成交金额"), 36 | FloatGains("浮动收益"), 37 | Trending("日走势"), 38 | TodayGainsReckon("今日收益参考"), 39 | UpdateDate("更新日期"), 40 | UpdateTime("更新时间"); 41 | 42 | companion object { 43 | private val format = DecimalFormat("###,###.##") 44 | private val formatPercent = DecimalFormat("###,###.##%") 45 | private val valueMap = values().associateBy { it.s } 46 | 47 | fun getByName(s: String): Col { 48 | return valueMap[s]!! 49 | } 50 | } 51 | 52 | fun getName(): String { 53 | return s 54 | } 55 | 56 | fun getAttr(model: UserStockModel): Any { 57 | val bean = model.stock 58 | try { 59 | return when (this) { 60 | StockCode -> bean.code 61 | StockName -> bean.name 62 | CurrentPrice -> bean.currentPrice.format() 63 | DayHighPrice -> bean.todayHighPrice.format() 64 | DayLowPrice -> bean.todayLowPrice.format() 65 | PriceByOne -> bean.priceByTwo.format() 66 | DealShares -> bean.dealShares.format() 67 | TotalDealAmount -> bean.totalTransactionAmount.format() 68 | TodayOpenPrice -> bean.todayOpeningPrice.format() 69 | YesterdayClosePrice -> bean.yesterdayClosingPrice.format() 70 | FloatGains -> "" 71 | Trending -> "" 72 | TodayGainsReckon -> "" 73 | UpdateDate -> bean.date 74 | UpdateTime -> bean.time 75 | } 76 | } catch (e: Throwable) { 77 | return "-" 78 | } 79 | } 80 | 81 | private fun Float.format() = format.format(this) 82 | private fun Float.formatPercent() = formatPercent.format(this / 100) 83 | } 84 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/http/Http.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.http 2 | 3 | import com.dengzii.plugin.fund.utils.Logger 4 | import org.apache.http.HttpResponse 5 | import org.apache.http.client.HttpClient 6 | import org.apache.http.client.config.RequestConfig 7 | import org.apache.http.client.methods.HttpGet 8 | import org.apache.http.client.methods.RequestBuilder 9 | import org.apache.http.impl.client.HttpClients 10 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager 11 | import java.io.BufferedReader 12 | import java.io.IOException 13 | import java.io.InputStreamReader 14 | import java.nio.charset.Charset 15 | import java.util.concurrent.TimeUnit 16 | 17 | /** 18 | * @author https://github.com/dengzii/ 19 | */ 20 | class Http { 21 | 22 | private lateinit var client: HttpClient 23 | 24 | companion object { 25 | 26 | private var instance: Http? = null 27 | 28 | @JvmStatic 29 | fun getInstance(): Http { 30 | synchronized(Http::class.java) { 31 | if (instance == null) { 32 | instance = Http() 33 | val connectionManager = PoolingHttpClientConnectionManager(100, TimeUnit.SECONDS) 34 | connectionManager.maxTotal = 200 35 | connectionManager.defaultMaxPerRoute = 100 36 | val requestConfig = 37 | RequestConfig.custom().setConnectionRequestTimeout(2000).setSocketTimeout(2000).build() 38 | instance!!.client = 39 | HttpClients.custom().setConnectionManager(connectionManager) 40 | .setDefaultRequestConfig(requestConfig) 41 | .build() 42 | } 43 | return instance!! 44 | } 45 | } 46 | } 47 | 48 | @Throws(IOException::class, InterruptedException::class) 49 | fun get(url: String): String? { 50 | return get(url, Charset.forName("utf8")) 51 | } 52 | 53 | @Throws(IOException::class, InterruptedException::class) 54 | fun get(url: String, charset: Charset): String? { 55 | val get = HttpGet(url) 56 | 57 | val response: HttpResponse = client.execute(get) 58 | Logger.log("Http.get", "${response.statusLine.statusCode} $url") 59 | if (response.statusLine.statusCode != 200) { 60 | throw IOException("Error, HTTP Code ${response.statusLine.statusCode}") 61 | } 62 | val br = response.entity.content.bufferedReader(charset) 63 | val text = br.readText() 64 | get.releaseConnection() 65 | return text 66 | } 67 | 68 | 69 | @Throws(IOException::class) 70 | fun post(url: String, param: Map): String { 71 | val requestBuilder = RequestBuilder.post(url) 72 | param.forEach { (name: String?, value: String?) -> requestBuilder.addParameter(name, value) } 73 | val response: HttpResponse = client.execute(requestBuilder.build()) 74 | val inputStream = response.entity.content 75 | val builder = StringBuilder() 76 | val br = BufferedReader(InputStreamReader(inputStream)) 77 | while (true) { 78 | val s = br.readLine() 79 | if (s == null || s.isEmpty()) { 80 | break 81 | } 82 | builder.append(s) 83 | } 84 | return builder.toString() 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/AbstractPollTask.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import java.text.ParseException; 4 | import java.text.SimpleDateFormat; 5 | import java.util.ArrayList; 6 | import java.util.Calendar; 7 | import java.util.Date; 8 | import java.util.List; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.ScheduledFuture; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * @author https://github.com/dengzii/ 16 | */ 17 | public abstract class AbstractPollTask implements PollTask, SubScribeSource { 18 | 19 | private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); 20 | private final List> subscribers = new ArrayList<>(); 21 | 22 | private ScheduledFuture future; 23 | 24 | private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm"); 25 | private final Calendar nowTime = Calendar.getInstance(); 26 | private Calendar beginTime; 27 | private Calendar endTime; 28 | 29 | public AbstractPollTask() { 30 | try { 31 | Date begin = dateFormat.parse("9:00"); 32 | Date end = dateFormat.parse("15:00"); 33 | beginTime = Calendar.getInstance(); 34 | beginTime.setTime(begin); 35 | endTime = Calendar.getInstance(); 36 | endTime.setTime(end); 37 | } catch (ParseException e) { 38 | e.printStackTrace(); 39 | } 40 | } 41 | 42 | @Override 43 | public void start(long durationMilliSec) { 44 | if (future != null && !future.isCancelled()) { 45 | future.cancel(true); 46 | } 47 | future = executor.scheduleAtFixedRate(() -> { 48 | T result; 49 | try { 50 | result = update(); 51 | } catch (Throwable t) { 52 | t.printStackTrace(); 53 | checkStop(); 54 | return; 55 | } 56 | for (Subscriber subscriber : subscribers) { 57 | subscriber.onUpdate(result); 58 | } 59 | checkStop(); 60 | }, 0, durationMilliSec, TimeUnit.MILLISECONDS); 61 | } 62 | 63 | private void checkStop(){ 64 | if (!isTimeRange()) { 65 | stop(); 66 | } 67 | } 68 | 69 | private boolean isTimeRange() { 70 | Date now = null; 71 | try { 72 | now = dateFormat.parse(dateFormat.format(new Date())); 73 | } catch (ParseException e) { 74 | return false; 75 | } 76 | nowTime.setTime(now); 77 | return nowTime.before(endTime) && nowTime.after(beginTime); 78 | } 79 | 80 | @Override 81 | public void stop() { 82 | try { 83 | future.cancel(true); 84 | }catch (Throwable t){ 85 | t.printStackTrace(); 86 | } 87 | } 88 | 89 | @Override 90 | public boolean isStopped() { 91 | return future.isCancelled(); 92 | } 93 | 94 | @Override 95 | public void subscribe(Subscriber subscriber) { 96 | subscribers.add(subscriber); 97 | } 98 | 99 | @Override 100 | public void unsubscribe(Subscriber subscriber) { 101 | subscribers.remove(subscriber); 102 | } 103 | 104 | @Override 105 | public void unsubscribeAll() { 106 | subscribers.clear(); 107 | } 108 | 109 | abstract T update(); 110 | } 111 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/SinaStockApi.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import com.dengzii.plugin.fund.api.bean.StockUpdateBean; 4 | import com.dengzii.plugin.fund.http.Http; 5 | 6 | import java.io.IOException; 7 | import java.nio.charset.Charset; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class SinaStockApi implements StockApi { 13 | 14 | @Override 15 | public AbstractPollTask> subscribeUpdate(List stockList) { 16 | return new AbstractPollTask>() { 17 | @Override 18 | List update() { 19 | if (stockList.isEmpty()) { 20 | return Collections.emptyList(); 21 | } 22 | List updateBeans = new ArrayList<>(stockList.size()); 23 | StringBuilder b = new StringBuilder("http://hq.sinajs.cn/list="); 24 | for (String s : stockList) { 25 | b.append(s).append(","); 26 | } 27 | try { 28 | String response = Http.getInstance().get(b.toString(), Charset.forName("gb18030")); 29 | if (response == null) { 30 | return Collections.emptyList(); 31 | } 32 | response = response.replaceAll("(var hq_str_s[zh]\\d+=\")|(\";)", ""); 33 | String[] lines = response.split("\n"); 34 | if (lines.length != stockList.size()) { 35 | return Collections.emptyList(); 36 | } 37 | for (int i = 0; i < lines.length; i++) { 38 | String line = lines[i]; 39 | String[] p = line.split(","); 40 | StockUpdateBean bean = new StockUpdateBean(); 41 | bean.setCode(stockList.get(i)); 42 | bean.setName(p[0]); 43 | bean.setTodayOpeningPrice(parseFloat(p[1])); 44 | bean.setYesterdayClosingPrice(parseFloat(p[2])); 45 | bean.setCurrentPrice(parseFloat(p[3])); 46 | bean.setTodayHighPrice(parseFloat(p[4])); 47 | bean.setTodayLowPrice(parseFloat(p[5])); 48 | bean.setUnitPriceByOne(parseFloat(p[6])); 49 | bean.setUnitPriceSaleOne(parseFloat(p[7])); 50 | bean.setUnitPriceByTwo(parseFloat(p[13])); 51 | bean.setPriceByTwo(parseFloat(p[12])); 52 | bean.setDealShares(parseFloat(p[8])); 53 | bean.setTotalTransactionAmount(parseFloat(p[9])); 54 | bean.setDate(p[30]); 55 | bean.setTime(p[31]); 56 | updateBeans.add(bean); 57 | } 58 | System.out.println("SinaStockApi.update: " + updateBeans.size()); 59 | return updateBeans; 60 | } catch (IOException e) { 61 | e.printStackTrace(); 62 | } catch (InterruptedException e) { 63 | // ignore 64 | } 65 | return Collections.emptyList(); 66 | } 67 | }; 68 | } 69 | 70 | private static float parseFloat(String str) { 71 | 72 | try { 73 | return Float.parseFloat(str); 74 | } catch (Throwable e) { 75 | return 0.0f; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/EditStockForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/XDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.util.WindowStateService 5 | import java.awt.Dimension 6 | import java.awt.Point 7 | import java.awt.Rectangle 8 | import java.awt.Toolkit 9 | import java.awt.event.KeyEvent 10 | import java.awt.event.WindowAdapter 11 | import java.awt.event.WindowEvent 12 | import javax.annotation.OverridingMethodsMustInvokeSuper 13 | import javax.swing.JComponent 14 | import javax.swing.JDialog 15 | import javax.swing.JPanel 16 | import javax.swing.KeyStroke 17 | 18 | /** 19 | * The class implements some frequently-used feature of [JDialog]. 20 | * 21 | * - Add dialog lifecycle callback, like opened, closed etc. 22 | * - Default be modal. 23 | * - Default dispose on dialog close. 24 | * - Persist the size, location, and restore it the next time opened. 25 | * 26 | * @author https://github.com/dengzii 27 | */ 28 | abstract class XDialog() : JDialog() { 29 | 30 | var persistDialogState = true 31 | var project: Project? = null 32 | 33 | private val keyWindowStatePersist = this::class.java.name 34 | 35 | constructor(title: String) : this() { 36 | this.title = title 37 | } 38 | 39 | init { 40 | isModal = true 41 | defaultCloseOperation = DISPOSE_ON_CLOSE 42 | addWindowListener(object : WindowAdapter() { 43 | 44 | override fun windowOpened(e: WindowEvent?) { 45 | onOpened() 46 | (contentPane as? JPanel)?.registerKeyboardAction({ 47 | onClosing() 48 | hideAndDispose() 49 | }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) 50 | super.windowOpened(e) 51 | } 52 | 53 | override fun windowClosing(e: WindowEvent) { 54 | onClosing() 55 | } 56 | 57 | override fun windowClosed(e: WindowEvent?) { 58 | super.windowClosed(e) 59 | onClosed() 60 | } 61 | }) 62 | } 63 | 64 | fun getLocationCenterOfScreen(): Point { 65 | val screen = toolkit.screenSize 66 | val x = screen.width / 2 - width / 2 67 | val y = screen.height / 2 - height / 2 68 | return Point(x, y) 69 | } 70 | 71 | fun packAndShow() { 72 | pack() 73 | isVisible = true 74 | } 75 | 76 | fun hideAndDispose() { 77 | isVisible = false 78 | dispose() 79 | } 80 | 81 | @OverridingMethodsMustInvokeSuper 82 | open fun onOpened() { 83 | if (persistDialogState) { 84 | restoreState() 85 | } 86 | } 87 | 88 | @OverridingMethodsMustInvokeSuper 89 | open fun onClosing() { 90 | if (persistDialogState) { 91 | persistState() 92 | } 93 | } 94 | 95 | @OverridingMethodsMustInvokeSuper 96 | open fun onClosed() { 97 | 98 | } 99 | 100 | /** 101 | * Doing some work about persistent. 102 | */ 103 | open fun persistState() { 104 | WindowStateService.getInstance().putBounds(keyWindowStatePersist, Rectangle(x, y, width, height)) 105 | } 106 | 107 | /** 108 | * Restore the state of dialog. 109 | */ 110 | open fun restoreState() { 111 | val bounds = WindowStateService.getInstance().getBounds(keyWindowStatePersist) 112 | ?: Rectangle().apply { 113 | val defaultSize = getDefaultSize() 114 | val screenSize = Toolkit.getDefaultToolkit().screenSize 115 | if (defaultSize == null) { 116 | height = 300 117 | width = 500 118 | } else { 119 | height = defaultSize.height 120 | width = defaultSize.width 121 | } 122 | y = screenSize!!.height / 2 - height / 2 123 | x = screenSize.width / 2 - width / 2 124 | } 125 | setBounds(bounds) 126 | } 127 | 128 | open fun getDefaultSize(): Dimension? { 129 | return null 130 | } 131 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/bean/FundBean.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api.bean; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author https://github.com/dengzii/ 9 | */ 10 | public class FundBean { 11 | 12 | @SerializedName(value = "fundcode", alternate = {"fundCode"}) 13 | private String fundCode; 14 | @SerializedName(value = "name", alternate = {"fund_name"}) 15 | private String fundName; 16 | 17 | private String typeName; 18 | private String pingYingAbbr; 19 | private String pingYing; 20 | 21 | @SerializedName("jzrq") 22 | private String cutOffDate; 23 | @SerializedName("dwjz") 24 | private float netValue; 25 | @SerializedName("gsz") 26 | private float netValueReckon; 27 | @SerializedName("gszzl") 28 | private float growthRateReckon; 29 | @SerializedName("gztime") 30 | private String updateTime; 31 | 32 | private String tags; 33 | 34 | public String getTags() { 35 | return tags; 36 | } 37 | 38 | public void setTags(String tags) { 39 | this.tags = tags; 40 | } 41 | 42 | private List last30DayNetValue; 43 | 44 | public List getLast30DayNetValue() { 45 | return last30DayNetValue; 46 | } 47 | 48 | public void setLast30DayNetValue(List last30DayNetValue) { 49 | this.last30DayNetValue = last30DayNetValue; 50 | } 51 | 52 | public FundBean(String fundCode){ 53 | this.fundCode = fundCode; 54 | } 55 | 56 | public FundBean(){ 57 | } 58 | 59 | public String getFundCode() { 60 | return fundCode; 61 | } 62 | 63 | public void setFundCode(String fundCode) { 64 | this.fundCode = fundCode; 65 | } 66 | 67 | public String getFundName() { 68 | return fundName; 69 | } 70 | 71 | public void setFundName(String fundName) { 72 | this.fundName = fundName; 73 | } 74 | 75 | public String getTypeName() { 76 | return typeName; 77 | } 78 | 79 | public void setTypeName(String typeName) { 80 | this.typeName = typeName; 81 | } 82 | 83 | public String getPingYingAbbr() { 84 | return pingYingAbbr; 85 | } 86 | 87 | public void setPingYingAbbr(String pingYingAbbr) { 88 | this.pingYingAbbr = pingYingAbbr; 89 | } 90 | 91 | public String getPingYing() { 92 | return pingYing; 93 | } 94 | 95 | public void setPingYing(String pingYing) { 96 | this.pingYing = pingYing; 97 | } 98 | 99 | public String getCutOffDate() { 100 | return cutOffDate; 101 | } 102 | 103 | public void setCutOffDate(String cutOffDate) { 104 | this.cutOffDate = cutOffDate; 105 | } 106 | 107 | public float getNetValue() { 108 | return netValue; 109 | } 110 | 111 | public void setNetValue(float netValue) { 112 | this.netValue = netValue; 113 | } 114 | 115 | public float getNetValueReckon() { 116 | return netValueReckon; 117 | } 118 | 119 | public void setNetValueReckon(float netValueReckon) { 120 | this.netValueReckon = netValueReckon; 121 | } 122 | 123 | public float getGrowthRateReckon() { 124 | return growthRateReckon; 125 | } 126 | 127 | public void setGrowthRateReckon(float growthRateReckon) { 128 | this.growthRateReckon = growthRateReckon; 129 | } 130 | 131 | public String getUpdateTime() { 132 | return updateTime; 133 | } 134 | 135 | public void setUpdateTime(String updateTime) { 136 | this.updateTime = updateTime; 137 | } 138 | 139 | @Override 140 | public String toString() { 141 | return "FundBean{" + 142 | "fundCode='" + fundCode + '\'' + 143 | ", fundName='" + fundName + '\'' + 144 | ", typeName='" + typeName + '\'' + 145 | ", pingYingAbbr='" + pingYingAbbr + '\'' + 146 | ", pingYing='" + pingYing + '\'' + 147 | ", cutOffDate='" + cutOffDate + '\'' + 148 | ", netValue=" + netValue + 149 | ", netValueReckon=" + netValueReckon + 150 | ", growthRateReckon=" + growthRateReckon + 151 | ", updateTime='" + updateTime + '\'' + 152 | '}'; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/bean/StockUpdateBean.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api.bean; 2 | 3 | public class StockUpdateBean { 4 | 5 | private String name; 6 | private String code; 7 | private float todayOpeningPrice; 8 | private float yesterdayClosingPrice; 9 | private float currentPrice; 10 | private float todayTopPrice; 11 | private float todayLowPrice; 12 | private float dealShares; 13 | private float totalTransactionAmount; 14 | private float unitPriceByOne; 15 | private float priceByOne; 16 | private float unitPriceByTwo; 17 | private float priceByTwo; 18 | 19 | private float unitPriceSaleOne; 20 | private float priceSaleOne; 21 | private float unitPriceSaleTwo; 22 | private float priceSaleTwo; 23 | 24 | private String date; 25 | private String time; 26 | 27 | public float getPriceSaleOne() { 28 | return priceSaleOne; 29 | } 30 | 31 | public void setPriceSaleOne(float priceSaleOne) { 32 | this.priceSaleOne = priceSaleOne; 33 | } 34 | 35 | public float getUnitPriceSaleTwo() { 36 | return unitPriceSaleTwo; 37 | } 38 | 39 | public void setUnitPriceSaleTwo(float unitPriceSaleTwo) { 40 | this.unitPriceSaleTwo = unitPriceSaleTwo; 41 | } 42 | 43 | public float getPriceSaleTwo() { 44 | return priceSaleTwo; 45 | } 46 | 47 | public void setPriceSaleTwo(float priceSaleTwo) { 48 | this.priceSaleTwo = priceSaleTwo; 49 | } 50 | 51 | public float getUnitPriceSaleOne() { 52 | return unitPriceSaleOne; 53 | } 54 | 55 | public void setUnitPriceSaleOne(float unitPriceSaleOne) { 56 | this.unitPriceSaleOne = unitPriceSaleOne; 57 | } 58 | 59 | public String getCode() { 60 | return code; 61 | } 62 | 63 | public void setCode(String code) { 64 | this.code = code; 65 | } 66 | 67 | public String getName() { 68 | return name; 69 | } 70 | 71 | public void setName(String name) { 72 | this.name = name; 73 | } 74 | 75 | public float getTodayOpeningPrice() { 76 | return todayOpeningPrice; 77 | } 78 | 79 | public void setTodayOpeningPrice(float todayOpeningPrice) { 80 | this.todayOpeningPrice = todayOpeningPrice; 81 | } 82 | 83 | public float getYesterdayClosingPrice() { 84 | return yesterdayClosingPrice; 85 | } 86 | 87 | public void setYesterdayClosingPrice(float yesterdayClosingPrice) { 88 | this.yesterdayClosingPrice = yesterdayClosingPrice; 89 | } 90 | 91 | public float getCurrentPrice() { 92 | return currentPrice; 93 | } 94 | 95 | public void setCurrentPrice(float currentPrice) { 96 | this.currentPrice = currentPrice; 97 | } 98 | 99 | public float getTodayHighPrice() { 100 | return todayTopPrice; 101 | } 102 | 103 | public void setTodayHighPrice(float todayTopPrice) { 104 | this.todayTopPrice = todayTopPrice; 105 | } 106 | 107 | public float getTodayLowPrice() { 108 | return todayLowPrice; 109 | } 110 | 111 | public void setTodayLowPrice(float todayLowPrice) { 112 | this.todayLowPrice = todayLowPrice; 113 | } 114 | 115 | public float getDealShares() { 116 | return dealShares; 117 | } 118 | 119 | public void setDealShares(float dealShares) { 120 | this.dealShares = dealShares; 121 | } 122 | 123 | public float getTotalTransactionAmount() { 124 | return totalTransactionAmount; 125 | } 126 | 127 | public void setTotalTransactionAmount(float totalTransactionAmount) { 128 | this.totalTransactionAmount = totalTransactionAmount; 129 | } 130 | 131 | public float getUnitPriceByOne() { 132 | return unitPriceByOne; 133 | } 134 | 135 | public void setUnitPriceByOne(float unitPriceByOne) { 136 | this.unitPriceByOne = unitPriceByOne; 137 | } 138 | 139 | public float getPriceByOne() { 140 | return priceByOne; 141 | } 142 | 143 | public void setPriceByOne(float priceByOne) { 144 | this.priceByOne = priceByOne; 145 | } 146 | 147 | public float getUnitPriceByTwo() { 148 | return unitPriceByTwo; 149 | } 150 | 151 | public void setUnitPriceByTwo(float unitPriceByTwo) { 152 | this.unitPriceByTwo = unitPriceByTwo; 153 | } 154 | 155 | public float getPriceByTwo() { 156 | return priceByTwo; 157 | } 158 | 159 | public void setPriceByTwo(float priceByTwo) { 160 | this.priceByTwo = priceByTwo; 161 | } 162 | 163 | public String getDate() { 164 | return date; 165 | } 166 | 167 | public void setDate(String date) { 168 | this.date = date; 169 | } 170 | 171 | public String getTime() { 172 | return time; 173 | } 174 | 175 | public void setTime(String time) { 176 | this.time = time; 177 | } 178 | 179 | @Override 180 | public String toString() { 181 | return "StockUpdateBean{" + 182 | "name='" + name + '\'' + 183 | ", todayOpeningPrice=" + todayOpeningPrice + 184 | ", currentPrice=" + currentPrice + 185 | ", todayTopPrice=" + todayTopPrice + 186 | ", todayLowPrice=" + todayLowPrice + 187 | ", date='" + date + '\'' + 188 | ", time='" + time + '\'' + 189 | '}'; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/SettingsForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/ui/TableAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools.ui 2 | 3 | import com.intellij.ui.components.JBTextField 4 | import com.intellij.util.ui.AbstractTableCellEditor 5 | import java.awt.Component 6 | import javax.swing.JTable 7 | import javax.swing.table.AbstractTableModel 8 | import javax.swing.table.TableCellRenderer 9 | import javax.swing.table.TableColumn 10 | import javax.swing.table.TableColumnModel 11 | 12 | /** 13 | * ## The JTable Adapter 14 | * 15 | * This is an adapter use for auto manager JTable data model. 16 | * 17 | * @param tableData The table data of each column and each row, you should ensure each row has the same size. 18 | * @param columnInfo The column model express how to display and edit. 19 | * 20 | * @author https://github.com/dengzii 21 | */ 22 | open class TableAdapter(private val tableData: MutableList>, 23 | private val columnInfo: List>) : AbstractTableModel() { 24 | 25 | private lateinit var jTable: JTable 26 | 27 | // the map of table column header value and ColumnInfo 28 | // using for find ColumnInfo when don't know column index. 29 | private var columnInfoMap = mutableMapOf>() 30 | 31 | // the table cell adapter 32 | var cellAdapter: CellAdapter = DefaultCellAdapter() 33 | 34 | fun setup(table: JTable) { 35 | jTable = table 36 | table.model = this 37 | table.columnModel = DelegateTableColumnModel(table.columnModel) 38 | } 39 | 40 | fun eachColumn(action: (TableColumn, Int) -> Unit) { 41 | for (i in 0 until jTable.columnModel.columnCount) { 42 | action(jTable.columnModel.getColumn(i), i) 43 | } 44 | } 45 | 46 | /** 47 | * Wrapper of JTable's default column model. 48 | * 49 | * Using for custom column renderer and editor. 50 | */ 51 | inner class DelegateTableColumnModel(columnModel: TableColumnModel) : TableColumnModelDecorator(columnModel) { 52 | 53 | override fun getColumn(columnIndex: Int): TableColumn { 54 | val column = super.getColumn(columnIndex) 55 | column.cellRenderer = cellAdapter 56 | column.cellEditor = cellAdapter 57 | columnInfoMap[column.headerValue]?.columnWidth?.takeIf { 58 | it > 0 59 | }?.let { 60 | column.preferredWidth = it 61 | } 62 | return column 63 | } 64 | } 65 | 66 | abstract class CellAdapter : AbstractTableCellEditor(), TableCellRenderer 67 | 68 | /** 69 | * Definition how cell render and edit. 70 | */ 71 | inner class DefaultCellAdapter : CellAdapter() { 72 | 73 | private lateinit var editorComponent: Component 74 | private lateinit var rendererComponent: Component 75 | private var value: Any? = null 76 | private var editLocation = Pair(-1, -1) 77 | 78 | override fun getCellEditorValue(): Any? { 79 | val v = columnInfo[editLocation.second].getEditorValue( 80 | editorComponent, value, editLocation.first, editLocation.second) 81 | return v ?: (editorComponent as? JBTextField)?.text ?: value 82 | } 83 | 84 | override fun getTableCellEditorComponent(table: JTable?, value: Any?, isSelected: Boolean, 85 | row: Int, column: Int): Component? { 86 | // temp the old value before edit cell 87 | this.value = value 88 | editLocation = Pair(row, column) 89 | val header = jTable.columnModel.getColumn(column).headerValue 90 | // get the edit component from ColumnInfo 91 | editorComponent = columnInfoMap[header]?.getEditComponent(value, row, column) ?: return null 92 | return editorComponent 93 | } 94 | 95 | override fun getTableCellRendererComponent(table: JTable?, value: Any?, isSelected: Boolean, 96 | hasFocus: Boolean, row: Int, column: Int): Component? { 97 | val header = jTable.columnModel.getColumn(column).headerValue 98 | rendererComponent = columnInfoMap[header]?.getRendererComponent(value, row, column) ?: return null 99 | return rendererComponent 100 | } 101 | } 102 | 103 | override fun fireTableStructureChanged() { 104 | // when table structure changed, the columns may changed. 105 | columnInfoMap.clear() 106 | columnInfo.forEach { 107 | columnInfoMap[it.colName] = it 108 | } 109 | super.fireTableStructureChanged() 110 | } 111 | 112 | override fun getColumnClass(columnIndex: Int): Class<*> { 113 | return columnInfo[columnIndex].columnClass 114 | } 115 | 116 | override fun getColumnName(column: Int): String { 117 | return columnInfo[column].colName 118 | } 119 | 120 | override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean { 121 | return columnInfo[columnIndex].isCellEditable(tableData[rowIndex].getOrNull(columnIndex)) 122 | } 123 | 124 | override fun setValueAt(aValue: Any?, rowIndex: Int, columnIndex: Int) { 125 | tableData[rowIndex][columnIndex] = aValue 126 | } 127 | 128 | override fun getRowCount(): Int { 129 | return tableData.size 130 | } 131 | 132 | override fun getColumnCount(): Int { 133 | return columnInfo.size 134 | } 135 | 136 | override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? { 137 | return tableData[rowIndex].getOrNull(columnIndex) 138 | } 139 | 140 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/LineChart.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui 2 | 3 | import java.awt.Graphics 4 | import java.awt.Polygon 5 | import java.text.DecimalFormat 6 | import javax.swing.JPanel 7 | import kotlin.math.roundToInt 8 | 9 | /** 10 | * @author https://github.com/dengzii/ 11 | */ 12 | open class LineChart : JPanel() { 13 | 14 | private var minValue = 1.0 15 | private var maxValue = 1.0 16 | 17 | private var valueDiff = 0.0 18 | 19 | private var showLabel = true 20 | 21 | private val labelWith = 26 22 | private val labelHeight = 20 23 | 24 | private val data = mutableListOf() 25 | 26 | fun setData(data: List) { 27 | this.data.clear() 28 | this.data.addAll(data) 29 | if (data.isEmpty()) { 30 | return 31 | } 32 | val sorted = data.sortedBy { it } 33 | minValue = sorted.first() 34 | maxValue = sorted.last() 35 | val ofs = (maxValue - minValue) / getScaleCountY() 36 | valueDiff = maxValue + ofs - minValue 37 | } 38 | 39 | override fun invalidate() { 40 | super.invalidate() 41 | showLabel = height > 200 && width > 300 42 | val diff = maxValue - minValue 43 | valueDiff = when { 44 | height > 200 -> diff + (diff) / getScaleCountY() 45 | else -> maxValue - minValue 46 | } 47 | } 48 | 49 | private fun getLabelWidth(): Int { 50 | return if (showLabel) labelWith else 0 51 | } 52 | 53 | private fun getLabelHeight(): Int { 54 | return if (showLabel) labelHeight else 0 55 | } 56 | 57 | private fun polygonMode(): Boolean { 58 | return height < 200 || width < 300 59 | } 60 | 61 | override fun paint(g: Graphics) { 62 | super.paint(g) 63 | if (getDateCount() == 0) { 64 | return 65 | } 66 | g.color = foreground 67 | // Translate coordinate to left bottom 68 | g.translate(getLabelWidth(), height - getLabelHeight()) 69 | 70 | val scaleY = (height - getLabelHeight()) / getScaleCountY() + 0.0 71 | val scaleYValue = valueDiff / getScaleCountY() 72 | val scaleX = (width - getLabelWidth()) / getScaleCuntX().toDouble() 73 | 74 | drawScaleX(g, scaleX) 75 | drawScaleY(g, scaleY, scaleYValue) 76 | 77 | // draw data line 78 | var preY = 0.0 79 | val p = Polygon() 80 | for (i in 0 until getDateCount()) { 81 | val value = getValueAt(i) 82 | val y = ((value - minValue) / scaleYValue) * -scaleY 83 | if (i == 0) { 84 | if (polygonMode()) { 85 | p.addPoint(0, y.roundToInt()) 86 | } 87 | preY = y 88 | continue 89 | } 90 | val x1 = scaleX * (i - 1) 91 | val y1 = preY 92 | val x2 = scaleX * i 93 | if (showLabel) { 94 | g.drawString(getValueLabelAt(i), x2, y) 95 | } 96 | if (polygonMode()) { 97 | p.addPoint(x2.roundToInt(), y.roundToInt()) 98 | } else { 99 | g.drawLine(x1, y1, x2, y) 100 | } 101 | preY = y 102 | } 103 | if (polygonMode()) { 104 | p.addPoint((scaleX * getDateCount()).roundToInt(), 0) 105 | p.addPoint(0, 0) 106 | p.addPoint(0, (((getValueAt(0) - minValue) / scaleYValue) * -scaleY).roundToInt()) 107 | g.fillPolygon(p) 108 | } 109 | } 110 | 111 | open fun getDateCount(): Int { 112 | return data.size 113 | } 114 | 115 | open fun getValueAt(index: Int): Double { 116 | return data[index] 117 | } 118 | 119 | open fun getValueLabelAt(index: Int): String { 120 | return data[index].formatStr() 121 | } 122 | 123 | open fun getScaleLabelY(scaleIndex: Int, scaleValue: Double): String { 124 | return scaleValue.formatStr() 125 | } 126 | 127 | open fun getScaleLabelX(scaleIndex: Int): String { 128 | return scaleIndex.toString() 129 | } 130 | 131 | open fun drawScaleY(g: Graphics, scaleSize: Double, scaleValue: Double) { 132 | g.drawLine(0.0, 0.0, 0.0, -(getScaleCountY() - 1) * scaleSize) 133 | if (showLabel) { 134 | // draw scale string 135 | for (i in 0 until getScaleCountY()) { 136 | val y = i * -scaleSize 137 | g.drawLine(0.0, y, 3.0, y) 138 | val s = (scaleValue * i + minValue) 139 | g.drawString(getScaleLabelY(i, s), -labelWith.toDouble(), y) 140 | } 141 | } 142 | } 143 | 144 | open fun drawScaleX(g: Graphics, scaleSize: Double) { 145 | g.drawLine(0.0, 0.0, width - getLabelWidth() - scaleSize, 0.0) 146 | for (i in 0 until getScaleCuntX()) { 147 | if (showLabel) { 148 | g.drawLine(scaleSize * i, 0.0, scaleSize * i, -3.0) 149 | g.drawString(getScaleLabelX(i), scaleSize * i - 5, 10.0) 150 | } 151 | } 152 | } 153 | 154 | private fun getScaleCountY() = when { 155 | height > 300 -> 12 156 | height > 200 -> 12 157 | height > 100 -> 12 158 | else -> 12 159 | } 160 | 161 | private fun getScaleCuntX(): Int { 162 | return if (height > 200) data.size else data.size - 1 163 | } 164 | 165 | private fun Graphics.drawLine(x1: Double, y1: Double, x2: Double, y2: Double) { 166 | drawLine(x1.roundToInt(), y1.roundToInt(), x2.roundToInt(), y2.roundToInt()) 167 | } 168 | 169 | private fun Graphics.drawString(str: String, x: Double, y: Double) { 170 | drawString(str, x.roundToInt(), y.roundToInt()) 171 | } 172 | 173 | private val f = DecimalFormat("##.##") 174 | private fun Double.formatStr(): String { 175 | return f.format(this) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/design/EditFundGroupForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
134 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/fund/EditFundGroupListDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui.fund 2 | 3 | import com.dengzii.plugin.fund.PluginConfig 4 | import com.dengzii.plugin.fund.api.TianTianFundApi 5 | import com.dengzii.plugin.fund.api.bean.FundBean 6 | import com.dengzii.plugin.fund.design.EditFundGroupForm 7 | import com.dengzii.plugin.fund.model.FundGroup 8 | import com.dengzii.plugin.fund.model.UserFundModel 9 | import com.dengzii.plugin.fund.tools.invokeLater 10 | import com.dengzii.plugin.fund.tools.ui.onClick 11 | import com.dengzii.plugin.fund.utils.async 12 | import com.intellij.openapi.ui.showOkCancelDialog 13 | import java.awt.Dimension 14 | import java.awt.event.KeyAdapter 15 | import java.awt.event.KeyEvent 16 | import java.awt.event.MouseAdapter 17 | import java.awt.event.MouseEvent 18 | import javax.swing.DefaultListModel 19 | import javax.swing.ListSelectionModel 20 | 21 | /** 22 | * @author https://github.com/dengzii/ 23 | */ 24 | class EditFundGroupListDialog( 25 | private val allFunds: List, 26 | private val fundGroup: FundGroup, 27 | private val callback: (callback: FundGroup) -> Unit 28 | ) : EditFundGroupForm("编辑基金列表") { 29 | 30 | private val searchListModel = DefaultListModel() 31 | private val searchResult = mutableListOf() 32 | private val selectedListModel = DefaultListModel() 33 | 34 | private var listDither1 = 0L 35 | private var listDither2 = 0L 36 | 37 | init { 38 | isModal = true 39 | } 40 | 41 | companion object { 42 | fun show(origin: FundGroup, callback: (callback: FundGroup) -> Unit) { 43 | val allFunds = PluginConfig.allFund 44 | if (allFunds.isEmpty()) { 45 | async { 46 | try { 47 | TianTianFundApi().fundList 48 | } catch (e: Exception) { 49 | e.printStackTrace() 50 | invokeLater { 51 | showOkCancelDialog("请求基金接口出错了", "${e.message}", "知道了") 52 | } 53 | emptyList() 54 | } 55 | }.callback { 56 | PluginConfig.allFund = it 57 | EditFundGroupListDialog(it, origin.clone(), callback).packAndShow() 58 | } 59 | } else { 60 | EditFundGroupListDialog(allFunds, origin.clone(), callback).packAndShow() 61 | } 62 | } 63 | } 64 | 65 | override fun onOpened() { 66 | super.onOpened() 67 | textFieldGroupName.text = fundGroup.groupName 68 | 69 | buttonApply.onClick { 70 | isVisible = false 71 | fundGroup.groupName = textFieldGroupName.text.trim() 72 | callback(fundGroup) 73 | dispose() 74 | } 75 | buttonCancel.onClick { 76 | hideAndDispose() 77 | } 78 | 79 | textFieldSearch.addKeyListener(object : KeyAdapter() { 80 | override fun keyReleased(e: KeyEvent?) { 81 | if (e?.keyCode != KeyEvent.VK_ENTER) { 82 | return 83 | } 84 | val keywords = textFieldSearch.text.trim().uppercase() 85 | if (keywords.isBlank()) { 86 | searchResult.clear() 87 | searchListModel.clear() 88 | return 89 | } 90 | listSearch.isVisible = true 91 | searchListModel.clear() 92 | searchResult.clear() 93 | allFunds.forEach { 94 | if (it.tags.contains(keywords)) { 95 | searchResult.add(it) 96 | searchListModel.addElement("${it.fundName}-${it.fundCode}") 97 | } 98 | } 99 | } 100 | }) 101 | listSearch.model = searchListModel 102 | listSelected.model = selectedListModel 103 | 104 | listSearch.selectionMode = ListSelectionModel.SINGLE_SELECTION 105 | listSelected.selectionMode = ListSelectionModel.SINGLE_SELECTION 106 | 107 | listSearch.addListSelectionListener { 108 | if (System.currentTimeMillis() - listDither1 < 300) { 109 | return@addListSelectionListener 110 | } 111 | val selectedIndex = listSearch.selectedIndex 112 | if (selectedIndex == -1) { 113 | return@addListSelectionListener 114 | } 115 | listDither1 = System.currentTimeMillis() 116 | val selected = searchResult[selectedIndex] 117 | if (fundGroup.fundList.containsKey(selected.fundCode)) { 118 | return@addListSelectionListener 119 | } 120 | fundGroup.fundList[selected.fundCode] = UserFundModel(selected).apply { 121 | addDate = System.currentTimeMillis() 122 | } 123 | selectedListModel.addElement("${selected.fundName}-${selected.fundCode}") 124 | } 125 | listSelected.addListSelectionListener { 126 | if (System.currentTimeMillis() - listDither2 < 300) { 127 | return@addListSelectionListener 128 | } 129 | val selectedIndex = listSelected.selectedIndex 130 | if (selectedIndex == -1) { 131 | return@addListSelectionListener 132 | } 133 | listDither2 = System.currentTimeMillis() 134 | val code = selectedListModel.get(selectedIndex).split("-")[1] 135 | fundGroup.fundList.remove(code) 136 | selectedListModel.remove(selectedIndex) 137 | } 138 | 139 | listSearch.addMouseListener(object : MouseAdapter() { 140 | override fun mouseClicked(e: MouseEvent) { 141 | super.mouseClicked(e) 142 | if (e.button != MouseEvent.BUTTON3) { 143 | return 144 | } 145 | } 146 | }) 147 | 148 | allFunds.forEach { 149 | searchResult.add(it) 150 | searchListModel.addElement("${it.fundName}-${it.fundCode}") 151 | } 152 | 153 | fundGroup.fundList.forEach { (_, u) -> 154 | selectedListModel.addElement("${u.fundBean.fundName}-${u.fundBean.fundCode}") 155 | } 156 | 157 | textFieldSearch.requestFocus() 158 | } 159 | 160 | override fun getDefaultSize(): Dimension { 161 | return Dimension(900, 700) 162 | } 163 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/tools/PersistentConfig.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.tools 2 | 3 | import com.dengzii.plugin.fund.tools.PersistentConfig.ObjectSerializer 4 | import com.google.gson.Gson 5 | import com.google.gson.JsonParseException 6 | import com.intellij.ide.util.PropertiesComponent 7 | import com.intellij.openapi.project.Project 8 | import kotlin.properties.ReadWriteProperty 9 | import kotlin.reflect.KClass 10 | import kotlin.reflect.KProperty 11 | 12 | /** 13 | * Make data persistence more easy. 14 | * 15 | * Delegate field's getter/setter to [PropertiesComponent], the complex type will serialize/deserialize 16 | * use gson by default, you can custom [ObjectSerializer] instead it. 17 | * 18 | * @param propertiesComp The [PropertiesComponent] 19 | * @param project The project use for obtain [PropertiesComponent], ignored when propertiesComp is non-null. 20 | * @param keySuffix The key suffix use for persist. 21 | * @param objectSerializer The serialize/deserialize factory for complex type. 22 | * 23 | * @author https://github.com/dengzii 24 | */ 25 | open class PersistentConfig( 26 | propertiesComp: PropertiesComponent? = null, 27 | project: Project? = null, 28 | private val keySuffix: String = "KEY", 29 | private val objectSerializer: ObjectSerializer = JsonObjectSerializer() 30 | ) { 31 | 32 | private val propertiesComponent: PropertiesComponent = propertiesComp ?: if (project == null) { 33 | PropertiesComponent.getInstance() 34 | } else { 35 | PropertiesComponent.getInstance(project) 36 | } 37 | 38 | /** 39 | * Return the delegate for field need to persist. 40 | * 41 | * @param defaultValue The default when load value failed. 42 | * @param keyName The key name use for persist. 43 | * 44 | * @return [PropertyDelegate] 45 | */ 46 | inline fun persistentProperty(defaultValue: T, keyName: String? = null): PropertyDelegate { 47 | return when (defaultValue) { 48 | is Int?, 49 | is Boolean?, 50 | is Float?, 51 | is String?, 52 | is Array<*>? -> { 53 | PropertyDelegate(defaultValue, T::class, keyName) 54 | } 55 | else -> PropertyDelegate(defaultValue, T::class, keyName) 56 | } 57 | } 58 | 59 | /** 60 | * The interface defines how to serializer/deserializer the object not primitive type. 61 | */ 62 | interface ObjectSerializer { 63 | fun serialize(obj: Any, clazz: KClass<*>): String 64 | fun deserialize(str: String, clazz: KClass<*>): Any 65 | } 66 | 67 | /** 68 | * The json serializer/deserializer use gson. 69 | */ 70 | class JsonObjectSerializer : ObjectSerializer { 71 | 72 | private val gson: Gson = Gson().newBuilder() 73 | .setLenient() 74 | .serializeNulls() 75 | .create() 76 | 77 | @Throws(JsonParseException::class) 78 | override fun serialize(obj: Any, clazz: KClass<*>): String { 79 | return gson.toJson(obj) 80 | } 81 | 82 | @Throws(JsonParseException::class) 83 | override fun deserialize(str: String, clazz: KClass<*>): Any { 84 | return gson.fromJson(str, clazz.java) 85 | } 86 | } 87 | 88 | /** 89 | * This class is a delegate class for a field need to persist. 90 | * 91 | * @param default The default when read property failed, the following situation will return [default]: 92 | * 1, The key does not exist. 93 | * 2, Read value successful but serialize/deserialize failed. 94 | * 3, An exception was caught. 95 | * @param clazz The KClass of field. 96 | * @param keyName The key name, the field name is used when null. 97 | */ 98 | inner class PropertyDelegate 99 | constructor( 100 | private val default: T, 101 | private val clazz: KClass<*>, 102 | private val keyName: String? = null 103 | ) : ReadWriteProperty { 104 | 105 | @Suppress("UNCHECKED_CAST") 106 | override fun getValue(thisRef: PersistentConfig, property: KProperty<*>): T? { 107 | val keyName = getKeyName(property) 108 | return try { 109 | with(thisRef.propertiesComponent) { 110 | when (clazz) { 111 | Int::class -> getInt(keyName, default as Int) 112 | Boolean::class -> getBoolean(keyName, default as Boolean) 113 | String::class -> getValue(keyName, default as String) 114 | Float::class -> getFloat(keyName, default as Float) 115 | Array::class -> getValues(keyName) 116 | // deserialize to object 117 | else -> { 118 | val v = getValue(keyName) 119 | if (v != null) { 120 | objectSerializer.deserialize(v, clazz) 121 | } else { 122 | default 123 | } 124 | } 125 | } 126 | } as T 127 | } catch (ignore: TypeCastException) { 128 | default 129 | } catch (e: Throwable) { 130 | e.printStackTrace() 131 | default 132 | } 133 | } 134 | 135 | override fun setValue(thisRef: PersistentConfig, property: KProperty<*>, value: T?) { 136 | val keyName = getKeyName(property) 137 | with(thisRef.propertiesComponent) { 138 | when (value) { 139 | is Int -> setValue(keyName, value, default as Int) 140 | is Boolean -> setValue(keyName, value, default as Boolean) 141 | is String -> setValue(keyName, value, default as String) 142 | is Float -> setValue(keyName, value, default as Float) 143 | is Array<*> -> { 144 | val arr = value.filterIsInstance().toTypedArray() 145 | setValues(keyName, arr) 146 | } 147 | null -> unsetValue(keyName) 148 | else -> { 149 | try { 150 | val serialized = objectSerializer.serialize(value, clazz) 151 | setValue(keyName, serialized) 152 | } catch (e: Throwable) { 153 | throw RuntimeException("Type unsupported.", e) 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | private fun getKeyName(property: KProperty<*>): String { 161 | return "${keySuffix}_${keyName ?: property.name}" 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/stock/StockPanel.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui.stock 2 | 3 | import com.dengzii.plugin.fund.PluginConfig 4 | import com.dengzii.plugin.fund.api.AbstractPollTask 5 | import com.dengzii.plugin.fund.api.SinaStockApi 6 | import com.dengzii.plugin.fund.api.bean.StockUpdateBean 7 | import com.dengzii.plugin.fund.conf.FundTheme 8 | import com.dengzii.plugin.fund.conf.StockColConfig 9 | import com.dengzii.plugin.fund.design.StockPanelForm 10 | import com.dengzii.plugin.fund.model.UserStockModel 11 | import com.dengzii.plugin.fund.tools.ui.ActionToolBarUtils 12 | import com.dengzii.plugin.fund.tools.ui.ColumnInfo 13 | import com.dengzii.plugin.fund.tools.ui.TableAdapter 14 | import com.dengzii.plugin.fund.ui.ToolWindowPanel 15 | import com.intellij.icons.AllIcons 16 | import com.intellij.openapi.actionSystem.ActionToolbarPosition 17 | import com.intellij.openapi.actionSystem.AnActionEvent 18 | import com.intellij.openapi.project.Project 19 | import com.intellij.openapi.wm.ToolWindow 20 | import com.intellij.ui.ToolbarDecorator 21 | import com.intellij.ui.content.ContentFactory 22 | import java.awt.BorderLayout 23 | import java.awt.Component 24 | import javax.swing.Icon 25 | import javax.swing.JLabel 26 | import javax.swing.SwingConstants 27 | import javax.swing.table.TableRowSorter 28 | 29 | /** 30 | * @author https://github.com/dengzii/ 31 | */ 32 | class StockPanel : StockPanelForm(), ToolWindowPanel { 33 | 34 | private val tableData = mutableListOf>() 35 | private val columnInfos = mutableListOf>() 36 | private val tableAdapter = TableAdapter(tableData, columnInfos) 37 | private lateinit var stocks: MutableMap 38 | private lateinit var project: Project 39 | 40 | private lateinit var colConfig: List 41 | private var pollTask: AbstractPollTask>? = null 42 | private val api = SinaStockApi() 43 | private var pollDuration = 20000L 44 | 45 | override fun onCreate(project: Project, toolWindow: ToolWindow) { 46 | val factory = ContentFactory.SERVICE.getInstance() 47 | val content = factory.createContent(contentPanel, "Stock", false) 48 | this.project = project 49 | init() 50 | toolWindow.contentManager.addContent(content) 51 | } 52 | 53 | private fun init() { 54 | 55 | stocks = PluginConfig.stockList.toMutableMap() 56 | 57 | tableStock.rowHeight = 30 58 | tableAdapter.setup(tableStock) 59 | 60 | val toolBar = ToolbarDecorator.createDecorator(tableStock) 61 | .addExtraActions( 62 | action("编辑", AllIcons.Actions.Edit) { 63 | EditStockDialog.show(stocks) { 64 | stocks.clear() 65 | stocks.putAll(it) 66 | PluginConfig.stockList = it 67 | updateStockList() 68 | updatePollTask() 69 | } 70 | }, 71 | action("立即刷新", AllIcons.Actions.Refresh) { 72 | updatePollTask() 73 | }, 74 | // action("设置", AllIcons.General.Settings) { 75 | // ShowSettingsUtil.getInstance().editConfigurable(project, PluginConfigurable()) 76 | // } 77 | ) 78 | .setToolbarPosition(ActionToolbarPosition.LEFT) 79 | 80 | contentPanel.add(toolBar.createPanel(), BorderLayout.CENTER) 81 | 82 | initTableColumnInfo() 83 | initTableSorter() 84 | updatePollTask() 85 | } 86 | 87 | override fun onInit(toolWindow: ToolWindow) { 88 | 89 | } 90 | 91 | override fun onWindowActive() { 92 | 93 | } 94 | 95 | override fun onWindowHide() { 96 | 97 | } 98 | 99 | override fun onWindowShow() { 100 | 101 | } 102 | 103 | private fun initTableColumnInfo() { 104 | columnInfos.clear() 105 | colConfig = PluginConfig.stockColConfig.columns 106 | colConfig.forEach { 107 | when (PluginConfig.fundTheme) { 108 | FundTheme.Default -> it.getName() 109 | else -> it.name 110 | } 111 | val columnInfo = when (it) { 112 | StockColConfig.Col.StockCode, 113 | StockColConfig.Col.StockName, 114 | StockColConfig.Col.UpdateDate, 115 | StockColConfig.Col.UpdateTime -> { 116 | AlignColInfo(it.getName(), SwingConstants.CENTER) 117 | } 118 | else -> { 119 | AlignColInfo(it.getName(), SwingConstants.RIGHT) 120 | } 121 | } 122 | columnInfos.add(columnInfo) 123 | } 124 | tableAdapter.fireTableStructureChanged() 125 | } 126 | 127 | private fun updateStockList() { 128 | tableData.clear() 129 | stocks.forEach { s -> 130 | val col = mutableListOf() 131 | colConfig.forEach { 132 | col.add(it.getAttr(s.value)) 133 | } 134 | tableData.add(col) 135 | } 136 | tableAdapter.fireTableDataChanged() 137 | } 138 | 139 | private fun updatePollTask() { 140 | pollTask?.stop() 141 | pollTask?.unsubscribeAll() 142 | pollTask = api.subscribeUpdate(stocks.map { it.key }) 143 | pollTask?.subscribe { 144 | it.forEach { s -> 145 | stocks[s.code]!!.update(s) 146 | } 147 | updateStockList() 148 | } 149 | pollTask?.start(pollDuration) 150 | } 151 | 152 | private fun initTableSorter() { 153 | val sorter = TableRowSorter(tableAdapter) 154 | val numberComparator = Comparator { o1, o2 -> 155 | val s1 = o1.replace("%", "").replace(",", "").toDouble() 156 | val s2 = o2.replace("%", "").replace(",", "").toDouble() 157 | s1.compareTo(s2) 158 | } 159 | colConfig.forEachIndexed { index, col -> 160 | when (col) { 161 | StockColConfig.Col.StockName, 162 | StockColConfig.Col.Trending, 163 | StockColConfig.Col.UpdateTime, 164 | StockColConfig.Col.UpdateDate -> { 165 | } 166 | else -> sorter.setComparator(index, numberComparator) 167 | } 168 | } 169 | tableStock.rowSorter = sorter 170 | } 171 | 172 | private fun action(hint: String, icon: Icon, block: (AnActionEvent) -> Unit) = 173 | ActionToolBarUtils.createActionButton(hint, icon, block).apply { 174 | contextComponent = contentPanel 175 | } 176 | 177 | private class AlignColInfo(name: String, val alignment: Int) : ColumnInfo(name, false) { 178 | override fun getRendererComponent(item: Any?, row: Int, col: Int): Component { 179 | val l = JLabel() 180 | l.horizontalAlignment = alignment 181 | l.text = item?.toString() ?: "-" 182 | return l 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/api/TianTianFundApi.java: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.api; 2 | 3 | import com.dengzii.plugin.fund.api.bean.FundBean; 4 | import com.dengzii.plugin.fund.api.bean.NetValueBean; 5 | import com.dengzii.plugin.fund.http.Http; 6 | import com.dengzii.plugin.fund.utils.GsonUtils; 7 | import com.dengzii.plugin.fund.utils.Logger; 8 | import com.google.gson.reflect.TypeToken; 9 | import org.apache.http.impl.execchain.RequestAbortedException; 10 | 11 | import java.io.IOException; 12 | import java.lang.reflect.Type; 13 | import java.util.ArrayList; 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | /** 18 | * @author https://github.com/dengzii/ 19 | */ 20 | public class TianTianFundApi implements FundApi { 21 | 22 | @Override 23 | public List getFundList() throws IOException, InterruptedException { 24 | String response = Http.getInstance().get("http://fund.eastmoney.com/js/fundcode_search.js").trim(); 25 | if (response.isEmpty()) { 26 | return Collections.emptyList(); 27 | } 28 | response = response.replaceAll("(var r = \\[)|( )|(];)|(\")", ""); 29 | response = response.substring(2, response.length() - 1); 30 | String[] fund = response.split("],\\["); 31 | List fundBeans = new ArrayList<>(); 32 | for (String f : fund) { 33 | String[] sp = f.split(","); 34 | FundBean fb = new FundBean(); 35 | try { 36 | fb.setFundCode(sp[0]); 37 | fb.setPingYingAbbr(sp[1]); 38 | fb.setFundName(sp[2]); 39 | fb.setTypeName(sp[3]); 40 | try { 41 | fb.setPingYing(sp[4]); 42 | } catch (Throwable e) { 43 | fb.setPingYing("-"); 44 | e.printStackTrace(); 45 | } 46 | fb.setTags(fb.getFundCode() + "," + fb.getPingYing() + "," + fb.getPingYingAbbr() + "," + fb.getFundName()); 47 | fundBeans.add(fb); 48 | } catch (Exception e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | return fundBeans; 53 | } 54 | 55 | @Override 56 | public FundBean getFundNewestDetail(String fundCode) { 57 | try { 58 | String response = Http.getInstance().get(String.format("http://fundgz.1234567.com.cn/js/%s.js?rt=%d", 59 | fundCode, System.currentTimeMillis())); 60 | if (response == null) { 61 | return null; 62 | } 63 | response = response.substring(8, response.length() - 2); 64 | Type t = new TypeToken() { 65 | }.getType(); 66 | Logger.log("TianTianApi.getFundNewestDetail", response); 67 | return GsonUtils.fromJson(response, t); 68 | } catch (InterruptedException | RequestAbortedException e) { 69 | // ignore 70 | } catch (Throwable e) { 71 | e.printStackTrace(); 72 | } 73 | return null; 74 | } 75 | 76 | @Override 77 | public AbstractPollTask> updateFundList(final List fundBeans) { 78 | return new AbstractPollTask>() { 79 | @Override 80 | List update() { 81 | for (FundBean fundBean : fundBeans) { 82 | try { 83 | FundBean f = getFundNewestDetail(fundBean.getFundCode()); 84 | if (f == null) { 85 | continue; 86 | } 87 | fundBean.setCutOffDate(f.getCutOffDate()); 88 | fundBean.setNetValue(f.getNetValue()); 89 | fundBean.setNetValueReckon(f.getNetValueReckon()); 90 | fundBean.setGrowthRateReckon(f.getGrowthRateReckon()); 91 | fundBean.setUpdateTime(f.getUpdateTime()); 92 | 93 | if (fundBean.getLast30DayNetValue() == null) { 94 | List netValueHistory = getNetValueHistory(f.getFundCode(), 1, 30); 95 | fundBean.setLast30DayNetValue(netValueHistory); 96 | } 97 | } catch (Throwable e) { 98 | e.printStackTrace(); 99 | } 100 | } 101 | return fundBeans; 102 | } 103 | }; 104 | } 105 | 106 | @Override 107 | public NetValueBean getNewestNetValue(String fundCode) { 108 | List h = getNetValueHistory(fundCode, 1, 1); 109 | if (h.isEmpty()) { 110 | return null; 111 | } 112 | return h.get(0); 113 | } 114 | 115 | @Override 116 | public List getNetValueHistory3Month(String fundCode) { 117 | 118 | List n = new ArrayList<>(); 119 | for (int i = 0; i < 2; i++) { 120 | n.addAll(getNetValueHistory(fundCode, i + 1, 49)); 121 | } 122 | return n; 123 | } 124 | 125 | @Override 126 | public List getNetValueHistory(String fundCode, int page, int pageSize) { 127 | String url = String.format("https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=%s&page=%d&per=%d", 128 | fundCode, page, pageSize); 129 | List netValueBeans = new ArrayList<>(); 130 | try { 131 | String response = Http.getInstance().get(url); 132 | response = response.replaceAll("(var apidata=\\{ content:\")|(\",records:1748,pages:36,curpage:1};)", ""); 133 | response = response.substring(187); 134 | response = response.substring(0, response.length() - 21); 135 | response = response.replaceAll("()|" + 136 | "()|" + 137 | "()|" + 138 | "()|" + 139 | "()", ","); 140 | String[] lines = response.split(""); 141 | for (String line : lines) { 142 | String[] sp = line.split(","); 143 | NetValueBean bean = new NetValueBean(); 144 | bean.setDate(sp[1]); 145 | bean.setNetValue(parseFloat(sp[2])); 146 | bean.setGrowthRate(parseFloat(sp[4])); 147 | bean.setSubscribeStatus(sp[5]); 148 | bean.setRedemptionStatus(sp[6]); 149 | bean.setDividend(sp[7].replaceAll("", "")); 150 | netValueBeans.add(bean); 151 | } 152 | } catch (InterruptedException | RequestAbortedException e) { 153 | // ignore 154 | } catch (IOException e) { 155 | e.printStackTrace(); 156 | } 157 | return netValueBeans; 158 | } 159 | 160 | private float parseFloat(String s) { 161 | if (s.isEmpty()) { 162 | return 0; 163 | } 164 | try { 165 | return Float.parseFloat(s.replace("%", "")); 166 | } catch (Throwable e) { 167 | e.printStackTrace(); 168 | } 169 | return 0; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/com/dengzii/plugin/fund/ui/fund/FundPanel.kt: -------------------------------------------------------------------------------- 1 | package com.dengzii.plugin.fund.ui.fund 2 | 3 | import com.dengzii.plugin.fund.PluginConfig 4 | import com.dengzii.plugin.fund.PluginConfigurable 5 | import com.dengzii.plugin.fund.api.AbstractPollTask 6 | import com.dengzii.plugin.fund.api.TianTianFundApi 7 | import com.dengzii.plugin.fund.api.bean.FundBean 8 | import com.dengzii.plugin.fund.conf.FundColConfig 9 | import com.dengzii.plugin.fund.conf.FundTheme 10 | import com.dengzii.plugin.fund.design.FundPanelForm 11 | import com.dengzii.plugin.fund.model.FundGroup 12 | import com.dengzii.plugin.fund.tools.ui.ActionToolBarUtils 13 | import com.dengzii.plugin.fund.tools.ui.ColumnInfo 14 | import com.dengzii.plugin.fund.tools.ui.TableAdapter 15 | import com.dengzii.plugin.fund.ui.ColoredColInfo 16 | import com.dengzii.plugin.fund.ui.ToolWindowPanel 17 | import com.intellij.icons.AllIcons 18 | import com.intellij.openapi.actionSystem.ActionToolbarPosition 19 | import com.intellij.openapi.actionSystem.AnActionEvent 20 | import com.intellij.openapi.options.ShowSettingsUtil 21 | import com.intellij.openapi.project.Project 22 | import com.intellij.openapi.wm.ToolWindow 23 | import com.intellij.ui.ToolbarDecorator 24 | import com.intellij.ui.content.ContentFactory 25 | import java.awt.BorderLayout 26 | import javax.swing.Icon 27 | import javax.swing.table.TableRowSorter 28 | 29 | /** 30 | * @author https://github.com/dengzii/ 31 | */ 32 | class FundPanel : FundPanelForm(), ToolWindowPanel { 33 | 34 | private lateinit var fundData: FundGroup 35 | private val tableData = mutableListOf>() 36 | private val columnInfos = mutableListOf>() 37 | private val tableAdapter = TableAdapter(tableData, columnInfos) 38 | 39 | private val funds = mutableListOf() 40 | private var pollDuration = 20000L 41 | private val api = TianTianFundApi() 42 | private var pollTask: AbstractPollTask>? = null 43 | private lateinit var colConfig: List 44 | private var project: Project? = null 45 | 46 | private fun init() { 47 | pollDuration = PluginConfig.fundRefreshDuration * 1000L 48 | 49 | fundData = PluginConfig.fundGroup.getOrElse("default-group") { FundGroup() } 50 | 51 | tableFund.rowHeight = 40 52 | tableFund.columnSelectionAllowed = false 53 | tableFund.rowSelectionAllowed = false 54 | tableAdapter.setup(tableFund) 55 | 56 | val toolBar = ToolbarDecorator.createDecorator(tableFund) 57 | .addExtraActions( 58 | action("编辑", AllIcons.Actions.Edit) { 59 | EditFundGroupListDialog.show(fundData) { 60 | fundData = it 61 | PluginConfig.fundGroup = mutableMapOf(Pair(fundData.groupName, fundData)) 62 | updateFundList() 63 | updatePollTask() 64 | } 65 | }, 66 | action("立即刷新", AllIcons.Actions.Refresh) { 67 | updatePollTask() 68 | }, 69 | action("设置", AllIcons.General.Settings) { 70 | ShowSettingsUtil.getInstance().editConfigurable(project, PluginConfigurable()) 71 | 72 | }) 73 | .setToolbarPosition(ActionToolbarPosition.LEFT) 74 | 75 | initTableColumnInfo() 76 | updateFundList() 77 | updatePollTask() 78 | initTableSorter() 79 | contentPanel.add(toolBar.createPanel(), BorderLayout.CENTER) 80 | 81 | PluginConfigurable.setConfigChangeListener { 82 | initTableColumnInfo() 83 | updateFundList() 84 | } 85 | } 86 | 87 | override fun onCreate(project: Project, toolWindow: ToolWindow) { 88 | val factory = ContentFactory.SERVICE.getInstance() 89 | val content = factory.createContent(contentPanel, "Fund", false) 90 | init() 91 | toolWindow.contentManager.addContent(content) 92 | } 93 | 94 | override fun onInit(toolWindow: ToolWindow) { 95 | 96 | } 97 | 98 | override fun onWindowActive() { 99 | 100 | } 101 | 102 | override fun onWindowHide() { 103 | // pollTask?.stop() 104 | } 105 | 106 | override fun onWindowShow() { 107 | // pollTask?.start(pollDuration) 108 | } 109 | 110 | private fun initTableColumnInfo() { 111 | columnInfos.clear() 112 | colConfig = PluginConfig.fundColConfig.columns 113 | colConfig.forEach { 114 | when (PluginConfig.fundTheme) { 115 | FundTheme.Default -> it.getName() 116 | else -> it.name 117 | } 118 | val n = it.getName() 119 | val columnInfo = when (it) { 120 | FundColConfig.Col.FundName -> { 121 | FundCodeColInfo(n) { 122 | // To fund detail. 123 | } 124 | } 125 | FundColConfig.Col.Last30DayNetValue -> NetValueHistoryColInfo(n) 126 | FundColConfig.Col.GrowthRateReckon -> ColoredColInfo(n) 127 | FundColConfig.Col.TotalYield -> ColoredColInfo(n) 128 | FundColConfig.Col.TotalGains -> ColoredColInfo(n) 129 | FundColConfig.Col.GainsReckon -> ColoredColInfo(n) 130 | else -> ColumnInfo.new(n, false) 131 | } 132 | columnInfos.add(columnInfo) 133 | } 134 | tableAdapter.fireTableStructureChanged() 135 | } 136 | 137 | private fun updateFundList() { 138 | tableData.clear() 139 | funds.clear() 140 | fundData.fundList.forEach { (_, m) -> 141 | val col = mutableListOf() 142 | funds.add(m.fundBean) 143 | colConfig.forEach { 144 | col.add(it.getAttr(m)) 145 | } 146 | tableData.add(col) 147 | } 148 | tableAdapter.fireTableDataChanged() 149 | } 150 | 151 | private fun updatePollTask() { 152 | pollTask?.stop() 153 | pollTask?.unsubscribeAll() 154 | pollTask = api.updateFundList(fundData.fundList.map { it.value.fundBean }) 155 | pollTask?.subscribe { 156 | it.forEach { f -> 157 | fundData.fundList[f.fundCode]?.updateFund(f) 158 | } 159 | updateFundList() 160 | } 161 | pollTask?.start(pollDuration) 162 | } 163 | 164 | private fun initTableSorter() { 165 | val sorter = TableRowSorter(tableAdapter) 166 | val numberComparator = Comparator { o1, o2 -> 167 | val s1 = o1.replace("%", "").replace(",", "").toDouble() 168 | val s2 = o2.replace("%", "").replace(",", "").toDouble() 169 | s1.compareTo(s2) 170 | } 171 | colConfig.forEachIndexed { index, col -> 172 | when (col) { 173 | FundColConfig.Col.FundName -> { 174 | sorter.setComparator(index, Comparator { s1, s2 -> 175 | s1.compareTo(s2) 176 | }) 177 | } 178 | FundColConfig.Col.NetValueReckon -> sorter.setComparator(index, numberComparator) 179 | FundColConfig.Col.GrowthRateReckon -> sorter.setComparator(index, numberComparator) 180 | FundColConfig.Col.BuyingPrice -> sorter.setComparator(index, numberComparator) 181 | FundColConfig.Col.TotalYield -> sorter.setComparator(index, numberComparator) 182 | FundColConfig.Col.TotalGains -> sorter.setComparator(index, numberComparator) 183 | FundColConfig.Col.HoldingShare -> sorter.setComparator(index, numberComparator) 184 | FundColConfig.Col.GainsReckon -> sorter.setComparator(index, numberComparator) 185 | else -> { 186 | } 187 | } 188 | } 189 | tableFund.rowSorter = sorter 190 | } 191 | 192 | private fun action(hint: String, icon: Icon, block: (AnActionEvent) -> Unit) = 193 | ActionToolBarUtils.createActionButton(hint, icon, block).apply { 194 | contextComponent = contentPanel 195 | } 196 | } --------------------------------------------------------------------------------