feat: bootstrap TimiHosts desktop app
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,3 +1,9 @@
|
||||
/.claude
|
||||
/CLAUDE.md
|
||||
/AGENTS.md
|
||||
/logs
|
||||
/TimiHosts.yaml
|
||||
|
||||
# ---> Java
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
14
.idea/compiler.xml
generated
Normal file
14
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="timi-hosts" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/encodings.xml
generated
Normal file
7
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
20
.idea/jarRepositories.xml
generated
Normal file
20
.idea/jarRepositories.xml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/misc.xml
generated
Normal file
12
.idea/misc.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="temurin-23" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
14
README.md
14
README.md
@ -1,3 +1,13 @@
|
||||
# TimiHost
|
||||
# TimiHosts
|
||||
|
||||
host 管理
|
||||
Hosts 管理
|
||||
|
||||
## 运行
|
||||
|
||||
1. 先在 `E:\IDEAProject\timi-inject` 执行 `mvn -DskipTests package` 生成本地 jar
|
||||
2. 在本项目执行 `mvn javafx:run`
|
||||
|
||||
## 说明
|
||||
|
||||
- 本项目不使用 FXML,所有界面由代码构建
|
||||
- 依赖 `E:\IDEAProject\timi-inject\target\timi-inject-0.0.2.jar`
|
||||
|
||||
58
pom.xml
Normal file
58
pom.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.imyeyu.hosts</groupId>
|
||||
<artifactId>timi-hosts</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<javafx.version>21.0.2</javafx.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.inject</groupId>
|
||||
<artifactId>timi-inject</artifactId>
|
||||
<version>0.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.fx.ui</groupId>
|
||||
<artifactId>timi-fx-ui</artifactId>
|
||||
<version>0.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.fx.icon</groupId>
|
||||
<artifactId>timi-fx-icon</artifactId>
|
||||
<version>0.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.36</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.5.24</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>0.0.8</version>
|
||||
<configuration>
|
||||
<mainClass>com.imyeyu.hosts.Launcher</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
41
src/main/java/com/imyeyu/hosts/Launcher.java
Normal file
41
src/main/java/com/imyeyu/hosts/Launcher.java
Normal file
@ -0,0 +1,41 @@
|
||||
package com.imyeyu.hosts;
|
||||
|
||||
import com.imyeyu.config.ConfigLoader;
|
||||
import com.imyeyu.fx.config.BindingsConfig;
|
||||
import com.imyeyu.hosts.config.Config;
|
||||
import com.imyeyu.hosts.ui.FXApplication;
|
||||
import com.imyeyu.inject.annotation.Bean;
|
||||
import com.imyeyu.inject.annotation.Configuration;
|
||||
import com.imyeyu.java.bean.Language;
|
||||
import com.imyeyu.lang.mapper.ResourcesLanguageMap;
|
||||
import com.imyeyu.lang.multi.ResourcesMultilingual;
|
||||
import javafx.application.Application;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2026-01-13 10:40
|
||||
*/
|
||||
@Configuration
|
||||
public final class Launcher {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Application.launch(FXApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Config config() {
|
||||
ConfigLoader<Config> configLoader = new ConfigLoader<>("TimiHosts.yaml", Config.class);
|
||||
BindingsConfig.addAllFXConverter(configLoader);
|
||||
return configLoader.load();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourcesMultilingual multilingual() {
|
||||
ResourcesMultilingual multilingual = new ResourcesMultilingual();
|
||||
multilingual.setActivated(Language.Enum.zh_CN);
|
||||
ResourcesLanguageMap map = new ResourcesLanguageMap(Language.Enum.zh_CN);
|
||||
map.load("lang/%s.lang");
|
||||
multilingual.add(Language.Enum.zh_CN, map);
|
||||
return multilingual;
|
||||
}
|
||||
}
|
||||
15
src/main/java/com/imyeyu/hosts/config/Config.java
Normal file
15
src/main/java/com/imyeyu/hosts/config/Config.java
Normal file
@ -0,0 +1,15 @@
|
||||
package com.imyeyu.hosts.config;
|
||||
|
||||
import com.imyeyu.java.bean.Language;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2026-01-13 10:53
|
||||
*/
|
||||
@Data
|
||||
public class Config {
|
||||
|
||||
private ObjectProperty<Language.Enum> lang;
|
||||
}
|
||||
118
src/main/java/com/imyeyu/hosts/model/Host.java
Normal file
118
src/main/java/com/imyeyu/hosts/model/Host.java
Normal file
@ -0,0 +1,118 @@
|
||||
package com.imyeyu.hosts.model;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* 映射对象
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-04-25 17:57
|
||||
*/
|
||||
public class Host {
|
||||
|
||||
private final BooleanProperty activated;
|
||||
private final StringProperty ip;
|
||||
private final StringProperty host;
|
||||
private final StringProperty description;
|
||||
|
||||
public Host() {
|
||||
this.activated = new SimpleBooleanProperty(false);
|
||||
this.ip = new SimpleStringProperty("");
|
||||
this.host = new SimpleStringProperty("");
|
||||
this.description = new SimpleStringProperty("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param activated 激活状态
|
||||
* @param ip IP 地址
|
||||
* @param host 域名
|
||||
* @param description 备注
|
||||
*/
|
||||
public Host(boolean activated, String ip, String host, String description) {
|
||||
this.activated = new SimpleBooleanProperty(activated);
|
||||
this.ip = new SimpleStringProperty(ip);
|
||||
this.host = new SimpleStringProperty(host);
|
||||
this.description = new SimpleStringProperty(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置激活状态
|
||||
*
|
||||
* @param activated true 为激活
|
||||
*/
|
||||
public void setActivated(boolean activated) {
|
||||
this.activated.set(activated);
|
||||
}
|
||||
|
||||
/** @return true 为激活 */
|
||||
public boolean isActivated() {
|
||||
return activated.get();
|
||||
}
|
||||
|
||||
/** @return 激活监听 */
|
||||
public BooleanProperty activatedProperty() {
|
||||
return activated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 IP
|
||||
*
|
||||
* @param ip IP
|
||||
*/
|
||||
public void setIP(String ip) {
|
||||
this.ip.set(ip);
|
||||
}
|
||||
|
||||
/** @return IP */
|
||||
public String getIP() {
|
||||
return ip.get();
|
||||
}
|
||||
|
||||
/** @return IP 监听 */
|
||||
public StringProperty ipProperty() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置域名
|
||||
*
|
||||
* @param host 域名
|
||||
*/
|
||||
public void setHost(String host) {
|
||||
this.host.set(host);
|
||||
}
|
||||
|
||||
/** @return 域名 */
|
||||
public String getHost() {
|
||||
return host.get();
|
||||
}
|
||||
|
||||
/** @return 域名监听 */
|
||||
public StringProperty hostProperty() {
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置备注
|
||||
*
|
||||
* @param description 备注
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description.set(description);
|
||||
}
|
||||
|
||||
/** @return 备注 */
|
||||
public String getDescription() {
|
||||
return description.get();
|
||||
}
|
||||
|
||||
/** @return 备注监听 */
|
||||
public StringProperty descriptionProperty() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
72
src/main/java/com/imyeyu/hosts/service/HostService.java
Normal file
72
src/main/java/com/imyeyu/hosts/service/HostService.java
Normal file
@ -0,0 +1,72 @@
|
||||
package com.imyeyu.hosts.service;
|
||||
|
||||
import com.imyeyu.hosts.model.Host;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
import javafx.collections.ObservableList;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hosts 规则服务
|
||||
*
|
||||
* @author Timi
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class HostService {
|
||||
|
||||
private final HostStore hostStore;
|
||||
private final HostsFileReader reader;
|
||||
private final HostsFileWriter writer;
|
||||
|
||||
/**
|
||||
* 构造函数注入
|
||||
*
|
||||
* @param hostStore 数据存储
|
||||
* @param reader 文件读取器
|
||||
* @param writer 文件写入器
|
||||
*/
|
||||
public HostService(HostStore hostStore, HostsFileReader reader, HostsFileWriter writer) {
|
||||
this.hostStore = hostStore;
|
||||
this.reader = reader;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则列表
|
||||
*
|
||||
* @return 规则列表
|
||||
*/
|
||||
public ObservableList<Host> getEntries() {
|
||||
return hostStore.getEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新数据 - 从系统 hosts 文件中重新加载
|
||||
*
|
||||
* @return 加载的 hosts 映射数量
|
||||
* @throws IOException 读取文件失败时抛出
|
||||
*/
|
||||
public int refresh() throws IOException {
|
||||
List<Host> hosts = reader.read();
|
||||
hostStore.getEntries().setAll(hosts);
|
||||
log.info("refreshed, loaded {} hosts mapping", hosts.size());
|
||||
return hosts.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据 - 将当前 hosts 列表保存到系统 hosts 文件
|
||||
*
|
||||
* @return 保存的 hosts 映射数量
|
||||
* @throws IOException 写入文件失败时抛出
|
||||
*/
|
||||
public int save() throws IOException {
|
||||
List<Host> hosts = new ArrayList<>(hostStore.getEntries());
|
||||
writer.write(hosts);
|
||||
log.info("saved, wrote {} hosts mapping", hosts.size());
|
||||
return hosts.size();
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/imyeyu/hosts/service/HostStore.java
Normal file
30
src/main/java/com/imyeyu/hosts/service/HostStore.java
Normal file
@ -0,0 +1,30 @@
|
||||
package com.imyeyu.hosts.service;
|
||||
|
||||
import com.imyeyu.hosts.model.Host;
|
||||
import com.imyeyu.hosts.ui.components.HostTable;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.PostConstruct;
|
||||
import javafx.collections.ObservableList;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Hosts 规则数据存储。
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-13 16:07
|
||||
*/
|
||||
@Component
|
||||
public class HostStore {
|
||||
|
||||
@Inject
|
||||
private HostTable table;
|
||||
|
||||
@Getter
|
||||
private ObservableList<Host> entries;
|
||||
|
||||
@PostConstruct
|
||||
private void postConstruct() {
|
||||
entries = table.getItems();
|
||||
}
|
||||
}
|
||||
59
src/main/java/com/imyeyu/hosts/service/HostsFileReader.java
Normal file
59
src/main/java/com/imyeyu/hosts/service/HostsFileReader.java
Normal file
@ -0,0 +1,59 @@
|
||||
package com.imyeyu.hosts.service;
|
||||
|
||||
import com.imyeyu.hosts.model.Host;
|
||||
import com.imyeyu.hosts.util.HostsParser;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hosts 文件读取器
|
||||
*
|
||||
* <p>负责从系统 hosts 文件中读取并解析 hosts 映射</p>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-13 17:20
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HostsFileReader {
|
||||
|
||||
/**
|
||||
* 读取并解析系统 hosts 文件
|
||||
*
|
||||
* @return Host 对象列表
|
||||
* @throws IOException 文件读取失败时抛出
|
||||
*/
|
||||
public List<Host> read() throws IOException {
|
||||
Path hostsPath = getHostsFilePath();
|
||||
log.info("read hosts file: {}", hostsPath);
|
||||
|
||||
List<String> lines = Files.readAllLines(hostsPath, StandardCharsets.UTF_8);
|
||||
List<Host> hosts = HostsParser.parse(lines);
|
||||
|
||||
log.info("read {} hosts mapping", hosts.size());
|
||||
return hosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统 hosts 文件路径
|
||||
*
|
||||
* @return hosts 文件路径
|
||||
*/
|
||||
private Path getHostsFilePath() {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("win")) {
|
||||
// Windows 系统
|
||||
return Paths.get(System.getenv("SystemRoot"), "System32", "drivers", "etc", "hosts");
|
||||
} else {
|
||||
// Linux/Mac 系统
|
||||
return Paths.get("/etc/hosts");
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/main/java/com/imyeyu/hosts/service/HostsFileWriter.java
Normal file
67
src/main/java/com/imyeyu/hosts/service/HostsFileWriter.java
Normal file
@ -0,0 +1,67 @@
|
||||
package com.imyeyu.hosts.service;
|
||||
|
||||
import com.imyeyu.hosts.model.Host;
|
||||
import com.imyeyu.hosts.util.HostsParser;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hosts 文件写入器
|
||||
*
|
||||
* <p>负责将 hosts 映射写入到系统 hosts 文件</p>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-01-13
|
||||
*/
|
||||
@Component
|
||||
public class HostsFileWriter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HostsFileWriter.class);
|
||||
private static final DateTimeFormatter BACKUP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
/**
|
||||
* 将 Host 列表写入系统 hosts 文件
|
||||
*
|
||||
* @param hosts Host 对象列表
|
||||
* @throws IOException 文件写入失败时抛出
|
||||
*/
|
||||
public void write(List<Host> hosts) throws IOException {
|
||||
Path hostsPath = getHostsFilePath();
|
||||
// 格式化为文本行
|
||||
List<String> lines = HostsParser.format(hosts);
|
||||
// 写入文件
|
||||
try {
|
||||
Files.write(hostsPath, lines, StandardCharsets.UTF_8);
|
||||
log.info("wrote {} hosts mapping", hosts.size());
|
||||
} catch (AccessDeniedException e) {
|
||||
log.error("wrote hosts fail for access denied", e);
|
||||
throw new IOException("权限不足,请以管理员身份运行本程序", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统 hosts 文件路径
|
||||
*
|
||||
* @return hosts 文件路径
|
||||
*/
|
||||
private Path getHostsFilePath() {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("win")) {
|
||||
// Windows 系统
|
||||
return Paths.get(System.getenv("SystemRoot"), "System32", "drivers", "etc", "hosts");
|
||||
} else {
|
||||
// Linux/Mac 系统
|
||||
return Paths.get("/etc/hosts");
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/main/java/com/imyeyu/hosts/ui/FXApplication.java
Normal file
34
src/main/java/com/imyeyu/hosts/ui/FXApplication.java
Normal file
@ -0,0 +1,34 @@
|
||||
package com.imyeyu.hosts.ui;
|
||||
|
||||
import com.imyeyu.fx.inject.FXTimiInject;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.hosts.ui.ctrl.Main;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.TimiInjectApplication;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* TimiHosts 应用入口。
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-13 16:06
|
||||
*/
|
||||
@TimiInjectApplication("com.imyeyu.hosts")
|
||||
public class FXApplication extends Application implements TimiFXUI {
|
||||
|
||||
@Inject
|
||||
private Main main;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
FXTimiInject.run(FXApplication.class, this, stage);
|
||||
|
||||
Scene scene = new Scene(main, 900, 560);
|
||||
scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT, "style.css");
|
||||
stage.setTitle("TimiHosts");
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
}
|
||||
}
|
||||
260
src/main/java/com/imyeyu/hosts/ui/components/HostTable.java
Normal file
260
src/main/java/com/imyeyu/hosts/ui/components/HostTable.java
Normal file
@ -0,0 +1,260 @@
|
||||
package com.imyeyu.hosts.ui.components;
|
||||
|
||||
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.ui.components.IconButton;
|
||||
import com.imyeyu.hosts.model.Host;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import com.imyeyu.inject.annotation.PostConstruct;
|
||||
import com.imyeyu.lang.multi.ResourcesMultilingual;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-04-25 18:04
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HostTable extends TableView<Host> implements TimiFXUI {
|
||||
|
||||
private final ResourcesMultilingual multilingual;
|
||||
|
||||
@PostConstruct
|
||||
private void postConstruct() {
|
||||
// 选择
|
||||
TableColumn<Host, Boolean> activated = new TableColumn<>(multilingual.text("main.activated"));
|
||||
activated.setCellValueFactory(new PropertyValueFactory<>("activated"));
|
||||
activated.setPrefWidth(60);
|
||||
activated.setSortable(false);
|
||||
activated.setCellFactory(cell -> new TableCell<>() {
|
||||
|
||||
private final CheckBox checkBox;
|
||||
private BooleanProperty nowBind; // 当前绑定
|
||||
|
||||
{
|
||||
checkBox = new CheckBox();
|
||||
checkBox.focusedProperty().addListener((obs, o, isFocused) -> {
|
||||
if (isFocused) {
|
||||
getSelectionModel().clearAndSelect(getIndex());
|
||||
}
|
||||
});
|
||||
setAlignment(Pos.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Boolean item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
// 动态绑定
|
||||
BooleanProperty thisBind = getTableRow().getItem().activatedProperty();
|
||||
if (nowBind == null) {
|
||||
nowBind = thisBind;
|
||||
checkBox.selectedProperty().bindBidirectional(nowBind);
|
||||
} else {
|
||||
if (nowBind != thisBind) {
|
||||
checkBox.selectedProperty().unbindBidirectional(nowBind);
|
||||
nowBind = thisBind;
|
||||
checkBox.selectedProperty().bindBidirectional(nowBind);
|
||||
}
|
||||
}
|
||||
setGraphic(checkBox);
|
||||
}
|
||||
}
|
||||
});
|
||||
getColumns().add(activated);
|
||||
// IP
|
||||
TableColumn<Host, String> ip = new TableColumn<>("IP");
|
||||
ip.setCellValueFactory(new PropertyValueFactory<>("ip"));
|
||||
ip.setSortable(false);
|
||||
ip.setPrefWidth(140);
|
||||
ip.setCellFactory(cell -> new TableCell<>() {
|
||||
|
||||
private final TextField tf;
|
||||
private StringProperty nowBind; // 当前绑定
|
||||
|
||||
{
|
||||
tf = new TextField();
|
||||
tf.getStyleClass().add(CSS.BORDER_N);
|
||||
tf.focusedProperty().addListener((obs, o, isFocused) -> {
|
||||
if (isFocused) {
|
||||
getSelectionModel().clearAndSelect(getIndex());
|
||||
}
|
||||
});
|
||||
setPadding(Insets.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
// 动态绑定
|
||||
StringProperty thisBind = getTableRow().getItem().ipProperty();
|
||||
if (nowBind == null) {
|
||||
nowBind = thisBind;
|
||||
tf.textProperty().bindBidirectional(nowBind);
|
||||
} else {
|
||||
if (nowBind != thisBind) {
|
||||
tf.textProperty().unbindBidirectional(nowBind);
|
||||
nowBind = thisBind;
|
||||
tf.textProperty().bindBidirectional(nowBind);
|
||||
}
|
||||
}
|
||||
setGraphic(tf);
|
||||
}
|
||||
}
|
||||
});
|
||||
getColumns().add(ip);
|
||||
// 域名列表
|
||||
TableColumn<Host, String> hosts = new TableColumn<>(multilingual.text("main.host"));
|
||||
hosts.setCellValueFactory(new PropertyValueFactory<>("host"));
|
||||
hosts.setSortable(false);
|
||||
hosts.setCellFactory(cell -> new TableCell<>() {
|
||||
|
||||
private final TextField tf;
|
||||
private StringProperty nowBind; // 当前绑定
|
||||
|
||||
{
|
||||
tf = new TextField();
|
||||
tf.getStyleClass().add(CSS.BORDER_N);
|
||||
tf.focusedProperty().addListener((obs, o, isFocused) -> {
|
||||
if (isFocused) {
|
||||
getSelectionModel().clearAndSelect(getIndex());
|
||||
}
|
||||
});
|
||||
setPadding(Insets.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
// 动态绑定
|
||||
StringProperty thisBind = getTableRow().getItem().hostProperty();
|
||||
if (nowBind == null) {
|
||||
nowBind = thisBind;
|
||||
tf.textProperty().bindBidirectional(nowBind);
|
||||
} else {
|
||||
if (nowBind != thisBind) {
|
||||
tf.textProperty().unbindBidirectional(nowBind);
|
||||
nowBind = thisBind;
|
||||
tf.textProperty().bindBidirectional(nowBind);
|
||||
}
|
||||
}
|
||||
setGraphic(tf);
|
||||
}
|
||||
}
|
||||
});
|
||||
getColumns().add(hosts);
|
||||
|
||||
// 说明
|
||||
TableColumn<Host, String> description = new TableColumn<>(multilingual.text("main.description"));
|
||||
description.setCellValueFactory(new PropertyValueFactory<>("description"));
|
||||
description.setSortable(false);
|
||||
description.setPrefWidth(260);
|
||||
description.setCellFactory(cell -> new TableCell<>() {
|
||||
|
||||
private final TextField tf;
|
||||
private StringProperty nowBind; // 当前绑定
|
||||
|
||||
{
|
||||
tf = new TextField();
|
||||
tf.getStyleClass().add(CSS.BORDER_N);
|
||||
tf.focusedProperty().addListener((obs, o, isFocused) -> {
|
||||
if (isFocused) {
|
||||
getSelectionModel().clearAndSelect(getIndex());
|
||||
}
|
||||
});
|
||||
setPadding(Insets.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
// 动态绑定
|
||||
StringProperty thisBind = getTableRow().getItem().descriptionProperty();
|
||||
if (nowBind == null) {
|
||||
nowBind = thisBind;
|
||||
tf.textProperty().bindBidirectional(nowBind);
|
||||
} else {
|
||||
if (nowBind != thisBind) {
|
||||
tf.textProperty().unbindBidirectional(nowBind);
|
||||
nowBind = thisBind;
|
||||
tf.textProperty().bindBidirectional(nowBind);
|
||||
}
|
||||
}
|
||||
setGraphic(tf);
|
||||
}
|
||||
}
|
||||
});
|
||||
getColumns().add(description);
|
||||
|
||||
// 删除列
|
||||
TableColumn<Host, String> delete = new TableColumn<>();
|
||||
delete.setPrefWidth(32);
|
||||
delete.setSortable(false);
|
||||
delete.setResizable(false);
|
||||
delete.setReorderable(false);
|
||||
delete.setCellFactory(cell -> new TableCell<>() {
|
||||
|
||||
private final IconButton remove;
|
||||
|
||||
{
|
||||
remove = new IconButton(TimiFXIcon.fromName("FAIL", Colorful.RED)).autoSize();
|
||||
remove.setOnAction(e -> getItems().remove(getTableRow().getItem()));
|
||||
setAlignment(Pos.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
if (remove.visibleProperty().isBound()) {
|
||||
remove.visibleProperty().unbind();
|
||||
}
|
||||
remove.visibleProperty().bind(getTableRow().hoverProperty());
|
||||
setGraphic(remove);
|
||||
}
|
||||
}
|
||||
});
|
||||
getColumns().add(delete);
|
||||
|
||||
DoubleBinding db = widthProperty().subtract(8);
|
||||
db = db.subtract(activated.widthProperty());
|
||||
db = db.subtract(ip.widthProperty());
|
||||
db = db.subtract(description.widthProperty());
|
||||
db = db.subtract(delete.widthProperty());
|
||||
hosts.prefWidthProperty().bind(db);
|
||||
|
||||
getStyleClass().addAll(CSS.BORDER_N, CSS.EDITABLE_TABLE, "host-table");
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
// 滚动时编辑区主动失去焦点
|
||||
addEventFilter(ScrollEvent.ANY, e -> requestFocus());
|
||||
}
|
||||
}
|
||||
90
src/main/java/com/imyeyu/hosts/ui/ctrl/Main.java
Normal file
90
src/main/java/com/imyeyu/hosts/ui/ctrl/Main.java
Normal file
@ -0,0 +1,90 @@
|
||||
package com.imyeyu.hosts.ui.ctrl;
|
||||
|
||||
import com.imyeyu.hosts.config.Config;
|
||||
import com.imyeyu.hosts.model.Host;
|
||||
import com.imyeyu.hosts.service.HostService;
|
||||
import com.imyeyu.hosts.ui.view.MainView;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import com.imyeyu.inject.annotation.PostConstruct;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
|
||||
/**
|
||||
* 主控制器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-13 10:41
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class Main extends MainView {
|
||||
|
||||
private final Config config;
|
||||
|
||||
private final HostService hostService;
|
||||
|
||||
/**
|
||||
* 初始化控制器
|
||||
*/
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
add.setOnAction(e -> table.getItems().add(new Host()));
|
||||
// 绑定保存按钮事件
|
||||
save.setOnAction(e -> {
|
||||
try {
|
||||
int count = hostService.save();
|
||||
labelTips.info("已保存 %d 条映射".formatted(count));
|
||||
} catch (AccessDeniedException ex) {
|
||||
log.error("写入 hosts 失败: 权限不足", ex);
|
||||
labelTips.error("保存失败: 权限不足,请以管理员身份运行");
|
||||
} catch (IOException ex) {
|
||||
log.error("写入 hosts 失败", ex);
|
||||
labelTips.error("保存失败: %s".formatted(ex.getMessage()));
|
||||
}
|
||||
});
|
||||
// 加载初始数据
|
||||
try {
|
||||
int count = hostService.refresh();
|
||||
labelTips.info("已加载 %d 条映射".formatted(count));
|
||||
} catch (AccessDeniedException e) {
|
||||
log.error("读取 hosts 失败: 权限不足", e);
|
||||
labelTips.error("读取失败: 权限不足,请以管理员身份运行");
|
||||
} catch (NoSuchFileException e) {
|
||||
log.error("读取 hosts 失败: 文件不存在", e);
|
||||
labelTips.error("读取失败: hosts 文件不存在");
|
||||
} catch (IOException e) {
|
||||
log.error("读取 hosts 失败", e);
|
||||
labelTips.error("读取失败: %s".formatted(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void label() {
|
||||
Callback updateStatus = () -> {
|
||||
int total = table.getItems().size();
|
||||
long activated = table.getItems().stream().filter(Host::isActivated).count();
|
||||
labelTotal.setText("总数:%s".formatted(total));
|
||||
labelActivated.setText("激活:%s".formatted(activated));
|
||||
};
|
||||
// 监听列表变化
|
||||
table.getItems().addListener((ListChangeListener<Host>) c -> {
|
||||
while (c.next()) {
|
||||
for (Host host : c.getAddedSubList()) {
|
||||
host.activatedProperty().addListener((obs, o, n) -> updateStatus.handler());
|
||||
}
|
||||
}
|
||||
updateStatus.handler();
|
||||
});
|
||||
for (Host host : table.getItems()) {
|
||||
host.activatedProperty().addListener((obs, o, n) -> updateStatus.handler());
|
||||
}
|
||||
updateStatus.handler();
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/imyeyu/hosts/ui/view/MainView.java
Normal file
49
src/main/java/com/imyeyu/hosts/ui/view/MainView.java
Normal file
@ -0,0 +1,49 @@
|
||||
package com.imyeyu.hosts.ui.view;
|
||||
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.ui.components.LabelTips;
|
||||
import com.imyeyu.hosts.ui.components.HostTable;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.PostConstruct;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
/**
|
||||
* 主界面。
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-13 10:45
|
||||
*/
|
||||
public abstract class MainView extends BorderPane implements TimiFXUI {
|
||||
|
||||
@Inject
|
||||
protected HostTable table;
|
||||
|
||||
protected final Label labelTotal = new Label();
|
||||
protected final Label labelActivated = new Label();
|
||||
protected final LabelTips labelTips = new LabelTips();
|
||||
protected final Button add = new Button("新增");
|
||||
protected final Button save = new Button("保存");
|
||||
|
||||
@PostConstruct
|
||||
private void build() {
|
||||
setCenter(table);
|
||||
setBottom(new BorderPane() {{
|
||||
setBorder(Stroke.TOP);
|
||||
setPadding(new Insets(4, 6, 4, 6));
|
||||
setLeft(new HBox(24) {{
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
getChildren().addAll(labelTotal, labelActivated);
|
||||
}});
|
||||
setRight(new HBox(6) {{
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
getChildren().addAll(labelTips, add, save);
|
||||
}});
|
||||
}});
|
||||
setBorder(Stroke.TOP);
|
||||
}
|
||||
}
|
||||
175
src/main/java/com/imyeyu/hosts/util/HostsParser.java
Normal file
175
src/main/java/com/imyeyu/hosts/util/HostsParser.java
Normal file
@ -0,0 +1,175 @@
|
||||
package com.imyeyu.hosts.util;
|
||||
|
||||
import com.imyeyu.hosts.model.Host;
|
||||
import com.imyeyu.utils.Regex;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Hosts 文件解析器
|
||||
*
|
||||
* <p>提供 hosts 文件内容与 Host 模型的双向转换功能</p>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-01-13
|
||||
*/
|
||||
@Slf4j
|
||||
public class HostsParser {
|
||||
|
||||
// IPv4 正则表达式
|
||||
private static final Pattern IPV4_PATTERN = Pattern.compile(Regex.IPv4);
|
||||
|
||||
// IPv6 正则表达式(简化版)
|
||||
private static final Pattern IPV6_PATTERN = Pattern.compile(Regex.IPv6);
|
||||
|
||||
// 域名正则表达式(RFC 1123)
|
||||
private static final Pattern DOMAIN_PATTERN = Pattern.compile(Regex.DOMAIN);
|
||||
|
||||
/**
|
||||
* 解析 hosts 文件内容为 Host 对象列表
|
||||
*
|
||||
* @param lines hosts 文件的所有行
|
||||
* @return Host 对象列表
|
||||
*/
|
||||
public static List<Host> parse(List<String> lines) {
|
||||
List<Host> hosts = new ArrayList<>();
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String line = lines.get(i).trim();
|
||||
// 跳过空行
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// 跳过纯注释行(不包含 IP 和域名)
|
||||
if (line.startsWith("#")) {
|
||||
String content = line.substring(1).trim();
|
||||
// 如果去掉 # 后还是注释或空行,跳过
|
||||
if (!content.contains(" ")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try {
|
||||
List<Host> parsed = parseLine(line);
|
||||
hosts.addAll(parsed);
|
||||
} catch (Exception e) {
|
||||
log.warn("skip invalid hosts line {}: {}", i + 1, line, e);
|
||||
}
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单行 hosts 配置
|
||||
*
|
||||
* @param line 单行内容
|
||||
* @return Host 对象列表(一个 IP 可能对应多个域名)
|
||||
*/
|
||||
private static List<Host> parseLine(String line) {
|
||||
List<Host> hosts = new ArrayList<>();
|
||||
// 检查是否被注释(未激活)
|
||||
boolean activated = true;
|
||||
if (line.startsWith("#")) {
|
||||
activated = false;
|
||||
line = line.substring(1).trim();
|
||||
}
|
||||
// 提取行尾注释
|
||||
String description = "";
|
||||
int commentIndex = line.indexOf('#');
|
||||
if (commentIndex > 0) {
|
||||
description = line.substring(commentIndex + 1).trim();
|
||||
line = line.substring(0, commentIndex).trim();
|
||||
}
|
||||
// 分割 IP 和域名
|
||||
String[] parts = line.split("\\s+");
|
||||
if (parts.length < 2) {
|
||||
return hosts; // 格式不正确,返回空列表
|
||||
}
|
||||
String ip = parts[0];
|
||||
// 验证 IP 格式
|
||||
if (!isValidIP(ip)) {
|
||||
log.warn("invalid IP address: {}", ip);
|
||||
return hosts;
|
||||
}
|
||||
// 一个 IP 可能对应多个域名,拆分为多条记录
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
String host = parts[i];
|
||||
// 验证域名格式
|
||||
if (!isValidHost(host)) {
|
||||
log.warn("invalid domain: {}", host);
|
||||
continue;
|
||||
}
|
||||
Host hostObj = new Host(activated, ip, host, description);
|
||||
hosts.add(hostObj);
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 Host 对象列表为 hosts 文件内容
|
||||
*
|
||||
* @param hosts Host 对象列表
|
||||
* @return hosts 文件的文本行
|
||||
*/
|
||||
public static List<String> format(List<Host> hosts) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (Host host : hosts) {
|
||||
String line = formatLine(host);
|
||||
lines.add(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化单个 Host 对象为 hosts 文件的一行
|
||||
*
|
||||
* @param host Host 对象
|
||||
* @return 格式化后的文本行
|
||||
*/
|
||||
private static String formatLine(Host host) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
// 未激活时添加 # 前缀
|
||||
if (!host.isActivated()) {
|
||||
sb.append("# ");
|
||||
}
|
||||
// IP 和域名,使用格式化对齐
|
||||
sb.append(String.format("%-20s %s", host.getIP(), host.getHost()));
|
||||
// 添加备注
|
||||
String description = host.getDescription();
|
||||
if (description != null && !description.isEmpty()) {
|
||||
sb.append(" # ").append(description);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 IP 地址格式(支持 IPv4 和 IPv6)
|
||||
*
|
||||
* @param ip IP 地址字符串
|
||||
* @return 是否为有效的 IP 地址
|
||||
*/
|
||||
private static boolean isValidIP(String ip) {
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return IPV4_PATTERN.matcher(ip).matches() || IPV6_PATTERN.matcher(ip).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证域名格式(符合 RFC 1123 规范)
|
||||
*
|
||||
* @param host 域名字符串
|
||||
* @return 是否为有效的域名
|
||||
*/
|
||||
private static boolean isValidHost(String host) {
|
||||
if (host == null || host.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// localhost 是特殊情况
|
||||
if ("localhost".equals(host)) {
|
||||
return true;
|
||||
}
|
||||
return DOMAIN_PATTERN.matcher(host).matches();
|
||||
}
|
||||
}
|
||||
1
src/main/resources/TimiHosts.yaml
Normal file
1
src/main/resources/TimiHosts.yaml
Normal file
@ -0,0 +1 @@
|
||||
lang: zh_cn
|
||||
6
src/main/resources/lang/zh_CN.lang
Normal file
6
src/main/resources/lang/zh_CN.lang
Normal file
@ -0,0 +1,6 @@
|
||||
lang=zh_CN
|
||||
|
||||
main.title=Hosts Manager
|
||||
main.host=地址
|
||||
main.activated=激活
|
||||
main.description=说明
|
||||
23
src/main/resources/logback.xml
Normal file
23
src/main/resources/logback.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<configuration>
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>[%d{HH:mm:ss.SSS}][%-5level][%-30logger{30}] %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>./logs/debug.log</file>
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>[%d{HH:mm:ss.SSS}][%-5level][%-30logger{30}] %msg%n</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>./logs/debug/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
<root level="info">
|
||||
<appender-ref ref="file" />
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
</configuration>
|
||||
26
src/main/resources/style.css
Normal file
26
src/main/resources/style.css
Normal file
@ -0,0 +1,26 @@
|
||||
/* 表格 */
|
||||
.host-table .table-row-cell:odd {
|
||||
-fx-background-color: -fx-table-cell-border-color, -fx-control-inner-background;
|
||||
}
|
||||
.host-table .table-row-cell:filled:selected,
|
||||
.host-table .table-row-cell:filled:selected:focused {
|
||||
-fx-padding: 0;
|
||||
-fx-text-fill: -fx-selection-bar-text;
|
||||
-fx-background: #99D1FF;
|
||||
-fx-border-width: 0;
|
||||
-fx-background-color: #99D1FF;
|
||||
-fx-background-insets: 0;
|
||||
-fx-table-cell-border-color: #99D1FF;
|
||||
}
|
||||
.host-table .table-cell {
|
||||
-fx-padding: 0;
|
||||
-fx-cell-size: 0;
|
||||
-fx-border-width: .083333em .083333em .083333em 0; /* 1 1 1 0 */
|
||||
}
|
||||
.host-table .table-row-cell .text-field {
|
||||
-fx-background-color: -timi-fx-color, #FFF;
|
||||
-fx-background-insets: 0, 0;
|
||||
}
|
||||
.host-table .table-row-cell .text-field:focused {
|
||||
-fx-background-insets: 0, 1;
|
||||
}
|
||||
Reference in New Issue
Block a user