├── .gitignore ├── .DS_Store ├── src ├── test │ └── testData │ │ └── rename │ │ ├── foo.xml │ │ └── foo_after.xml └── main │ ├── resources │ ├── .DS_Store │ ├── icons │ │ ├── icon.png │ │ ├── referenceBy.svg │ │ ├── referenceBy_dark.svg │ │ ├── referenceTo.svg │ │ ├── referenceTo_dark.svg │ │ ├── inlayGlobe.svg │ │ ├── inlayGlobe_dark.svg │ │ ├── favicon.svg │ │ ├── favicon_dark.svg │ │ ├── help.svg │ │ └── help_dark.svg │ ├── messages │ │ └── MyBundle.properties │ └── META-INF │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ └── java │ └── com │ └── github │ └── yuyuanweb │ └── mianshiyaplugin │ ├── utils │ ├── LogUtils.java │ ├── ThemeUtil.java │ ├── UserUtil.java │ ├── ContentUtil.java │ ├── FileUtils.java │ └── PanelUtil.java │ ├── constant │ ├── PageConstant.java │ ├── SearchConstant.java │ ├── TextConstant.java │ ├── CommonConstant.java │ ├── SearchComboBoxConstant.java │ ├── IconConstant.java │ ├── ViewConstant.java │ └── KeyConstant.java │ ├── model │ ├── response │ │ ├── User.java │ │ ├── TagGroup.java │ │ ├── QuestionBankCategory.java │ │ ├── QuestionAnswer.java │ │ ├── Question.java │ │ └── QuestionBank.java │ ├── common │ │ ├── BaseResponse.java │ │ ├── Page.java │ │ └── PageRequest.java │ ├── dto │ │ ├── DoQuestionInfoVO.java │ │ ├── QuestionAnswerQueryRequest.java │ │ ├── QuestionBankQueryRequest.java │ │ ├── QuestionBankCategoryBankQueryRequest.java │ │ ├── QuestionQueryRequest.java │ │ └── TagCategoryQueryRequest.java │ └── enums │ │ ├── ErrorCode.java │ │ ├── UserRoleEnum.java │ │ ├── NeedVipEnum.java │ │ ├── WebTypeEnum.java │ │ ├── DifficultyEnum.java │ │ └── QuestionDifficultyEnum.java │ ├── view │ ├── MTabModel.java │ └── LoginPanel.java │ ├── file │ ├── language │ │ ├── CommonLanguage.java │ │ └── MsycLanguage.java │ ├── provider │ │ ├── CustomTabTitleProvider.java │ │ ├── QuestionProvider.java │ │ ├── BrowserFileEditorProvider.java │ │ ├── QuestionAnswerProvider.java │ │ ├── CommonProvider.java │ │ ├── OuterBrowserFileEditorProvider.java │ │ ├── ConvergeProvider.java │ │ ├── EditorProvider.java │ │ └── SplitTextEditorProvider.java │ ├── type │ │ ├── CommonFileType.java │ │ └── MsycFileType.java │ └── preview │ │ ├── EditorWithPreview.java │ │ ├── CommonPreview.java │ │ ├── BrowserFileEditor.java │ │ ├── OuterBrowserFileEditorPreview.java │ │ └── SplitFileEditor.java │ ├── api │ ├── interceptor │ │ ├── LogInterceptor.java │ │ ├── HeaderInterceptor.java │ │ └── ResponseInterceptor.java │ └── MianShiYaApi.java │ ├── listeners │ └── ApplicationLifecycleListener.java │ ├── actions │ ├── OpenUrlAction.java │ ├── QuestionAction.java │ ├── LoginAction.java │ ├── LogoutAction.java │ └── QuestionBankAction.java │ ├── adapter │ └── DateTypeAdapter.java │ ├── config │ ├── ApiConfig.java │ └── GlobalState.java │ ├── manager │ └── CookieManager.java │ └── toolWindow │ └── MyToolWindowFactory.java ├── imgs ├── img.png └── install.gif ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── settings.gradle.kts ├── qodana.yml ├── gradle.properties ├── CHANGELOG.md ├── .run ├── Run Plugin.run.xml ├── Run Tests.run.xml └── Run Verifications.run.xml ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .intellijPlatform 4 | .qodana 5 | build 6 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyuanweb/mianshiya-plugin/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/test/testData/rename/foo.xml: -------------------------------------------------------------------------------- 1 | 2 | 1>Foo 3 | 4 | -------------------------------------------------------------------------------- /src/test/testData/rename/foo_after.xml: -------------------------------------------------------------------------------- 1 | 2 | Foo 3 | 4 | -------------------------------------------------------------------------------- /imgs/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyuanweb/mianshiya-plugin/HEAD/imgs/img.png -------------------------------------------------------------------------------- /imgs/install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyuanweb/mianshiya-plugin/HEAD/imgs/install.gif -------------------------------------------------------------------------------- /src/main/resources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyuanweb/mianshiya-plugin/HEAD/src/main/resources/.DS_Store -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyuanweb/mianshiya-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyuanweb/mianshiya-plugin/HEAD/src/main/resources/icons/icon.png -------------------------------------------------------------------------------- /src/main/resources/messages/MyBundle.properties: -------------------------------------------------------------------------------- 1 | projectService=Project service: {0} 2 | randomLabel=The random number is: {0} 3 | shuffle=Shuffle 4 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 3 | } 4 | 5 | rootProject.name = "mianshiya-plugin" 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | linter: jetbrains/qodana-jvm-community:latest 6 | projectJDK: "17" 7 | profile: 8 | name: qodana.recommended 9 | exclude: 10 | - name: All 11 | paths: 12 | - .qodana 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/utils/LogUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.utils; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | 5 | 6 | public class LogUtils { 7 | 8 | public static final Logger LOG = Logger.getInstance("#com.github.yuyuanweb.mianshiyaplugin"); 9 | } 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | pluginGroup = com.github.yuyuanweb.mianshiyaplugin 2 | pluginName = Mianshiya 3 | pluginVersion = 0.5.6 4 | 5 | pluginSinceBuild = 213 6 | pluginUntilBuild = 252.* 7 | 8 | platformType = IC 9 | platformVersion = 2021.3 10 | #platformVersion = 2023.3 11 | 12 | # ,-Dide.browser.jcef.log.level=verbose,-Duser.language=en-US 13 | runIdeJvmArgs = -Dfile.encoding=utf-8 -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/PageConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | /** 4 | * 分页 常量 5 | * 6 | * @author pine 7 | */ 8 | public interface PageConstant { 9 | /** 10 | * 可支持的分页数 11 | */ 12 | int[] PAGE_SIZES = {10, 20, 50}; 13 | 14 | int PAGE_SIZE = PAGE_SIZES[1]; 15 | 16 | int FIRST_PAGE = 1; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/response/User.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * @author pine 9 | */ 10 | @Data 11 | public class User { 12 | 13 | private Long id; 14 | 15 | private String userName; 16 | 17 | private String userRole; 18 | 19 | private Date vipExpireTime; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/response/TagGroup.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 分组标签 9 | * @author pine 10 | */ 11 | @Data 12 | public class TagGroup { 13 | 14 | /** 15 | * 组名 16 | */ 17 | String name; 18 | 19 | /** 20 | * 标签名列表 21 | */ 22 | List tagList; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/view/MTabModel.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.view; 2 | 3 | import javax.swing.table.DefaultTableModel; 4 | 5 | /** 6 | * 禁止编辑的数据模型 7 | * 8 | * @author pine 9 | */ 10 | public class MTabModel extends DefaultTableModel { 11 | 12 | // 自定义表格模型,确保表格不可编辑 13 | @Override 14 | public boolean isCellEditable(int row, int column) { 15 | return false; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/common/BaseResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.common; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 通用返回类 9 | * 10 | * @param 泛型 11 | * @author pine 12 | */ 13 | @Data 14 | public class BaseResponse implements Serializable { 15 | 16 | private int code; 17 | 18 | private T data; 19 | 20 | private String message; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/SearchConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | /** 4 | * 搜索默认值常量 5 | * 6 | * @author pine 7 | */ 8 | public interface SearchConstant { 9 | 10 | /** 11 | * 题库分类 id 12 | */ 13 | Long DEFAULT_QUESTION_BANK_CATEGORY_ID = 0L; 14 | 15 | /** 16 | * 插件的一些方法不允许 null 值,当 questionBankId 为 null 时,使用的占位值 17 | */ 18 | Long QUESTION_BANK_NULL_ID = -99L; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/TextConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | /** 4 | * 文本常量 5 | * 6 | * @author pine 7 | */ 8 | public interface TextConstant { 9 | 10 | String TITLE_SEARCH_PLACE_HOLDER = "搜索题目"; 11 | 12 | String SEARCH = "搜索"; 13 | 14 | String LOGIN = "请先登录"; 15 | 16 | String NO_SOLUTION = "暂无回答"; 17 | 18 | String QUESTION = "题目"; 19 | 20 | String QUESTION_BANK = "题库"; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # mianshiya-plugin Changelog 4 | 5 | ## [Unreleased] 6 | ### Added 7 | - Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template) 8 | 9 | ## 变更日志模版 10 |

0.5.0

11 |

Added

12 |

13 |

Changed

14 |

15 |

Deprecated

16 |

17 |

Removed

18 |

19 |

Fixed

20 |

21 |

Security

22 |

-------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/response/QuestionBankCategory.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 题库分类对象 question_bank_category 9 | * 10 | * @author pine 11 | */ 12 | @Data 13 | public class QuestionBankCategory implements Serializable { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | /** 18 | * id 19 | */ 20 | private Long id; 21 | 22 | /** 23 | * 名称 24 | */ 25 | private String name; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/CommonConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | /** 4 | * @author pine 5 | */ 6 | public interface CommonConstant { 7 | 8 | String HOST = "https://api.mianshiya.com/"; 9 | 10 | String API = HOST + "api/"; 11 | 12 | String WEB_HOST = "https://www.mianshiya.com/"; 13 | 14 | String HELP_DOC = "https://yuyuanweb.yuque.com/org-wiki-yuyuanweb-zvq1bg/oue2nx/mogtz6438xrubwuy"; 15 | 16 | String VIP = WEB_HOST + "vip"; 17 | 18 | String PLUGIN_QD = WEB_HOST + "embed/q/%s/%s?theme=%s"; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/language/CommonLanguage.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.language; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 4 | import com.intellij.lang.Language; 5 | 6 | /** 7 | * @author pine 8 | */ 9 | public class CommonLanguage extends Language { 10 | 11 | public static final String LANGUAGE_NAME = KeyConstant.EDITOR_FILE_POSTFIX + "QUESTION"; 12 | 13 | public static final CommonLanguage INSTANCE = new CommonLanguage(); 14 | 15 | protected CommonLanguage() { 16 | super(LANGUAGE_NAME); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/language/MsycLanguage.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.language; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 4 | import com.intellij.lang.Language; 5 | 6 | /** 7 | * @author pine 8 | */ 9 | public class MsycLanguage extends Language { 10 | 11 | public static final String LANGUAGE_NAME = KeyConstant.EDITOR_FILE_POSTFIX_CONTENT + "QUESTION"; 12 | 13 | public static final MsycLanguage INSTANCE_C = new MsycLanguage(); 14 | 15 | protected MsycLanguage() { 16 | super(LANGUAGE_NAME); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/response/QuestionAnswer.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 题目答案 9 | * 10 | * @author pine 11 | */ 12 | @Data 13 | public class QuestionAnswer implements Serializable { 14 | 15 | /** 16 | * id 17 | */ 18 | private Long id; 19 | 20 | /** 21 | * 内容 22 | */ 23 | private String content; 24 | 25 | /** 26 | * 内容类型:0-富文本, 1-Markdown 27 | */ 28 | private Integer contentType; 29 | 30 | private User user; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/dto/DoQuestionInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | 8 | /** 9 | * 刷题信息视图 10 | * 11 | * @author pine 12 | */ 13 | @Data 14 | public class DoQuestionInfoVO implements Serializable { 15 | 16 | /** 17 | * 题目 id 列表信息 18 | */ 19 | private List questionIdList; 20 | 21 | /** 22 | * 当前题目所属题库中的下标 23 | */ 24 | private Integer currentQuestionIndex; 25 | 26 | /** 27 | * 题库的总题目数 28 | */ 29 | private Integer totalQuestionNum; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/dto/QuestionAnswerQueryRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.dto; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.model.common.PageRequest; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 查询请求 11 | * 12 | * @author pine 13 | */ 14 | @EqualsAndHashCode(callSuper = true) 15 | @Data 16 | public class QuestionAnswerQueryRequest extends PageRequest implements Serializable { 17 | 18 | /** 19 | * 题目 id 20 | */ 21 | private Long questionId; 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | } -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/common/Page.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.common; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | /** 9 | * 分页 10 | * 11 | * @author pine 12 | */ 13 | @Data 14 | public class Page { 15 | /** 16 | * 总数 17 | */ 18 | protected long total; 19 | 20 | /** 21 | * 每页显示条数,默认 10 22 | */ 23 | protected long size; 24 | 25 | /** 26 | * 当前页 27 | */ 28 | protected long current; 29 | 30 | /** 31 | * 查询数据列表 32 | */ 33 | protected List records = Collections.emptyList(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/SearchComboBoxConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | /** 4 | * 搜索条筛选框 常量 5 | * 6 | * @author pine 7 | */ 8 | public interface SearchComboBoxConstant { 9 | 10 | String QUESTION_BANK_ID_FILE_NAME = "questionBankId"; 11 | 12 | String TAG_LIST_FILE_NAME = "tagList"; 13 | 14 | String DIFFICULTY_FILE_NAME = "difficulty"; 15 | 16 | String NEED_VIP_FILE_NAME = "needVip"; 17 | 18 | String QUESTION_BANK_PLACEHOLDER = "题库"; 19 | 20 | String TAG_LIST_PLACEHOLDER = "标签"; 21 | 22 | String DIFFICULTY_PLACEHOLDER = "难度"; 23 | 24 | String NEED_VIP_PLACEHOLDER = "会员专属"; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/dto/QuestionBankQueryRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.dto; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.model.common.PageRequest; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * 查询请求 12 | * 13 | * @author pine 14 | */ 15 | @EqualsAndHashCode(callSuper = true) 16 | @Data 17 | public class QuestionBankQueryRequest extends PageRequest implements Serializable { 18 | 19 | /** 20 | * 标签列表 21 | */ 22 | private List tagList; 23 | 24 | private static final long serialVersionUID = 1L; 25 | } -------------------------------------------------------------------------------- /src/main/resources/icons/referenceBy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/referenceBy_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/referenceTo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/dto/QuestionBankCategoryBankQueryRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.dto; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.SearchConstant; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | /** 8 | * 题库分类和题库关联查询请求 9 | * 10 | * @author pine 11 | */ 12 | @EqualsAndHashCode(callSuper = true) 13 | @Data 14 | public class QuestionBankCategoryBankQueryRequest extends QuestionBankQueryRequest { 15 | 16 | /** 17 | * 题库分类 id 18 | */ 19 | private Long questionBankCategoryId = SearchConstant.DEFAULT_QUESTION_BANK_CATEGORY_ID; 20 | 21 | private static final long serialVersionUID = 1L; 22 | } -------------------------------------------------------------------------------- /src/main/resources/icons/referenceTo_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/common/PageRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.common; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 4 | import com.github.yuyuanweb.mianshiyaplugin.constant.PageConstant; 5 | import lombok.Data; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * 分页请求 11 | * 12 | * @author pine 13 | */ 14 | @Data 15 | public class PageRequest { 16 | 17 | /** 18 | * 当前页号 19 | */ 20 | private long current = 1; 21 | 22 | /** 23 | * 页面大小 24 | */ 25 | // private long pageSize = Objects.requireNonNull(GlobalState.getInstance().getState()).pageSize; 26 | private long pageSize = PageConstant.PAGE_SIZE; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/IconConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | import com.intellij.openapi.util.IconLoader; 4 | 5 | import javax.swing.*; 6 | 7 | /** 8 | * @author pine 9 | */ 10 | public interface IconConstant { 11 | 12 | ClassLoader CLASS_LOADER = IconConstant.class.getClassLoader(); 13 | 14 | Icon HELP = IconLoader.findIcon("/icons/help.svg", CLASS_LOADER); 15 | 16 | Icon WEB = IconLoader.findIcon("/icons/inlayGlobe.svg", CLASS_LOADER); 17 | 18 | Icon LOGO = IconLoader.findIcon("/icons/favicon.svg", CLASS_LOADER); 19 | 20 | Icon LOGOUT = IconLoader.findIcon("/icons/referenceBy.svg", CLASS_LOADER); 21 | 22 | Icon LOGIN = IconLoader.findIcon("/icons/referenceTo.svg", CLASS_LOADER); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/icons/inlayGlobe.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/icons/inlayGlobe_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/enums/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.enums; 2 | 3 | /** 4 | * 错误码 5 | * 6 | * @author 程序员鱼皮 7 | * @from 编程导航知识星球 8 | */ 9 | public enum ErrorCode { 10 | 11 | SUCCESS(0, "ok"), 12 | NOT_LOGIN_ERROR(40100, "未登录"), 13 | NO_VIP_AUTH_ERROR(40102, "无会员权限"), 14 | ; 15 | 16 | /** 17 | * 状态码 18 | */ 19 | private final int code; 20 | 21 | /** 22 | * 信息 23 | */ 24 | private final String message; 25 | 26 | ErrorCode(int code, String message) { 27 | this.code = code; 28 | this.message = message; 29 | } 30 | 31 | public int getCode() { 32 | return code; 33 | } 34 | 35 | public String getMessage() { 36 | return message; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/api/interceptor/LogInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.api.interceptor; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import okhttp3.Interceptor; 5 | import okhttp3.Request; 6 | import okhttp3.Response; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * 请求拦截器 13 | * 14 | * @author pine 15 | */ 16 | public class LogInterceptor implements Interceptor { 17 | 18 | private static final Logger logger = Logger.getInstance(LogInterceptor.class); 19 | 20 | @NotNull 21 | @Override 22 | public Response intercept(Chain chain) throws IOException { 23 | Request originalRequest = chain.request(); 24 | logger.warn(originalRequest.method() + " " + originalRequest.url()); 25 | return chain.proceed(originalRequest); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/listeners/ApplicationLifecycleListener.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.listeners; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import com.github.yuyuanweb.mianshiyaplugin.utils.FileUtils; 5 | import com.intellij.ide.AppLifecycleListener; 6 | import com.intellij.openapi.diagnostic.Logger; 7 | 8 | /** 9 | * @author pine 10 | */ 11 | public class ApplicationLifecycleListener implements AppLifecycleListener { 12 | 13 | private static final Logger logger = Logger.getInstance(ApplicationLifecycleListener.class); 14 | 15 | @Override 16 | public void appWillBeClosed(boolean isRestart) { 17 | // 删除插件产生的临时文件 18 | AppLifecycleListener.super.appWillBeClosed(isRestart); 19 | boolean del = FileUtil.del(FileUtils.getTempDir()); 20 | if (!del) { 21 | logger.warn("缓存文件删除失败"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/actions/OpenUrlAction.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.actions; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.intellij.openapi.project.DumbAware; 6 | import com.intellij.ide.BrowserUtil; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import javax.swing.*; 10 | 11 | /** 12 | * @author pine 13 | */ 14 | public class OpenUrlAction extends AnAction implements DumbAware { 15 | 16 | private final String url; 17 | 18 | // 构造函数 19 | public OpenUrlAction(String text, String url, Icon icon) { 20 | // Action 名称 21 | super(text, text, icon); 22 | this.url = url; 23 | } 24 | 25 | @Override 26 | public void actionPerformed(@NotNull AnActionEvent e) { 27 | // 使用默认浏览器打开指定网址 28 | BrowserUtil.browse(url); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/actions/QuestionAction.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.actions; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.view.QuestionListManager; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import javax.swing.*; 9 | 10 | /** 11 | * @author pine 12 | */ 13 | public class QuestionAction extends AnAction { 14 | 15 | public QuestionAction() { 16 | } 17 | 18 | public QuestionAction(String text, Icon icon) { 19 | super(text, text, icon); 20 | } 21 | 22 | @Override 23 | public void actionPerformed(@NotNull AnActionEvent anActionEvent) { 24 | QuestionListManager questionListManager = new QuestionListManager(); 25 | questionListManager.addQuestionTab(null, anActionEvent.getProject()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/ViewConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | /** 4 | * 视图名称常量 5 | * 6 | * @author pine 7 | */ 8 | public interface ViewConstant { 9 | 10 | String CONVERGE_PREVIEW = "ConvergePreview"; 11 | 12 | String QUESTION_ANSWER_PREVIEW = "QuestionAnswerPreview"; 13 | 14 | String SPLITTER = "Splitter"; 15 | 16 | String SPLIT_FILE_EDITOR = "SplitFileEditor"; 17 | 18 | String PARENT_SPLIT_KEY = "parentSplit"; 19 | 20 | String BEST_QUESTION_ANSWER_PROVIDER = "BestQuestionAnswerProvider"; 21 | 22 | String COMMON_PROVIDER = "CommonProvider"; 23 | 24 | String COMMON_PREVIEW = "CommonPreview"; 25 | 26 | String PARENT_SPLIT_EDITOR = "ParentSplitEditor"; 27 | 28 | String BEST_QUESTION_ANSWER_PREVIEW = "BestQuestionAnswerPreview"; 29 | 30 | String QUESTION_ANSWER_PROVIDER = "QuestionAnswerProvider"; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/CustomTabTitleProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 4 | import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | /** 10 | * 自定义 tab 栏的文件名 11 | * 12 | * @author pine 13 | */ 14 | public class CustomTabTitleProvider implements EditorTabTitleProvider { 15 | @Nullable 16 | @Override 17 | public String getEditorTabTitle(Project project, VirtualFile file) { 18 | if (KeyConstant.EDITOR_FILE_POSTFIX_CONTENT.equals(file.getExtension())) { 19 | // 返回自定义名称,不显示后缀 20 | return file.getNameWithoutExtension(); 21 | } 22 | // 返回 null 保持默认行为 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/response/Question.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | 8 | /** 9 | * 题目视图 10 | * 11 | * @author pine 12 | */ 13 | @Data 14 | public class Question implements Serializable { 15 | 16 | /** 17 | * id 18 | */ 19 | private Long id; 20 | 21 | /** 22 | * 编号 23 | */ 24 | private Long questionNum; 25 | 26 | /** 27 | * 标题 28 | */ 29 | private String title; 30 | 31 | /** 32 | * 内容 33 | */ 34 | private String content; 35 | 36 | /** 37 | * 标签列表 38 | */ 39 | private List tagList; 40 | 41 | /** 42 | * 最佳题目答案 43 | */ 44 | private QuestionAnswer bestQuestionAnswer; 45 | 46 | /** 47 | * 难度:简单 = 1,中等 = 3,困难 = 5 48 | */ 49 | private Integer difficulty; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/QuestionProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import com.github.yuyuanweb.mianshiyaplugin.utils.FileUtils; 5 | import com.intellij.openapi.fileEditor.FileEditor; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.vfs.LocalFileSystem; 8 | import com.intellij.openapi.vfs.VirtualFile; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | /** 12 | * 问题页面 13 | * 已废弃 14 | * 15 | * @author pine 16 | */ 17 | public class QuestionProvider extends CommonProvider { 18 | 19 | @Override 20 | public @NotNull FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { 21 | VirtualFile contentVf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(FileUtil.touch(FileUtils.getTempDir() + file.getName())); 22 | 23 | return super.createEditor(project, contentVf); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/dto/QuestionQueryRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.dto; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.model.common.PageRequest; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * 查询请求 12 | * 13 | * @author pine 14 | */ 15 | @EqualsAndHashCode(callSuper = true) 16 | @Data 17 | public class QuestionQueryRequest extends PageRequest implements Serializable { 18 | 19 | /** 20 | * 标题 21 | */ 22 | private String title; 23 | 24 | /** 25 | * 标签列表 26 | */ 27 | private List tagList; 28 | 29 | /** 30 | * 仅vip可见(1 表示仅会员可见) 31 | */ 32 | private Integer needVip; 33 | 34 | /** 35 | * 题库id 36 | */ 37 | private Long questionBankId; 38 | 39 | /** 40 | * 难度 41 | */ 42 | private Integer difficulty; 43 | 44 | private static final long serialVersionUID = 1L; 45 | } -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/adapter/DateTypeAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.adapter; 2 | 3 | import cn.hutool.core.date.DatePattern; 4 | import cn.hutool.core.date.DateUtil; 5 | import com.google.gson.TypeAdapter; 6 | import com.google.gson.stream.JsonReader; 7 | import com.google.gson.stream.JsonWriter; 8 | 9 | import java.io.IOException; 10 | import java.util.Date; 11 | 12 | /** 13 | * 处理日期解析异常 14 | * 15 | * @author pine 16 | */ 17 | public class DateTypeAdapter extends TypeAdapter { 18 | 19 | @Override 20 | public void write(JsonWriter out, Date value) throws IOException { 21 | if (value == null) { 22 | out.nullValue(); 23 | } else { 24 | out.value(DateUtil.format(value, DatePattern.NORM_DATETIME_PATTERN)); 25 | } 26 | } 27 | 28 | @Override 29 | public Date read(JsonReader in) throws IOException { 30 | String dateStr = in.nextString(); 31 | return DateUtil.parse(dateStr); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/api/interceptor/HeaderInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.api.interceptor; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 4 | import okhttp3.Interceptor; 5 | import okhttp3.Request; 6 | import okhttp3.Response; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * 请求拦截器 13 | * 14 | * @author pine 15 | */ 16 | public class HeaderInterceptor implements Interceptor { 17 | @NotNull 18 | @Override 19 | public Response intercept(Chain chain) throws IOException { 20 | Request originalRequest = chain.request(); 21 | Request.Builder requestBuilder = originalRequest.newBuilder() 22 | // 添加 cookie 23 | .header("Cookie", GlobalState.getInstance().getSavedCookie()) 24 | .header("User-Agent", "jetbrains-plugin"); 25 | Request request = requestBuilder.build(); 26 | return chain.proceed(request); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.run/Run Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | true 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/utils/ThemeUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.utils; 2 | 3 | import com.intellij.openapi.editor.colors.EditorColorsManager; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * 主题工具类 9 | * 10 | * @author pine 11 | */ 12 | public class ThemeUtil { 13 | 14 | private static final String DARK = "dark"; 15 | 16 | private static final String LIGHT = "light"; 17 | 18 | /** 19 | * 判断当前颜色是不是暗色 20 | */ 21 | public static boolean isDarkColor(Color color) { 22 | // 使用 RGB 值计算亮度 23 | double brightness = (0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue()); 24 | // 128 是亮度的阈值 25 | return brightness < 128; 26 | } 27 | 28 | /** 29 | * 获取该用的主题 30 | */ 31 | public static String getTheme() { 32 | Color background = EditorColorsManager.getInstance().getGlobalScheme().getDefaultBackground(); 33 | boolean darkColor = ThemeUtil.isDarkColor(background); 34 | if (darkColor) { 35 | return DARK; 36 | } else { 37 | return LIGHT; 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /.run/Run Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/dto/TagCategoryQueryRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.dto; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.model.common.PageRequest; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | 12 | /** 13 | * 标签类别对象查询请求 14 | * 15 | * @author 程序员鱼皮 16 | * @from 编程导航知识星球 17 | */ 18 | @Data 19 | @ToString 20 | @EqualsAndHashCode(callSuper = true) 21 | public class TagCategoryQueryRequest extends PageRequest implements Serializable { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * id 27 | */ 28 | private Long id; 29 | 30 | 31 | /** 32 | * 名称 33 | */ 34 | private String name; 35 | 36 | 37 | /** 38 | * 创建用户 id 39 | */ 40 | private Long userId; 41 | 42 | 43 | /** 44 | * 标签列表 45 | */ 46 | private List tagList; 47 | 48 | 49 | /** 50 | * 至少有一个标签 51 | */ 52 | private List orTagList; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 面试鸭 IDEA 插件 2 | 3 | **面试鸭** 是一个帮助程序员在 JetBrains 系列产品中刷面试题的插件。插件题目全面、题解优质、支持多种筛选条件,是一款摸鱼刷题必备神器。 4 | 5 | ## 特性 6 | 7 | - **题目丰富**:拥有 20+ 题库分类,近 200 题库,6000+ 题目,涵盖了几乎所有主流编程方向的面试题。 8 | - **高质量题解**:许多题解来自大厂面试官原创,抓住核心要点,通俗易懂,配有图片辅助理解。 9 | - **快捷键支持**:支持自定义快捷键,轻松一键打开或收起插件。 10 | - **多条件筛选**:可通过题库、标签、难度等条件筛选,快速找到自己想要的题目。 11 | - **讨论互动**:题目详情页面分为题目、推荐答案、讨论区三个子 tab,方便交流讨论。 12 | - **多端同步**:支持 IDEA 及 JetBrains 系列其他产品,如 WebStorm、PyCharm 等。同时支持小程序和 Web 端,数据三端同步。 13 | - **开源**:插件完全开源,代码托管在 GitHub,欢迎贡献和 Star 支持。 14 | 15 | ## 安装与使用 16 | 17 | 1. 打开 IntelliJ IDEA,进入 `Settings` > `Plugins`。 18 | 2. 搜索 `mianshiya`,点击安装。 19 | 20 | ![install.gif](imgs/install.gif) 21 | 22 | 搜索不到的小伙伴可以通过 JetBrains 插件市场下载, 23 | > JetBrains 插件市场地址:https://plugins.jetbrains.com/plugin/25258-mianshiya 24 | 25 | 下载到本地之后按照下图步骤安装即可: 26 | 27 | ![img.png](imgs/img.png) 28 | 29 | 30 | ## 三端兼容 31 | 32 | - **JetBrains 系列产品**:支持 IDEA、WebStorm、PyCharm 等 2021.3 及以上版本。 33 | - **小程序端**:随时随地刷题。 34 | - **Web 端**:[面试鸭 Web 端](https://mianshiya.com) 35 | 36 | ## 开源项目 37 | 38 | 插件代码完全开源,欢迎大家 Star 和贡献代码! 39 | 40 | GitHub 地址:[https://github.com/yuyuanweb/mianshiya-plugin](https://github.com/yuyuanweb/mianshiya-plugin) 41 | 42 | ## 反馈与支持 43 | 44 | 如有问题或建议,欢迎通过 GitHub issue 提交反馈,或者直接联系我们。 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/BrowserFileEditorProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.file.preview.BrowserFileEditor; 4 | import com.intellij.openapi.fileEditor.FileEditor; 5 | import com.intellij.openapi.fileEditor.FileEditorPolicy; 6 | import com.intellij.openapi.fileEditor.FileEditorProvider; 7 | import com.intellij.openapi.project.Project; 8 | import com.intellij.openapi.vfs.VirtualFile; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | /** 12 | * 内嵌浏览器 提供者 13 | * 14 | * @author pine 15 | */ 16 | public class BrowserFileEditorProvider implements FileEditorProvider { 17 | 18 | @Override 19 | public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { 20 | return true; 21 | } 22 | 23 | @Override 24 | public @NotNull FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { 25 | return new BrowserFileEditor(project, file); 26 | } 27 | 28 | @Override 29 | public @NotNull String getEditorTypeId() { 30 | return "browser-file-editor"; 31 | } 32 | 33 | @Override 34 | public @NotNull FileEditorPolicy getPolicy() { 35 | return FileEditorPolicy.PLACE_AFTER_DEFAULT_EDITOR; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/type/CommonFileType.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.type; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.file.language.CommonLanguage; 5 | import com.intellij.icons.AllIcons; 6 | import com.intellij.openapi.fileTypes.LanguageFileType; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | 12 | /** 13 | * @author pine 14 | */ 15 | public class CommonFileType extends LanguageFileType { 16 | 17 | public static final CommonFileType INSTANCE = new CommonFileType(); 18 | 19 | private CommonFileType() { 20 | super(CommonLanguage.INSTANCE); 21 | } 22 | 23 | @NotNull 24 | @Override 25 | public String getName() { 26 | return KeyConstant.EDITOR_FILE_POSTFIX + "QUESTION"; 27 | } 28 | 29 | @NotNull 30 | @Override 31 | public String getDescription() { 32 | return KeyConstant.EDITOR_FILE_POSTFIX; 33 | } 34 | 35 | @NotNull 36 | @Override 37 | public String getDefaultExtension() { 38 | return KeyConstant.EDITOR_FILE_POSTFIX; 39 | } 40 | 41 | @Nullable 42 | @Override 43 | public Icon getIcon() { 44 | return AllIcons.Actions.AddFile; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/type/MsycFileType.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.type; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.IconConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 5 | import com.github.yuyuanweb.mianshiyaplugin.file.language.MsycLanguage; 6 | import com.intellij.openapi.fileTypes.LanguageFileType; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | 12 | /** 13 | * @author pine 14 | */ 15 | public class MsycFileType extends LanguageFileType { 16 | 17 | public static final MsycFileType INSTANCE_C = new MsycFileType(); 18 | 19 | private MsycFileType() { 20 | super(MsycLanguage.INSTANCE_C); 21 | } 22 | 23 | @NotNull 24 | @Override 25 | public String getName() { 26 | return KeyConstant.EDITOR_FILE_POSTFIX_CONTENT + "QUESTION"; 27 | } 28 | 29 | @NotNull 30 | @Override 31 | public String getDescription() { 32 | return KeyConstant.EDITOR_FILE_POSTFIX_CONTENT; 33 | } 34 | 35 | @NotNull 36 | @Override 37 | public String getDefaultExtension() { 38 | return KeyConstant.EDITOR_FILE_POSTFIX_CONTENT; 39 | } 40 | 41 | @Nullable 42 | @Override 43 | public Icon getIcon() { 44 | return IconConstant.LOGO; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/QuestionAnswerProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.ViewConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.file.preview.QuestionAnswerPreview; 5 | import com.intellij.openapi.fileEditor.FileEditor; 6 | import com.intellij.openapi.fileEditor.FileEditorPolicy; 7 | import com.intellij.openapi.fileEditor.WeighedFileEditorProvider; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.openapi.vfs.VirtualFile; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | /** 13 | * 回答页面 14 | * 已废弃 15 | * 16 | * @author pine 17 | */ 18 | @Deprecated 19 | public class QuestionAnswerProvider extends WeighedFileEditorProvider { 20 | 21 | 22 | @Override 23 | public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { 24 | return true; 25 | } 26 | 27 | @Override 28 | public @NotNull FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { 29 | return new QuestionAnswerPreview(project, file); 30 | } 31 | 32 | @Override 33 | public @NotNull String getEditorTypeId() { 34 | return ViewConstant.QUESTION_ANSWER_PROVIDER; 35 | } 36 | 37 | @Override 38 | public @NotNull FileEditorPolicy getPolicy() { 39 | return FileEditorPolicy.HIDE_DEFAULT_EDITOR; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/constant/KeyConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.constant; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.WebTypeEnum; 4 | import com.intellij.openapi.util.Key; 5 | 6 | /** 7 | * key 常量 8 | * 9 | * @author pine 10 | */ 11 | public interface KeyConstant { 12 | 13 | String QUESTION_ID = "questionId"; 14 | 15 | String QUESTION_BANK_ID = "QuestionBankId"; 16 | 17 | String WEB_TYPE = "webType"; 18 | 19 | Key QUESTION_ID_KEY = new Key<>(KeyConstant.QUESTION_ID); 20 | 21 | Key WEB_TYPE_KEY = new Key<>(KeyConstant.WEB_TYPE); 22 | 23 | Key QUESTION_BANK_ID_KEY = new Key<>(KeyConstant.QUESTION_BANK_ID); 24 | 25 | String QUESTION_BANK_ZH = "题库大全"; 26 | String QUESTION_BANK = "QUESTION_BANK"; 27 | 28 | String QUESTION_ZH = "题目大全"; 29 | String QUESTION = "QUESTION"; 30 | 31 | String WEB_ZH = "网页端"; 32 | String WEB = "WEB"; 33 | 34 | String HELP_ZH = "帮助"; 35 | String HELP = "HELP"; 36 | 37 | String LOGIN_ZH = "登录"; 38 | String LOGIN = "LOGIN"; 39 | 40 | String LOGOUT_ZH = "注销"; 41 | String LOGOUT = "LOGOUT"; 42 | 43 | String VIP_ZH = "会员"; 44 | String VIP = "VIP"; 45 | 46 | String PLUGIN_NAME = "面试鸭"; 47 | 48 | String ACTION_BAR = "ToolWindowToolbar"; 49 | 50 | String EDITOR_FILE_POSTFIX = "msy"; 51 | 52 | String EDITOR_FILE_POSTFIX_CONTENT = "msyc"; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | junit = "4.13.2" 4 | lombok = "1.18.24" 5 | hutool = "5.8.25" 6 | retrofit2 = "2.9.0" 7 | converter-gson = "2.9.0" 8 | flexmark = "0.62.2" 9 | 10 | # plugins 11 | changelog = "2.2.1" 12 | intelliJPlatform = "2.0.1" 13 | kotlin = "2.0.20" 14 | kover = "0.8.3" 15 | qodana = "2024.1.9" 16 | #foojay-resolver-convention = "0.8.0" 17 | 18 | [libraries] 19 | junit = { group = "junit", name = "junit", version.ref = "junit" } 20 | lombok = { group = "org.projectlombok", name = "lombok", version.ref = "lombok" } 21 | hutool = { group = "cn.hutool", name = "hutool-core", version.ref = "hutool" } 22 | retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" } 23 | converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "converter-gson" } 24 | flexmark = { group = "com.vladsch.flexmark", name = "flexmark", version.ref = "flexmark" } 25 | 26 | [plugins] 27 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 28 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 29 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 30 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 31 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } 32 | #foojay-resolver-convention = { id = "org.gradle.toolchains.foojay-resolver-convention", version = "foojay-resolver-convention" } 33 | -------------------------------------------------------------------------------- /src/main/resources/icons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/resources/icons/favicon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/CommonProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.ViewConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.file.type.CommonFileType; 5 | import com.github.yuyuanweb.mianshiyaplugin.file.preview.CommonPreview; 6 | import com.intellij.openapi.fileEditor.FileEditor; 7 | import com.intellij.openapi.fileEditor.FileEditorPolicy; 8 | import com.intellij.openapi.fileEditor.WeighedFileEditorProvider; 9 | import com.intellij.openapi.fileTypes.FileType; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.openapi.vfs.VirtualFile; 12 | import com.intellij.ui.jcef.JBCefApp; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | /** 16 | * @author pine 17 | */ 18 | public class CommonProvider extends WeighedFileEditorProvider { 19 | 20 | 21 | @Override 22 | public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { 23 | FileType fileType = file.getFileType(); 24 | return fileType == CommonFileType.INSTANCE && JBCefApp.isSupported(); 25 | } 26 | 27 | @Override 28 | public @NotNull FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { 29 | return new CommonPreview(project, file); 30 | } 31 | 32 | @Override 33 | public @NotNull String getEditorTypeId() { 34 | return ViewConstant.COMMON_PROVIDER; 35 | } 36 | 37 | @Override 38 | public @NotNull FileEditorPolicy getPolicy() { 39 | return FileEditorPolicy.HIDE_DEFAULT_EDITOR; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/utils/UserUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.utils; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.UserRoleEnum; 4 | import com.github.yuyuanweb.mianshiyaplugin.model.response.User; 5 | 6 | import java.util.Arrays; 7 | import java.util.Date; 8 | import java.util.List; 9 | 10 | /** 11 | * @author pine 12 | */ 13 | public class UserUtil { 14 | 15 | /** 16 | * VIP 角色枚举列表 17 | */ 18 | public static final List VIP_ROLE_ENUM_LIST = Arrays.asList( 19 | UserRoleEnum.VIP, 20 | UserRoleEnum.SVIP, 21 | UserRoleEnum.FVIP 22 | ); 23 | 24 | public static final List VIP_ROLE_LIMIT_DATE_ENUM_LIST = Arrays.asList( 25 | UserRoleEnum.VIP, 26 | UserRoleEnum.SVIP 27 | ); 28 | 29 | 30 | public static boolean hasVipAuth(User user) { 31 | if (user == null) { 32 | return false; 33 | } 34 | String userRole = user.getUserRole(); 35 | UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(userRole); 36 | Date vipExpireTime = user.getVipExpireTime(); 37 | // 是管理员或运营,有权限 38 | if (UserRoleEnum.ADMIN.equals(userRoleEnum) || UserRoleEnum.OPERATOR.equals(userRoleEnum)) { 39 | return true; 40 | } 41 | // 不是 VIP 或超级 VIP 或永久 VIP 42 | if (!VIP_ROLE_ENUM_LIST.contains(userRoleEnum)) { 43 | return false; 44 | } 45 | // VIP 已过期 46 | if (VIP_ROLE_LIMIT_DATE_ENUM_LIST.contains(userRoleEnum)) { 47 | return vipExpireTime != null && !vipExpireTime.before(new Date()); 48 | } 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/enums/UserRoleEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.enums; 2 | 3 | import org.apache.commons.lang3.ObjectUtils; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * 用户角色枚举 11 | * 12 | * @author pine 13 | */ 14 | public enum UserRoleEnum { 15 | 16 | USER("用户", "user"), 17 | OPERATOR("运营人员", "operator"), 18 | VIP("vip", "vip"), 19 | SVIP("svip", "svip"), 20 | FVIP("fvip", "fvip"), 21 | ADMIN("管理员", "admin"), 22 | BAN("被封号", "ban"); 23 | 24 | private final String text; 25 | 26 | private final String value; 27 | 28 | UserRoleEnum(String text, String value) { 29 | this.text = text; 30 | this.value = value; 31 | } 32 | 33 | /** 34 | * 获取值列表 35 | * 36 | * @return {@link List}<{@link String}> 37 | */ 38 | public static List getValues() { 39 | return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList()); 40 | } 41 | 42 | /** 43 | * 根据 value 获取枚举 44 | * 45 | * @param value 值 46 | * @return {@link UserRoleEnum} 47 | */ 48 | public static UserRoleEnum getEnumByValue(String value) { 49 | if (ObjectUtils.isEmpty(value)) { 50 | return null; 51 | } 52 | for (UserRoleEnum item : UserRoleEnum.values()) { 53 | if (item.value.equals(value)) { 54 | return item; 55 | } 56 | } 57 | return null; 58 | } 59 | 60 | public String getValue() { 61 | return value; 62 | } 63 | 64 | public String getText() { 65 | return text; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/OuterBrowserFileEditorProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.ViewConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.WebTypeEnum; 5 | import com.github.yuyuanweb.mianshiyaplugin.file.preview.OuterBrowserFileEditorPreview; 6 | import com.intellij.openapi.fileEditor.FileEditor; 7 | import com.intellij.openapi.fileEditor.FileEditorPolicy; 8 | import com.intellij.openapi.fileEditor.WeighedFileEditorProvider; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.vfs.VirtualFile; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | /** 14 | * @author pine 15 | */ 16 | public class OuterBrowserFileEditorProvider extends WeighedFileEditorProvider { 17 | 18 | private WebTypeEnum webTypeEnum; 19 | 20 | public OuterBrowserFileEditorProvider() { 21 | } 22 | 23 | public OuterBrowserFileEditorProvider(WebTypeEnum webTypeEnum) { 24 | this.webTypeEnum = webTypeEnum; 25 | } 26 | 27 | @Override 28 | public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { 29 | return true; 30 | } 31 | 32 | @Override 33 | public @NotNull FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { 34 | return new OuterBrowserFileEditorPreview(project, file, webTypeEnum); 35 | } 36 | 37 | @Override 38 | public @NotNull String getEditorTypeId() { 39 | return ViewConstant.BEST_QUESTION_ANSWER_PROVIDER; 40 | } 41 | 42 | @Override 43 | public @NotNull FileEditorPolicy getPolicy() { 44 | return FileEditorPolicy.HIDE_DEFAULT_EDITOR; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/icons/help_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/enums/NeedVipEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.enums; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.ObjectUtils; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * 是否会员专属 13 | * 14 | * @author pine 15 | */ 16 | @Getter 17 | public enum NeedVipEnum { 18 | 19 | NO("否", 0), 20 | YES("是", 1), 21 | ; 22 | 23 | private final String text; 24 | 25 | private final int value; 26 | 27 | NeedVipEnum(String text, int value) { 28 | this.text = text; 29 | this.value = value; 30 | } 31 | 32 | /** 33 | * 获取值列表 34 | */ 35 | public static List getValues() { 36 | return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList()); 37 | } 38 | 39 | /** 40 | * 根据 text 获取枚举 41 | */ 42 | public static NeedVipEnum getEnumByText(String text) { 43 | if (StrUtil.isBlank(text)) { 44 | return null; 45 | } 46 | for (NeedVipEnum item : NeedVipEnum.values()) { 47 | if (item.text.equals(text)) { 48 | return item; 49 | } 50 | } 51 | return null; 52 | } 53 | 54 | /** 55 | * 根据 value 获取枚举 56 | */ 57 | public static NeedVipEnum getEnumByValue(Integer value) { 58 | if (ObjectUtils.isEmpty(value)) { 59 | return null; 60 | } 61 | for (NeedVipEnum questionDifficultyEnum : NeedVipEnum.values()) { 62 | if (questionDifficultyEnum.value == value) { 63 | return questionDifficultyEnum; 64 | } 65 | } 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/config/ApiConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.config; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.adapter.DateTypeAdapter; 4 | import com.github.yuyuanweb.mianshiyaplugin.api.MianShiYaApi; 5 | import com.github.yuyuanweb.mianshiyaplugin.api.interceptor.HeaderInterceptor; 6 | import com.github.yuyuanweb.mianshiyaplugin.api.interceptor.LogInterceptor; 7 | import com.github.yuyuanweb.mianshiyaplugin.api.interceptor.ResponseInterceptor; 8 | import com.github.yuyuanweb.mianshiyaplugin.constant.CommonConstant; 9 | import com.google.gson.Gson; 10 | import com.google.gson.GsonBuilder; 11 | import okhttp3.OkHttpClient; 12 | import retrofit2.Retrofit; 13 | import retrofit2.converter.gson.GsonConverterFactory; 14 | 15 | import java.util.Date; 16 | 17 | /** 18 | * 初始化请求工具 19 | * 20 | * @author pine 21 | */ 22 | public class ApiConfig { 23 | 24 | public static MianShiYaApi mianShiYaApi; 25 | 26 | static { 27 | // 自定义 Gson 实例 28 | Gson gson = new GsonBuilder() 29 | .registerTypeAdapter(Date.class, new DateTypeAdapter()) 30 | .create(); 31 | OkHttpClient client = new OkHttpClient.Builder() 32 | .addInterceptor(new HeaderInterceptor()) 33 | .addInterceptor(new LogInterceptor()) 34 | .addInterceptor(new ResponseInterceptor()) 35 | .build(); 36 | String mianShiYaBaseUrl = CommonConstant.API; 37 | Retrofit retrofit = new Retrofit.Builder() 38 | .baseUrl(mianShiYaBaseUrl) 39 | .addConverterFactory(GsonConverterFactory.create(gson)) 40 | .client(client) 41 | .build(); 42 | 43 | mianShiYaApi = retrofit.create(MianShiYaApi.class); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/config/GlobalState.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.config; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.PageConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.model.response.User; 5 | import com.intellij.openapi.application.ApplicationManager; 6 | import com.intellij.openapi.components.*; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * 全局配置 12 | * 13 | * @author pine 14 | */ 15 | @State(name = "GlobalState", storages = {@Storage("mainshiya.xml")}) 16 | public class GlobalState implements PersistentStateComponent { 17 | 18 | public static class State { 19 | public String cookie = ""; 20 | public int pageSize = PageConstant.PAGE_SIZE; 21 | public User user = null; 22 | } 23 | 24 | private State state = new State(); 25 | 26 | @Nullable 27 | @Override 28 | public State getState() { 29 | return state; 30 | } 31 | 32 | @Override 33 | public void loadState(@NotNull State state) { 34 | this.state = state; 35 | } 36 | 37 | public void saveCookie(String cookie) { 38 | state.cookie = cookie; 39 | } 40 | 41 | public void saveUser(User user) { 42 | state.user = user; 43 | } 44 | 45 | public String getSavedCookie() { 46 | return state.cookie; 47 | } 48 | 49 | public User getSavedUser() { 50 | return state.user; 51 | } 52 | 53 | public void removeSavedCookie() { 54 | state.cookie = ""; 55 | } 56 | 57 | public void removeSavedUser() { 58 | state.user = null; 59 | } 60 | 61 | public static GlobalState getInstance() { 62 | return ApplicationManager.getApplication().getService(GlobalState.class); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/enums/WebTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.enums; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.ObjectUtils; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Objects; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * 内嵌的网页类型 14 | * 15 | * @author pine 16 | */ 17 | @Getter 18 | public enum WebTypeEnum { 19 | 20 | QUESTION("题目", "question"), 21 | ANSWER("回答", "answer"), 22 | COMMENT("评论", "comment"), 23 | ; 24 | 25 | private final String text; 26 | 27 | private final String value; 28 | 29 | WebTypeEnum(String text, String value) { 30 | this.text = text; 31 | this.value = value; 32 | } 33 | 34 | /** 35 | * 获取值列表 36 | */ 37 | public static List getValues() { 38 | return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList()); 39 | } 40 | 41 | /** 42 | * 根据 text 获取枚举 43 | */ 44 | public static WebTypeEnum getEnumByText(String text) { 45 | if (StrUtil.isBlank(text)) { 46 | return null; 47 | } 48 | for (WebTypeEnum item : WebTypeEnum.values()) { 49 | if (item.text.equals(text)) { 50 | return item; 51 | } 52 | } 53 | return null; 54 | } 55 | 56 | /** 57 | * 根据 value 获取枚举 58 | */ 59 | public static WebTypeEnum getEnumByValue(String value) { 60 | if (ObjectUtils.isEmpty(value)) { 61 | return null; 62 | } 63 | for (WebTypeEnum item : WebTypeEnum.values()) { 64 | if (Objects.equals(item.value, value)) { 65 | return item; 66 | } 67 | } 68 | return null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/enums/DifficultyEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.enums; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.ObjectUtils; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * 题目难度枚举 13 | * 简单 = 1,中等 = 3,困难 = 5 14 | * 15 | * @author pine 16 | */ 17 | @Getter 18 | public enum DifficultyEnum { 19 | 20 | SIMPLE("简单", 1), 21 | MIDDLE("中等", 3), 22 | HARD("困难", 5), 23 | ; 24 | 25 | private final String text; 26 | 27 | private final int value; 28 | 29 | DifficultyEnum(String text, int value) { 30 | this.text = text; 31 | this.value = value; 32 | } 33 | 34 | /** 35 | * 获取值列表 36 | */ 37 | public static List getValues() { 38 | return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList()); 39 | } 40 | 41 | /** 42 | * 根据 text 获取枚举 43 | */ 44 | public static DifficultyEnum getEnumByText(String text) { 45 | if (StrUtil.isBlank(text)) { 46 | return null; 47 | } 48 | for (DifficultyEnum item : DifficultyEnum.values()) { 49 | if (item.text.equals(text)) { 50 | return item; 51 | } 52 | } 53 | return null; 54 | } 55 | 56 | /** 57 | * 根据 value 获取枚举 58 | */ 59 | public static DifficultyEnum getEnumByValue(Integer value) { 60 | if (ObjectUtils.isEmpty(value)) { 61 | return null; 62 | } 63 | for (DifficultyEnum questionDifficultyEnum : DifficultyEnum.values()) { 64 | if (questionDifficultyEnum.value == value) { 65 | return questionDifficultyEnum; 66 | } 67 | } 68 | return null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/enums/QuestionDifficultyEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.enums; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.ObjectUtils; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * 难度枚举 13 | * 14 | * @author pine 15 | */ 16 | @Getter 17 | public enum QuestionDifficultyEnum { 18 | 19 | SIMPLE("简单", 1), 20 | MEDIUM("中等", 3), 21 | DIFFICULTY("困难", 5); 22 | 23 | private final String text; 24 | 25 | private final int value; 26 | 27 | QuestionDifficultyEnum(String text, int value) { 28 | this.text = text; 29 | this.value = value; 30 | } 31 | 32 | /** 33 | * 获取值列表 34 | */ 35 | public static List getValues() { 36 | return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList()); 37 | } 38 | 39 | /** 40 | * 根据 text 获取枚举 41 | */ 42 | public static QuestionDifficultyEnum getEnumByText(String text) { 43 | if (StrUtil.isBlank(text)) { 44 | return null; 45 | } 46 | for (QuestionDifficultyEnum item : QuestionDifficultyEnum.values()) { 47 | if (item.text.equals(text)) { 48 | return item; 49 | } 50 | } 51 | return null; 52 | } 53 | 54 | /** 55 | * 根据 value 获取枚举 56 | */ 57 | public static QuestionDifficultyEnum getEnumByValue(Integer value) { 58 | if (ObjectUtils.isEmpty(value)) { 59 | return null; 60 | } 61 | for (QuestionDifficultyEnum questionDifficultyEnum : QuestionDifficultyEnum.values()) { 62 | if (questionDifficultyEnum.value == value) { 63 | return questionDifficultyEnum; 64 | } 65 | } 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/model/response/QuestionBank.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.model.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | /** 10 | * 题库视图 11 | * 12 | * @author pine 13 | */ 14 | @Data 15 | public class QuestionBank implements Serializable { 16 | /** 17 | * id 18 | */ 19 | private Long id; 20 | 21 | /** 22 | * 标题 23 | */ 24 | private String title; 25 | 26 | /** 27 | * 描述 28 | */ 29 | private String description; 30 | 31 | /** 32 | * 图片 33 | */ 34 | private String picture; 35 | 36 | 37 | /** 38 | * 标签列表 39 | */ 40 | private List tagList; 41 | /** 42 | * 浏览数 43 | */ 44 | private Integer viewNum; 45 | 46 | /** 47 | * 状态:0-待审核, 1-通过, 2-拒绝 48 | */ 49 | private Integer reviewStatus; 50 | 51 | /** 52 | * 审核信息 53 | */ 54 | private String reviewMessage; 55 | 56 | /** 57 | * 审核人 id 58 | */ 59 | private Long reviewerId; 60 | 61 | /** 62 | * 审核时间 63 | */ 64 | private Date reviewTime; 65 | 66 | /** 67 | * 优先级 68 | */ 69 | private Integer priority; 70 | 71 | /** 72 | * 创建用户 id 73 | */ 74 | private Long userId; 75 | 76 | /** 77 | * 编辑时间 78 | */ 79 | private Date editTime; 80 | 81 | /** 82 | * 创建时间 83 | */ 84 | private Date createTime; 85 | 86 | /** 87 | * 更新时间 88 | */ 89 | private Date updateTime; 90 | 91 | /** 92 | * 会员题目数量 93 | */ 94 | private Long vipCount; 95 | /** 96 | * 所有题目数量 97 | */ 98 | private Long allCount; 99 | /** 100 | * 是否所有题目是会员 101 | */ 102 | private Boolean isAllQuestionVip; 103 | 104 | private static final long serialVersionUID = 1L; 105 | } -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/actions/LoginAction.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.actions; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 5 | import com.github.yuyuanweb.mianshiyaplugin.manager.CookieManager; 6 | import com.github.yuyuanweb.mianshiyaplugin.model.response.User; 7 | import com.github.yuyuanweb.mianshiyaplugin.utils.PanelUtil; 8 | import com.github.yuyuanweb.mianshiyaplugin.view.LoginPanel; 9 | import com.intellij.openapi.actionSystem.AnAction; 10 | import com.intellij.openapi.actionSystem.AnActionEvent; 11 | import com.intellij.openapi.actionSystem.DefaultActionGroup; 12 | import com.intellij.openapi.application.ApplicationManager; 13 | import com.intellij.openapi.project.DumbAware; 14 | import com.intellij.openapi.project.ProjectManager; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import javax.swing.*; 18 | 19 | /** 20 | * 登录 21 | * 22 | * @author pine 23 | */ 24 | public class LoginAction extends AnAction implements DumbAware { 25 | 26 | private final DefaultActionGroup actionGroup; 27 | 28 | // 构造函数 29 | public LoginAction(String text, Icon icon, DefaultActionGroup actionGroup) { 30 | // Action 名称 31 | super(text, text, icon); 32 | this.actionGroup = actionGroup; 33 | } 34 | 35 | @Override 36 | public void actionPerformed(@NotNull AnActionEvent e) { 37 | LoginPanel loginPanel = new LoginPanel(ProjectManager.getInstance().getDefaultProject()); 38 | loginPanel.showAndGet(); 39 | 40 | GlobalState globalState = GlobalState.getInstance(); 41 | String cookie = globalState.getSavedCookie(); 42 | if (StrUtil.isBlank(cookie)) { 43 | return; 44 | } 45 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 46 | User loginUser = CookieManager.getLoginUser(); 47 | if (loginUser == null) { 48 | return; 49 | } 50 | 51 | PanelUtil.modifyActionGroupWhenLogin(actionGroup, loginUser); 52 | }); 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/utils/ContentUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.utils; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 4 | import com.intellij.openapi.actionSystem.ActionManager; 5 | import com.intellij.openapi.actionSystem.ActionToolbar; 6 | import com.intellij.openapi.actionSystem.DefaultActionGroup; 7 | import com.intellij.openapi.application.ApplicationManager; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.openapi.wm.ToolWindowManager; 10 | import com.intellij.ui.components.JBPanel; 11 | import com.intellij.ui.content.Content; 12 | import com.intellij.ui.content.ContentFactory; 13 | import com.intellij.ui.content.ContentManager; 14 | import com.intellij.util.ui.JBUI; 15 | 16 | import javax.swing.*; 17 | import java.awt.*; 18 | 19 | /** 20 | * content 页 工具 21 | * @author pine 22 | */ 23 | public class ContentUtil { 24 | 25 | public static void createContent(JComponent component, String title, boolean isLockable, Project project) { 26 | if (component == null) { 27 | throw new IllegalArgumentException("component is null"); 28 | } 29 | ActionManager actionManager = ActionManager.getInstance(); 30 | DefaultActionGroup actionGroup = (DefaultActionGroup) actionManager.getAction(KeyConstant.ACTION_BAR); 31 | ActionToolbar actionToolbar = actionManager.createActionToolbar(KeyConstant.ACTION_BAR, actionGroup, true); 32 | // 创建主面板 33 | JPanel mainPanel = new JBPanel<>(new BorderLayout()); 34 | mainPanel.setBorder(JBUI.Borders.empty()); 35 | actionToolbar.setTargetComponent(mainPanel); 36 | mainPanel.add(actionToolbar.getComponent(), BorderLayout.NORTH); 37 | mainPanel.add(component, BorderLayout.CENTER); 38 | 39 | // ContentFactory contentFactory = ContentFactory.getInstance(); 40 | ContentFactory contentFactory = ApplicationManager.getApplication().getService(ContentFactory.class); 41 | Content content = contentFactory.createContent(mainPanel, title, isLockable); 42 | ContentManager contentManager = ToolWindowManager.getInstance(project).getToolWindow(KeyConstant.PLUGIN_NAME).getContentManager(); 43 | contentManager.addContent(content); 44 | contentManager.setSelectedContent(content); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/api/interceptor/ResponseInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.api.interceptor; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 4 | import com.github.yuyuanweb.mianshiyaplugin.constant.CommonConstant; 5 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.ErrorCode; 6 | import com.github.yuyuanweb.mianshiyaplugin.model.common.BaseResponse; 7 | import com.github.yuyuanweb.mianshiyaplugin.view.LoginPanel; 8 | import com.google.gson.Gson; 9 | import com.intellij.openapi.application.ApplicationManager; 10 | import com.intellij.openapi.project.ProjectManager; 11 | import okhttp3.*; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.io.IOException; 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | 18 | /** 19 | * 响应拦截器 20 | * 21 | * @author pine 22 | */ 23 | public class ResponseInterceptor implements Interceptor { 24 | 25 | private final static Gson GSON = new Gson(); 26 | 27 | private static final Set NO_NEED_LOGIN_PANEL = new HashSet<>(){{ 28 | add("api/user/get/login"); 29 | }}; 30 | 31 | @NotNull 32 | @Override 33 | public Response intercept(Chain chain) throws IOException { 34 | Request originalRequest = chain.request(); 35 | Response response = chain.proceed(originalRequest); 36 | MediaType mediaType = response.body().contentType(); 37 | 38 | ResponseBody body = response.body(); 39 | if (body == null) { 40 | return chain.proceed(originalRequest); 41 | } 42 | // body.string() 调用后会把响应体清空,所以后面要重新构建响应体 43 | String data = body.string(); 44 | BaseResponse baseResponse = GSON.fromJson(data, BaseResponse.class); 45 | String apiPath = originalRequest.url().toString().replace(CommonConstant.HOST, ""); 46 | if (NO_NEED_LOGIN_PANEL.contains(apiPath)) { 47 | return chain.proceed(originalRequest); 48 | } 49 | if (baseResponse.getCode() == ErrorCode.NOT_LOGIN_ERROR.getCode()) { 50 | ApplicationManager.getApplication().invokeAndWait(() -> { 51 | LoginPanel loginPanel = new LoginPanel(ProjectManager.getInstance().getDefaultProject()); 52 | loginPanel.showAndGet(); 53 | }); 54 | originalRequest = originalRequest.newBuilder().header("Cookie", GlobalState.getInstance().getSavedCookie()).build(); 55 | return chain.proceed(originalRequest); 56 | } 57 | 58 | return response.newBuilder() 59 | .body(ResponseBody.create(mediaType, data)) 60 | .build(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/preview/EditorWithPreview.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.preview; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.ViewConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.utils.LogUtils; 5 | import com.intellij.codeHighlighting.BackgroundEditorHighlighter; 6 | import com.intellij.ide.structureView.StructureViewBuilder; 7 | import com.intellij.openapi.actionSystem.ActionGroup; 8 | import com.intellij.openapi.actionSystem.DefaultActionGroup; 9 | import com.intellij.openapi.application.ApplicationManager; 10 | import com.intellij.openapi.fileEditor.*; 11 | import com.intellij.openapi.util.Key; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | /** 16 | * 分栏 文件编辑器 17 | * 18 | * @author pine 19 | */ 20 | public class EditorWithPreview extends TextEditorWithPreview { 21 | 22 | public static final Key PARENT_SPLIT_EDITOR_KEY = Key.create(ViewConstant.PARENT_SPLIT_EDITOR); 23 | 24 | public EditorWithPreview(@NotNull TextEditor editor, @NotNull FileEditor preview) { 25 | super(editor, preview, "Question Editor", Layout.SHOW_EDITOR); 26 | editor.putUserData(PARENT_SPLIT_EDITOR_KEY, this); 27 | preview.putUserData(PARENT_SPLIT_EDITOR_KEY, this); 28 | 29 | // 将 UI 更新放到 invokeLater 中 30 | ApplicationManager.getApplication().invokeLater(() -> { 31 | try { 32 | // 删除工具栏 33 | getComponent().remove(0); 34 | getComponent().revalidate(); 35 | getComponent().repaint(); 36 | } catch (Exception e) { 37 | // 处理异常 38 | LogUtils.LOG.error(e.getMessage()); 39 | } 40 | }); 41 | } 42 | 43 | @NotNull 44 | @Override 45 | protected ActionGroup createViewActionGroup() { 46 | return new DefaultActionGroup( 47 | getShowEditorAction() 48 | ); 49 | } 50 | 51 | @Nullable 52 | @Override 53 | public BackgroundEditorHighlighter getBackgroundHighlighter() { 54 | return getTextEditor().getBackgroundHighlighter(); 55 | } 56 | 57 | @Nullable 58 | @Override 59 | public FileEditorLocation getCurrentLocation() { 60 | return getTextEditor().getCurrentLocation(); 61 | } 62 | 63 | @Nullable 64 | @Override 65 | public StructureViewBuilder getStructureViewBuilder() { 66 | return getTextEditor().getStructureViewBuilder(); 67 | } 68 | 69 | 70 | @NotNull 71 | @Override 72 | public TextEditor getTextEditor() { 73 | if (((TextEditor) myPreview).getEditor() == null) { 74 | return myEditor; 75 | } 76 | return (TextEditor) myPreview; 77 | } 78 | 79 | @NotNull 80 | public FileEditor getPreviewEditor() { 81 | return myPreview == getTextEditor() ? myEditor : myPreview; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/ConvergeProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.file.preview.ConvergePreview; 4 | import com.intellij.openapi.fileEditor.*; 5 | import com.intellij.openapi.project.DumbAware; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.vfs.VirtualFile; 8 | import org.jetbrains.annotations.NonNls; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.Arrays; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * 聚合 提供者 16 | * 17 | * @author pine 18 | */ 19 | public class ConvergeProvider implements AsyncFileEditorProvider, DumbAware { 20 | 21 | 22 | @NotNull 23 | protected final FileEditorProvider[] editorProviders; 24 | protected final String[] names; 25 | 26 | @NotNull 27 | private final String myEditorTypeId; 28 | 29 | public ConvergeProvider(@NotNull FileEditorProvider[] editorProviders, @NotNull String[] names) { 30 | this.editorProviders = editorProviders; 31 | this.names = names; 32 | this.myEditorTypeId = "tab-provider[" + Arrays.stream(editorProviders).map(FileEditorProvider::getEditorTypeId).collect(Collectors.joining(";")) + "]"; 33 | } 34 | 35 | 36 | @NotNull 37 | @Override 38 | public Builder createEditorAsync(@NotNull final Project project, @NotNull final VirtualFile file) { 39 | final Builder[] builders = new Builder[editorProviders.length]; 40 | for (int i = 0; i < editorProviders.length; i++) { 41 | FileEditorProvider provider = editorProviders[i]; 42 | builders[i] = new Builder() { 43 | @NotNull 44 | @Override 45 | public FileEditor build() { 46 | return provider.createEditor(project, file); 47 | } 48 | }; 49 | } 50 | return new Builder() { 51 | @Override 52 | public TextEditor build() { 53 | FileEditor[] fileEditors = new FileEditor[editorProviders.length]; 54 | for (int i = 0; i < builders.length; i++) { 55 | fileEditors[i] = builders[i].build(); 56 | } 57 | return createSplitEditor(fileEditors, project, file); 58 | } 59 | }; 60 | } 61 | 62 | protected TextEditor createSplitEditor(@NotNull FileEditor[] fileEditors, Project project, VirtualFile file) { 63 | return new ConvergePreview(fileEditors, names, project, file); 64 | } 65 | 66 | @Override 67 | public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { 68 | return true; 69 | } 70 | 71 | @Override 72 | public @NotNull FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { 73 | return createEditorAsync(project, file).build(); 74 | } 75 | 76 | @Override 77 | public @NotNull @NonNls String getEditorTypeId() { 78 | return myEditorTypeId; 79 | } 80 | 81 | @Override 82 | public @NotNull FileEditorPolicy getPolicy() { 83 | return FileEditorPolicy.HIDE_DEFAULT_EDITOR; 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/api/MianShiYaApi.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.api; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.model.common.BaseResponse; 4 | import com.github.yuyuanweb.mianshiyaplugin.model.common.Page; 5 | import com.github.yuyuanweb.mianshiyaplugin.model.common.PageRequest; 6 | import com.github.yuyuanweb.mianshiyaplugin.model.dto.*; 7 | import com.github.yuyuanweb.mianshiyaplugin.model.response.*; 8 | import retrofit2.Call; 9 | import retrofit2.http.*; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * 面试鸭接口 15 | * 16 | * @author pine 17 | */ 18 | public interface MianShiYaApi { 19 | 20 | /** 21 | * 获取登录用户信息 22 | */ 23 | @GET("user/get/login") 24 | Call> getLoginUser(); 25 | 26 | /** 27 | * 获取题库列表 28 | */ 29 | @POST("questionBankCategory/list_questionBank") 30 | Call>> getQuestionBankList( 31 | @Body QuestionBankCategoryBankQueryRequest queryRequest 32 | ); 33 | 34 | /** 35 | * 获取题目列表 36 | */ 37 | @POST("question/list/page/vo") 38 | Call>> getQuestionList( 39 | @Body QuestionQueryRequest queryRequest 40 | ); 41 | 42 | /** 43 | * 搜索题目列表 44 | */ 45 | @POST("question/search") 46 | Call>> searchQuestionList( 47 | @Body QuestionQueryRequest queryRequest 48 | ); 49 | 50 | @POST("question_bank/list_question") 51 | Call>> listQuestionByQuestionBank( 52 | @Body QuestionQueryRequest queryRequest 53 | ); 54 | 55 | 56 | /** 57 | * 获取刷题信息 58 | * 59 | * @param questionBankId 题库 id 60 | * @param questionId 题目 id 61 | * @return {@link Call }<{@link BaseResponse }<{@link DoQuestionInfoVO }>> 62 | */ 63 | @GET("question_bank/do_question_info") 64 | Call> getDoQuestionInfo( 65 | @Query("questionBankId") long questionBankId, 66 | @Query("questionId") long questionId 67 | ); 68 | 69 | /** 70 | * 获取回答列表 71 | */ 72 | @POST("question_answer/list/by_question") 73 | Call>> listQuestionAnswerByQuestionId( 74 | @Body QuestionAnswerQueryRequest queryRequest 75 | ); 76 | 77 | /** 78 | * 获取题库分类列表 79 | */ 80 | @POST("questionBankCategory/list") 81 | Call>> listQuestionBankCategory( 82 | @Body PageRequest pageRequest 83 | ); 84 | 85 | /** 86 | * 获取题库列表 87 | */ 88 | @POST("question_bank/list/page/vo") 89 | Call>> listQuestionBankVoByPage( 90 | @Body QuestionBankQueryRequest queryRequest 91 | ); 92 | 93 | /** 94 | * 获取标签分类列表 95 | */ 96 | @POST("tagCategory/list") 97 | Call>> listTagCategory( 98 | @Body TagCategoryQueryRequest queryRequest 99 | ); 100 | 101 | /** 102 | * 用户退出登录 103 | */ 104 | @POST("user/logout") 105 | Call> userLogout(); 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/EditorProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 4 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.WebTypeEnum; 5 | import com.github.yuyuanweb.mianshiyaplugin.file.preview.EditorWithPreview; 6 | import com.github.yuyuanweb.mianshiyaplugin.file.type.MsycFileType; 7 | import com.intellij.openapi.diagnostic.Logger; 8 | import com.intellij.openapi.fileEditor.AsyncFileEditorProvider; 9 | import com.intellij.openapi.fileEditor.FileEditor; 10 | import com.intellij.openapi.fileEditor.FileEditorProvider; 11 | import com.intellij.openapi.fileEditor.TextEditor; 12 | import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorProvider; 13 | import com.intellij.openapi.fileTypes.FileType; 14 | import com.intellij.openapi.project.Project; 15 | import com.intellij.openapi.vfs.VirtualFile; 16 | import com.intellij.ui.jcef.JBCefApp; 17 | import com.intellij.util.keyFMap.KeyFMap; 18 | import org.jetbrains.annotations.NotNull; 19 | 20 | /** 21 | * @author pine 22 | */ 23 | public class EditorProvider extends SplitTextEditorProvider { 24 | 25 | public static final Logger LOG = Logger.getInstance("#com.github.yuyuanweb.mianshiyaplugin"); 26 | 27 | public EditorProvider() { 28 | super(new PsiAwareTextEditorProvider(), new ConvergeProvider(new FileEditorProvider[]{new OuterBrowserFileEditorProvider(WebTypeEnum.QUESTION), new OuterBrowserFileEditorProvider(WebTypeEnum.ANSWER), new OuterBrowserFileEditorProvider(WebTypeEnum.COMMENT)}, new String[]{"题目", "推荐答案", "讨论区"})); 29 | } 30 | 31 | @Override 32 | public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { 33 | FileType fileType = file.getFileType(); 34 | if (fileType == MsycFileType.INSTANCE_C && JBCefApp.isSupported()) { 35 | return true; 36 | } 37 | try { 38 | if (!file.exists()) { 39 | return false; 40 | } 41 | KeyFMap keyFMap = file.get(); 42 | Long questionId = keyFMap.get(KeyConstant.QUESTION_ID_KEY); 43 | if (questionId == null) { 44 | return false; 45 | } 46 | } catch (Throwable e) { 47 | return false; 48 | } 49 | return true; 50 | } 51 | 52 | @NotNull 53 | @Override 54 | public AsyncFileEditorProvider.Builder createEditorAsync(@NotNull Project project, @NotNull VirtualFile file) { 55 | 56 | final AsyncFileEditorProvider.Builder firstBuilder = getBuilderFromEditorProvider(this.myFirstProvider, project, file); 57 | final AsyncFileEditorProvider.Builder secondBuilder = getBuilderFromEditorProvider(this.mySecondProvider, project, file); 58 | return new AsyncFileEditorProvider.Builder() { 59 | @NotNull 60 | @Override 61 | public FileEditor build() { 62 | return createSplitEditor(firstBuilder.build(), secondBuilder.build()); 63 | } 64 | }; 65 | } 66 | 67 | @Override 68 | protected FileEditor createSplitEditor(@NotNull FileEditor firstEditor, @NotNull FileEditor secondEditor) { 69 | 70 | return new EditorWithPreview((TextEditor) secondEditor, firstEditor); 71 | } 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/actions/LogoutAction.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.actions; 2 | 3 | import cn.hutool.core.util.BooleanUtil; 4 | import com.github.yuyuanweb.mianshiyaplugin.config.ApiConfig; 5 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 6 | import com.github.yuyuanweb.mianshiyaplugin.constant.IconConstant; 7 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 8 | import com.github.yuyuanweb.mianshiyaplugin.model.common.BaseResponse; 9 | import com.intellij.openapi.actionSystem.*; 10 | import com.intellij.openapi.application.ApplicationManager; 11 | import com.intellij.openapi.project.DumbAware; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import javax.swing.*; 15 | import java.io.IOException; 16 | 17 | import static com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant.LOGIN; 18 | import static com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant.LOGIN_ZH; 19 | 20 | /** 21 | * 注销 22 | * 23 | * @author pine 24 | */ 25 | public class LogoutAction extends AnAction implements DumbAware { 26 | 27 | private final DefaultActionGroup actionGroup; 28 | 29 | // 构造函数 30 | public LogoutAction(String text, Icon icon, DefaultActionGroup actionGroup) { 31 | // Action 名称 32 | super(text, text, icon); 33 | this.actionGroup = actionGroup; 34 | } 35 | 36 | @Override 37 | public void actionPerformed(@NotNull AnActionEvent e) { 38 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 39 | // 1. 调用退出登录接口 40 | Boolean logout = null; 41 | int code = 0; 42 | try { 43 | BaseResponse response = ApiConfig.mianShiYaApi.userLogout().execute().body(); 44 | code = response.getCode(); 45 | logout = response.getData(); 46 | } catch (IOException ex) { 47 | throw new RuntimeException(ex); 48 | } 49 | if (code == 0 && BooleanUtil.isFalse(logout)) { 50 | return; 51 | } 52 | ApplicationManager.getApplication().invokeLater(() -> { 53 | // 2. 删除本地存储的登录态 54 | GlobalState globalState = GlobalState.getInstance(); 55 | globalState.removeSavedCookie(); 56 | globalState.removeSavedUser(); 57 | // 3. 更改 actionGroup 58 | ActionManager actionManager = ActionManager.getInstance(); 59 | // 3.1 删除 注销 60 | AnAction logoutAction = actionManager.getAction(KeyConstant.LOGOUT); 61 | if (logoutAction == null) { 62 | return; 63 | } 64 | actionGroup.remove(logoutAction); 65 | actionManager.unregisterAction(KeyConstant.LOGOUT); 66 | 67 | // 3.2 删除 会员 68 | AnAction vipAction = actionManager.getAction(KeyConstant.VIP); 69 | if (vipAction == null) { 70 | return; 71 | } 72 | actionGroup.remove(vipAction); 73 | actionManager.unregisterAction(KeyConstant.VIP); 74 | 75 | // 3.3 增加 登录 76 | LoginAction loginAction = new LoginAction(LOGIN_ZH, IconConstant.LOGIN, actionGroup); 77 | actionGroup.add(loginAction); 78 | actionManager.registerAction(LOGIN, loginAction); 79 | }); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/preview/CommonPreview.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.preview; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.ViewConstant; 4 | import com.intellij.openapi.editor.Document; 5 | import com.intellij.openapi.fileEditor.FileDocumentManager; 6 | import com.intellij.openapi.fileEditor.FileEditor; 7 | import com.intellij.openapi.fileEditor.FileEditorLocation; 8 | import com.intellij.openapi.fileEditor.FileEditorState; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.util.UserDataHolderBase; 11 | import com.intellij.openapi.vfs.VirtualFile; 12 | import com.intellij.ui.components.JBLabel; 13 | import com.intellij.ui.components.JBPanel; 14 | import com.intellij.util.ui.JBUI; 15 | import com.intellij.util.ui.components.BorderLayoutPanel; 16 | import org.jetbrains.annotations.NotNull; 17 | import org.jetbrains.annotations.Nullable; 18 | import org.jsoup.Jsoup; 19 | 20 | import javax.swing.*; 21 | import java.awt.*; 22 | import java.beans.PropertyChangeListener; 23 | 24 | /** 25 | * 普通 文件编辑器 26 | * 27 | * @author pine 28 | */ 29 | public class CommonPreview extends UserDataHolderBase implements FileEditor { 30 | 31 | private final VirtualFile myFile; 32 | private final Document myDocument; 33 | 34 | private BorderLayoutPanel myHtmlPanelWrapper; 35 | 36 | public CommonPreview(@NotNull Project project, @NotNull VirtualFile file) { 37 | myFile = file; 38 | myDocument = FileDocumentManager.getInstance().getDocument(myFile); 39 | } 40 | 41 | @Override 42 | public @NotNull JComponent getComponent() { 43 | if (myHtmlPanelWrapper == null) { 44 | myHtmlPanelWrapper = JBUI.Panels.simplePanel(); 45 | 46 | JBLabel loadingLabel = new JBLabel("Loading......"); 47 | myHtmlPanelWrapper.addToTop(loadingLabel); 48 | try { 49 | JBPanel tempPanel = new JBPanel<>(new BorderLayout()); 50 | 51 | JBLabel jbLabel = new JBLabel(Jsoup.parse(myDocument.getText()).html()); 52 | tempPanel.add(jbLabel); 53 | myHtmlPanelWrapper.addToTop(tempPanel.getComponent(0)); 54 | 55 | } catch (Throwable e) { 56 | myHtmlPanelWrapper.addToTop(new JBLabel("Your environment does not support JCEF.
Check the Registry 'ide.browser.jcef.enabled'.
" + e.getMessage() + "")); 57 | } finally { 58 | myHtmlPanelWrapper.remove(loadingLabel); 59 | myHtmlPanelWrapper.repaint(); 60 | } 61 | } 62 | 63 | return myHtmlPanelWrapper; 64 | } 65 | 66 | @Override 67 | public @Nullable JComponent getPreferredFocusedComponent() { 68 | return null; 69 | } 70 | 71 | @Override 72 | public @NotNull String getName() { 73 | return ViewConstant.COMMON_PREVIEW; 74 | } 75 | 76 | @Override 77 | public void setState(@NotNull FileEditorState state) { 78 | } 79 | 80 | @Override 81 | public boolean isModified() { 82 | return false; 83 | } 84 | 85 | @Override 86 | public boolean isValid() { 87 | return true; 88 | } 89 | 90 | @Override 91 | public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { 92 | 93 | } 94 | 95 | @Override 96 | public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { 97 | 98 | } 99 | 100 | @Override 101 | public void dispose() { 102 | } 103 | 104 | @Override 105 | public @Nullable VirtualFile getFile() { 106 | return myFile; 107 | } 108 | 109 | @Override 110 | public @Nullable FileEditorLocation getCurrentLocation() { 111 | return null; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/manager/CookieManager.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.manager; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.github.yuyuanweb.mianshiyaplugin.config.ApiConfig; 5 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 6 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 7 | import com.github.yuyuanweb.mianshiyaplugin.model.response.User; 8 | import com.github.yuyuanweb.mianshiyaplugin.utils.PanelUtil; 9 | import com.intellij.openapi.actionSystem.ActionManager; 10 | import com.intellij.openapi.actionSystem.DefaultActionGroup; 11 | import com.intellij.openapi.diagnostic.Logger; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.cef.network.CefCookieManager; 14 | 15 | import javax.swing.*; 16 | import java.io.IOException; 17 | 18 | /** 19 | * Cookie 管理器 20 | * 21 | * @author pine 22 | */ 23 | @Slf4j 24 | public class CookieManager { 25 | 26 | private static final Logger logger = Logger.getInstance(CookieManager.class); 27 | 28 | /** 29 | * 获取当前登录用户 30 | */ 31 | public static User getLoginUser() { 32 | User user = null; 33 | try { 34 | user = ApiConfig.mianShiYaApi.getLoginUser().execute().body().getData(); 35 | } catch (IOException e) { 36 | log.error("Failed to get login user", e); 37 | // throw new RuntimeException(e); 38 | } 39 | return user; 40 | } 41 | 42 | public static void handleCookie(CefCookieManager cefCookieManager, String currentUrl, Runnable afterLogin) { 43 | logger.warn("Starting handleCookie for URL: " + currentUrl); 44 | 45 | // 建议使用 visitUrlCookies 而不是 visitAllCookies,更加精准 46 | // 如果没有 currentUrl,还是可以用 visitAllCookies,但必须改 return 逻辑 47 | cefCookieManager.visitUrlCookies(currentUrl, true, (cefCookie, count, total, boolRef) -> { 48 | String targetName = "SESSION"; 49 | String name = cefCookie.name; 50 | 51 | // 打印调试,看看现在有哪些 Cookie 52 | logger.warn("Scanning cookie: " + name + " | value: " + cefCookie.value + " | path: " + cefCookie.path); 53 | 54 | // 如果名字不对,或者值为空,返回 true (继续找下一个) 55 | if (!targetName.equals(name) || StrUtil.isBlank(cefCookie.value)) { 56 | return true; // 【关键修改】继续遍历 57 | } 58 | 59 | // --- 找到 SESSION 了 --- 60 | 61 | GlobalState globalState = GlobalState.getInstance(); 62 | String oldCookie = globalState.getSavedCookie(); 63 | String newCookie = targetName + "=" + cefCookie.value; 64 | 65 | // 如果 Cookie 没变,停止遍历 (或者继续遍历也没事,看你需求) 66 | if (oldCookie.equals(newCookie)) { 67 | // 找到了但没变,可以选择停止遍历 68 | return false; 69 | } 70 | 71 | // 保存新 Cookie 72 | globalState.saveCookie(newCookie); 73 | 74 | // 获取用户信息逻辑 75 | User loginUser = CookieManager.getLoginUser(); 76 | if (loginUser != null) { 77 | globalState.saveUser(loginUser); 78 | ActionManager actionManager = ActionManager.getInstance(); 79 | DefaultActionGroup actionGroup = (DefaultActionGroup) actionManager.getAction(KeyConstant.ACTION_BAR); 80 | 81 | // UI 操作务必在 EDT 线程 82 | SwingUtilities.invokeLater(() -> { 83 | PanelUtil.modifyActionGroupWhenLogin(actionGroup, loginUser); 84 | afterLogin.run(); 85 | }); 86 | 87 | // 找到了并处理成功,停止遍历 88 | return false; 89 | } else { 90 | globalState.removeSavedCookie(); 91 | globalState.removeSavedUser(); 92 | } 93 | 94 | // 如果获取用户失败,可能 Session 无效,继续找下一个(极少情况)或者停止 95 | return true; 96 | }); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.github.yuyuanweb.mianshiyaplugin 4 | Mianshiya 5 | yuyuanweb 6 | 7 | com.intellij.modules.platform 8 | 9 | messages.MyBundle 10 | 11 | Plugin Introduction 13 |

This is a plug-in version of mianshiya system, which provides a more convenient and flexible way to practice questions.

14 |

面试鸭 是一个帮助程序员在 JetBrains 系列产品中刷面试题的插件。插件题目全面、题解优质、支持多种筛选条件,是一款摸鱼刷题必备神器。

15 | 16 |

特性

17 | 18 |
    19 |
  • 题目丰富:拥有 20+ 题库分类,近 200 题库,9000+ 题目,涵盖了几乎所有主流编程方向的面试题。
  • 20 |
  • 高质量题解:许多题解来自大厂面试官原创,抓住核心要点,通俗易懂,配有图片辅助理解。
  • 21 |
  • 快捷键支持:支持自定义快捷键,轻松一键打开或收起插件。
  • 22 |
  • 多条件筛选:可通过题库、标签、难度等条件筛选,快速找到自己想要的题目。
  • 23 |
  • 讨论互动:题目详情页面分为题目、推荐答案、讨论区三个子 tab,方便交流讨论。
  • 24 |
  • 多端同步:支持 IDEA 及 JetBrains 系列其他产品,如 WebStorm、PyCharm 等。同时支持小程序和 Web 端,数据三端同步。
  • 25 |
  • 开源:插件完全开源,代码托管在 GitHub,欢迎贡献和 Star 支持。
  • 26 |
27 | 28 |

安装与使用

29 | 30 |
    31 |
  1. 打开 IntelliJ IDEA,进入 Settings > Plugins
  2. 32 |
  3. 搜索 mianshiya,点击安装。
  4. 33 |
34 | 35 |

三端兼容

36 | 37 |
    38 |
  • JetBrains 系列产品:支持 IDEA、WebStorm、PyCharm 等 2021.3 及以上版本。
  • 39 |
  • 小程序端:随时随地刷题。
  • 40 |
  • Web 端面试鸭 Web 端
  • 41 |
42 | 43 |

开源项目

44 | 45 |

插件代码完全开源,欢迎大家 Star 和贡献代码!

46 | 47 |

GitHub 地址:https://github.com/yuyuanweb/mianshiya-plugin

48 | 49 |

反馈与支持

50 | 51 |

如有问题或建议,欢迎通过 GitHub issue 提交反馈,或者直接联系我们。

52 | ]]>
53 | 54 | 0.5.4 56 |

fix

57 |

修复内嵌页面的登录问题

58 |

修复下一题功能的评论区未刷新问题

59 | ]]>
60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 72 | 75 | 76 | 79 | 80 | 81 | 82 | 84 | 85 | 86 |
87 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.utils; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.text.StrPool; 5 | import cn.hutool.core.util.RandomUtil; 6 | import cn.hutool.core.util.StrUtil; 7 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 8 | import com.intellij.openapi.application.ApplicationManager; 9 | import com.intellij.openapi.fileEditor.FileEditorManager; 10 | import com.intellij.openapi.fileEditor.OpenFileDescriptor; 11 | import com.intellij.openapi.project.Project; 12 | import com.intellij.openapi.vfs.LocalFileSystem; 13 | import com.intellij.openapi.vfs.VirtualFile; 14 | import com.intellij.openapi.vfs.newvfs.RefreshQueue; 15 | import com.intellij.util.keyFMap.KeyFMap; 16 | 17 | import java.io.*; 18 | import java.nio.charset.StandardCharsets; 19 | 20 | /** 21 | * @author pine 22 | */ 23 | public class FileUtils { 24 | 25 | public static int FILE_NAME_LENGTH = 7; 26 | 27 | public static String getTempDir() { 28 | String tmpDirPath = FileUtil.getTmpDirPath(); 29 | String suffix = "mianshiya/"; 30 | return tmpDirPath.endsWith("/") 31 | ? tmpDirPath + suffix 32 | : tmpDirPath + "/" + suffix; 33 | } 34 | 35 | public static File openArticle(Project project, Boolean isOpenEditor) { 36 | String filePath = FileUtils.getTempDir() + RandomUtil.randomString(10) + StrPool.DOT + KeyConstant.EDITOR_FILE_POSTFIX; 37 | 38 | File file = FileUtil.touch(filePath); 39 | if (!file.exists()) { 40 | FileUtil.writeString("content", filePath, StandardCharsets.UTF_8); 41 | } 42 | if (isOpenEditor) { 43 | FileUtils.openFileEditor(file, project); 44 | } 45 | return file; 46 | } 47 | 48 | public static void openNewEditorTab(Project project, Long questionId, Long questionBankId, Long questionNum, String questionTitle) { 49 | 50 | // 创建一个临时文件并写入内容 51 | if (StrUtil.isBlank(questionTitle)) { 52 | questionTitle = RandomUtil.randomString(FILE_NAME_LENGTH); 53 | } 54 | String fileName; 55 | if (questionTitle.length() > FILE_NAME_LENGTH) { 56 | fileName = questionTitle.substring(0, FILE_NAME_LENGTH) + "…"; 57 | } else { 58 | fileName = questionTitle; 59 | } 60 | String filePath = FileUtils.getTempDir() + questionNum + ". " + fileName + StrPool.DOT + KeyConstant.EDITOR_FILE_POSTFIX_CONTENT; 61 | File tempFile = FileUtil.touch(filePath); 62 | 63 | if (!tempFile.exists()) { 64 | FileUtil.writeString(questionTitle, filePath, StandardCharsets.UTF_8); 65 | } 66 | FileUtils.openFileEditorAndSaveState(tempFile, project, questionId, questionBankId); 67 | } 68 | 69 | public static void openFileEditorAndSaveState(File file, Project project, Long questionId, Long questionBankId) { 70 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 71 | VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); 72 | KeyFMap map = KeyFMap.EMPTY_MAP.plus(KeyConstant.QUESTION_ID_KEY, questionId); 73 | map = map.plus(KeyConstant.QUESTION_BANK_ID_KEY, questionBankId); 74 | assert vf != null; 75 | vf.set(map); 76 | ApplicationManager.getApplication().invokeLater(() -> { 77 | if (ApplicationManager.getApplication().isDisposed()) { 78 | return; 79 | } 80 | OpenFileDescriptor descriptor = new OpenFileDescriptor(project, vf); 81 | FileEditorManager.getInstance(project).openTextEditor(descriptor, false); 82 | }); 83 | }); 84 | } 85 | 86 | public static void openFileEditor(File file, Project project) { 87 | ApplicationManager.getApplication().invokeLater(() -> { 88 | VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); 89 | OpenFileDescriptor descriptor = new OpenFileDescriptor(project, vf); 90 | FileEditorManager.getInstance(project).openTextEditor(descriptor, false); 91 | RefreshQueue.getInstance().refresh(false, false, null, vf); 92 | }); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/view/LoginPanel.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.view; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.github.yuyuanweb.mianshiyaplugin.constant.CommonConstant; 5 | import com.github.yuyuanweb.mianshiyaplugin.manager.CookieManager; 6 | import com.intellij.openapi.diagnostic.Logger; 7 | import com.intellij.openapi.project.Project; 8 | import com.intellij.openapi.ui.DialogWrapper; 9 | import com.intellij.openapi.util.Disposer; 10 | import com.intellij.ui.components.JBScrollPane; 11 | import com.intellij.ui.jcef.JCEFHtmlPanel; 12 | import com.intellij.util.ui.JBUI; 13 | import com.intellij.util.ui.components.BorderLayoutPanel; 14 | import org.cef.browser.CefBrowser; 15 | import org.cef.handler.CefLoadHandlerAdapter; 16 | import org.cef.network.CefCookieManager; 17 | import org.jetbrains.annotations.NotNull; 18 | import org.jetbrains.annotations.Nullable; 19 | 20 | import javax.swing.*; 21 | import java.awt.*; 22 | 23 | /** 24 | * @author pine 25 | */ 26 | public class LoginPanel extends DialogWrapper { 27 | 28 | private static final Logger logger = Logger.getInstance(LoginPanel.class); 29 | 30 | private final BorderLayoutPanel panel = JBUI.Panels.simplePanel(); 31 | 32 | private final Action okAction; 33 | 34 | public LoginPanel(@Nullable Project project) { 35 | super(project, null, false, IdeModalityType.IDE, false); 36 | okAction = new OkAction() { 37 | }; 38 | JcefPanel jcefPanel; 39 | try { 40 | jcefPanel = new JcefPanel(); 41 | } catch (IllegalArgumentException e) { 42 | jcefPanel = new JcefPanel(true); 43 | } 44 | Disposer.register(getDisposable(), jcefPanel); 45 | jcefPanel.getComponent().setMinimumSize(new Dimension(1000, 500)); 46 | jcefPanel.getComponent().setPreferredSize(new Dimension(1500, 800)); 47 | panel.addToTop(new JBScrollPane(jcefPanel.getComponent(), JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER)); 48 | 49 | setModal(true); 50 | init(); 51 | setTitle("Login"); 52 | 53 | // 调整对话框大小 54 | pack(); 55 | } 56 | 57 | @Override 58 | protected @Nullable JComponent createCenterPanel() { 59 | return panel; 60 | } 61 | 62 | @NotNull 63 | @Override 64 | protected Action getOKAction() { 65 | return okAction; 66 | } 67 | 68 | @Override 69 | protected void doOKAction() { 70 | super.doOKAction(); 71 | } 72 | 73 | private class JcefPanel extends JCEFHtmlPanel { 74 | 75 | 76 | private CefLoadHandlerAdapter cefLoadHandler; 77 | 78 | public JcefPanel(boolean old) { 79 | super(null); 80 | init(); 81 | } 82 | 83 | public JcefPanel() { 84 | super(null, null); 85 | init(); 86 | } 87 | 88 | private void init() { 89 | logger.warn("init----------------------------------------------------------------------------------------------------"); 90 | getJBCefClient().addLoadHandler(cefLoadHandler = new CefLoadHandlerAdapter() { 91 | 92 | @Override 93 | public void onLoadingStateChange(CefBrowser browser, boolean isLoading, boolean canGoBack, boolean canGoForward) { 94 | // 建议:只在 isLoading = false (页面加载完毕) 时检查, 95 | // 或者如果你是 SPA (单页应用),可能需要更频繁的检查。 96 | // 但原来的逻辑每次变化都检查也没大问题,就是性能损耗。 97 | 98 | String url = browser.getURL(); 99 | if (StrUtil.isNotBlank(url) && url.contains("user/login")) { // 简单的防卫,防止在别的页面瞎检查 100 | CefCookieManager cefCookieManager = getJBCefCookieManager().getCefCookieManager(); 101 | // 传入 url 102 | CookieManager.handleCookie(cefCookieManager, url, LoginPanel.this::doOKAction); 103 | } 104 | } 105 | }, getCefBrowser()); 106 | loadURL(CommonConstant.WEB_HOST + "user/login"); 107 | } 108 | 109 | @Override 110 | public void dispose() { 111 | SwingUtilities.invokeLater(() -> { 112 | getJBCefClient().removeLoadHandler(cefLoadHandler, getCefBrowser()); 113 | super.dispose(); 114 | }); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/provider/SplitTextEditorProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.provider; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.file.preview.SplitFileEditor; 4 | import com.intellij.openapi.fileEditor.*; 5 | import com.intellij.openapi.project.DumbAware; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.vfs.VirtualFile; 8 | import org.jdom.Attribute; 9 | import org.jdom.Element; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | /** 13 | * @author pine 14 | */ 15 | public abstract class SplitTextEditorProvider implements AsyncFileEditorProvider, DumbAware { 16 | 17 | private static final String FIRST_EDITOR = "first_editor"; 18 | private static final String SECOND_EDITOR = "second_editor"; 19 | private static final String SPLIT_LAYOUT = "split_layout"; 20 | 21 | @NotNull 22 | protected final FileEditorProvider myFirstProvider; 23 | @NotNull 24 | protected final FileEditorProvider mySecondProvider; 25 | 26 | @NotNull 27 | private final String myEditorTypeId; 28 | 29 | public SplitTextEditorProvider(@NotNull FileEditorProvider firstProvider, @NotNull FileEditorProvider secondProvider) { 30 | myFirstProvider = firstProvider; 31 | mySecondProvider = secondProvider; 32 | 33 | myEditorTypeId = "split-provider[" + myFirstProvider.getEditorTypeId() + ";" + mySecondProvider.getEditorTypeId() + "]"; 34 | } 35 | 36 | @Override 37 | public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { 38 | return myFirstProvider.accept(project, file) && mySecondProvider.accept(project, file); 39 | } 40 | 41 | @NotNull 42 | @Override 43 | public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { 44 | return createEditorAsync(project, file).build(); 45 | } 46 | 47 | @NotNull 48 | @Override 49 | public String getEditorTypeId() { 50 | return myEditorTypeId; 51 | } 52 | 53 | @NotNull 54 | @Override 55 | public Builder createEditorAsync(@NotNull final Project project, @NotNull final VirtualFile file) { 56 | final Builder firstBuilder = getBuilderFromEditorProvider(myFirstProvider, project, file); 57 | final Builder secondBuilder = getBuilderFromEditorProvider(mySecondProvider, project, file); 58 | 59 | return new Builder() { 60 | @Override 61 | public FileEditor build() { 62 | return createSplitEditor(firstBuilder.build(), secondBuilder.build()); 63 | } 64 | }; 65 | } 66 | 67 | @NotNull 68 | @Override 69 | public FileEditorState readState(@NotNull Element sourceElement, @NotNull Project project, @NotNull VirtualFile file) { 70 | Element child = sourceElement.getChild(FIRST_EDITOR); 71 | FileEditorState firstState = null; 72 | if (child != null) { 73 | firstState = myFirstProvider.readState(child, project, file); 74 | } 75 | child = sourceElement.getChild(SECOND_EDITOR); 76 | FileEditorState secondState = null; 77 | if (child != null) { 78 | secondState = mySecondProvider.readState(child, project, file); 79 | } 80 | 81 | final Attribute attribute = sourceElement.getAttribute(SPLIT_LAYOUT); 82 | 83 | final String layoutName; 84 | if (attribute != null) { 85 | layoutName = attribute.getValue(); 86 | } else { 87 | layoutName = null; 88 | } 89 | 90 | return new SplitFileEditor.MyFileEditorState(layoutName, firstState, secondState); 91 | } 92 | 93 | @Override 94 | public void writeState(@NotNull FileEditorState state, @NotNull Project project, @NotNull Element targetElement) { 95 | if (!(state instanceof SplitFileEditor.MyFileEditorState)) { 96 | return; 97 | } 98 | final SplitFileEditor.MyFileEditorState compositeState = (SplitFileEditor.MyFileEditorState) state; 99 | 100 | Element child = new Element(FIRST_EDITOR); 101 | if (compositeState.getFirstState() != null) { 102 | myFirstProvider.writeState(compositeState.getFirstState(), project, child); 103 | targetElement.addContent(child); 104 | } 105 | 106 | child = new Element(SECOND_EDITOR); 107 | if (compositeState.getSecondState() != null) { 108 | mySecondProvider.writeState(compositeState.getSecondState(), project, child); 109 | targetElement.addContent(child); 110 | } 111 | 112 | if (compositeState.getSplitLayout() != null) { 113 | targetElement.setAttribute(SPLIT_LAYOUT, compositeState.getSplitLayout()); 114 | } 115 | } 116 | 117 | protected abstract FileEditor createSplitEditor(@NotNull FileEditor firstEditor, @NotNull FileEditor secondEditor); 118 | 119 | @NotNull 120 | @Override 121 | public FileEditorPolicy getPolicy() { 122 | return FileEditorPolicy.HIDE_DEFAULT_EDITOR; 123 | } 124 | 125 | @NotNull 126 | public static Builder getBuilderFromEditorProvider(@NotNull final FileEditorProvider provider, 127 | @NotNull final Project project, 128 | @NotNull final VirtualFile file) { 129 | return new Builder() { 130 | @Override 131 | public FileEditor build() { 132 | return provider.createEditor(project, file); 133 | } 134 | }; 135 | 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/preview/BrowserFileEditor.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.preview; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 4 | import com.github.yuyuanweb.mianshiyaplugin.constant.CommonConstant; 5 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 6 | import com.github.yuyuanweb.mianshiyaplugin.manager.CookieManager; 7 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.WebTypeEnum; 8 | import com.github.yuyuanweb.mianshiyaplugin.utils.ThemeUtil; 9 | import com.intellij.openapi.fileEditor.FileEditor; 10 | import com.intellij.openapi.fileEditor.FileEditorLocation; 11 | import com.intellij.openapi.fileEditor.FileEditorState; 12 | import com.intellij.openapi.project.Project; 13 | import com.intellij.openapi.util.Key; 14 | import com.intellij.openapi.vfs.VirtualFile; 15 | import com.intellij.ui.jcef.JBCefBrowser; 16 | import com.intellij.ui.jcef.JBCefCookie; 17 | import com.intellij.ui.jcef.JBCefCookieManager; 18 | import lombok.Getter; 19 | import org.cef.browser.CefBrowser; 20 | import org.cef.handler.CefLoadHandlerAdapter; 21 | import org.cef.network.CefCookieManager; 22 | import org.jetbrains.annotations.NotNull; 23 | import org.jetbrains.annotations.Nullable; 24 | 25 | import javax.swing.*; 26 | import java.awt.*; 27 | import java.beans.PropertyChangeListener; 28 | import java.net.URI; 29 | 30 | /** 31 | * 内嵌浏览器 文件编辑器 32 | * 33 | * @author pine 34 | */ 35 | public class BrowserFileEditor implements FileEditor { 36 | 37 | @Getter 38 | private final JBCefBrowser jbCefBrowser; 39 | private final JPanel panel; 40 | // 持有文件引用以检查 isValid 41 | private final VirtualFile file; 42 | 43 | @Getter 44 | private final WebTypeEnum webTypeEnum; 45 | 46 | public BrowserFileEditor(@NotNull Project project, @NotNull VirtualFile file) { 47 | this.file = file; 48 | this.jbCefBrowser = new JBCefBrowser(); 49 | this.panel = new JPanel(new BorderLayout()); 50 | this.panel.add(jbCefBrowser.getComponent(), BorderLayout.CENTER); 51 | // 使用 JCEF 组件作为焦点组件,防止输入焦点丢失 52 | this.panel.setFocusable(true); 53 | 54 | // 1. 在加载 URL 之前先注入 Cookie 55 | injectCookie(); 56 | 57 | // 2. 合并 LoadHandler,只添加一次 58 | jbCefBrowser.getJBCefClient().addLoadHandler(new CefLoadHandlerAdapter() { 59 | @Override 60 | public void onLoadingStateChange(CefBrowser browser, boolean isLoading, boolean canGoBack, boolean canGoForward) { 61 | // 只在加载结束时检查,避免重复执行 62 | if (!isLoading) { 63 | CefCookieManager cefCookieManager = jbCefBrowser.getJBCefCookieManager().getCefCookieManager(); 64 | // 传入当前 URL 65 | String currentUrl = browser.getURL(); 66 | if (currentUrl != null && !currentUrl.isEmpty()) { 67 | CookieManager.handleCookie(cefCookieManager, currentUrl, () -> { 68 | }); 69 | } 70 | } 71 | } 72 | }, jbCefBrowser.getCefBrowser()); 73 | 74 | Long questionId = file.get().get(KeyConstant.QUESTION_ID_KEY); 75 | webTypeEnum = file.get().get(KeyConstant.WEB_TYPE_KEY); 76 | if (questionId != null && webTypeEnum != null) { 77 | String theme = ThemeUtil.getTheme(); 78 | String url = String.format(CommonConstant.PLUGIN_QD, questionId, webTypeEnum.getValue(), theme); 79 | jbCefBrowser.loadURL(url); 80 | } 81 | } 82 | 83 | /** 84 | * 注入全局 Cookie 到当前浏览器实例 85 | */ 86 | private void injectCookie() { 87 | try { 88 | String cookieStr = GlobalState.getInstance().getSavedCookie(); 89 | if (cookieStr == null || !cookieStr.contains("=")) { 90 | return; 91 | } 92 | 93 | JBCefCookieManager cookieManager = jbCefBrowser.getJBCefCookieManager(); 94 | String value = cookieStr.replace("SESSION=", ""); 95 | 96 | String domain = ".mianshiya.com"; 97 | try { 98 | URI uri = new URI(CommonConstant.WEB_HOST); 99 | String host = uri.getHost(); 100 | if (host != null) { 101 | // 如果 host 是 www.mianshiya.com,处理成 .mianshiya.com 102 | domain = host.startsWith("www.") ? host.substring(3) : "." + host; 103 | } 104 | } catch (Exception ignored) { 105 | } 106 | 107 | JBCefCookie jbCefCookie = new JBCefCookie( 108 | "SESSION", 109 | value, 110 | domain, 111 | "/", 112 | true, 113 | true 114 | ); 115 | 116 | // 立即设置 Cookie 117 | cookieManager.setCookie(CommonConstant.WEB_HOST, jbCefCookie, false); 118 | } catch (Exception e) { 119 | // 记录日志,防止 Cookie 注入失败影响整个编辑器加载 120 | e.printStackTrace(); 121 | } 122 | } 123 | 124 | @Override 125 | public @NotNull JComponent getComponent() { 126 | return panel; 127 | } 128 | 129 | @Override 130 | public @Nullable JComponent getPreferredFocusedComponent() { 131 | return jbCefBrowser.getComponent(); 132 | } 133 | 134 | @Override 135 | public @NotNull String getName() { 136 | return "Browser Editor"; 137 | } 138 | 139 | @Override 140 | public void setState(@NotNull FileEditorState state) { 141 | } 142 | 143 | @Override 144 | public boolean isModified() { 145 | return false; 146 | } 147 | 148 | @Override 149 | public boolean isValid() { 150 | // 必须返回文件是否有效,否则 IDEA 可能会异常关闭此 Tab 151 | return file.isValid(); 152 | } 153 | 154 | @Override 155 | public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { 156 | } 157 | 158 | @Override 159 | public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { 160 | } 161 | 162 | @Override 163 | public void dispose() { 164 | // 最好把 component 从 parent 移除 165 | panel.removeAll(); 166 | jbCefBrowser.dispose(); 167 | } 168 | 169 | @Override 170 | public @Nullable T getUserData(@NotNull Key key) { 171 | return null; 172 | } 173 | 174 | @Override 175 | public void putUserData(@NotNull Key key, @Nullable T value) { 176 | } 177 | 178 | @Override 179 | public @Nullable FileEditorLocation getCurrentLocation() { 180 | return null; 181 | } 182 | } -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/toolWindow/MyToolWindowFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.toolWindow; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.github.yuyuanweb.mianshiyaplugin.actions.*; 5 | import com.github.yuyuanweb.mianshiyaplugin.config.ApiConfig; 6 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 7 | import com.github.yuyuanweb.mianshiyaplugin.constant.CommonConstant; 8 | import com.github.yuyuanweb.mianshiyaplugin.constant.IconConstant; 9 | import com.github.yuyuanweb.mianshiyaplugin.model.common.BaseResponse; 10 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.ErrorCode; 11 | import com.github.yuyuanweb.mianshiyaplugin.model.response.User; 12 | import com.intellij.icons.AllIcons; 13 | import com.intellij.openapi.actionSystem.*; 14 | import com.intellij.openapi.actionSystem.impl.SimpleDataContext; 15 | import com.intellij.openapi.application.ApplicationManager; 16 | import com.intellij.openapi.diagnostic.Logger; 17 | import com.intellij.openapi.project.Project; 18 | import com.intellij.openapi.wm.ToolWindow; 19 | import com.intellij.openapi.wm.ToolWindowFactory; 20 | import com.intellij.ui.components.JBPanel; 21 | import com.intellij.ui.content.Content; 22 | import com.intellij.ui.content.ContentFactory; 23 | import com.intellij.util.ui.JBUI; 24 | import lombok.extern.slf4j.Slf4j; 25 | import org.jetbrains.annotations.NotNull; 26 | 27 | import java.awt.*; 28 | import java.io.IOException; 29 | 30 | import static com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant.*; 31 | 32 | /** 33 | * @author pine 34 | */ 35 | @Slf4j 36 | public class MyToolWindowFactory implements ToolWindowFactory { 37 | 38 | private static final Logger logger = Logger.getInstance(MyToolWindowFactory.class); 39 | 40 | public MyToolWindowFactory() {} 41 | 42 | @Override 43 | public void createToolWindowContent(@NotNull Project project, ToolWindow toolWindow) { 44 | // 创建主面板 45 | JBPanel mainPanel = new JBPanel<>(new BorderLayout()); 46 | mainPanel.setBorder(JBUI.Borders.empty()); 47 | 48 | // 获取 ContentFactory 49 | // ContentFactory contentFactory = ContentFactory.getInstance(); 50 | // 使用旧写法,兼容旧版本 51 | ContentFactory contentFactory = ApplicationManager.getApplication().getService(ContentFactory.class); 52 | Content content = contentFactory.createContent(mainPanel, "首页", true); 53 | content.setCloseable(false); 54 | 55 | // 创建工具栏并添加到面板顶部,传入 content ,便于改 displayName 56 | ActionToolbar actionToolbar = createToolbar(content, mainPanel); 57 | mainPanel.add(actionToolbar.getComponent(), BorderLayout.NORTH); 58 | 59 | QuestionBankAction questionBankAction = new QuestionBankAction(mainPanel); 60 | 61 | DataContext dataContext = SimpleDataContext.getSimpleContext(CommonDataKeys.PROJECT, project); 62 | 63 | // 构建 AnActionEvent 对象 64 | AnActionEvent event = AnActionEvent.createFromAnAction(questionBankAction, null, "somePlace", dataContext); 65 | // 手动触发 action 66 | questionBankAction.actionPerformed(event); 67 | 68 | // 将内容添加到 Tool Window 中 69 | toolWindow.getContentManager().addContent(content); 70 | } 71 | 72 | private ActionToolbar createToolbar(Content content, JBPanel mainPanel) { 73 | // 定义一个动作组 74 | ActionManager actionManager = ActionManager.getInstance(); 75 | DefaultActionGroup actionGroup = new DefaultActionGroup(); 76 | 77 | DefaultActionGroup action = (DefaultActionGroup) actionManager.getAction(ACTION_BAR); 78 | if (action != null) { 79 | // 创建工具栏 80 | ActionToolbar actionToolbar = actionManager.createActionToolbar(ACTION_BAR, action, true); 81 | // 设置目标组件 82 | actionToolbar.setTargetComponent(mainPanel); 83 | return actionToolbar; 84 | } 85 | 86 | QuestionBankAction questionBankAction = new QuestionBankAction(QUESTION_BANK_ZH, AllIcons.Scope.ChangedFilesAll); 87 | actionGroup.add(questionBankAction); 88 | actionManager.registerAction(QUESTION_BANK, questionBankAction); 89 | 90 | QuestionAction questionAction = new QuestionAction(QUESTION_ZH, AllIcons.Scope.ChangedFiles); 91 | actionGroup.add(questionAction); 92 | actionManager.registerAction(QUESTION, questionAction); 93 | 94 | OpenUrlAction webAction = new OpenUrlAction(WEB_ZH, CommonConstant.WEB_HOST, IconConstant.WEB); 95 | actionGroup.add(webAction); 96 | actionManager.registerAction(WEB, webAction); 97 | 98 | OpenUrlAction helpDocAction = new OpenUrlAction(HELP_ZH, CommonConstant.HELP_DOC, IconConstant.HELP); 99 | actionGroup.add(helpDocAction); 100 | actionManager.registerAction(HELP, helpDocAction); 101 | 102 | GlobalState globalState = GlobalState.getInstance(); 103 | String cookie = globalState.getSavedCookie(); 104 | if (StrUtil.isBlank(cookie)) { 105 | LoginAction loginAction = new LoginAction(LOGIN_ZH, IconConstant.LOGIN, actionGroup); 106 | actionGroup.add(loginAction); 107 | actionManager.registerAction(LOGIN, loginAction); 108 | } else { 109 | User loginUser = null; 110 | try { 111 | BaseResponse response = ApiConfig.mianShiYaApi.getLoginUser().execute().body(); 112 | if (response != null && response.getCode() == ErrorCode.SUCCESS.getCode()) { 113 | loginUser = response.getData(); 114 | globalState.saveUser(loginUser); 115 | } 116 | } catch (IOException e) { 117 | logger.warn("获取登录用户失败"); 118 | } 119 | // 未登录 120 | if (loginUser == null) { 121 | LoginAction loginAction = new LoginAction(LOGIN_ZH, IconConstant.LOGIN, actionGroup); 122 | actionGroup.add(loginAction); 123 | actionManager.registerAction(LOGIN, loginAction); 124 | } else { 125 | OpenUrlAction vipAction = new OpenUrlAction(VIP_ZH, CommonConstant.VIP, AllIcons.General.User); 126 | actionGroup.add(vipAction); 127 | actionManager.registerAction(VIP, vipAction); 128 | 129 | LogoutAction logoutAction = new LogoutAction(LOGOUT_ZH, IconConstant.LOGOUT, actionGroup); 130 | actionGroup.add(logoutAction); 131 | actionManager.registerAction(LOGOUT, logoutAction); 132 | 133 | String userName = loginUser.getUserName(); 134 | if (StrUtil.isNotBlank(userName)) { 135 | content.setDisplayName(userName); 136 | } 137 | } 138 | } 139 | 140 | // 创建工具栏 141 | actionManager.registerAction(ACTION_BAR, actionGroup); 142 | ActionToolbar actionToolbar = actionManager.createActionToolbar(ACTION_BAR, actionGroup, true); 143 | // 设置目标组件 144 | actionToolbar.setTargetComponent(mainPanel); 145 | return actionToolbar; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/preview/OuterBrowserFileEditorPreview.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.preview; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 4 | import com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant; 5 | import com.github.yuyuanweb.mianshiyaplugin.constant.TextConstant; 6 | import com.github.yuyuanweb.mianshiyaplugin.constant.ViewConstant; 7 | import com.github.yuyuanweb.mianshiyaplugin.model.enums.WebTypeEnum; 8 | import com.github.yuyuanweb.mianshiyaplugin.model.response.User; 9 | import com.github.yuyuanweb.mianshiyaplugin.file.provider.BrowserFileEditorProvider; 10 | import com.github.yuyuanweb.mianshiyaplugin.utils.FileUtils; 11 | import com.github.yuyuanweb.mianshiyaplugin.view.LoginPanel; 12 | import com.intellij.openapi.application.ApplicationManager; 13 | import com.intellij.openapi.fileEditor.*; 14 | import com.intellij.openapi.project.Project; 15 | import com.intellij.openapi.project.ProjectManager; 16 | import com.intellij.openapi.util.Disposer; 17 | import com.intellij.openapi.util.UserDataHolderBase; 18 | import com.intellij.openapi.vfs.LocalFileSystem; 19 | import com.intellij.openapi.vfs.VirtualFile; 20 | import com.intellij.ui.components.JBLabel; 21 | import com.intellij.ui.components.JBScrollPane; 22 | import com.intellij.util.keyFMap.KeyFMap; 23 | import com.intellij.util.ui.JBUI; 24 | import com.intellij.util.ui.components.BorderLayoutPanel; 25 | import lombok.Getter; 26 | import org.jetbrains.annotations.Nls; 27 | import org.jetbrains.annotations.NotNull; 28 | import org.jetbrains.annotations.Nullable; 29 | 30 | import javax.swing.*; 31 | import java.beans.PropertyChangeListener; 32 | import java.io.File; 33 | 34 | /** 35 | * 内嵌浏览器外层 文件编辑器 36 | * 37 | * @author pine 38 | */ 39 | public class OuterBrowserFileEditorPreview extends UserDataHolderBase implements FileEditor { 40 | 41 | private final Project project; 42 | 43 | @Override 44 | public @Nullable FileEditorLocation getCurrentLocation() { 45 | return null; 46 | } 47 | 48 | private final VirtualFile file; 49 | private JBScrollPane jbScrollPane; 50 | private BorderLayoutPanel myComponent; 51 | private FileEditor fileEditor; 52 | 53 | @Getter 54 | private FileEditor newEditor; 55 | 56 | private boolean isLoad = false; 57 | 58 | private final WebTypeEnum webTypeEnum; 59 | 60 | public OuterBrowserFileEditorPreview(Project project, VirtualFile file, WebTypeEnum webTypeEnum) { 61 | this.project = project; 62 | this.file = file; 63 | this.webTypeEnum = webTypeEnum; 64 | if (!WebTypeEnum.COMMENT.equals(webTypeEnum)) { 65 | initComponent(); 66 | } 67 | } 68 | 69 | @Override 70 | public @NotNull JComponent getComponent() { 71 | if (myComponent == null) { 72 | myComponent = JBUI.Panels.simplePanel(); 73 | jbScrollPane = new JBScrollPane(JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JBScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 74 | myComponent.addToCenter(jbScrollPane); 75 | // COMMENT 类型不会在构造函数中初始化 76 | if (isLoad && WebTypeEnum.COMMENT.equals(webTypeEnum)) { 77 | initComponent(); 78 | } 79 | } 80 | return myComponent; 81 | } 82 | 83 | private void initComponent() { 84 | isLoad = true; 85 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 86 | try { 87 | KeyFMap keyFMap = file.get(); 88 | Long questionId = keyFMap.get(KeyConstant.QUESTION_ID_KEY); 89 | Long questionBankId = keyFMap.get(KeyConstant.QUESTION_BANK_ID_KEY); 90 | ApplicationManager.getApplication().invokeLater(() -> { 91 | if (questionId == null) { 92 | jbScrollPane.setViewportView(new JBLabel(TextConstant.LOGIN)); 93 | return; 94 | } 95 | this.openArticle(questionId, questionBankId); 96 | }); 97 | } catch (Exception e) { 98 | e.printStackTrace(); 99 | } 100 | }); 101 | } 102 | 103 | private void openArticle(Long questionId, Long questionBankId) { 104 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 105 | User loginUser = GlobalState.getInstance().getSavedUser(); 106 | ApplicationManager.getApplication().invokeLater(() -> { 107 | if (loginUser == null && WebTypeEnum.QUESTION.equals(webTypeEnum)) { 108 | LoginPanel loginPanel = new LoginPanel(ProjectManager.getInstance().getDefaultProject()); 109 | loginPanel.showAndGet(); 110 | } 111 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 112 | File file = null; 113 | file = FileUtils.openArticle(project, false); 114 | if (!file.exists()) { 115 | myComponent.addToCenter(new JBLabel("No solution")); 116 | } else { 117 | VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); 118 | if (vf != null) { 119 | KeyFMap map = KeyFMap.EMPTY_MAP.plus(KeyConstant.QUESTION_ID_KEY, questionId); 120 | map = map.plus(KeyConstant.WEB_TYPE_KEY, webTypeEnum); 121 | map = map.plus(KeyConstant.QUESTION_BANK_ID_KEY, questionBankId); 122 | vf.set(map); 123 | BrowserFileEditorProvider contentProvider = new BrowserFileEditorProvider(); 124 | newEditor = contentProvider.createEditor(project, vf); 125 | ApplicationManager.getApplication().invokeLater(() -> { 126 | if (fileEditor != null) { 127 | jbScrollPane.setViewportView(new JBLabel("Loading......")); 128 | FileEditor temp = fileEditor; 129 | Disposer.dispose(temp); 130 | } 131 | fileEditor = newEditor; 132 | Disposer.register(this, fileEditor); 133 | BorderLayoutPanel browserComponent = JBUI.Panels.simplePanel(newEditor.getComponent()); 134 | browserComponent.addToCenter(newEditor.getComponent()); 135 | jbScrollPane.setViewportView(browserComponent); 136 | }); 137 | } 138 | } 139 | }); 140 | }); 141 | }); 142 | } 143 | 144 | @Override 145 | public @Nullable JComponent getPreferredFocusedComponent() { 146 | return myComponent; 147 | } 148 | 149 | @Override 150 | public @Nls(capitalization = Nls.Capitalization.Title) @NotNull String getName() { 151 | return ViewConstant.BEST_QUESTION_ANSWER_PREVIEW; 152 | } 153 | 154 | @Override 155 | public void setState(@NotNull FileEditorState state) { 156 | if (state instanceof ConvergePreview.TabFileEditorState) { 157 | if (!isLoad && ((ConvergePreview.TabFileEditorState) state).isLoad()) { 158 | initComponent(); 159 | } 160 | } else if (state instanceof ConvergePreview.TabSelectFileEditorState) { 161 | if (!isLoad) { 162 | initComponent(); 163 | } 164 | } 165 | } 166 | 167 | @Override 168 | public boolean isModified() { 169 | return false; 170 | } 171 | 172 | @Override 173 | public boolean isValid() { 174 | return false; 175 | } 176 | 177 | @Override 178 | public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { 179 | 180 | } 181 | 182 | @Override 183 | public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { 184 | 185 | } 186 | 187 | @Override 188 | public void dispose() { 189 | if (fileEditor != null) { 190 | Disposer.dispose(fileEditor); 191 | } 192 | } 193 | 194 | @Override 195 | public @Nullable VirtualFile getFile() { 196 | if (fileEditor != null) { 197 | return fileEditor.getFile(); 198 | } else { 199 | return null; 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/utils/PanelUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.utils; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.actions.LogoutAction; 4 | import com.github.yuyuanweb.mianshiyaplugin.actions.OpenUrlAction; 5 | import com.github.yuyuanweb.mianshiyaplugin.config.GlobalState; 6 | import com.github.yuyuanweb.mianshiyaplugin.constant.*; 7 | import com.github.yuyuanweb.mianshiyaplugin.model.response.User; 8 | import com.github.yuyuanweb.mianshiyaplugin.view.LoginPanel; 9 | import com.github.yuyuanweb.mianshiyaplugin.view.MTabModel; 10 | import com.intellij.icons.AllIcons; 11 | import com.intellij.ide.BrowserUtil; 12 | import com.intellij.openapi.actionSystem.ActionManager; 13 | import com.intellij.openapi.actionSystem.AnAction; 14 | import com.intellij.openapi.actionSystem.DefaultActionGroup; 15 | import com.intellij.openapi.application.ApplicationManager; 16 | import com.intellij.openapi.project.ProjectManager; 17 | import com.intellij.openapi.ui.ComboBox; 18 | import com.intellij.ui.JBColor; 19 | import com.intellij.ui.components.JBLabel; 20 | import com.intellij.ui.components.JBOptionButton; 21 | import com.intellij.ui.components.JBPanel; 22 | import com.intellij.ui.components.JBTabbedPane; 23 | import com.intellij.ui.table.JBTable; 24 | 25 | import javax.swing.*; 26 | import javax.swing.table.DefaultTableCellRenderer; 27 | import javax.swing.table.TableCellRenderer; 28 | import javax.swing.table.TableColumn; 29 | import java.awt.*; 30 | import java.awt.event.ActionEvent; 31 | import java.awt.event.ActionListener; 32 | import java.awt.event.MouseAdapter; 33 | import java.awt.event.MouseEvent; 34 | import java.util.List; 35 | import java.util.Objects; 36 | import java.util.function.BiConsumer; 37 | import java.util.stream.Collectors; 38 | 39 | import static com.github.yuyuanweb.mianshiyaplugin.constant.KeyConstant.LOGOUT_ZH; 40 | import static com.github.yuyuanweb.mianshiyaplugin.constant.PageConstant.PAGE_SIZE; 41 | 42 | /** 43 | * @author pine 44 | */ 45 | public class PanelUtil { 46 | 47 | public static JPanel createClosePanel(String title, JPanel tabPanel, JBTabbedPane tabbedPane) { 48 | JPanel tabLabel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); 49 | tabLabel.setOpaque(false); 50 | 51 | JLabel tabTitle = new JLabel(title); 52 | tabLabel.add(tabTitle); 53 | 54 | JButton closeButton = new JButton(AllIcons.Actions.Close); 55 | closeButton.setPreferredSize(new Dimension(16, 16)); 56 | closeButton.setBorder(BorderFactory.createCompoundBorder()); 57 | closeButton.setContentAreaFilled(false); 58 | 59 | closeButton.addActionListener(new ActionListener() { 60 | @Override 61 | public void actionPerformed(ActionEvent e) { 62 | // 使用组件引用动态获取索引 63 | int tabIndex = tabbedPane.indexOfComponent(tabPanel); 64 | if (tabIndex >= 0) { 65 | tabbedPane.remove(tabIndex); 66 | } 67 | } 68 | }); 69 | 70 | // 添加鼠标事件监听器以处理悬浮效果 71 | closeButton.addMouseListener(new MouseAdapter() { 72 | @Override 73 | public void mouseEntered(MouseEvent e) { 74 | closeButton.setOpaque(true); 75 | closeButton.setBackground(JBColor.LIGHT_GRAY); 76 | } 77 | 78 | @Override 79 | public void mouseExited(MouseEvent e) { 80 | closeButton.setOpaque(false); 81 | closeButton.setBackground(null); 82 | } 83 | }); 84 | 85 | tabLabel.add(closeButton); 86 | 87 | return tabLabel; 88 | } 89 | 90 | public static JBTable createTablePanel(MTabModel tableModel, BiConsumer consumer, int columnIndex) { 91 | // 创建表格 92 | JBTable table = new JBTable(tableModel); 93 | table.setFillsViewportHeight(true); 94 | 95 | // 鼠标双击事件监听 96 | table.addMouseListener(new MouseAdapter() { 97 | @Override 98 | public void mouseClicked(MouseEvent mouseEvent) { 99 | if (mouseEvent.getClickCount() == 2) { 100 | consumer.accept(table, mouseEvent); 101 | } 102 | } 103 | }); 104 | 105 | // 自定义单元格渲染器 106 | TableCellRenderer categoryRenderer = new DefaultTableCellRenderer() { 107 | @Override 108 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 109 | if (value instanceof List) { 110 | List list = (List) value; 111 | JBLabel jbLabel = new JBLabel(); 112 | jbLabel.setText(list.stream() 113 | .map(Object::toString) 114 | .collect(Collectors.joining(" "))); 115 | jbLabel.setOpaque(true); // 确保背景颜色生效 116 | jbLabel.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground()); 117 | jbLabel.setForeground(isSelected ? table.getSelectionForeground() : table.getForeground()); 118 | return jbLabel; 119 | } 120 | return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 121 | } 122 | }; 123 | table.getColumnModel().getColumn(columnIndex).setCellRenderer(categoryRenderer); 124 | 125 | // 设置列宽为0,使列存在但不可见 126 | TableColumn column = table.getColumnModel().getColumn(0); 127 | column.setMinWidth(0); 128 | column.setMaxWidth(0); 129 | column.setPreferredWidth(0); 130 | table.setFillsViewportHeight(true); 131 | 132 | return table; 133 | } 134 | 135 | public static void updatePaginationPanel(JBPanel paginationPanel, long total, int[] currentPage, BiConsumer loadPage) { 136 | paginationPanel.removeAll(); 137 | // int pageSize = Objects.requireNonNull(GlobalState.getInstance().getState()).pageSize; 138 | long totalPage = (long) Math.ceil((double) total / PAGE_SIZE); 139 | 140 | JBLabel pageLabel = new JBLabel("第 " + currentPage[0] + " / " + totalPage + " 页"); 141 | 142 | JButton prevButton = new JButton("上一页"); 143 | JButton nextButton = new JButton("下一页"); 144 | 145 | prevButton.setEnabled(currentPage[0] > 1); 146 | nextButton.setEnabled(currentPage[0] < totalPage); 147 | 148 | prevButton.addActionListener(e -> { 149 | if (currentPage[0] > 1) { 150 | currentPage[0]--; 151 | loadPage.accept(currentPage[0], PAGE_SIZE); 152 | } 153 | }); 154 | 155 | nextButton.addActionListener(e -> { 156 | if (currentPage[0] < totalPage) { 157 | currentPage[0]++; 158 | loadPage.accept(currentPage[0], PAGE_SIZE); 159 | } 160 | }); 161 | 162 | paginationPanel.add(prevButton); 163 | paginationPanel.add(pageLabel); 164 | paginationPanel.add(nextButton); 165 | 166 | paginationPanel.revalidate(); 167 | paginationPanel.repaint(); 168 | } 169 | 170 | public static JBPanel getNeedVipPanel() { 171 | JBPanel needVipPanel = new JBPanel<>(); 172 | AbstractAction needVipAction = new AbstractAction("仅会员可见内容,请先开通会员", AllIcons.General.User) { 173 | @Override 174 | public void actionPerformed(ActionEvent e) { 175 | BrowserUtil.browse(CommonConstant.VIP); 176 | } 177 | }; 178 | needVipPanel.add(new JBOptionButton(needVipAction, null)); 179 | return needVipPanel; 180 | } 181 | 182 | public static JBPanel getNeedLoginPanel() { 183 | JBPanel needLoginPanel = new JBPanel<>(); 184 | AbstractAction needVipAction = new AbstractAction(TextConstant.LOGIN, AllIcons.General.User) { 185 | @Override 186 | public void actionPerformed(ActionEvent e) { 187 | LoginPanel loginPanel = new LoginPanel(ProjectManager.getInstance().getDefaultProject()); 188 | loginPanel.show(); 189 | } 190 | }; 191 | needLoginPanel.add(new JBOptionButton(needVipAction, null)); 192 | return needLoginPanel; 193 | } 194 | 195 | public static void modifyActionGroupWhenLogin(DefaultActionGroup actionGroup, User loginUser) { 196 | ApplicationManager.getApplication().invokeLater(() -> { 197 | ActionManager actionManager = ActionManager.getInstance(); 198 | 199 | // 删除 登录 200 | AnAction loginAction = actionManager.getAction(KeyConstant.LOGIN); 201 | if (loginAction == null) { 202 | return; 203 | } 204 | actionGroup.remove(loginAction); 205 | actionManager.unregisterAction(KeyConstant.LOGIN); 206 | 207 | // 增加 会员 208 | OpenUrlAction vipAction = new OpenUrlAction(loginUser.getUserName(), CommonConstant.VIP, AllIcons.General.User); 209 | actionGroup.add(vipAction); 210 | actionManager.registerAction(KeyConstant.VIP, vipAction); 211 | 212 | // 增加 注销 213 | LogoutAction logoutAction = new LogoutAction(LOGOUT_ZH, IconConstant.LOGOUT, actionGroup); 214 | actionGroup.add(logoutAction); 215 | actionManager.registerAction(KeyConstant.LOGOUT, logoutAction); 216 | }); 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/actions/QuestionBankAction.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.actions; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.config.ApiConfig; 4 | import com.github.yuyuanweb.mianshiyaplugin.constant.PageConstant; 5 | import com.github.yuyuanweb.mianshiyaplugin.constant.SearchConstant; 6 | import com.github.yuyuanweb.mianshiyaplugin.constant.TextConstant; 7 | import com.github.yuyuanweb.mianshiyaplugin.model.common.Page; 8 | import com.github.yuyuanweb.mianshiyaplugin.model.common.PageRequest; 9 | import com.github.yuyuanweb.mianshiyaplugin.model.dto.QuestionBankCategoryBankQueryRequest; 10 | import com.github.yuyuanweb.mianshiyaplugin.model.response.QuestionBank; 11 | import com.github.yuyuanweb.mianshiyaplugin.model.response.QuestionBankCategory; 12 | import com.github.yuyuanweb.mianshiyaplugin.utils.ContentUtil; 13 | import com.github.yuyuanweb.mianshiyaplugin.utils.PanelUtil; 14 | import com.github.yuyuanweb.mianshiyaplugin.view.MTabModel; 15 | import com.github.yuyuanweb.mianshiyaplugin.view.QuestionListManager; 16 | import com.intellij.openapi.actionSystem.AnAction; 17 | import com.intellij.openapi.actionSystem.AnActionEvent; 18 | import com.intellij.openapi.application.ApplicationManager; 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.ui.Gray; 21 | import com.intellij.ui.JBColor; 22 | import com.intellij.ui.components.*; 23 | import com.intellij.ui.table.JBTable; 24 | import com.intellij.util.ui.WrapLayout; 25 | import org.jetbrains.annotations.NotNull; 26 | 27 | import java.util.List; 28 | 29 | import javax.swing.*; 30 | import java.awt.*; 31 | import java.awt.event.MouseAdapter; 32 | import java.awt.event.MouseEvent; 33 | import java.io.IOException; 34 | 35 | /** 36 | * @author pine 37 | */ 38 | public class QuestionBankAction extends AnAction { 39 | 40 | private JBPanel paginationPanel; 41 | private JBPanel labelPanel; 42 | private MTabModel tableModel; 43 | private JBPanel mainPanel; 44 | 45 | private final int[] currentPage = new int[]{1}; 46 | private final QuestionBankCategoryBankQueryRequest queryRequest = new QuestionBankCategoryBankQueryRequest(); 47 | 48 | public QuestionBankAction() { 49 | } 50 | 51 | public QuestionBankAction(String text, Icon icon) { 52 | super(text, text, icon); 53 | } 54 | 55 | public QuestionBankAction(JBPanel mainPanel) { 56 | this.mainPanel = mainPanel; 57 | } 58 | 59 | @Override 60 | public void actionPerformed(@NotNull AnActionEvent anActionEvent) { 61 | // 创建新的 tab 页 62 | JBPanel tabPanel = new JBPanel<>(new BorderLayout()); 63 | 64 | Project project = anActionEvent.getProject(); 65 | if (mainPanel != null) { 66 | mainPanel.add(tabPanel, BorderLayout.CENTER); 67 | } else { 68 | ContentUtil.createContent(tabPanel, TextConstant.QUESTION_BANK, false, project); 69 | } 70 | 71 | // 标签栏 72 | this.loadLabelPanel(); 73 | tabPanel.add(labelPanel, BorderLayout.NORTH); 74 | 75 | // 数据表格 76 | this.loadDataTable(tabPanel, project); 77 | 78 | // 分页条 79 | paginationPanel = new JBPanel<>(new GridBagLayout()); 80 | tabPanel.add(paginationPanel, BorderLayout.SOUTH); 81 | } 82 | 83 | private Page fetchDataFromApi(QuestionBankCategoryBankQueryRequest queryRequest) { 84 | if (queryRequest == null) { 85 | throw new IllegalArgumentException("queryRequest cannot be null"); 86 | } 87 | try { 88 | if (SearchConstant.DEFAULT_QUESTION_BANK_CATEGORY_ID.equals(queryRequest.getQuestionBankCategoryId())) { 89 | return ApiConfig.mianShiYaApi.listQuestionBankVoByPage(queryRequest).execute().body().getData(); 90 | } else { 91 | return ApiConfig.mianShiYaApi.getQuestionBankList(queryRequest).execute().body().getData(); 92 | } 93 | } catch (IOException e) { 94 | throw new RuntimeException(e); 95 | } 96 | } 97 | 98 | /** 99 | * 加载顶部标签数据 100 | */ 101 | private void loadLabelPanel() { 102 | labelPanel = new JBPanel<>(new WrapLayout(FlowLayout.LEFT, 5, 5)); 103 | // 维护当前选中的标签 104 | // 通过数组的方式来持有可变的对象引用 105 | JBLabel[] selectedLabel = {null}; 106 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 107 | List tagList; 108 | try { 109 | tagList = ApiConfig.mianShiYaApi.listQuestionBankCategory(new PageRequest()).execute().body().getData(); 110 | } catch (IOException e) { 111 | return; 112 | } 113 | // 自定义淡化颜色 114 | // 比 LIGHT_GRAY 更淡的灰色 115 | JBColor customLightGray = new JBColor(Gray._220, Gray._80); 116 | // 比 GRAY 更淡的灰色 117 | JBColor customGray = new JBColor(Gray._180, Gray._40); 118 | 119 | ApplicationManager.getApplication().invokeLater(() -> { 120 | for (QuestionBankCategory tag : tagList) { 121 | JBLabel label = new JBLabel(tag.getName()); 122 | label.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); 123 | label.setOpaque(true); 124 | // 使用自定义淡灰色 125 | label.setBackground(customLightGray); 126 | label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 127 | 128 | // 点击事件 129 | label.addMouseListener(new MouseAdapter() { 130 | @Override 131 | public void mouseClicked(MouseEvent e) { 132 | if (selectedLabel[0] != null && selectedLabel[0] != label) { 133 | // 取消上一个高亮的标签 134 | selectedLabel[0].setBackground(customLightGray); 135 | } 136 | 137 | if (label == selectedLabel[0]) { 138 | // 如果当前点击的是已经选中的标签,取消高亮 139 | label.setBackground(customLightGray); 140 | // 取消筛选条件 141 | queryRequest.setQuestionBankCategoryId(SearchConstant.DEFAULT_QUESTION_BANK_CATEGORY_ID); 142 | searchAndLoadData(queryRequest); 143 | // 清空选中标签 144 | selectedLabel[0] = null; 145 | } else { 146 | // 高亮当前标签 147 | label.setBackground(customGray); 148 | currentPage[0] = PageConstant.FIRST_PAGE; 149 | queryRequest.setQuestionBankCategoryId(tag.getId()); 150 | queryRequest.setCurrent(PageConstant.FIRST_PAGE); 151 | searchAndLoadData(queryRequest); 152 | // 更新当前选中标签 153 | selectedLabel[0] = label; 154 | } 155 | } 156 | 157 | @Override 158 | public void mouseEntered(MouseEvent e) { 159 | if (label != selectedLabel[0]) { 160 | // 鼠标悬浮效果,仅当标签未被选中时生效 161 | label.setBackground(customGray); 162 | } 163 | } 164 | 165 | @Override 166 | public void mouseExited(MouseEvent e) { 167 | if (label != selectedLabel[0]) { 168 | // 鼠标离开时恢复原始背景色,仅当标签未被选中时生效 169 | label.setBackground(customLightGray); 170 | } 171 | } 172 | }); 173 | 174 | labelPanel.add(label); 175 | } 176 | }); 177 | }); 178 | } 179 | 180 | private void searchAndLoadData(QuestionBankCategoryBankQueryRequest queryRequest) { 181 | if (tableModel == null) { 182 | return; 183 | } 184 | // 清空现有表格数据 185 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 186 | Page data = this.fetchDataFromApi(queryRequest); 187 | ApplicationManager.getApplication().invokeLater(() -> { 188 | tableModel.setRowCount(0); 189 | 190 | // 添加新数据到表格模型 191 | for (QuestionBank row : data.getRecords()) { 192 | tableModel.addRow(new Object[]{row.getId().toString(), row.getTitle(), row.getTagList()}); 193 | } 194 | 195 | // 重新渲染表格 196 | tableModel.fireTableDataChanged(); 197 | PanelUtil.updatePaginationPanel(paginationPanel, data.getTotal(), currentPage, this::loadPage); 198 | }); 199 | }); 200 | } 201 | 202 | private void loadPage(int page, int pageSize) { 203 | // 实现分页逻辑,并更新表格数据 204 | queryRequest.setPageSize(pageSize); 205 | queryRequest.setCurrent(page); 206 | this.searchAndLoadData(queryRequest); 207 | } 208 | 209 | private void loadDataTable(JBPanel tabPanel, Project project) { 210 | ApplicationManager.getApplication().executeOnPooledThread(() -> { 211 | Page data = this.fetchDataFromApi(queryRequest); 212 | // 创建表格数据模型 213 | ApplicationManager.getApplication().invokeLater(() -> { 214 | tableModel = new MTabModel(); 215 | tableModel.addColumn("id"); 216 | tableModel.addColumn("题库名称"); 217 | tableModel.addColumn("所属分类"); 218 | 219 | // 将数据添加到表格模型 220 | for (QuestionBank row : data.getRecords()) { 221 | tableModel.addRow(new Object[]{row.getId().toString(), row.getTitle(), row.getTagList()}); 222 | } 223 | 224 | JBTable table = PanelUtil.createTablePanel(tableModel, (tempTable, mouseEvent) -> { 225 | int selectedRow = tempTable.getSelectedRow(); 226 | if (selectedRow != -1) { 227 | // 获取选中行的数据 228 | String id = (String) tempTable.getValueAt(selectedRow, 0); 229 | 230 | // 打开包含该行数据的新选项卡 231 | QuestionListManager questionListManager = new QuestionListManager(); 232 | questionListManager.addQuestionTab(Long.valueOf(id), project); 233 | } 234 | }, 2); 235 | // 将表格添加到滚动面板 236 | JBScrollPane scrollPane = new JBScrollPane(table); 237 | // 确保表格充满视口 238 | scrollPane.setViewportView(table); 239 | tabPanel.add(scrollPane, BorderLayout.CENTER); 240 | 241 | // 更新分页条 242 | PanelUtil.updatePaginationPanel(paginationPanel, data.getTotal(), currentPage, this::loadPage); 243 | }); 244 | }); 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/github/yuyuanweb/mianshiyaplugin/file/preview/SplitFileEditor.java: -------------------------------------------------------------------------------- 1 | package com.github.yuyuanweb.mianshiyaplugin.file.preview; 2 | 3 | import com.github.yuyuanweb.mianshiyaplugin.constant.ViewConstant; 4 | import com.intellij.codeHighlighting.BackgroundEditorHighlighter; 5 | import com.intellij.ide.structureView.StructureViewBuilder; 6 | import com.intellij.openapi.fileEditor.*; 7 | import com.intellij.openapi.util.Disposer; 8 | import com.intellij.openapi.util.Key; 9 | import com.intellij.openapi.util.Pair; 10 | import com.intellij.openapi.util.UserDataHolderBase; 11 | import com.intellij.openapi.vfs.VirtualFile; 12 | import com.intellij.openapi.wm.IdeFocusManager; 13 | import com.intellij.ui.JBSplitter; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | 17 | import javax.swing.*; 18 | import java.awt.*; 19 | import java.beans.PropertyChangeEvent; 20 | import java.beans.PropertyChangeListener; 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | import static com.intellij.openapi.fileEditor.TextEditorWithPreview.DEFAULT_LAYOUT_FOR_FILE; 25 | 26 | /** 27 | * 分栏 文件编辑器 28 | * 29 | * @author pine 30 | */ 31 | public abstract class SplitFileEditor extends UserDataHolderBase implements FileEditor { 32 | 33 | public static final Key PARENT_SPLIT_KEY = Key.create(ViewConstant.PARENT_SPLIT_KEY); 34 | private static final String MY_PROPORTION_KEY = ViewConstant.SPLIT_FILE_EDITOR + ViewConstant.SPLITTER; 35 | 36 | @NotNull 37 | protected final E1 myMainEditor; 38 | @NotNull 39 | protected final E2 mySecondEditor; 40 | @NotNull 41 | private final JComponent myComponent; 42 | @NotNull 43 | private SplitEditorLayout mySplitEditorLayout = SplitEditorLayout.SPLIT; 44 | 45 | @NotNull 46 | private final MyListenersMultimap myListenersGenerator = new MyListenersMultimap(); 47 | 48 | private SplitEditorToolbar myToolbarWrapper; 49 | 50 | public SplitFileEditor(@NotNull E1 mainEditor, @NotNull E2 secondEditor) { 51 | myMainEditor = mainEditor; 52 | mySecondEditor = secondEditor; 53 | 54 | adjustDefaultLayout(mainEditor); 55 | myComponent = createComponent(); 56 | 57 | if (myMainEditor instanceof TextEditor) { 58 | myMainEditor.putUserData(PARENT_SPLIT_KEY, this); 59 | } 60 | if (mySecondEditor instanceof TextEditor) { 61 | mySecondEditor.putUserData(PARENT_SPLIT_KEY, this); 62 | } 63 | } 64 | 65 | private void adjustDefaultLayout(E1 editor) { 66 | TextEditorWithPreview.Layout layout = getAndResetPredefinedLayoutForEditor(editor); 67 | if (layout != null) { 68 | switch (layout) { 69 | case SHOW_EDITOR: 70 | mySplitEditorLayout = SplitEditorLayout.FIRST; 71 | break; 72 | case SHOW_PREVIEW: 73 | mySplitEditorLayout = SplitEditorLayout.SECOND; 74 | break; 75 | case SHOW_EDITOR_AND_PREVIEW: 76 | mySplitEditorLayout = SplitEditorLayout.SPLIT; 77 | break; 78 | } 79 | } 80 | } 81 | 82 | @Nullable 83 | private static TextEditorWithPreview.Layout getAndResetPredefinedLayoutForEditor(FileEditor editor) { 84 | VirtualFile file = editor.getFile(); 85 | if (file != null) { 86 | TextEditorWithPreview.Layout layout = file.getUserData(DEFAULT_LAYOUT_FOR_FILE); 87 | if (layout != null) { 88 | file.putUserData(DEFAULT_LAYOUT_FOR_FILE, null); 89 | return layout; 90 | } 91 | } 92 | 93 | return null; 94 | } 95 | 96 | @NotNull 97 | private JComponent createComponent() { 98 | boolean myVerticalSplitOption = true; 99 | JBSplitter mySplitter = new JBSplitter(!myVerticalSplitOption, 0.5f, 0.15f, 0.85f); 100 | mySplitter.setSplitterProportionKey(MY_PROPORTION_KEY); 101 | mySplitter.setFirstComponent(myMainEditor.getComponent()); 102 | mySplitter.setSecondComponent(mySecondEditor.getComponent()); 103 | mySplitter.setDividerWidth(3); 104 | 105 | final JPanel result = new JPanel(new BorderLayout()); 106 | result.add(mySplitter, BorderLayout.CENTER); 107 | adjustEditorsVisibility(); 108 | 109 | return result; 110 | } 111 | 112 | private void invalidateLayout(boolean requestFocus) { 113 | adjustEditorsVisibility(); 114 | myToolbarWrapper.refresh(); 115 | myComponent.repaint(); 116 | 117 | if (!requestFocus) { 118 | return; 119 | } 120 | 121 | final JComponent focusComponent = getPreferredFocusedComponent(); 122 | if (focusComponent != null) { 123 | IdeFocusManager.findInstanceByComponent(focusComponent).requestFocus(focusComponent, true); 124 | } 125 | } 126 | 127 | private void adjustEditorsVisibility() { 128 | myMainEditor.getComponent().setVisible(mySplitEditorLayout.showFirst); 129 | mySecondEditor.getComponent().setVisible(mySplitEditorLayout.showSecond); 130 | } 131 | 132 | @NotNull 133 | @Override 134 | public JComponent getComponent() { 135 | return myComponent; 136 | } 137 | 138 | @Nullable 139 | @Override 140 | public JComponent getPreferredFocusedComponent() { 141 | if (mySplitEditorLayout.showFirst) { 142 | return myMainEditor.getPreferredFocusedComponent(); 143 | } 144 | if (mySplitEditorLayout.showSecond) { 145 | return mySecondEditor.getPreferredFocusedComponent(); 146 | } 147 | return null; 148 | } 149 | 150 | @NotNull 151 | @Override 152 | public FileEditorState getState(@NotNull FileEditorStateLevel level) { 153 | return new MyFileEditorState(mySplitEditorLayout.name(), myMainEditor.getState(level), mySecondEditor.getState(level)); 154 | } 155 | 156 | @Override 157 | public void setState(@NotNull FileEditorState state) { 158 | if (state instanceof SplitFileEditor.MyFileEditorState) { 159 | SplitFileEditor.MyFileEditorState compositeState = (SplitFileEditor.MyFileEditorState) state; 160 | if (compositeState.getFirstState() != null) { 161 | myMainEditor.setState(compositeState.getFirstState()); 162 | } 163 | if (compositeState.getSecondState() != null) { 164 | mySecondEditor.setState(compositeState.getSecondState()); 165 | } 166 | if (compositeState.getSplitLayout() != null) { 167 | mySplitEditorLayout = SplitEditorLayout.valueOf(compositeState.getSplitLayout()); 168 | invalidateLayout(true); 169 | } 170 | } 171 | } 172 | 173 | @Override 174 | public boolean isModified() { 175 | return myMainEditor.isModified() || mySecondEditor.isModified(); 176 | } 177 | 178 | @Override 179 | public boolean isValid() { 180 | return myMainEditor.isValid() && mySecondEditor.isValid(); 181 | } 182 | 183 | @Override 184 | public void selectNotify() { 185 | myMainEditor.selectNotify(); 186 | mySecondEditor.selectNotify(); 187 | } 188 | 189 | @Override 190 | public void deselectNotify() { 191 | myMainEditor.deselectNotify(); 192 | mySecondEditor.deselectNotify(); 193 | } 194 | 195 | @Override 196 | public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { 197 | myMainEditor.addPropertyChangeListener(listener); 198 | mySecondEditor.addPropertyChangeListener(listener); 199 | 200 | final DoublingEventListenerDelegate delegate = myListenersGenerator.addListenerAndGetDelegate(listener); 201 | myMainEditor.addPropertyChangeListener(delegate); 202 | mySecondEditor.addPropertyChangeListener(delegate); 203 | } 204 | 205 | @Override 206 | public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { 207 | myMainEditor.removePropertyChangeListener(listener); 208 | mySecondEditor.removePropertyChangeListener(listener); 209 | 210 | final DoublingEventListenerDelegate delegate = myListenersGenerator.removeListenerAndGetDelegate(listener); 211 | if (delegate != null) { 212 | myMainEditor.removePropertyChangeListener(delegate); 213 | mySecondEditor.removePropertyChangeListener(delegate); 214 | } 215 | } 216 | 217 | @Nullable 218 | @Override 219 | public BackgroundEditorHighlighter getBackgroundHighlighter() { 220 | return myMainEditor.getBackgroundHighlighter(); 221 | } 222 | 223 | @Nullable 224 | @Override 225 | public FileEditorLocation getCurrentLocation() { 226 | return myMainEditor.getCurrentLocation(); 227 | } 228 | 229 | @Nullable 230 | @Override 231 | public StructureViewBuilder getStructureViewBuilder() { 232 | return myMainEditor.getStructureViewBuilder(); 233 | } 234 | 235 | @Override 236 | public void dispose() { 237 | Disposer.dispose(myMainEditor); 238 | Disposer.dispose(mySecondEditor); 239 | } 240 | 241 | public static class MyFileEditorState implements FileEditorState { 242 | @Nullable 243 | private final String mySplitLayout; 244 | @Nullable 245 | private final FileEditorState myFirstState; 246 | @Nullable 247 | private final FileEditorState mySecondState; 248 | 249 | public MyFileEditorState(@Nullable String splitLayout, @Nullable FileEditorState firstState, @Nullable FileEditorState secondState) { 250 | mySplitLayout = splitLayout; 251 | myFirstState = firstState; 252 | mySecondState = secondState; 253 | } 254 | 255 | @Nullable 256 | public String getSplitLayout() { 257 | return mySplitLayout; 258 | } 259 | 260 | @Nullable 261 | public FileEditorState getFirstState() { 262 | return myFirstState; 263 | } 264 | 265 | @Nullable 266 | public FileEditorState getSecondState() { 267 | return mySecondState; 268 | } 269 | 270 | @Override 271 | public boolean canBeMergedWith(@NotNull FileEditorState otherState, @NotNull FileEditorStateLevel level) { 272 | return otherState instanceof SplitFileEditor.MyFileEditorState 273 | && (myFirstState == null || myFirstState.canBeMergedWith(((MyFileEditorState) otherState).myFirstState, level)) 274 | && (mySecondState == null || mySecondState.canBeMergedWith(((MyFileEditorState) otherState).mySecondState, level)); 275 | } 276 | } 277 | 278 | private class DoublingEventListenerDelegate implements PropertyChangeListener { 279 | @NotNull 280 | private final PropertyChangeListener myDelegate; 281 | 282 | private DoublingEventListenerDelegate(@NotNull PropertyChangeListener delegate) { 283 | myDelegate = delegate; 284 | } 285 | 286 | @Override 287 | public void propertyChange(PropertyChangeEvent evt) { 288 | myDelegate.propertyChange(new PropertyChangeEvent(SplitFileEditor.this, evt.getPropertyName(), evt.getOldValue(), evt.getNewValue())); 289 | } 290 | } 291 | 292 | private class MyListenersMultimap { 293 | private final Map> myMap = new HashMap<>(); 294 | 295 | @NotNull 296 | public DoublingEventListenerDelegate addListenerAndGetDelegate(@NotNull PropertyChangeListener listener) { 297 | if (!myMap.containsKey(listener)) { 298 | myMap.put(listener, Pair.create(1, new DoublingEventListenerDelegate(listener))); 299 | } else { 300 | myMap.computeIfPresent(listener, (k, oldPair) -> Pair.create(oldPair.getFirst() + 1, oldPair.getSecond())); 301 | } 302 | 303 | return myMap.get(listener).getSecond(); 304 | } 305 | 306 | @Nullable 307 | public DoublingEventListenerDelegate removeListenerAndGetDelegate(@NotNull PropertyChangeListener listener) { 308 | final Pair oldPair = myMap.get(listener); 309 | if (oldPair == null) { 310 | return null; 311 | } 312 | 313 | if (oldPair.getFirst() == 1) { 314 | myMap.remove(listener); 315 | } else { 316 | myMap.put(listener, Pair.create(oldPair.getFirst() - 1, oldPair.getSecond())); 317 | } 318 | return oldPair.getSecond(); 319 | } 320 | } 321 | 322 | public enum SplitEditorLayout { 323 | FIRST(true, false, "editor only"), 324 | SECOND(false, true, "preview only"), 325 | SPLIT(true, true, "editor and preview"); 326 | 327 | public final boolean showFirst; 328 | public final boolean showSecond; 329 | public final String presentationName; 330 | 331 | SplitEditorLayout(boolean showFirst, boolean showSecond, String presentationName) { 332 | this.showFirst = showFirst; 333 | this.showSecond = showSecond; 334 | this.presentationName = presentationName; 335 | } 336 | 337 | @Override 338 | public String toString() { 339 | return String.format("Show %s", presentationName); 340 | } 341 | } 342 | } 343 | 344 | --------------------------------------------------------------------------------