Commit 4e88417b by liangyuhang

init...

parents
### java template
.svn/
.idea/
.settings/
target/
out/
.classpath
.project
.mymetadata
.DS_Store
rebel.xml
*.ipr
*.iws
*.iml
DependencyManager/target/
.gradle
.idea
.qodana
build
\ No newline at end of file
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Plugin" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log"/>
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value=""/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="runIde"/>
</list>
</option>
<option name="vmOptions" value=""/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2"/>
</configuration>
</component>
\ No newline at end of file
![novel1.gif](src%2Fmain%2Fresources%2Fimages%2Fnovel1.gif)
![novel2.gif](src%2Fmain%2Fresources%2Fimages%2Fnovel2.gif)
![novel3.gif](src%2Fmain%2Fresources%2Fimages%2Fnovel3.gif)
\ No newline at end of file
plugins {
id("java")
id("org.jetbrains.intellij") version "1.15.0"
}
group = "com.liang.novel"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
// Configure Gradle IntelliJ Plugin
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
intellij {
version.set("2022.2.5")
type.set("IC") // Target IDE Platform
plugins.set(listOf(/* Plugin Dependencies */))
}
tasks {
// Set the JVM compatibility versions
withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
options.encoding = "UTF-8" //防止中文乱码
}
patchPluginXml {
sinceBuild.set("222")
untilBuild.set("232.*")
}
signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
privateKey.set(System.getenv("PRIVATE_KEY"))
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}
publishPlugin {
token.set(System.getenv("PUBLISH_TOKEN"))
}
}
# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
kotlin.stdlib.default.dependency=false
# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
org.gradle.configuration-cache=true
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
org.gradle.caching=true
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "lnovel-intellij-plugin"
\ No newline at end of file
package com.liang.novel.plugin;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.PluginManagerCore;
import com.intellij.openapi.extensions.PluginId;
public class NovelPluginManager {
public static final String PLUGIN_ID = "com.liang.novel.lnovel-intellij-plugin";
public static String currentVersion() {
IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId(PLUGIN_ID));
return plugin.getVersion();
}
}
package com.liang.novel.plugin;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupActivity;
import com.liang.novel.plugin.notifications.NovelNotification;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
public class PluginStarter implements StartupActivity, DumbAware {
@Override
public void runActivity(@NotNull Project project) {
String version = PropertiesComponent.getInstance().getValue(NovelPluginManager.PLUGIN_ID);
//空的就是第一次
if (StringUtils.isEmpty(version)) {
PropertiesComponent.getInstance().setValue(NovelPluginManager.PLUGIN_ID, NovelPluginManager.currentVersion());
NovelNotification.notifyWelcome(project);
}
}
}
package com.liang.novel.plugin.actions;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.ui.components.JBList;
import com.liang.novel.plugin.icons.Icons;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.pojo.Chapter;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.ReadUI;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class BookMarkAction extends DumbAwareAction {
private ReadUI readUI;
public BookMarkAction(ReadUI readUI) {
this.readUI = readUI;
getTemplatePresentation().setText("书签");
getTemplatePresentation().setIcon(Icons.BOOK_MARK_ICON);
}
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
List<Chapter> dataList = readUI.getDataList();
JBList<Chapter> jbList = readUI.getJbList();
Integer currentChapterIndex = dataList.get(jbList.getSelectedIndex()).getIndex();
Book book = readUI.getBook();
List<Chapter> chapterList = book.getChapterList();
Chapter chapter = chapterList.get(currentChapterIndex);
chapter.setBookmark(!chapter.getBookmark());
//持久化
NovelState.getInstance().setNewBook(book);
//异步刷新章节列表
readUI.refreshChapterListAsync(dataList);
}
@Override
public void update(@NotNull AnActionEvent e) {
Book currentReadBook = NovelState.getInstance().getCurrentReadBook();
JBList<Chapter> jbList = readUI.getJbList();
int selectedIndex = jbList.getSelectedIndex();
Presentation presentation = e.getPresentation();
presentation.setEnabled(currentReadBook != null && selectedIndex != -1);
}
}
package com.liang.novel.plugin.actions;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.project.DumbAware;
import com.liang.novel.plugin.icons.Icons;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.ReadUI;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public class BookMarkListAction extends ToggleAction implements DumbAware {
private ReadUI readUI;
private Boolean onlyBookMarkChapter = false;
public BookMarkListAction(ReadUI readUI) {
this.readUI = readUI;
getTemplatePresentation().setText("书签列表");
getTemplatePresentation().setIcon(Icons.BOOK_MARK_LIST_ICON);
}
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}
@Override
public boolean isSelected(@NotNull AnActionEvent e) {
return onlyBookMarkChapter;
}
@Override
public void setSelected(@NotNull AnActionEvent e, boolean state) {
Book currentReadBook = NovelState.getInstance().getCurrentReadBook();
JComponent leftPanel = readUI.getLeftPanel();
if (!leftPanel.isVisible()) {
leftPanel.setVisible(true);
}
onlyBookMarkChapter = !onlyBookMarkChapter;
readUI.refreshChapterListAsync(currentReadBook, onlyBookMarkChapter);
}
@Override
public void update(@NotNull AnActionEvent e) {
Book currentReadBook = NovelState.getInstance().getCurrentReadBook();
Presentation presentation = e.getPresentation();
presentation.setEnabled(currentReadBook != null);
}
}
package com.liang.novel.plugin.actions;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import com.liang.novel.plugin.icons.Icons;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.ToolWindowUI;
import org.apache.commons.collections.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.util.Arrays;
import java.util.List;
public class BookSelectAction extends DumbAwareAction {
private ToolWindowUI toolWindowUI;
public BookSelectAction(ToolWindowUI toolWindowUI) {
this.toolWindowUI = toolWindowUI;
getTemplatePresentation().setText("书架");
getTemplatePresentation().setIcon(AllIcons.Actions.ListFiles);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
NovelState instance = NovelState.getInstance();
List<Book> bookList = instance.getBookList();
JBPopupFactory.getInstance()
.createPopupChooserBuilder(bookList)
.setTitle("书架")
.setItemChosenCallback(book -> {
//设置当前在读
int selectedIndex = bookList.indexOf(book);
instance.setCurrentReadBookIndex(selectedIndex);
//修改toolwindow的标题
ToolWindow toolWindow = ToolWindowManager.getInstance(e.getProject()).getToolWindow("lnovel");
toolWindow.setTitle(book.getBookName());
//刷新面板
SwingUtilities.invokeLater(() -> toolWindowUI.updateReadUI());
})
.createPopup()
//.showUnderneathOf(e.getInputEvent().getComponent()); //显示在Action下方显示
.showInScreenCoordinates(e.getInputEvent().getComponent(), getPoint(e.getInputEvent().getComponent())); //想在右侧展示还得自己写坐标...
}
private Point getPoint(Component component) {
Point locationOnScreen = component.getLocationOnScreen();
int x = locationOnScreen.x + component.getWidth();
int y = locationOnScreen.y;
return new Point(x, y);
}
@Override
public void update(@NotNull AnActionEvent e) {
List<Book> bookList = NovelState.getInstance().getBookList();
Presentation presentation = e.getPresentation();
presentation.setEnabled(CollectionUtils.isNotEmpty(bookList));
}
}
package com.liang.novel.plugin.actions;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.util.NlsActions;
import com.liang.novel.plugin.icons.Icons;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.ReadUI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
public class FontSizeDecreaseAction extends DumbAwareAction {
private ReadUI readUI;
public FontSizeDecreaseAction(ReadUI readUI) {
this.readUI = readUI;
getTemplatePresentation().setText("字号减小");
getTemplatePresentation().setIcon(Icons.A_MINUS_ICON);
}
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
NovelState instance = NovelState.getInstance();
Integer fontSize = instance.getFontSize();
if (fontSize > 12) {
fontSize = fontSize - 1;
instance.setFontSize(fontSize);
readUI.setFont(new Font(instance.getFontType(), Font.PLAIN, fontSize));
}
}
@Override
public void update(@NotNull AnActionEvent e) {
Presentation presentation = e.getPresentation();
NovelState instance = NovelState.getInstance();
Boolean useCustomFont = instance.getUseCustomFont();
if (useCustomFont) {
Integer fontSize = instance.getFontSize();
if (fontSize > 12) {
presentation.setEnabled(true);
presentation.setText("字号减少");
}else{
presentation.setEnabled(false);
presentation.setText("已是最小字号");
}
}else{
presentation.setEnabled(false);
presentation.setText("请在setting中开启字体");
}
}
}
package com.liang.novel.plugin.actions;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.util.NlsActions;
import com.liang.novel.plugin.icons.Icons;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.ReadUI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
public class FontSizeIncreaseAction extends DumbAwareAction {
private ReadUI readUI;
public FontSizeIncreaseAction(ReadUI readUI) {
this.readUI = readUI;
getTemplatePresentation().setText("字号增加");
getTemplatePresentation().setIcon(Icons.A_PLUS_ICON);
}
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
NovelState instance = NovelState.getInstance();
Integer fontSize = instance.getFontSize();
if (fontSize < 24) {
fontSize = fontSize + 1;
instance.setFontSize(fontSize);
readUI.setFont(new Font(instance.getFontType(), Font.PLAIN, fontSize));
}
}
@Override
public void update(@NotNull AnActionEvent e) {
Presentation presentation = e.getPresentation();
NovelState instance = NovelState.getInstance();
Boolean useCustomFont = instance.getUseCustomFont();
if (useCustomFont) {
Integer fontSize = instance.getFontSize();
if (fontSize < 24) {
presentation.setEnabled(true);
presentation.setText("字号增加");
}else{
presentation.setEnabled(false);
presentation.setText("已是最大字号");
}
}else{
presentation.setEnabled(false);
presentation.setText("请在setting中开启字体");
}
}
}
package com.liang.novel.plugin.actions;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
public class NovelSettingAction extends DumbAwareAction {
public NovelSettingAction() {
getTemplatePresentation().setText("Setting");
getTemplatePresentation().setIcon(AllIcons.General.GearPlain);
}
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getProject();
ShowSettingsUtil.getInstance().showSettingsDialog(project, "小说设置");
}
}
package com.liang.novel.plugin.actions;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.ToolWindowUI;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public class RefreshToolWindowUIAction extends DumbAwareAction {
private ToolWindowUI toolWindowUI;
public RefreshToolWindowUIAction(ToolWindowUI toolWindowUI) {
this.toolWindowUI = toolWindowUI;
getTemplatePresentation().setText("刷新");
getTemplatePresentation().setIcon(AllIcons.Actions.Refresh);
}
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Book currentReadBook = NovelState.getInstance().getCurrentReadBook();
//修改toolwindow的标题
ToolWindow toolWindow = ToolWindowManager.getInstance(e.getProject()).getToolWindow("lnovel");
if (currentReadBook != null) {
toolWindow.setTitle(currentReadBook.getBookName());
} else {
toolWindow.setTitle("");
}
//刷新面板
SwingUtilities.invokeLater(() -> toolWindowUI.updateReadUI());
}
@Override
public void update(@NotNull AnActionEvent e) {
Presentation presentation = e.getPresentation();
presentation.setEnabled(false);
Book toolWindowUIBook = toolWindowUI.getCurrentReadBook();
Book novelStatebook = NovelState.getInstance().getCurrentReadBook();
if (toolWindowUIBook != null) {
if (novelStatebook != null) {
if (!toolWindowUIBook.getUrl().equals(novelStatebook.getUrl())) {
presentation.setEnabled(true);
}
} else {
presentation.setEnabled(true);
}
} else {
if (novelStatebook != null) {
presentation.setEnabled(true);
}
}
}
}
package com.liang.novel.plugin.actions;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.util.NlsActions;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.ui.AnActionButton;
import com.liang.novel.plugin.icons.Icons;
import com.liang.novel.plugin.ui.ReadUI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
public class ToggleChapterAction extends DumbAwareAction {
private ReadUI readUI;
public ToggleChapterAction(ReadUI readUI) {
this.readUI = readUI;
getTemplatePresentation().setText("隐藏章节列表");
getTemplatePresentation().setIcon(AllIcons.Actions.ArrowCollapse);
}
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
JComponent leftPanel = readUI.getLeftPanel();
//隐藏或展示
leftPanel.setVisible(!leftPanel.isVisible());
}
@Override
public void update(@NotNull AnActionEvent e) {
JComponent leftPanel = readUI.getLeftPanel();
//更新图标
Presentation presentation = e.getPresentation();
if (leftPanel.isVisible()) {
presentation.setText("隐藏章节列表");
presentation.setIcon(AllIcons.Actions.ArrowCollapse);
}else{
presentation.setText("打开章节列表");
presentation.setIcon(AllIcons.Actions.ArrowExpand);
}
}
}
package com.liang.novel.plugin.factory;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.Separator;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.content.ContentManager;
import com.intellij.ui.content.ContentUI;
import com.liang.novel.plugin.actions.*;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.ToolWindowUI;
import org.apache.commons.collections.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.util.List;
public class ReadToolWindow implements ToolWindowFactory, DumbAware {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
ToolWindowUI toolWindowUI = new ToolWindowUI(project);
Book currentReadBook = toolWindowUI.getCurrentReadBook();
Content content = ContentFactory.getInstance().createContent(toolWindowUI.getContentPanel(), currentReadBook != null ? currentReadBook.getBookName() : "", false);
toolWindow.getContentManager().addContent(content);
}
}
package com.liang.novel.plugin.icons;
import com.intellij.openapi.util.IconLoader;
import javax.swing.*;
public class Icons {
public static final Icon BOOK_ICON = IconLoader.getIcon("/icons/book.svg", Icons.class);
public static final Icon BOOK_MARK_ICON = IconLoader.getIcon("/icons/bookmark.svg", Icons.class);
public static final Icon BOOK_MARK_LIST_ICON = IconLoader.getIcon("/icons/bookmarksList.svg", Icons.class);
public static final Icon A_PLUS_ICON = IconLoader.getIcon("/icons/a-plus.svg", Icons.class);
public static final Icon A_MINUS_ICON = IconLoader.getIcon("/icons/a-minus.svg", Icons.class);
}
package com.liang.novel.plugin.notifications;
import com.intellij.ide.BrowserUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationAction;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.liang.novel.plugin.icons.Icons;
import org.intellij.lang.annotations.Language;
public class NovelNotification {
@Language("HTML")
private final static String WELCOME_MESSAGE = "<p>欢迎使用 <span>lnovel 插件</span></p>";
public static final String GROUP_ID = "MyNotificationGroup";
private static final String GIT_LINK = "http://git.zhiweidata.top/liangyuhang/lnovel-intellij-plugin";
public static void notifyWelcome(Project project) {
Notification notification = new Notification(GROUP_ID, WELCOME_MESSAGE, NotificationType.INFORMATION);
addNotificationActions(notification, project);
notification.setIcon(Icons.BOOK_ICON);
notification.notify(project);
}
private static void addNotificationActions(Notification notification, Project project) {
notification.addAction(NotificationAction.createSimple("Setting", () -> ShowSettingsUtil.getInstance().showSettingsDialog(project, "小说设置")));
notification.addAction(NotificationAction.createSimple("Git", () -> BrowserUtil.browse(GIT_LINK)));
}
}
package com.liang.novel.plugin.pojo;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Book implements Serializable {
/**
* 书名
*/
private String bookName;
/**
* 本地地址或链接
*/
private String url;
/**
* 总字数
*/
private Integer count;
/**
* 上次读到哪章
*/
private Integer chapterIndex;
/**
* 章节列表
*/
private List<Chapter> chapterList;
public Book() {
this.bookName = "";
this.url = "";
this.count = 0;
this.chapterIndex = 0;
this.chapterList = new ArrayList<>();
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getChapterIndex() {
return chapterIndex;
}
public void setChapterIndex(Integer chapterIndex) {
this.chapterIndex = chapterIndex;
}
public List<Chapter> getChapterList() {
return chapterList;
}
public void setChapterList(List<Chapter> chapterList) {
this.chapterList = chapterList;
}
@Override
public String toString() {
return bookName;
}
}
package com.liang.novel.plugin.pojo;
import java.io.Serializable;
public class Chapter implements Serializable {
/**
* 章节索引
*/
private Integer index;
/**
* 章节标题
*/
private String title;
/**
* 当前章节所在行数
*/
private Integer row;
/**
* 本章字数
*/
private Integer count;
/**
* 是否添加书签
*/
private Boolean bookmark = false;
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Integer getRow() {
return row;
}
public void setRow(Integer row) {
this.row = row;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Boolean getBookmark() {
return bookmark;
}
public void setBookmark(Boolean bookmark) {
this.bookmark = bookmark;
}
@Override
public String toString() {
return title + " " + row;
}
}
package com.liang.novel.plugin.settings;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.util.NlsContexts;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.ui.SettingUI;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
public class NovelSetting implements Configurable {
private SettingUI settingUI;
private NovelState novelState;
public NovelSetting() {
this.settingUI = new SettingUI();
this.novelState = NovelState.getInstance();
}
@Override
public @NlsContexts.ConfigurableName String getDisplayName() {
return null;
}
@Override
public @Nullable JComponent createComponent() {
return settingUI.getContentPanel();
}
@Override
public boolean isModified() {
return !settingUI.getUseCustomFont().equals(novelState.getUseCustomFont()) ||
!settingUI.getFontType().equals(novelState.getFontType()) ||
!settingUI.getFontSize().equals(novelState.getFontSize());
}
@Override
public void apply() throws ConfigurationException {
novelState.setUseCustomFont(settingUI.getUseCustomFont());
novelState.setFontType(settingUI.getFontType());
novelState.setFontSize(settingUI.getFontSize());
//书籍相关直接保存,不用点apply
//novelState.setBookList(settingUI.getBookList());
//novelState.setCurrentReadBook(settingUI.getCurrentReadBook());
}
}
package com.liang.novel.plugin.state;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import com.liang.novel.plugin.pojo.Book;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@State(name = "novelState", storages = @Storage("NovelState.xml"))
public class NovelState implements PersistentStateComponent<NovelState> {
/**
* 是否使用自定义字体
*/
private Boolean useCustomFont;
/**
* 字体
*/
private String fontType;
/**
* 字号
*/
private Integer fontSize;
/**
* 当前正在读(书架中的第几本,用Integer而不是Book,因为会导致多存一个Book的内容)
*/
private Integer currentReadBookIndex;
/**
* 书架
*/
private List<Book> bookList;
public NovelState() {
this.useCustomFont = false;
this.fontType = "Monospaced";
this.fontSize = 12;
this.currentReadBookIndex = null;
this.bookList = new ArrayList<>();
}
public static NovelState getInstance() {
return ApplicationManager.getApplication().getService(NovelState.class);
}
@Override
public @Nullable NovelState getState() {
return this;
}
@Override
public void loadState(@NotNull NovelState state) {
XmlSerializerUtil.copyBean(state, this);
}
public Boolean getUseCustomFont() {
return useCustomFont;
}
public void setUseCustomFont(Boolean useCustomFont) {
this.useCustomFont = useCustomFont;
}
public String getFontType() {
return fontType;
}
public void setFontType(String fontType) {
this.fontType = fontType;
}
public Integer getFontSize() {
return fontSize;
}
public void setFontSize(Integer fontSize) {
this.fontSize = fontSize;
}
public Integer getCurrentReadBookIndex() {
return currentReadBookIndex;
}
public void setCurrentReadBookIndex(Integer currentReadBookIndex) {
this.currentReadBookIndex = currentReadBookIndex;
}
public List<Book> getBookList() {
return bookList;
}
public void setBookList(List<Book> bookList) {
this.bookList = bookList;
}
public void setNewBook(Book newBook){
for (int i = 0; i < bookList.size(); i++) {
Book book = bookList.get(i);
if (book.getUrl().equals(newBook.getUrl())) {
bookList.set(i, book);
}
}
setBookList(bookList);
}
public Book getCurrentReadBook() {
return currentReadBookIndex != null ? bookList.get(currentReadBookIndex) : null;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.liang.novel.plugin.ui.EditDialogUI">
<grid id="27dc6" binding="contentPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="d8acf" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="书名:"/>
</properties>
</component>
<component id="57d5" class="javax.swing.JTextField" binding="bookName">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="390a5" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="路径:"/>
</properties>
</component>
<component id="92c87" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="url">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
</children>
</grid>
</form>
package com.liang.novel.plugin.ui;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.*;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
public class EditDialogUI extends DialogWrapper {
private JPanel contentPanel;
private JTextField bookName;
private TextFieldWithBrowseButton url;
public EditDialogUI() {
super(true);
setTitle("编辑书籍");
init();
//禁止对话框大小可变
setResizable(false);
//设置TextFieldWithBrowseButton跟JTextField相同样式
bookName.setColumns(15);
url.setPreferredSize(bookName.getPreferredSize());
FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false);
descriptor.withFileFilter(file -> file.getName().toLowerCase().endsWith(".txt")); //只要txt
TextBrowseFolderListener listener = new TextBrowseFolderListener(descriptor);
url.addBrowseFolderListener(listener);
}
@Override
protected @Nullable JComponent createCenterPanel() {
return contentPanel;
}
@Override
protected void doOKAction() {
ValidationInfo validationInfo = doValidate();
if (validationInfo != null) {
setErrorText(validationInfo.message);
} else {
super.doOKAction();
}
}
@Override
protected @Nullable ValidationInfo doValidate() {
String urlText = url.getText();
if (!urlText.endsWith("txt")) {
return new ValidationInfo("请选择txt文件", url);
}
return null; //返回 null 表示验证通过
}
public String getBookName() {
return bookName.getText();
}
public void setBookName(String bookName) {
this.bookName.setText(bookName);
}
public String getUrl() {
return url.getText();
}
public void setUrl(String url) {
this.url.setText(url);
}
{
// GUI initializer generated by IntelliJ IDEA GUI Designer
// >>> IMPORTANT!! <<<
// DO NOT EDIT OR ADD ANY CODE HERE!
$$$setupUI$$$();
}
/**
* Method generated by IntelliJ IDEA GUI Designer
* >>> IMPORTANT!! <<<
* DO NOT edit this method OR call it in your code!
*
* @noinspection ALL
*/
private void $$$setupUI$$$() {
contentPanel = new JPanel();
contentPanel.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1));
final JLabel label1 = new JLabel();
label1.setText("书名:");
contentPanel.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
bookName = new JTextField();
contentPanel.add(bookName, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false));
final JLabel label2 = new JLabel();
label2.setText("路径:");
contentPanel.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
url = new TextFieldWithBrowseButton();
contentPanel.add(url, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
}
/**
* @noinspection ALL
*/
public JComponent $$$getRootComponent$$$() {
return contentPanel;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.liang.novel.plugin.ui.ReadUI">
<grid id="27dc6" binding="contentPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</form>
package com.liang.novel.plugin.ui;
import com.intellij.ui.ListSpeedSearch;
import com.intellij.ui.OnePixelSplitter;
import com.intellij.ui.ToolbarDecorator;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.liang.novel.plugin.icons.Icons;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.pojo.Chapter;
import com.liang.novel.plugin.ui.renderer.IconListCellRenderer;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.utils.NovelUtil;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class ReadUI {
private JPanel contentPanel;
private JComponent leftPanel;
private JComponent rightPanel;
private Book book;
private Font font;
private List<Chapter> dataList;
private JTextArea contentTextArea;
private DefaultListModel<Chapter> listModel;
private JBList<Chapter> jbList;
public ReadUI(Book book, Font font, Boolean isFullBoth) {
this.book = book;
this.font = font;
this.dataList = book != null ? book.getChapterList() : Collections.emptyList();
contentTextArea = new JTextArea();
//分离器,左边章节列表,右边内容
OnePixelSplitter splitter = new OnePixelSplitter(0.2f);
leftPanel = initLeftPanel();
splitter.setFirstComponent(leftPanel);
rightPanel = initRightPanel();
splitter.setSecondComponent(rightPanel);
GridConstraints gridConstraints = new GridConstraints();
if (isFullBoth) {
gridConstraints.setFill(GridConstraints.FILL_BOTH);
} else {
gridConstraints.setFill(GridConstraints.FILL_HORIZONTAL);
}
contentPanel.add(splitter, gridConstraints);
//快捷键监听,需要将焦点在内容面板
contentTextArea.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.isAltDown()) {
if (e.getKeyCode() == KeyEvent.VK_UP) {
up();
}
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
down();
}
}
}
});
}
private JComponent initLeftPanel() {
listModel = new DefaultListModel<>();
for (Chapter chapter : dataList) {
listModel.addElement(chapter);
}
jbList = new JBList<>(listModel);
//添加监听器,监听左边章节列表的选择事件
jbList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) { //确保事件只触发一次
int selectedIndex = jbList.getSelectedIndex();
if (selectedIndex != -1) {
//确保选中在当前页可见,滚动页面到选中那一条
jbList.ensureIndexIsVisible(selectedIndex);
//将章节内容放入右边内容面板
Chapter chapter = dataList.get(selectedIndex);
contentTextArea.setText(getChapterContent(chapter.getRow()));
//设置字体字号
if (font != null) {
contentTextArea.setFont(font);
}
//设置光标位置
contentTextArea.setCaretPosition(0);
//持久化(记录当前章节)
book.setChapterIndex(selectedIndex);
NovelState.getInstance().setNewBook(book);
}
}
});
//触发快速查找
new ListSpeedSearch<>(jbList);
//设置单元格的固定高度
jbList.setFixedCellHeight(20);
//设置可见行数
jbList.setVisibleRowCount(10);
//设置书签图标
jbList.setCellRenderer(new IconListCellRenderer(Icons.BOOK_MARK_ICON));
//默认选中
if (book != null) {
jbList.setSelectedIndex(book.getChapterIndex());
}
//创建工具栏装饰器
ToolbarDecorator toolbarDecorator = createToolbarDecorator(jbList);
return toolbarDecorator.createPanel();
}
private String getChapterContent(Integer chapterRow) {
if (book != null) {
return NovelUtil.getChapterContent(book.getUrl(), chapterRow);
}
return "";
}
private ToolbarDecorator createToolbarDecorator(JComponent jComponent) {
return ToolbarDecorator.createDecorator(jComponent)
.setRemoveAction(null) //移除删除操作按钮
.setMoveUpAction(anActionButton -> {
up();
})
.setMoveDownAction(anActionButton -> {
down();
});
}
private JComponent initRightPanel() {
contentTextArea.setLineWrap(true);
return new JBScrollPane(contentTextArea);
}
private void up() {
int selectedIndex = jbList.getSelectedIndex();
if (selectedIndex > 0) {
jbList.setSelectedIndex(selectedIndex - 1);
}
}
private void down() {
int selectedIndex = jbList.getSelectedIndex();
if (selectedIndex < dataList.size() - 1) {
jbList.setSelectedIndex(selectedIndex + 1);
}
}
public void refreshChapterListAsync(Book book) {
refreshChapterListAsync(book, false);
}
public void refreshChapterListAsync(Book book, Boolean onlyBookMarkChapter) {
if (book != null) {
this.book = book;
if (onlyBookMarkChapter) {
this.dataList = book.getChapterList().stream().filter(Chapter::getBookmark).collect(Collectors.toList());
} else {
this.dataList = book.getChapterList();
}
SwingUtilities.invokeLater(() -> doRefreshChapterList(onlyBookMarkChapter));
}
}
public void refreshChapterListAsync(List<Chapter> chapterList) {
refreshChapterListAsync(chapterList, false);
}
public void refreshChapterListAsync(List<Chapter> chapterList, Boolean onlyBookMarkChapter) {
if (chapterList != null) {
if (onlyBookMarkChapter) {
this.dataList = chapterList.stream().filter(Chapter::getBookmark).collect(Collectors.toList());
} else {
this.dataList = chapterList;
}
}
SwingUtilities.invokeLater(() -> doRefreshChapterList(onlyBookMarkChapter));
}
private void doRefreshChapterList(Boolean onlyBookMarkChapter) {
SwingWorker<Void, Void> worker = new SwingWorker<>() {
@Override
protected Void doInBackground() {
//更新数据模型
listModel.clear();
for (Chapter chapter : dataList) {
listModel.addElement(chapter);
}
return null;
}
@Override
protected void done() {
//在后台线程完成后,触发列表视图的重绘
jbList.repaint();
if (onlyBookMarkChapter) {
contentTextArea.setText("");
} else {
jbList.setSelectedIndex(book.getChapterIndex());
}
}
};
worker.execute();
}
public void setFont(Font font) {
if (font != null) {
this.font = font;
} else {
this.font = new Font("Monospaced", Font.PLAIN, 12); //重设为平台字体
}
if (StringUtils.isNotEmpty(contentTextArea.getText())) {
contentTextArea.setFont(this.font);
}
}
public void clearChapterList() {
listModel.clear();
}
public void clearContentText() {
contentTextArea.setText("");
}
public JPanel getContentPanel() {
return contentPanel;
}
public JComponent getLeftPanel() {
return leftPanel;
}
public JBList<Chapter> getJbList() {
return jbList;
}
public Book getBook() {
return book;
}
public List<Chapter> getDataList() {
return dataList;
}
{
// GUI initializer generated by IntelliJ IDEA GUI Designer
// >>> IMPORTANT!! <<<
// DO NOT EDIT OR ADD ANY CODE HERE!
$$$setupUI$$$();
}
/**
* Method generated by IntelliJ IDEA GUI Designer
* >>> IMPORTANT!! <<<
* DO NOT edit this method OR call it in your code!
*
* @noinspection ALL
*/
private void $$$setupUI$$$() {
contentPanel = new JPanel();
contentPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
}
/**
* @noinspection ALL
*/
public JComponent $$$getRootComponent$$$() {
return contentPanel;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.liang.novel.plugin.ui.SettingUI">
<grid id="27dc6" binding="contentPanel" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<vspacer id="7a40f">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<grid id="c19b3" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="b27e5" layout-manager="GridLayoutManager" row-count="1" column-count="5" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="f0707" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="字体:"/>
</properties>
</component>
<component id="19692" class="javax.swing.JComboBox" binding="fontTypeComboBox">
<constraints>
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<enabled value="false"/>
</properties>
</component>
<component id="543bf" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="字号:"/>
</properties>
</component>
<component id="8e781" class="javax.swing.JComboBox" binding="fontSizeComboBox">
<constraints>
<grid row="0" column="4" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<enabled value="false"/>
</properties>
</component>
<component id="8d5bd" class="javax.swing.JCheckBox" binding="fontCheckBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value=""/>
</properties>
</component>
</children>
</grid>
<grid id="83245" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="69df" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="上一章:"/>
</properties>
</component>
<component id="fda8b" class="javax.swing.JTextField" binding="preChapterText">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<enabled value="false"/>
<text value="Alt + ↑"/>
</properties>
</component>
<component id="90b6e" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="下一章:"/>
</properties>
</component>
<component id="84003" class="javax.swing.JTextField" binding="nextChapterText">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<enabled value="false"/>
<text value="Alt + ↓"/>
</properties>
</component>
</children>
</grid>
<hspacer id="97e15">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
</children>
</grid>
<grid id="8f8b6" binding="bookPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
<grid id="472f1" binding="readPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</children>
</grid>
</form>
package com.liang.novel.plugin.ui;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.*;
import com.intellij.ui.table.JBTable;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.uiDesigner.core.Spacer;
import com.liang.novel.plugin.ui.model.BookTableModel;
import com.liang.novel.plugin.state.NovelState;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.utils.NovelUtil;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class SettingUI {
public static final String EMPTY_MESSAGE = "书架中还没有书,快去添加吧";
private JPanel contentPanel;
private JComboBox<String> fontTypeComboBox;
private JComboBox<Integer> fontSizeComboBox;
private JPanel bookPanel;
private JPanel readPanel;
private JCheckBox fontCheckBox;
private JTextField preChapterText;
private JTextField nextChapterText;
private NovelState novelState;
private Boolean useCustomFont;
private String fontType;
private Integer fontSize;
private List<Book> bookList;
private Integer currentReadBookIndex;
private ReadUI readUI;
private JBTable jbTable;
private GridConstraints gridConstraints;
public SettingUI() {
this.novelState = NovelState.getInstance();
this.useCustomFont = novelState.getUseCustomFont();
this.fontType = novelState.getFontType();
this.fontSize = novelState.getFontSize();
this.bookList = novelState.getBookList();
this.currentReadBookIndex = novelState.getCurrentReadBookIndex();
gridConstraints = new GridConstraints();
gridConstraints.setFill(GridConstraints.FILL_HORIZONTAL); //水平占满
//初始化面板
initContentPanel();
}
/**
* 初始化面板(得先初始化readUI,别的面板要用)
*/
private void initContentPanel() {
//初始化阅读面板
initReadPanel();
//初始化书架面板
initBookPanel();
//初始化字体
intFont();
}
@SuppressWarnings("unchecked")
private void intFont() {
if (useCustomFont) {
fontCheckBox.setSelected(true);
fontTypeComboBox.setEnabled(true);
fontSizeComboBox.setEnabled(true);
}
//初始化字体类型
Font[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
List<String> fontFamilyList = Arrays.stream(fonts).map(Font::getFamily).distinct().toList(); //去重
DefaultComboBoxModel<String> fontTypeComboBoxModel = new DefaultComboBoxModel<>();
for (String fontFamily : fontFamilyList) {
fontTypeComboBoxModel.addElement(fontFamily);
}
fontTypeComboBox.setModel(fontTypeComboBoxModel);
fontTypeComboBox.setSelectedItem(fontType);
//初始化字体字号
DefaultComboBoxModel<Integer> fontSizeComboBoxModel = new DefaultComboBoxModel<>();
for (int i = 8; i <= 24; i++) {
fontSizeComboBoxModel.addElement(i);
}
fontSizeComboBox.setModel(fontSizeComboBoxModel);
fontSizeComboBox.setSelectedItem(fontSize);
//监听是否启用自定义字体
fontCheckBox.addItemListener(e -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
fontTypeComboBox.setEnabled(true);
fontSizeComboBox.setEnabled(true);
//重设字体
readUI.setFont(new Font(fontType, Font.PLAIN, fontSize));
} else {
fontTypeComboBox.setEnabled(false);
fontSizeComboBox.setEnabled(false);
//重设为平台字体
readUI.setFont(null);
}
});
//字体类型监听器
fontTypeComboBox.addActionListener(e -> {
JComboBox<String> cb = (JComboBox<String>) e.getSource();
//重设字体类型
this.fontType = (String) cb.getSelectedItem();
readUI.setFont(new Font(fontType, Font.PLAIN, getFontSize()));
});
//字体字号监听器
fontSizeComboBox.addActionListener(e -> {
JComboBox<Integer> cb = (JComboBox<Integer>) e.getSource();
//重设字体字号
this.fontSize = (Integer) cb.getSelectedItem();
readUI.setFont(new Font(getFontType(), Font.PLAIN, fontSize));
});
}
private void initBookPanel() {
jbTable = new JBTable();
//添加数据
jbTable.setModel(new BookTableModel(bookList));
//设置空数据时的提示语
if (bookList.isEmpty()) {
jbTable.getEmptyText().setText(EMPTY_MESSAGE);
}
//设置行高
jbTable.setRowHeight(25);
//设置行数
jbTable.setVisibleRowCount(5);
//设置选择模式为单选
jbTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
//设置默认选中的图书
if (currentReadBookIndex != null) {
jbTable.setRowSelectionInterval(currentReadBookIndex, currentReadBookIndex);
}
//监听表格的选择
jbTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
//获取选中的行索引
int selectedRow = jbTable.getSelectedRow();
if (selectedRow != -1) {
//修改当前正在读的图书
currentReadBookIndex = selectedRow;
//持久化
novelState.setCurrentReadBookIndex(currentReadBookIndex);
//异步刷新章节列表
readUI.refreshChapterListAsync(bookList.get(selectedRow));
} else {
//取消当前在读
novelState.setCurrentReadBookIndex(null);
}
}
});
//创建工具栏装饰器
ToolbarDecorator toolbarDecorator = createToolbarDecorator();
//添加到firstPanel内
bookPanel.add(toolbarDecorator.createPanel(), gridConstraints);
}
private ToolbarDecorator createToolbarDecorator() {
return ToolbarDecorator.createDecorator(jbTable)
.setAddAction(anActionButton -> { //添加
//文件选择器
FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false);
descriptor.withFileFilter(file -> file.getName().toLowerCase().endsWith(".txt")); //只要txt
VirtualFile virtualFile = FileChooser.chooseFile(descriptor, null, null);
//检查用户是否选择了文件
if (virtualFile != null) {
//显示进度条
ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
progressIndicator.setIndeterminate(true); //设置进度条为不确定状态
//获取文件名称
String fileName = virtualFile.getNameWithoutExtension();
String path = virtualFile.getPath();
Book book = new Book();
book.setBookName(fileName);
book.setUrl(path);
book.setChapterList(NovelUtil.parseNovelChapters(path));
bookList.add(book);
doAfterAction(bookList.size() - 1, true);
}, "正在添加图书", true, null); // 设置进度条标题
}
})
.setRemoveAction(anActionButton -> { //移除
int selectedRow = jbTable.getSelectedRow();
bookList.remove(selectedRow);
if (bookList.isEmpty()) {
jbTable.getEmptyText().setText(EMPTY_MESSAGE);
}
currentReadBookIndex = null;
//清空当前章节内容
readUI.clearContentText();
//清空当前章节列表
readUI.clearChapterList();
doAfterAction(bookList.size() - 1, true);
})
.setEditAction(anActionButton -> { //编辑
int selectedRow = jbTable.getSelectedRow();
Book book = bookList.get(selectedRow);
EditDialogUI editDialogUI = new EditDialogUI();
editDialogUI.setBookName(book.getBookName());
editDialogUI.setUrl(book.getUrl());
boolean isOk = editDialogUI.showAndGet();
if (isOk) {
boolean updateBookName = !editDialogUI.getBookName().equals(book.getBookName());
boolean updateBookUrl = !editDialogUI.getUrl().equals(book.getUrl());
//如果修改了书名
if (updateBookName) {
book.setBookName(editDialogUI.getBookName());
}
//如果修改了路径
if (updateBookUrl) {
book.setUrl(editDialogUI.getUrl());
//开启进度条
ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
book.setChapterList(NovelUtil.parseNovelChapters(editDialogUI.getUrl()));
}, "正在解析图书", false, null);
}
if (updateBookName || updateBookUrl) {
//替换bookList中对应的book
bookList.set(selectedRow, book);
//如果updateBookUrl了,就刷新table
doAfterAction(selectedRow, updateBookUrl);
}
}
})
.setMoveUpAction(anActionButton -> { //上移
int curBookIndex = jbTable.getSelectedRow();
if (curBookIndex > 0) {
int lastBookIndex = curBookIndex - 1;
//交换索引顺序
Collections.swap(bookList, curBookIndex, lastBookIndex);
doAfterAction(lastBookIndex, false);
}
})
.setMoveDownAction(anActionButton -> { //下移
int curBookIndex = jbTable.getSelectedRow();
if (curBookIndex < bookList.size() - 1) {
int nextBookIndex = curBookIndex + 1;
//交换索引顺序
Collections.swap(bookList, curBookIndex, nextBookIndex);
doAfterAction(nextBookIndex, false);
}
});
}
private void doAfterAction(Integer selectIndex, Boolean refreshTable) {
//持久化
novelState.setBookList(bookList);
//更新表格数据
if (refreshTable) {
jbTable.setModel(new BookTableModel(bookList));
}
//选中变更后的自己
if (selectIndex != -1) {
jbTable.setRowSelectionInterval(selectIndex, selectIndex);
}
}
private void initReadPanel() {
Book book = currentReadBookIndex != null ? bookList.get(currentReadBookIndex) : null;
Font font = null;
if (useCustomFont) {
font = new Font(fontType, Font.PLAIN, fontSize);
}
readUI = new ReadUI(book, font, true);
readUI.getContentPanel().setPreferredSize(new Dimension(0, 250));
readPanel.add(readUI.getContentPanel(), gridConstraints);
}
public JPanel getContentPanel() {
return contentPanel;
}
public Boolean getUseCustomFont() {
return fontCheckBox.isSelected();
}
public String getFontType() {
return fontTypeComboBox.getSelectedItem().toString();
}
public void setFontType(String fontType) {
fontTypeComboBox.setSelectedItem(fontType);
}
public Integer getFontSize() {
return (Integer) fontSizeComboBox.getSelectedItem();
}
public void setFontSize(Integer fontSize) {
fontSizeComboBox.setSelectedItem(fontSize);
}
{
// GUI initializer generated by IntelliJ IDEA GUI Designer
// >>> IMPORTANT!! <<<
// DO NOT EDIT OR ADD ANY CODE HERE!
$$$setupUI$$$();
}
/**
* Method generated by IntelliJ IDEA GUI Designer
* >>> IMPORTANT!! <<<
* DO NOT edit this method OR call it in your code!
*
* @noinspection ALL
*/
private void $$$setupUI$$$() {
contentPanel = new JPanel();
contentPanel.setLayout(new GridLayoutManager(4, 1, new Insets(0, 0, 0, 0), -1, -1));
final Spacer spacer1 = new Spacer();
contentPanel.add(spacer1, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, 1, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false));
final JPanel panel1 = new JPanel();
panel1.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1));
contentPanel.add(panel1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
final JPanel panel2 = new JPanel();
panel2.setLayout(new GridLayoutManager(1, 5, new Insets(0, 0, 0, 0), -1, -1));
panel1.add(panel2, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
final JLabel label1 = new JLabel();
label1.setText("字体:");
panel2.add(label1, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
fontTypeComboBox = new JComboBox();
fontTypeComboBox.setEnabled(false);
panel2.add(fontTypeComboBox, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
final JLabel label2 = new JLabel();
label2.setText("字号:");
panel2.add(label2, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
fontSizeComboBox = new JComboBox();
fontSizeComboBox.setEnabled(false);
panel2.add(fontSizeComboBox, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
fontCheckBox = new JCheckBox();
fontCheckBox.setText("");
panel2.add(fontCheckBox, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
final JPanel panel3 = new JPanel();
panel3.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1));
panel1.add(panel3, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
final JLabel label3 = new JLabel();
label3.setText("上一章:");
panel3.add(label3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
preChapterText = new JTextField();
preChapterText.setEnabled(false);
preChapterText.setText("Alt + ↑");
panel3.add(preChapterText, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false));
final JLabel label4 = new JLabel();
label4.setText("下一章:");
panel3.add(label4, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
nextChapterText = new JTextField();
nextChapterText.setEnabled(false);
nextChapterText.setText("Alt + ↓");
panel3.add(nextChapterText, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false));
final Spacer spacer2 = new Spacer();
panel1.add(spacer2, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false));
bookPanel = new JPanel();
bookPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
contentPanel.add(bookPanel, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
readPanel = new JPanel();
readPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
contentPanel.add(readPanel, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
}
/**
* @noinspection ALL
*/
public JComponent $$$getRootComponent$$$() {
return contentPanel;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.liang.novel.plugin.ui.ToolWindowUI">
<grid id="27dc6" binding="contentPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</form>
package com.liang.novel.plugin.ui;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.ActionLink;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.util.ui.components.BorderLayoutPanel;
import com.liang.novel.plugin.actions.*;
import com.liang.novel.plugin.pojo.Book;
import com.liang.novel.plugin.state.NovelState;
import javax.swing.*;
import java.awt.*;
import java.util.List;
public class ToolWindowUI {
private JPanel contentPanel;
private Project project;
private NovelState novelState;
private String fontType;
private Integer fontSize;
private List<Book> bookList;
private Integer currentReadBookIndex;
private Book book;
private ReadUI readUI;
public ToolWindowUI(Project project) {
this.project = project;
initContentPanel();
}
private void initContentPanel() {
this.novelState = NovelState.getInstance();
this.fontType = novelState.getFontType();
this.fontSize = novelState.getFontSize();
this.bookList = novelState.getBookList();
this.currentReadBookIndex = novelState.getCurrentReadBookIndex();
this.book = currentReadBookIndex != null ? bookList.get(currentReadBookIndex) : null;
//边界布局面板
BorderLayoutPanel borderLayoutPanel = new BorderLayoutPanel();
borderLayoutPanel.add(getReadUIPanel());
borderLayoutPanel.add(createActionToolbar().getComponent(), BorderLayout.WEST); //最左边放工具栏
GridConstraints gridConstraints = new GridConstraints();
gridConstraints.setFill(GridConstraints.FILL_BOTH); //上下左右全填满
contentPanel.add(borderLayoutPanel, gridConstraints);
}
private Component getReadUIPanel() {
if (bookList.isEmpty()) {
ActionLink link = new ActionLink("请添加图书", e -> {
ShowSettingsUtil.getInstance().showSettingsDialog(project, "小说设置");
});
return getCenteredComponent(link);
} else if (currentReadBookIndex == null) {
ActionLink link = new ActionLink("请选择图书", e -> {
});
return getCenteredComponent(link);
} else {
//初始化阅读面板
readUI = new ReadUI(book, new Font(fontType, Font.PLAIN, fontSize), true);
return readUI.getContentPanel();
}
}
private Component getCenteredComponent(Component component) {
JPanel panel = new JPanel(new GridBagLayout());
panel.add(component, new GridBagConstraints());
panel.setBackground(JBColor.WHITE);
return panel;
}
/**
* 从ActionGroup创建工具栏
*/
private ActionToolbar createActionToolbar() {
DefaultActionGroup actionGroup = getActionGroup();
ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("NovelToolBar", actionGroup, false);
toolbar.setTargetComponent(contentPanel);
return toolbar;
}
/**
* 创建一个ActionGroup
*/
private DefaultActionGroup getActionGroup() {
DefaultActionGroup actionGroup = new DefaultActionGroup();
if (bookList.isEmpty() || currentReadBookIndex == null || readUI == null) {
actionGroup.add(new RefreshToolWindowUIAction(this));
} else {
actionGroup.add(new RefreshToolWindowUIAction(this));
actionGroup.add(Separator.getInstance());
actionGroup.add(new ToggleChapterAction(readUI));
actionGroup.add(Separator.getInstance());
actionGroup.add(new BookSelectAction(this));
actionGroup.add(Separator.getInstance());
actionGroup.add(new BookMarkAction(readUI));
actionGroup.add(new BookMarkListAction(readUI));
actionGroup.add(Separator.getInstance());
actionGroup.add(new FontSizeIncreaseAction(readUI));
actionGroup.add(new FontSizeDecreaseAction(readUI));
actionGroup.add(Separator.getInstance());
actionGroup.add(new NovelSettingAction());
}
return actionGroup;
}
public void updateReadUI() {
contentPanel.remove(0);
initContentPanel();
}
public JPanel getContentPanel() {
return contentPanel;
}
public Book getCurrentReadBook() {
return book;
}
{
// GUI initializer generated by IntelliJ IDEA GUI Designer
// >>> IMPORTANT!! <<<
// DO NOT EDIT OR ADD ANY CODE HERE!
$$$setupUI$$$();
}
/**
* Method generated by IntelliJ IDEA GUI Designer
* >>> IMPORTANT!! <<<
* DO NOT edit this method OR call it in your code!
*
* @noinspection ALL
*/
private void $$$setupUI$$$() {
contentPanel = new JPanel();
contentPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
}
/**
* @noinspection ALL
*/
public JComponent $$$getRootComponent$$$() {
return contentPanel;
}
}
package com.liang.novel.plugin.ui.model;
import com.liang.novel.plugin.pojo.Book;
import javax.swing.table.AbstractTableModel;
import java.util.List;
/**
* 表格模型类
*/
public class BookTableModel extends AbstractTableModel {
private List<Book> dataList;
public BookTableModel(List<Book> dataList) {
this.dataList = dataList;
}
@Override
public int getRowCount() {
return dataList.size();
}
@Override
public int getColumnCount() {
return 2; // 两列:书名和路径
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
Book data = dataList.get(rowIndex);
return switch (columnIndex) {
case 0 -> data.getBookName();
case 1 -> data.getUrl();
default -> null;
};
}
@Override
public String getColumnName(int column) {
return switch (column) {
case 0 -> "书名";
case 1 -> "路径";
default -> null;
};
}
}
package com.liang.novel.plugin.ui.renderer;
import com.liang.novel.plugin.pojo.Chapter;
import javax.swing.*;
import java.awt.*;
/**
* 重新绘制的时候会调用getListCellRendererComponent
* 当列表获得焦点、失去焦点、鼠标移到列表上方等与外观有关的事件发生时,可能会引发重新绘制。
*/
public class IconListCellRenderer extends DefaultListCellRenderer {
private Icon icon;
public IconListCellRenderer(Icon icon) {
this.icon = icon;
}
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof Chapter chapter) {
if (chapter.getBookmark()) {
//设置文本和图标
label.setIcon(icon);
//可选,将图标放在文本右边
label.setHorizontalTextPosition(SwingConstants.RIGHT);
}
}
return label;
}
}
package com.liang.novel.plugin.utils;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.project.Project;
import org.apache.commons.lang3.StringUtils;
public class NotificationUtil {
public static void notify(String groupId, String content, Project project) {
notify(groupId, null, content, project);
}
public static void notify(String groupId, String title, String content, Project project) {
notify(groupId, title, content, NotificationType.INFORMATION, project);
}
public static void notify(String groupId, String title, String content, NotificationType notificationType, Project project) {
Notification notification = null;
if (StringUtils.isNotEmpty(title)) {
notification = new Notification(groupId, title, content, notificationType);
} else {
notification = new Notification(groupId, content, notificationType);
}
notification.notify(project);
}
}
package com.liang.novel.plugin.utils;
import com.intellij.openapi.ui.Messages;
import com.liang.novel.plugin.pojo.Chapter;
import org.apache.commons.lang3.StringUtils;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NovelUtil {
private static final String CHAPTER_TITLE_REG = "^(正文)?第[零〇一二三四五六七八九十百千万a-zA-Z0-9]{1,7}[章节卷集部篇回]\\s*(.+?)(\\r\\n|\\r|\\n|$)";
private static final Pattern CHAPTER_TITLE_PATTERN = Pattern.compile(CHAPTER_TITLE_REG);
public static List<Chapter> parseNovelChapters(String url) {
if (StringUtils.isEmpty(url)) {
return Collections.emptyList();
}
List<Chapter> chapterList = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(url), StandardCharsets.UTF_8))) {
String line;
int index = 0;
int currentLineNumber = 1;
while ((line = reader.readLine()) != null) {
Chapter chapter = parseNovelChapter(line, index, currentLineNumber);
if (chapter != null) {
chapterList.add(chapter);
index++;
}
currentLineNumber++;
}
} catch (IOException e) {
Messages.showErrorDialog("该路径下文件不存在:" + url, "出错了");
}
return chapterList;
}
private static Chapter parseNovelChapter(String line, int index, int currentLineNumber) {
Matcher matcher = CHAPTER_TITLE_PATTERN.matcher(line);
if (matcher.find()) {
String title = matcher.group();
Chapter chapter = new Chapter();
chapter.setIndex(index);
chapter.setTitle(title.trim());
chapter.setRow(currentLineNumber);
return chapter;
}
return null;
}
public static String getChapterContent(String url, Integer chapterRow) {
if (url.isEmpty()) {
return "";
}
if (chapterRow == null) {
chapterRow = 1;
}
StringBuilder contentBuilder = new StringBuilder();
String indent = " "; //四个空格作为缩进
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(url), StandardCharsets.UTF_8));) {
String line;
int currentLineNumber = 1;
while ((line = reader.readLine()) != null) {
if (currentLineNumber > chapterRow) {
Matcher matcher = CHAPTER_TITLE_PATTERN.matcher(line);
if (matcher.find()) {
return contentBuilder.toString();
}
if (StringUtils.isNotBlank(line)) {
line = indent + line.trim();
contentBuilder.append(line).append("\n");
}
}
currentLineNumber++;
}
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
}
<idea-plugin>
<id>com.liang.novel.lnovel-intellij-plugin</id>
<name>Lnovel</name>
<vendor email="git@liangyuhang.com" url="https://www.wjlyh.com">liangyuhang</vendor>
<description><![CDATA[
暂无
]]></description>
<depends>com.intellij.modules.platform</depends>
<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="com.liang.novel.plugin.PluginStarter"/>
<notificationGroup id="MyNotificationGroup" displayType="BALLOON"/>
<applicationConfigurable id="NovelSetting" instance="com.liang.novel.plugin.settings.NovelSetting" displayName="小说设置"/>
<applicationService serviceImplementation="com.liang.novel.plugin.state.NovelState"/>
<toolWindow id="lnovel"
factoryClass="com.liang.novel.plugin.factory.ReadToolWindow"
anchor="bottom"
icon="/icons/book_13.svg"/>
</extensions>
</idea-plugin>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="36px" height="36px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M919.579055 285.271858h-45.680644v28.668789h45.680644c7.722143 0 10.633049 8.355928 10.633049 16.078071v378.207881c0 38.616859-28.046267 67.48633-66.663125 67.486329h-280.155503c-23.172573 0-45.330475 21.402275-45.330475 44.574848v4.571648h-56.313693v-4.571648c0-23.172573-15.259987-44.574848-38.432559-44.574848H163.16227c-38.616859 0-73.561041-28.869471-73.561041-67.486329V330.018718c0-7.722143 9.808821-16.078071 17.530964-16.078071h38.782729v-28.668789H107.132193c-23.172573 0-45.175868 21.573264-45.175868 44.74686v378.207881c0 54.068312 47.138656 95.131233 101.205945 95.131233h280.154479c7.722143 0 10.787656 9.207801 10.787655 16.929944v7.003375c0 15.45043 15.784216 25.212152 31.23567 25.212153h56.0311c15.45043 0 24.337754-9.761723 24.337755-25.212153v-7.003375c0-7.722143 9.963428-16.929944 17.685571-16.929944H863.547955c54.068312 0 94.30803-41.063945 94.308029-95.131233V330.018718c0-23.172573-15.104356-44.74686-38.276929-44.74686z" fill="#FF7415" /><path d="M271.852816 285.271858h167.917194v28.668789h-167.917194zM271.852816 348.752748h167.917194v27.644903h-167.917194zM271.852816 411.209753h167.917194v28.668789h-167.917194zM580.042299 285.271858h167.917194v28.668789h-167.917194zM580.042299 348.752748h167.917194v27.644903h-167.917194zM580.042299 411.209753h167.917194v28.668789h-167.917194z" fill="#FF7415" /><path d="M842.536804 229.98205h-17.784888v-5.021134c0-30.894716-21.358248-58.459757-52.252964-58.459756H604.405651c-38.92812 0-72.900635 21.329579-91.050027 52.867295-18.149391-31.537716-52.121906-52.867295-91.050026-52.867295H254.21332c-30.894716 0-59.150879 27.564017-59.150879 58.459756v5.021134h-10.886973c-27.030573 0-52.593917 27.118627-52.593917 58.013343v378.20788c0 38.616859 33.415521 67.530357 70.10338 67.530357h255.640617c23.172573 0 42.023325 20.477706 42.023325 41.979298h28.01555c0-21.501592 18.849729-41.979298 42.023326-41.979298h255.640616c36.687859 0 63.205465-28.913498 63.205465-67.530357V287.995393c-0.002048-30.894716-18.667477-58.013343-45.697026-58.013343z m-238.131153-35.835986h168.092277c15.45043 0 24.60806 15.363399 24.60806 30.814852v399.219032c0 15.45043-9.15763 25.595085-24.60806 25.595085H604.405651c-30.453421 0-59.175452 15.523125-80.677045 36.292639V273.987617c0-42.481002 38.195018-79.841554 80.677045-79.841553zM222.706321 224.960916c0-15.45043 16.055546-30.814853 31.505975-30.814852H422.304574c42.481002 0 73.779129 37.359528 73.779129 79.841553v411.914186c-14.334395-20.717296-43.369735-36.12677-73.779129-36.12677H254.21332c-15.45043 0-31.505975-10.143632-31.505975-25.595085V224.960916z m-21.022414 481.12781c-21.237429 0-42.458477-16.71288-42.458476-39.885453V287.995393c0-15.184219 13.560337-30.368439 24.949013-30.368439h10.886973v366.552994c0 30.894716 28.256163 53.239989 59.150879 53.239989H422.304574c26.733646 0 50.311677 14.026205 64.132081 34.760907-8.879134-4.081207-18.718672-6.092118-29.113155-6.092118H201.683907z m657.879086-39.885453c0 23.172573-13.299246 39.885453-34.536676 39.885453H569.385701c-10.394484 0-20.234022 2.011935-29.114179 6.092118 13.820404-20.734702 37.399459-34.760907 64.133105-34.760907h168.092277c30.894716 0 52.252964-22.345273 52.252964-53.239989V257.626954h17.784888c11.387653 0 17.027213 15.184219 17.027213 30.368439v378.20788z" fill="#262626" /><path d="M439.77001 544.642488c0 7.736478-6.271298 14.007775-14.007775 14.007775H285.860592c-7.736478 0-14.007775-6.271298-14.007776-14.007775v-55.943046c0-7.736478 6.271298-14.007775 14.007776-14.007775h139.901643c7.736478 0 14.007775 6.271298 14.007775 14.007775v55.943046zM747.959493 544.642488c0 7.736478-6.271298 14.007775-14.007776 14.007775H594.050074c-7.736478 0-14.007775-6.271298-14.007775-14.007775v-55.943046c0-7.736478 6.271298-14.007775 14.007775-14.007775h139.901643c7.736478 0 14.007775 6.271298 14.007776 14.007775v55.943046z" fill="#E0E0E0" /></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1710301143565" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17007" xmlns:xlink="http://www.w3.org/1999/xlink" width="13.0126953125" height="13"><path d="M461.801387 37.342692A51.106067 51.106067 0 0 0 410.69532 0.035263H308.483186a51.106067 51.106067 0 0 0-51.106067 37.307429l-255.530335 919.909207a51.106067 51.106067 0 0 0 102.212134 27.597276L219.55863 562.202001h280.061247L615.119589 984.849175a51.106067 51.106067 0 0 0 51.106067 37.307429 51.106067 51.106067 0 0 0 13.798638 0 51.106067 51.106067 0 0 0 35.774247-62.860462zM248.178027 459.989867l99.14577-357.74247h24.530912l99.145771 357.74247zM972.862058 153.353464h-306.636402a51.106067 51.106067 0 0 0 0 102.212134h306.636402a51.106067 51.106067 0 0 0 0-102.212134z" fill="#1296db" p-id="17008"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1710299830710" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11199" xmlns:xlink="http://www.w3.org/1999/xlink" width="13.0126953125" height="13"><path d="M461.801387 37.342692A51.106067 51.106067 0 0 0 410.69532 0.035263H308.483186a51.106067 51.106067 0 0 0-51.106067 37.307429l-255.530335 919.909207a51.106067 51.106067 0 0 0 102.212134 27.597276L219.55863 562.202001h280.061247L615.119589 984.849175a51.106067 51.106067 0 0 0 51.106067 37.307429 51.106067 51.106067 0 0 0 13.798638 0 51.106067 51.106067 0 0 0 35.774247-62.860462zM248.178027 459.989867l99.14577-357.74247h24.530912l99.145771 357.74247zM972.862058 153.353464h-102.212134V51.14133a51.106067 51.106067 0 0 0-102.212134 0v102.212134h-102.212134a51.106067 51.106067 0 0 0 0 102.212134h102.212134v102.212135a51.106067 51.106067 0 0 0 102.212134 0V255.565598h102.212134a51.106067 51.106067 0 0 0 0-102.212134z" fill="#1296db" p-id="11200"></path></svg>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="18px" height="18px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M919.579055 285.271858h-45.680644v28.668789h45.680644c7.722143 0 10.633049 8.355928 10.633049 16.078071v378.207881c0 38.616859-28.046267 67.48633-66.663125 67.486329h-280.155503c-23.172573 0-45.330475 21.402275-45.330475 44.574848v4.571648h-56.313693v-4.571648c0-23.172573-15.259987-44.574848-38.432559-44.574848H163.16227c-38.616859 0-73.561041-28.869471-73.561041-67.486329V330.018718c0-7.722143 9.808821-16.078071 17.530964-16.078071h38.782729v-28.668789H107.132193c-23.172573 0-45.175868 21.573264-45.175868 44.74686v378.207881c0 54.068312 47.138656 95.131233 101.205945 95.131233h280.154479c7.722143 0 10.787656 9.207801 10.787655 16.929944v7.003375c0 15.45043 15.784216 25.212152 31.23567 25.212153h56.0311c15.45043 0 24.337754-9.761723 24.337755-25.212153v-7.003375c0-7.722143 9.963428-16.929944 17.685571-16.929944H863.547955c54.068312 0 94.30803-41.063945 94.308029-95.131233V330.018718c0-23.172573-15.104356-44.74686-38.276929-44.74686z" fill="#FF7415" /><path d="M271.852816 285.271858h167.917194v28.668789h-167.917194zM271.852816 348.752748h167.917194v27.644903h-167.917194zM271.852816 411.209753h167.917194v28.668789h-167.917194zM580.042299 285.271858h167.917194v28.668789h-167.917194zM580.042299 348.752748h167.917194v27.644903h-167.917194zM580.042299 411.209753h167.917194v28.668789h-167.917194z" fill="#FF7415" /><path d="M842.536804 229.98205h-17.784888v-5.021134c0-30.894716-21.358248-58.459757-52.252964-58.459756H604.405651c-38.92812 0-72.900635 21.329579-91.050027 52.867295-18.149391-31.537716-52.121906-52.867295-91.050026-52.867295H254.21332c-30.894716 0-59.150879 27.564017-59.150879 58.459756v5.021134h-10.886973c-27.030573 0-52.593917 27.118627-52.593917 58.013343v378.20788c0 38.616859 33.415521 67.530357 70.10338 67.530357h255.640617c23.172573 0 42.023325 20.477706 42.023325 41.979298h28.01555c0-21.501592 18.849729-41.979298 42.023326-41.979298h255.640616c36.687859 0 63.205465-28.913498 63.205465-67.530357V287.995393c-0.002048-30.894716-18.667477-58.013343-45.697026-58.013343z m-238.131153-35.835986h168.092277c15.45043 0 24.60806 15.363399 24.60806 30.814852v399.219032c0 15.45043-9.15763 25.595085-24.60806 25.595085H604.405651c-30.453421 0-59.175452 15.523125-80.677045 36.292639V273.987617c0-42.481002 38.195018-79.841554 80.677045-79.841553zM222.706321 224.960916c0-15.45043 16.055546-30.814853 31.505975-30.814852H422.304574c42.481002 0 73.779129 37.359528 73.779129 79.841553v411.914186c-14.334395-20.717296-43.369735-36.12677-73.779129-36.12677H254.21332c-15.45043 0-31.505975-10.143632-31.505975-25.595085V224.960916z m-21.022414 481.12781c-21.237429 0-42.458477-16.71288-42.458476-39.885453V287.995393c0-15.184219 13.560337-30.368439 24.949013-30.368439h10.886973v366.552994c0 30.894716 28.256163 53.239989 59.150879 53.239989H422.304574c26.733646 0 50.311677 14.026205 64.132081 34.760907-8.879134-4.081207-18.718672-6.092118-29.113155-6.092118H201.683907z m657.879086-39.885453c0 23.172573-13.299246 39.885453-34.536676 39.885453H569.385701c-10.394484 0-20.234022 2.011935-29.114179 6.092118 13.820404-20.734702 37.399459-34.760907 64.133105-34.760907h168.092277c30.894716 0 52.252964-22.345273 52.252964-53.239989V257.626954h17.784888c11.387653 0 17.027213 15.184219 17.027213 30.368439v378.20788z" fill="#262626" /><path d="M439.77001 544.642488c0 7.736478-6.271298 14.007775-14.007775 14.007775H285.860592c-7.736478 0-14.007775-6.271298-14.007776-14.007775v-55.943046c0-7.736478 6.271298-14.007775 14.007776-14.007775h139.901643c7.736478 0 14.007775 6.271298 14.007775 14.007775v55.943046zM747.959493 544.642488c0 7.736478-6.271298 14.007775-14.007776 14.007775H594.050074c-7.736478 0-14.007775-6.271298-14.007775-14.007775v-55.943046c0-7.736478 6.271298-14.007775 14.007775-14.007775h139.901643c7.736478 0 14.007775 6.271298 14.007776 14.007775v55.943046z" fill="#E0E0E0" /></svg>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="13px" height="13px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M919.579055 285.271858h-45.680644v28.668789h45.680644c7.722143 0 10.633049 8.355928 10.633049 16.078071v378.207881c0 38.616859-28.046267 67.48633-66.663125 67.486329h-280.155503c-23.172573 0-45.330475 21.402275-45.330475 44.574848v4.571648h-56.313693v-4.571648c0-23.172573-15.259987-44.574848-38.432559-44.574848H163.16227c-38.616859 0-73.561041-28.869471-73.561041-67.486329V330.018718c0-7.722143 9.808821-16.078071 17.530964-16.078071h38.782729v-28.668789H107.132193c-23.172573 0-45.175868 21.573264-45.175868 44.74686v378.207881c0 54.068312 47.138656 95.131233 101.205945 95.131233h280.154479c7.722143 0 10.787656 9.207801 10.787655 16.929944v7.003375c0 15.45043 15.784216 25.212152 31.23567 25.212153h56.0311c15.45043 0 24.337754-9.761723 24.337755-25.212153v-7.003375c0-7.722143 9.963428-16.929944 17.685571-16.929944H863.547955c54.068312 0 94.30803-41.063945 94.308029-95.131233V330.018718c0-23.172573-15.104356-44.74686-38.276929-44.74686z" fill="#FF7415" /><path d="M271.852816 285.271858h167.917194v28.668789h-167.917194zM271.852816 348.752748h167.917194v27.644903h-167.917194zM271.852816 411.209753h167.917194v28.668789h-167.917194zM580.042299 285.271858h167.917194v28.668789h-167.917194zM580.042299 348.752748h167.917194v27.644903h-167.917194zM580.042299 411.209753h167.917194v28.668789h-167.917194z" fill="#FF7415" /><path d="M842.536804 229.98205h-17.784888v-5.021134c0-30.894716-21.358248-58.459757-52.252964-58.459756H604.405651c-38.92812 0-72.900635 21.329579-91.050027 52.867295-18.149391-31.537716-52.121906-52.867295-91.050026-52.867295H254.21332c-30.894716 0-59.150879 27.564017-59.150879 58.459756v5.021134h-10.886973c-27.030573 0-52.593917 27.118627-52.593917 58.013343v378.20788c0 38.616859 33.415521 67.530357 70.10338 67.530357h255.640617c23.172573 0 42.023325 20.477706 42.023325 41.979298h28.01555c0-21.501592 18.849729-41.979298 42.023326-41.979298h255.640616c36.687859 0 63.205465-28.913498 63.205465-67.530357V287.995393c-0.002048-30.894716-18.667477-58.013343-45.697026-58.013343z m-238.131153-35.835986h168.092277c15.45043 0 24.60806 15.363399 24.60806 30.814852v399.219032c0 15.45043-9.15763 25.595085-24.60806 25.595085H604.405651c-30.453421 0-59.175452 15.523125-80.677045 36.292639V273.987617c0-42.481002 38.195018-79.841554 80.677045-79.841553zM222.706321 224.960916c0-15.45043 16.055546-30.814853 31.505975-30.814852H422.304574c42.481002 0 73.779129 37.359528 73.779129 79.841553v411.914186c-14.334395-20.717296-43.369735-36.12677-73.779129-36.12677H254.21332c-15.45043 0-31.505975-10.143632-31.505975-25.595085V224.960916z m-21.022414 481.12781c-21.237429 0-42.458477-16.71288-42.458476-39.885453V287.995393c0-15.184219 13.560337-30.368439 24.949013-30.368439h10.886973v366.552994c0 30.894716 28.256163 53.239989 59.150879 53.239989H422.304574c26.733646 0 50.311677 14.026205 64.132081 34.760907-8.879134-4.081207-18.718672-6.092118-29.113155-6.092118H201.683907z m657.879086-39.885453c0 23.172573-13.299246 39.885453-34.536676 39.885453H569.385701c-10.394484 0-20.234022 2.011935-29.114179 6.092118 13.820404-20.734702 37.399459-34.760907 64.133105-34.760907h168.092277c30.894716 0 52.252964-22.345273 52.252964-53.239989V257.626954h17.784888c11.387653 0 17.027213 15.184219 17.027213 30.368439v378.20788z" fill="#262626" /><path d="M439.77001 544.642488c0 7.736478-6.271298 14.007775-14.007775 14.007775H285.860592c-7.736478 0-14.007775-6.271298-14.007776-14.007775v-55.943046c0-7.736478 6.271298-14.007775 14.007776-14.007775h139.901643c7.736478 0 14.007775 6.271298 14.007775 14.007775v55.943046zM747.959493 544.642488c0 7.736478-6.271298 14.007775-14.007776 14.007775H594.050074c-7.736478 0-14.007775-6.271298-14.007775-14.007775v-55.943046c0-7.736478 6.271298-14.007775 14.007775-14.007775h139.901643c7.736478 0 14.007775 6.271298 14.007776 14.007775v55.943046z" fill="#E0E0E0" /></svg>
\ No newline at end of file
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3.5C3 2.39543 3.89543 1.5 5 1.5H10.9989C12.1034 1.5 12.9989 2.39543 12.9989 3.5V14.9184C12.9989 15.3379 12.5134 15.5709 12.1861 15.3085L7.99943 11.9524L3.81273 15.3085C3.48543 15.5709 3 15.3379 3 14.9184V3.5Z" fill="#FFAF0F"/>
</svg>
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 10C10 9.44772 10.4477 9 11 9H13.9994C14.5517 9 14.9994 9.44772 14.9994 10V15.7092C14.9994 15.9189 14.7567 16.0354 14.5931 15.9042L12.4997 14.2262L10.4064 15.9042C10.2427 16.0354 10 15.9189 10 15.7092V10Z" fill="#FFAF0F"/>
<rect x="2" y="12" width="6" height="1" rx="0.5" fill="#6C707E"/>
<rect x="2" y="6" width="12" height="1" rx="0.5" fill="#6C707E"/>
<rect x="2" y="9" width="6" height="1" rx="0.5" fill="#6C707E"/>
<rect x="2" y="3" width="12" height="1" rx="0.5" fill="#6C707E"/>
</svg>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment