commit 61ad4b52c5701692ee1fab90ce751b7076a8293f Author: zhaojun1998 Date: Mon Aug 19 21:57:02 2019 +0800 :tada: 初始化提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b682fee --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +.mvn/wrapper/** +!**/src/main/** +**/src/test/** + +mvnw +mvnw.cmd + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2eaaaed --- /dev/null +++ b/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.6.RELEASE + + + + im.zhaojun + zfile + 0.1 + zfile + 一个在线的文件浏览系统 + + + 1.8 + 1.3.2 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-test + test + + + + + cn.hutool + hutool-all + 4.5.11 + + + + org.springframework.boot + spring-boot-starter-cache + + + + com.github.ben-manes.caffeine + caffeine + 2.7.0 + + + + + com.upyun + java-sdk + 4.1.2 + + + + com.qiniu + qiniu-java-sdk + 7.2.23 + + + + com.huaweicloud + esdk-obs-java + 3.19.5 + + + + com.aliyun.oss + aliyun-sdk-oss + 3.5.0 + + + + org.nuxeo.onedrive + onedrive-java-client + 1.0 + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.xerial + sqlite-jdbc + + + + commons-net + commons-net + 3.6 + + + + com.qcloud + cos_api + 5.6.3 + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis.starter.version} + + + + + + + nuxeo + nuxeo + http://maven.nuxeo.org/nexus/content/groups/public + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/im/zhaojun/ZfileApplication.java b/src/main/java/im/zhaojun/ZfileApplication.java new file mode 100644 index 0000000..ce16e87 --- /dev/null +++ b/src/main/java/im/zhaojun/ZfileApplication.java @@ -0,0 +1,15 @@ +package im.zhaojun; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@SpringBootApplication +@EnableAspectJAutoProxy(exposeProxy = true) +public class ZfileApplication { + + public static void main(String[] args) { + SpringApplication.run(ZfileApplication.class, args); + } + +} diff --git a/src/main/java/im/zhaojun/aliyun/service/AliyunService.java b/src/main/java/im/zhaojun/aliyun/service/AliyunService.java new file mode 100644 index 0000000..0bad8d8 --- /dev/null +++ b/src/main/java/im/zhaojun/aliyun/service/AliyunService.java @@ -0,0 +1,109 @@ +package im.zhaojun.aliyun.service; + +import cn.hutool.core.util.URLUtil; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.model.*; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.StorageConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.StorageConfigService; +import im.zhaojun.common.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class AliyunService implements FileService { + + @Value("${zfile.cache.timeout}") + private Long timeout; + + @Resource + private StorageConfigService storageConfigService; + + private static final String BUCKET_NAME_KEY = "bucket-name"; + + private static final String ACCESS_KEY = "accessKey"; + + private static final String SECRET_KEY = "secretKey"; + + private static final String DOMAIN_KEY = "domain"; + + private static final String ENDPOINT_KEY = "endPoint"; + + private OSS ossClient; + + private String bucketName; + + private String domain; + + private boolean isPrivate; + + @Override + public void initMethod() { + Map stringStorageConfigMap = + storageConfigService.selectStorageConfigMapByKey(StorageTypeEnum.ALIYUN); + String accessKey = stringStorageConfigMap.get(ACCESS_KEY).getValue(); + String secretKey = stringStorageConfigMap.get(SECRET_KEY).getValue(); + String endPoint = stringStorageConfigMap.get(ENDPOINT_KEY).getValue(); + + bucketName = stringStorageConfigMap.get(BUCKET_NAME_KEY).getValue(); + domain = stringStorageConfigMap.get(DOMAIN_KEY).getValue(); + ossClient = new OSSClientBuilder().build(endPoint, accessKey, secretKey); + + AccessControlList bucketAcl = ossClient.getBucketAcl(bucketName); + CannedAccessControlList cannedACL = bucketAcl.getCannedACL(); + isPrivate = "Private".equals(cannedACL.name()); + } + + @Override + public List fileList(String path) { + path = StringUtils.removeFirstSeparator(path); + + List fileItemList = new ArrayList<>(); + ObjectListing objectListing = + ossClient.listObjects(new ListObjectsRequest(bucketName).withDelimiter("/").withPrefix(path)); + + for (OSSObjectSummary s : objectListing.getObjectSummaries()) { + FileItem fileItem = new FileItem(); + fileItem.setName(s.getKey().substring(path.length())); + fileItem.setSize(s.getSize()); + fileItem.setTime(s.getLastModified()); + fileItem.setType(FileTypeEnum.FILE); + fileItemList.add(fileItem); + } + + for (String commonPrefix : objectListing.getCommonPrefixes()) { + FileItem fileItem = new FileItem(); + fileItem.setName(commonPrefix.substring(path.length(), commonPrefix.length() - 1)); + fileItem.setType(FileTypeEnum.FOLDER); + fileItemList.add(fileItem); + } + + return fileItemList; + } + + @Override + public String getDownloadUrl(String path) throws Exception { + path = StringUtils.removeFirstSeparator(path); + path = URLDecoder.decode(path, "utf8"); + + if (isPrivate) { + Date expirationDate = new Date(new Date().getTime() + timeout * 1000); + URL url = ossClient.generatePresignedUrl(bucketName, path, expirationDate); + return URLUtil.complateUrl(domain, url.getFile()); + } else { + return URLUtil.complateUrl(domain, path); + } + } +} diff --git a/src/main/java/im/zhaojun/common/config/CaffeineConfiguration.java b/src/main/java/im/zhaojun/common/config/CaffeineConfiguration.java new file mode 100644 index 0000000..204341c --- /dev/null +++ b/src/main/java/im/zhaojun/common/config/CaffeineConfiguration.java @@ -0,0 +1,63 @@ +package im.zhaojun.common.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableCaching +public class CaffeineConfiguration { + + @Value("${zfile.cache.timeout}") + private Long timeout; + + public static final String CACHE_NAME = "zfile"; + + /** + * 个性化配置缓存 + */ + @Bean + public CacheManager cacheManager() { + SimpleCacheManager manager = new SimpleCacheManager(); + ArrayList caches = new ArrayList<>(); + caches.add(new CaffeineCache(CACHE_NAME, + Caffeine.newBuilder().recordStats() + .expireAfterWrite(timeout, TimeUnit.SECONDS) + .build()) + ); + manager.setCaches(caches); + return manager; + } + + @Bean + public KeyGenerator keyGenerator() { + return new KeyGenerator() { + @Override + public Object generate(Object target, Method method, Object... params) { + char separator = ':'; + StringBuilder strBuilder = new StringBuilder(); + // 类名 + strBuilder.append(target.getClass().getSimpleName()); + strBuilder.append(separator); + // 方法名 + strBuilder.append(method.getName()); + strBuilder.append(separator); + // 参数值 + for (Object object : params) { + strBuilder.append(object).append(","); + } + return strBuilder.toString(); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/config/RestConfig.java b/src/main/java/im/zhaojun/common/config/RestConfig.java new file mode 100644 index 0000000..f7cfd3f --- /dev/null +++ b/src/main/java/im/zhaojun/common/config/RestConfig.java @@ -0,0 +1,14 @@ +package im.zhaojun.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestConfig { + + @Bean + public RestTemplate restTemplate(){ + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/config/StorageTypeFactory.java b/src/main/java/im/zhaojun/common/config/StorageTypeFactory.java new file mode 100644 index 0000000..186bf18 --- /dev/null +++ b/src/main/java/im/zhaojun/common/config/StorageTypeFactory.java @@ -0,0 +1,49 @@ +package im.zhaojun.common.config; + +import im.zhaojun.aliyun.service.AliyunService; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.service.FileService; +import im.zhaojun.ftp.service.FtpService; +import im.zhaojun.huawei.service.HuaweiService; +import im.zhaojun.local.service.LocalService; +import im.zhaojun.qiniu.service.QiniuService; +import im.zhaojun.tencent.TencentService; +import im.zhaojun.upyun.service.UpYunService; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 存储类型工厂类 + */ +@Component +public class StorageTypeFactory implements ApplicationContextAware { + + private static Map storageTypeEnumFileServiceMap; + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext act) throws BeansException { + applicationContext = act; + storageTypeEnumFileServiceMap = act.getBeansOfType(FileService.class); + } + + public static T getTrafficMode(StorageTypeEnum type) { + String beanName = ""; + switch (type) { + case UPYUN: beanName = applicationContext.getBeanNamesForType(UpYunService.class)[0]; break; + case QINIU: beanName = applicationContext.getBeanNamesForType(QiniuService.class)[0]; break; + case HUAWEI: beanName = applicationContext.getBeanNamesForType(HuaweiService.class)[0]; break; + case FTP: beanName = applicationContext.getBeanNamesForType(FtpService.class)[0]; break; + case ALIYUN: beanName = applicationContext.getBeanNamesForType(AliyunService.class)[0]; break; + case LOCAL: beanName = applicationContext.getBeanNamesForType(LocalService.class)[0]; break; + case TENCENT: beanName = applicationContext.getBeanNamesForType(TencentService.class)[0]; break; + } + return (T) storageTypeEnumFileServiceMap.get(beanName); + } + +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/controller/FileController.java b/src/main/java/im/zhaojun/common/controller/FileController.java new file mode 100644 index 0000000..158e9d3 --- /dev/null +++ b/src/main/java/im/zhaojun/common/controller/FileController.java @@ -0,0 +1,120 @@ +package im.zhaojun.common.controller; + +import cn.hutool.core.util.URLUtil; +import im.zhaojun.common.config.StorageTypeFactory; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.ImageInfo; +import im.zhaojun.common.model.ResultBean; +import im.zhaojun.common.model.SiteConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.SystemConfigService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +@RestController +public class FileController { + + private FileService fileService; + + @Resource + private SystemConfigService configService; + + @GetMapping("/filelist") + public ResultBean list(String path, String sortBy, boolean descending) throws Exception { + List fileItems = fileService.fileList(path); + + // 排序, 先按照文件类型比较, 文件夹在前, 文件在后, 然后根据 sortBy 字段排序, 默认为升序; + fileItems.sort((o1, o2) -> { + FileTypeEnum o1Type = o1.getType(); + FileTypeEnum o2Type = o2.getType(); + + if (o1Type.equals(o2Type)) { + switch (sortBy) { + case "name": return o1.getName().compareTo(o2.getName()); + case "time": return o1.getTime().compareTo(o2.getTime()); + case "size": return o1.getSize().compareTo(o2.getSize()); + default: return o1.getName().compareTo(o2.getName()); + } + } + + if (o1Type.equals(FileTypeEnum.FOLDER)) { + return -1; + } else { + return 1; + } + }); + + if (descending) { + Collections.reverse(fileItems); + } + + return ResultBean.successData(fileItems); + } + + /** + * 获取下载链接 + * @param path 路径 + * @return 下载链接 + */ + @GetMapping("/downloadUrl") + public ResultBean getDownloadUrl(String path) throws Exception { + return ResultBean.successData(fileService.getDownloadUrl(path)); + } + + /** + * 获取文件类容, 仅限用于, txt, md, ini 等普通文本文件. + * @param path 文件路径 + * @return 文件内容 + */ + @GetMapping("/getContent") + public ResultBean getContent(String path) throws Exception { + return ResultBean.successData(fileService.getTextContent(path)); + } + + /** + * 获取系统配置信息和当前页的标题, 文件头, 文件尾信息 + * @param path 路径 + */ + @GetMapping("/getConfig") + public ResultBean getConfig(String path) throws Exception { + SiteConfig config = fileService.getConfig(path); + config.setSystemConfig(configService.getSystemConfig()); + return ResultBean.successData(config); + } + + /** + * 更新存储策略, 使用 @PostConstruct 注解, 以用于第一次启动时, 根据数据库的配置值, 读取默认的存储策略. + */ + @PostConstruct + @GetMapping("/updateStorageStrategy") + public ResultBean updateConfig() { + StorageTypeEnum storageStrategy = configService.getSystemConfig().getStorageStrategy(); + fileService = StorageTypeFactory.getTrafficMode(storageStrategy); + return ResultBean.success(); + } + + @GetMapping("clearCache") + public ResultBean clearCache() throws Exception { + fileService.clearCache(); + return ResultBean.success(); + } + + @GetMapping("/getImageInfo") + public ResultBean getImageInfo(String url) throws Exception { + return ResultBean.success(fileService.getImageInfo(url)); + } +} diff --git a/src/main/java/im/zhaojun/common/controller/IndexController.java b/src/main/java/im/zhaojun/common/controller/IndexController.java new file mode 100644 index 0000000..dcfe373 --- /dev/null +++ b/src/main/java/im/zhaojun/common/controller/IndexController.java @@ -0,0 +1,32 @@ +package im.zhaojun.common.controller; + +import im.zhaojun.common.service.SystemConfigService; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; + +import javax.annotation.Resource; + +@Controller +public class IndexController { + + @Resource + private SystemConfigService systemConfigService; + + @GetMapping("/") + public ModelAndView index(ModelAndView modelAndView) { + modelAndView.setViewName("index"); + modelAndView.addObject("systemConfig", systemConfigService.getSystemConfig()); + return modelAndView; + } + + @GetMapping("/admin") + public ModelAndView admin(ModelAndView modelAndView) { + modelAndView.setViewName("admin"); + modelAndView.addObject("systemConfig", systemConfigService.getSystemConfig()); + return modelAndView; + } + + + +} diff --git a/src/main/java/im/zhaojun/common/enums/FileTypeEnum.java b/src/main/java/im/zhaojun/common/enums/FileTypeEnum.java new file mode 100644 index 0000000..aa660c9 --- /dev/null +++ b/src/main/java/im/zhaojun/common/enums/FileTypeEnum.java @@ -0,0 +1,19 @@ +package im.zhaojun.common.enums; + +public enum FileTypeEnum { + FILE("File"), FOLDER("Folder"); + + private String value; + + FileTypeEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/enums/StorageTypeEnum.java b/src/main/java/im/zhaojun/common/enums/StorageTypeEnum.java new file mode 100644 index 0000000..13c517a --- /dev/null +++ b/src/main/java/im/zhaojun/common/enums/StorageTypeEnum.java @@ -0,0 +1,52 @@ +package im.zhaojun.common.enums; + +import java.util.HashMap; +import java.util.Map; + +public enum StorageTypeEnum { + + UPYUN("upyun", "又拍云 USS"), + QINIU("qiniu", "七牛云 KODO"), + HUAWEI("huawei", "华为云 OBS"), + ALIYUN("aliyun", "阿里云 OSS"), + FTP("ftp", "FTP"), + LOCAL("local", "本地存储"), + TENCENT("tencent", "腾讯云 COS");; + + private static Map enumMap = new HashMap<>(); + + static { + for (StorageTypeEnum type : StorageTypeEnum.values()) { + enumMap.put(type.getKey(), type); + } + } + + StorageTypeEnum(String key, String description) { + this.key = key; + this.description = description; + } + + private String key; + private String description; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public static StorageTypeEnum getEnum(String value) { + return enumMap.get(value); + } + +} diff --git a/src/main/java/im/zhaojun/common/enums/StorageTypeEnumTypeHandler.java b/src/main/java/im/zhaojun/common/enums/StorageTypeEnumTypeHandler.java new file mode 100644 index 0000000..0b44cfb --- /dev/null +++ b/src/main/java/im/zhaojun/common/enums/StorageTypeEnumTypeHandler.java @@ -0,0 +1,58 @@ +package im.zhaojun.common.enums; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class StorageTypeEnumTypeHandler extends BaseTypeHandler{ + + public StorageTypeEnumTypeHandler(Class type) { + if (type == null) + throw new IllegalArgumentException("Type argument cannot be null"); + StorageTypeEnum[] enums = type.getEnumConstants(); + if (enums == null) + throw new IllegalArgumentException(type.getSimpleName() + + " does not represent an enum type."); + } + + @Override + public StorageTypeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { + String i = rs.getString(columnName); + if (rs.wasNull()) { + return null; + } else { + return StorageTypeEnum.getEnum(i); + } + } + + @Override + public StorageTypeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String i = rs.getString(columnIndex); + if (rs.wasNull()) { + return null; + } else { + return StorageTypeEnum.getEnum(i); + } + } + + @Override + public StorageTypeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String i = cs.getString(columnIndex); + if (cs.wasNull()) { + return null; + } else { + return StorageTypeEnum.getEnum(i); + } + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, StorageTypeEnum parameter, JdbcType jdbcType) + throws SQLException { + ps.setString(i, parameter.getKey()); + } + +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/enums/ViewModeEnum.java b/src/main/java/im/zhaojun/common/enums/ViewModeEnum.java new file mode 100644 index 0000000..d8a1be1 --- /dev/null +++ b/src/main/java/im/zhaojun/common/enums/ViewModeEnum.java @@ -0,0 +1,38 @@ +package im.zhaojun.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.HashMap; +import java.util.Map; + +public enum ViewModeEnum { + + DETAILS("details"), ICONS("icons"), GRID("grid"); + + private static Map enumMap = new HashMap<>(); + + static { + for (ViewModeEnum type : ViewModeEnum.values()) { + enumMap.put(type.getValue(), type); + } + } + + String value; + + ViewModeEnum(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public static ViewModeEnum getEnum(String value) { + return enumMap.get(value); + } +} diff --git a/src/main/java/im/zhaojun/common/enums/ViewModeEnumTypeHandler.java b/src/main/java/im/zhaojun/common/enums/ViewModeEnumTypeHandler.java new file mode 100644 index 0000000..d17510c --- /dev/null +++ b/src/main/java/im/zhaojun/common/enums/ViewModeEnumTypeHandler.java @@ -0,0 +1,59 @@ +package im.zhaojun.common.enums; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ViewModeEnumTypeHandler extends BaseTypeHandler{ + + public ViewModeEnumTypeHandler(Class type) { + if (type == null) + throw new IllegalArgumentException("Type argument cannot be null"); + ViewModeEnum[] enums = type.getEnumConstants(); + if (enums == null) + throw new IllegalArgumentException(type.getSimpleName() + + " does not represent an enum type."); + } + + @Override + public ViewModeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { + String i = rs.getString(columnName); + if (rs.wasNull()) { + return null; + } else { + return ViewModeEnum.getEnum(i); + } + } + + @Override + public ViewModeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String i = rs.getString(columnIndex); + if (rs.wasNull()) { + return null; + } else { + return ViewModeEnum.getEnum(i); + } + } + + @Override + public ViewModeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String i = cs.getString(columnIndex); + if (cs.wasNull()) { + return null; + } else { + return ViewModeEnum.getEnum(i); + } + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, ViewModeEnum parameter, JdbcType jdbcType) + throws SQLException { + ps.setString(i, parameter.getValue()); + + } + +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/mapper/StorageConfigMapper.java b/src/main/java/im/zhaojun/common/mapper/StorageConfigMapper.java new file mode 100644 index 0000000..dfa6ba5 --- /dev/null +++ b/src/main/java/im/zhaojun/common/mapper/StorageConfigMapper.java @@ -0,0 +1,24 @@ +package im.zhaojun.common.mapper; + +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.StorageConfig; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface StorageConfigMapper { + int deleteByPrimaryKey(Integer id); + + int insert(StorageConfig record); + + int insertSelective(StorageConfig record); + + StorageConfig selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(StorageConfig record); + + int updateByPrimaryKey(StorageConfig record); + + List selectStorageConfigByType(StorageTypeEnum type); +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/mapper/SystemConfigMapper.java b/src/main/java/im/zhaojun/common/mapper/SystemConfigMapper.java new file mode 100644 index 0000000..f898aab --- /dev/null +++ b/src/main/java/im/zhaojun/common/mapper/SystemConfigMapper.java @@ -0,0 +1,16 @@ +package im.zhaojun.common.mapper; + +import im.zhaojun.common.model.SystemConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SystemConfigMapper { + + int insert(SystemConfig record); + + int updateByPrimaryKeySelective(SystemConfig record); + + int updateByPrimaryKey(SystemConfig record); + + SystemConfig selectFirstConfig(); +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/model/FileItem.java b/src/main/java/im/zhaojun/common/model/FileItem.java new file mode 100644 index 0000000..6ed124b --- /dev/null +++ b/src/main/java/im/zhaojun/common/model/FileItem.java @@ -0,0 +1,46 @@ +package im.zhaojun.common.model; + +import im.zhaojun.common.enums.FileTypeEnum; + +import java.io.Serializable; +import java.util.Date; + +public class FileItem implements Serializable { + + private String name; + private Date time; + private Long size; + private FileTypeEnum type; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getTime() { + return time; + } + + public void setTime(Date time) { + this.time = time; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public FileTypeEnum getType() { + return type; + } + + public void setType(FileTypeEnum type) { + this.type = type; + } +} diff --git a/src/main/java/im/zhaojun/common/model/FolderItem.java b/src/main/java/im/zhaojun/common/model/FolderItem.java new file mode 100644 index 0000000..a67ed35 --- /dev/null +++ b/src/main/java/im/zhaojun/common/model/FolderItem.java @@ -0,0 +1,14 @@ +package im.zhaojun.common.model; + +public class FolderItem { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/im/zhaojun/common/model/ImageInfo.java b/src/main/java/im/zhaojun/common/model/ImageInfo.java new file mode 100644 index 0000000..bee8b3d --- /dev/null +++ b/src/main/java/im/zhaojun/common/model/ImageInfo.java @@ -0,0 +1,27 @@ +package im.zhaojun.common.model; + +public class ImageInfo { + private Integer width; + private Integer height; + + public ImageInfo(Integer width, Integer height) { + this.width = width; + this.height = height; + } + + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + + public Integer getHeight() { + return height; + } + + public void setHeight(Integer height) { + this.height = height; + } +} diff --git a/src/main/java/im/zhaojun/common/model/ResultBean.java b/src/main/java/im/zhaojun/common/model/ResultBean.java new file mode 100644 index 0000000..c7787e7 --- /dev/null +++ b/src/main/java/im/zhaojun/common/model/ResultBean.java @@ -0,0 +1,79 @@ +package im.zhaojun.common.model; + +import java.io.Serializable; + +public class ResultBean implements Serializable { + + private static final long serialVersionUID = -8276264968757808344L; + + private static final int SUCCESS = 0; + + private static final int FAIL = -1; + + private String msg = "操作成功"; + + private int code = SUCCESS; + + private Object data; + + private ResultBean() { + super(); + } + + private ResultBean(String msg, Object data, int code) { + this.msg = msg; + this.data = data; + this.code = code; + } + + public static ResultBean success() { + return success("操作成功"); + } + + public static ResultBean success(String msg) { + return success(msg, null); + } + + public static ResultBean successData(Object data) { + return success("操作成功", data); + } + + public static ResultBean success(Object data) { + return success("操作成功", data); + } + + public static ResultBean success(String msg, Object data) { + return new ResultBean(msg, data, SUCCESS); + } + + public static ResultBean error(String msg) { + ResultBean resultBean = new ResultBean(); + resultBean.setCode(FAIL); + resultBean.setMsg(msg); + return resultBean; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/src/main/java/im/zhaojun/common/model/SiteConfig.java b/src/main/java/im/zhaojun/common/model/SiteConfig.java new file mode 100644 index 0000000..ade0a36 --- /dev/null +++ b/src/main/java/im/zhaojun/common/model/SiteConfig.java @@ -0,0 +1,38 @@ +package im.zhaojun.common.model; + +import java.io.Serializable; + +public class SiteConfig implements Serializable { + + private static final long serialVersionUID = 8811196207046121740L; + + private String header; + + private String footer; + + private SystemConfig systemConfig; + + public String getHeader() { + return header; + } + + public void setHeader(String header) { + this.header = header; + } + + public String getFooter() { + return footer; + } + + public void setFooter(String footer) { + this.footer = footer; + } + + public SystemConfig getSystemConfig() { + return systemConfig; + } + + public void setSystemConfig(SystemConfig systemConfig) { + this.systemConfig = systemConfig; + } +} diff --git a/src/main/java/im/zhaojun/common/model/StorageConfig.java b/src/main/java/im/zhaojun/common/model/StorageConfig.java new file mode 100644 index 0000000..39585a9 --- /dev/null +++ b/src/main/java/im/zhaojun/common/model/StorageConfig.java @@ -0,0 +1,56 @@ +package im.zhaojun.common.model; + +import im.zhaojun.common.enums.StorageTypeEnum; + +public class StorageConfig { + + private Integer id; + + private StorageTypeEnum type; + + private String key; + + private String title; + + private String value; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public StorageTypeEnum getType() { + return type; + } + + public void setType(StorageTypeEnum type) { + this.type = type; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/model/SystemConfig.java b/src/main/java/im/zhaojun/common/model/SystemConfig.java new file mode 100644 index 0000000..fa6f4da --- /dev/null +++ b/src/main/java/im/zhaojun/common/model/SystemConfig.java @@ -0,0 +1,90 @@ +package im.zhaojun.common.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.enums.ViewModeEnum; + +public class SystemConfig { + + @JsonIgnore + private Integer id; + + private String siteName; + + private ViewModeEnum mode; + + private Boolean sidebarEnable; + + private Boolean infoEnable; + + private Boolean searchEnable; + + private Boolean searchIgnoreCase; + + @JsonIgnore + private StorageTypeEnum storageStrategy; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getSiteName() { + return siteName; + } + + public void setSiteName(String siteName) { + this.siteName = siteName; + } + + public ViewModeEnum getMode() { + return mode; + } + + public void setMode(ViewModeEnum mode) { + this.mode = mode; + } + + public Boolean getSidebarEnable() { + return sidebarEnable; + } + + public void setSidebarEnable(Boolean sidebarEnable) { + this.sidebarEnable = sidebarEnable; + } + + public Boolean getInfoEnable() { + return infoEnable; + } + + public void setInfoEnable(Boolean infoEnable) { + this.infoEnable = infoEnable; + } + + public Boolean getSearchEnable() { + return searchEnable; + } + + public void setSearchEnable(Boolean searchEnable) { + this.searchEnable = searchEnable; + } + + public Boolean getSearchIgnoreCase() { + return searchIgnoreCase; + } + + public void setSearchIgnoreCase(Boolean searchIgnoreCase) { + this.searchIgnoreCase = searchIgnoreCase; + } + + public StorageTypeEnum getStorageStrategy() { + return storageStrategy; + } + + public void setStorageStrategy(StorageTypeEnum storageStrategy) { + this.storageStrategy = storageStrategy; + } +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/common/service/FileService.java b/src/main/java/im/zhaojun/common/service/FileService.java new file mode 100644 index 0000000..93489a8 --- /dev/null +++ b/src/main/java/im/zhaojun/common/service/FileService.java @@ -0,0 +1,75 @@ +package im.zhaojun.common.service; + +import cn.hutool.core.util.URLUtil; +import cn.hutool.http.HttpUtil; +import im.zhaojun.common.config.CaffeineConfiguration; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.ImageInfo; +import im.zhaojun.common.model.SiteConfig; +import im.zhaojun.common.util.StringUtils; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; + +import javax.annotation.PostConstruct; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.net.URL; +import java.net.URLDecoder; +import java.util.List; + +@CacheConfig(cacheNames = CaffeineConfiguration.CACHE_NAME, keyGenerator = "keyGenerator") +public interface FileService { + + @Cacheable + List fileList(String path) throws Exception; + + @Cacheable + String getDownloadUrl(String path) throws Exception; + + /** + * 构建指定路径下标题, 页头, 页尾 + * @param path 路径 + */ + @Cacheable + default SiteConfig getConfig(String path) throws Exception { + path = StringUtils.removeLastSeparator(path); + SiteConfig siteConfig = new SiteConfig(); + for (FileItem fileItem : fileList(path)) { + if ("readme.md".equalsIgnoreCase(fileItem.getName())) { + siteConfig.setFooter(getTextContent(path + "/" + fileItem.getName())); + } else if ("header.md".equalsIgnoreCase(fileItem.getName())) { + siteConfig.setHeader(getTextContent(path + "/" + fileItem.getName())); + } + } + return siteConfig; + } + + default String getTextContent(String path) throws Exception { + return HttpUtil.get(URLDecoder.decode(getDownloadUrl(path), "utf8")); + } + + @PostConstruct + default void initMethod() throws Exception {} + + /** + * 清除缓存. + */ + @CacheEvict(allEntries = true) + default void clearCache() throws Exception { + } + + /** + * 获取图片信息 + * @param url 图片 URL + * @return 图片的信息, 宽、高 + */ + @Cacheable + default ImageInfo getImageInfo(String url) throws Exception { + url = URLUtil.decode(url); + InputStream inputStream = new URL(url).openStream(); + BufferedImage sourceImg = ImageIO.read(inputStream); + return new ImageInfo(sourceImg.getWidth(), sourceImg.getHeight()); + } +} diff --git a/src/main/java/im/zhaojun/common/service/StorageConfigService.java b/src/main/java/im/zhaojun/common/service/StorageConfigService.java new file mode 100644 index 0000000..e939ebd --- /dev/null +++ b/src/main/java/im/zhaojun/common/service/StorageConfigService.java @@ -0,0 +1,30 @@ +package im.zhaojun.common.service; + +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.mapper.StorageConfigMapper; +import im.zhaojun.common.model.StorageConfig; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class StorageConfigService { + + @Resource + private StorageConfigMapper storageConfigMapper; + + public List selectStorageConfigByType(StorageTypeEnum storageTypeEnum) { + return storageConfigMapper.selectStorageConfigByType(storageTypeEnum); + } + + public Map selectStorageConfigMapByKey(StorageTypeEnum storageTypeEnum) { + Map map = new HashMap<>(); + for (StorageConfig storageConfig : selectStorageConfigByType(storageTypeEnum)) { + map.put(storageConfig.getKey(), storageConfig); + } + return map; + } +} diff --git a/src/main/java/im/zhaojun/common/service/SystemConfigService.java b/src/main/java/im/zhaojun/common/service/SystemConfigService.java new file mode 100644 index 0000000..0173cac --- /dev/null +++ b/src/main/java/im/zhaojun/common/service/SystemConfigService.java @@ -0,0 +1,20 @@ +package im.zhaojun.common.service; + +import im.zhaojun.common.mapper.SystemConfigMapper; +import im.zhaojun.common.model.SystemConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class SystemConfigService { + + @Resource + private SystemConfigMapper systemConfigMapper; + + @Cacheable("zfile") + public SystemConfig getSystemConfig() { + return systemConfigMapper.selectFirstConfig(); + } +} diff --git a/src/main/java/im/zhaojun/common/util/StringUtils.java b/src/main/java/im/zhaojun/common/util/StringUtils.java new file mode 100644 index 0000000..c8fcd31 --- /dev/null +++ b/src/main/java/im/zhaojun/common/util/StringUtils.java @@ -0,0 +1,44 @@ +package im.zhaojun.common.util; + +public class StringUtils { + + /** + * 移除 URL 中的第一个 '/' + * @return 如 path = '/folder1/file1', 返回 'folder1/file1' + */ + public static String removeFirstSeparator(String path) { + if (!"".equals(path) && path.charAt(0) == '/') { + path = path.substring(1); + } + return path; + } + + /** + * 移除 URL 中的最后一个 '/' + * @return 如 path = '/folder1/file1/', 返回 '/folder1/file1' + */ + public static String removeLastSeparator(String path) { + if (!"".equals(path) && path.charAt(path.length() - 1) == '/') { + path = path.substring(0, path.length() - 1); + } + return path; + } + + /** + * 将域名和路径组装成 URL, 主要用来处理分隔符 '/' + * @param domain 域名 + * @param path 路径 + * @return URL + */ + public static String concatDomainAndPath(String domain, String path) { + if (path != null && path.length() > 1 && path.charAt(0) != '/') { + path = '/' + path; + } + + if (domain.charAt(domain.length() - 1) == '/') { + domain = domain.substring(0, domain.length() - 2); + } + + return domain + path; + } +} diff --git a/src/main/java/im/zhaojun/ftp/service/FtpService.java b/src/main/java/im/zhaojun/ftp/service/FtpService.java new file mode 100644 index 0000000..1a87f62 --- /dev/null +++ b/src/main/java/im/zhaojun/ftp/service/FtpService.java @@ -0,0 +1,73 @@ +package im.zhaojun.ftp.service; + +import cn.hutool.core.util.URLUtil; +import cn.hutool.extra.ftp.Ftp; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.StorageConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.StorageConfigService; +import org.apache.commons.net.ftp.FTPFile; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +public class FtpService implements FileService { + + @Resource + private StorageConfigService storageConfigService; + + private static final String HOST_KEY = "host"; + + private static final String PORT_KEY = "port"; + + private static final String USERNAME_KEY = "username"; + + private static final String PASSWORD_KEY = "password"; + + private static final String DOMAIN_KEY = "domain"; + + private Ftp ftp; + + private String domain; + + @Override + public void initMethod() { + Map stringStorageConfigMap = + storageConfigService.selectStorageConfigMapByKey(StorageTypeEnum.FTP); + String host = stringStorageConfigMap.get(HOST_KEY).getValue(); + String port = stringStorageConfigMap.get(PORT_KEY).getValue(); + String username = stringStorageConfigMap.get(USERNAME_KEY).getValue(); + String password = stringStorageConfigMap.get(PASSWORD_KEY).getValue(); + domain = stringStorageConfigMap.get(DOMAIN_KEY).getValue(); + + ftp = new Ftp(host, Integer.parseInt(port), username, password); + } + + @Override + public List fileList(String path) { + FTPFile[] ftpFiles = ftp.lsFiles(path); + + List fileItemList = new ArrayList<>(); + + for (FTPFile ftpFile : ftpFiles) { + FileItem fileItem = new FileItem(); + fileItem.setName(ftpFile.getName()); + fileItem.setSize(ftpFile.getSize()); + fileItem.setTime(ftpFile.getTimestamp().getTime()); + fileItem.setType(ftpFile.isDirectory() ? FileTypeEnum.FOLDER : FileTypeEnum.FILE); + fileItemList.add(fileItem); + } + return fileItemList; + } + + @Override + public String getDownloadUrl(String path) { + return URLUtil.complateUrl(domain, path); + } +} diff --git a/src/main/java/im/zhaojun/huawei/service/HuaweiService.java b/src/main/java/im/zhaojun/huawei/service/HuaweiService.java new file mode 100644 index 0000000..8098292 --- /dev/null +++ b/src/main/java/im/zhaojun/huawei/service/HuaweiService.java @@ -0,0 +1,107 @@ +package im.zhaojun.huawei.service; + +import cn.hutool.core.util.URLUtil; +import com.obs.services.ObsClient; +import com.obs.services.model.*; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.StorageConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.StorageConfigService; +import im.zhaojun.common.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +public class HuaweiService implements FileService { + + private String bucketName; + + private String domain; + + @Value("${zfile.cache.timeout}") + private Long timeout; + + private static final String BUCKET_NAME_KEY = "bucket-name"; + + private static final String ACCESS_KEY = "accessKey"; + + private static final String SECRET_KEY = "secretKey"; + + private static final String DOMAIN_KEY = "domain"; + + private static final String ENDPOINT_KEY = "endPoint"; + + @Resource + private StorageConfigService storageConfigService; + + private ObsClient obsClient; + + @Override + public void initMethod() { + Map stringStorageConfigMap = + storageConfigService.selectStorageConfigMapByKey(StorageTypeEnum.HUAWEI); + String accessKey = stringStorageConfigMap.get(ACCESS_KEY).getValue(); + String secretKey = stringStorageConfigMap.get(SECRET_KEY).getValue(); + String endPoint = stringStorageConfigMap.get(ENDPOINT_KEY).getValue(); + + bucketName = stringStorageConfigMap.get(BUCKET_NAME_KEY).getValue(); + domain = stringStorageConfigMap.get(DOMAIN_KEY).getValue(); + obsClient = new ObsClient(accessKey, secretKey, endPoint); + } + + @Override + public List fileList(String path) { + path = StringUtils.removeFirstSeparator(path); + + List fileItemList = new ArrayList<>(); + + ListObjectsRequest listObjectsRequest = new ListObjectsRequest(); + listObjectsRequest.setBucketName(bucketName); + listObjectsRequest.setDelimiter("/"); + listObjectsRequest.setPrefix(path); + ObjectListing objectListing = obsClient.listObjects(listObjectsRequest); + List objects = objectListing.getObjects(); + + for (ObsObject object : objects) { + String fileName = object.getObjectKey(); + ObjectMetadata metadata = object.getMetadata(); + + FileItem fileItem = new FileItem(); + fileItem.setName(fileName.substring(path.length())); + fileItem.setSize(metadata.getContentLength()); + fileItem.setTime(metadata.getLastModified()); + fileItem.setType(FileTypeEnum.FILE); + fileItemList.add(fileItem); + } + + for (String commonPrefix : objectListing.getCommonPrefixes()) { + FileItem fileItem = new FileItem(); + fileItem.setName(commonPrefix.substring(0, commonPrefix.length() - 1)); + fileItem.setType(FileTypeEnum.FOLDER); + fileItemList.add(fileItem); + } + + return fileItemList; + } + + @Override + public String getDownloadUrl(String path) throws Exception { + path = StringUtils.removeFirstSeparator(path); + path = URLDecoder.decode(path, "utf8"); + TemporarySignatureRequest req = new TemporarySignatureRequest(HttpMethodEnum.GET, timeout); + req.setBucketName(bucketName); + req.setObjectKey(path); + TemporarySignatureResponse res = obsClient.createTemporarySignature(req); + URL url = new URL(res.getSignedUrl()); + return URLUtil.complateUrl(domain, url.getFile()); + } +} diff --git a/src/main/java/im/zhaojun/local/controller/LocalController.java b/src/main/java/im/zhaojun/local/controller/LocalController.java new file mode 100644 index 0000000..6368a27 --- /dev/null +++ b/src/main/java/im/zhaojun/local/controller/LocalController.java @@ -0,0 +1,60 @@ +package im.zhaojun.local.controller; + +import cn.hutool.core.util.URLUtil; +import im.zhaojun.common.util.StringUtils; +import im.zhaojun.local.service.LocalService; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import java.io.File; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.file.Files; +import java.util.Date; + +@Controller +public class LocalController { + + @Resource + private LocalService localService; + + @GetMapping("/local-download") + @ResponseBody + public ResponseEntity downAttachment(String fileName) throws IOException { + return export(new File(StringUtils.concatDomainAndPath(localService.getFilePath(), URLUtil.decode(fileName)))); + } + + private ResponseEntity export(File file) throws IOException { + // 获取文件 MIME 类型 + String fileMimeType = Files.probeContentType(file.toPath()); + + MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; + HttpHeaders headers = new HttpHeaders(); + + // 如果 + if (fileMimeType == null || "".equals(fileMimeType)) { + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add("Content-Disposition", "attachment; filename=" + file.getName()); + } else { + mediaType = MediaType.parseMediaType(fileMimeType); + } + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + headers.add("Last-Modified", new Date().toString()); + headers.add("ETag", String.valueOf(System.currentTimeMillis())); + return ResponseEntity + .ok() + .headers(headers) + .contentLength(file.length()) + .contentType(mediaType) + .body(new FileSystemResource(file)); + } + + +} diff --git a/src/main/java/im/zhaojun/local/service/LocalService.java b/src/main/java/im/zhaojun/local/service/LocalService.java new file mode 100644 index 0000000..8ba93c8 --- /dev/null +++ b/src/main/java/im/zhaojun/local/service/LocalService.java @@ -0,0 +1,99 @@ +package im.zhaojun.local.service; + +import cn.hutool.core.util.URLUtil; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.ImageInfo; +import im.zhaojun.common.model.StorageConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.StorageConfigService; +import im.zhaojun.common.util.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class LocalService implements FileService { + + private static final String FILE_PATH_KEY = "filePath"; + + @Resource + private StorageConfigService storageConfigService; + + private String filePath; + + @Override + public void initMethod() { + Map stringStorageConfigMap = + storageConfigService.selectStorageConfigMapByKey(StorageTypeEnum.LOCAL); + filePath = stringStorageConfigMap.get(FILE_PATH_KEY).getValue(); + } + + @Override + public List fileList(String path) throws Exception { + List fileItemList = new ArrayList<>(); + + String fullPath = StringUtils.concatDomainAndPath(filePath, path); + + File file = new File(fullPath); + File[] files = file.listFiles(); + + if (files == null) { + return fileItemList; + } + for (File f : files) { + FileItem fileItem = new FileItem(); + fileItem.setType(f.isDirectory() ? FileTypeEnum.FOLDER : FileTypeEnum.FILE); + fileItem.setTime(new Date(f.lastModified())); + fileItem.setSize(f.length()); + fileItem.setName(f.getName()); + fileItemList.add(fileItem); + } + + return fileItemList; + } + + @Override + public String getDownloadUrl(String path) throws Exception { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 网络协议 + String networkProtocol = request.getScheme(); + // 网络ip + String ip = request.getServerName(); + // 端口号 + int port = request.getServerPort(); + // 项目发布名称 + String webApp = request.getContextPath(); + return StringUtils.concatDomainAndPath(networkProtocol + "://" + ip + ":" + port + webApp, "local-download?fileName=" + path); + } + + @Override + public ImageInfo getImageInfo(String url) throws Exception { + String query = new URL(URLUtil.decode(url)).getQuery(); + url = url.replace(query, URLUtil.encode(query)); + InputStream inputStream = new URL(url).openStream(); + BufferedImage sourceImg = ImageIO.read(inputStream); + return new ImageInfo(sourceImg.getWidth(), sourceImg.getHeight()); + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } +} diff --git a/src/main/java/im/zhaojun/qiniu/service/QiniuService.java b/src/main/java/im/zhaojun/qiniu/service/QiniuService.java new file mode 100644 index 0000000..2f25e01 --- /dev/null +++ b/src/main/java/im/zhaojun/qiniu/service/QiniuService.java @@ -0,0 +1,112 @@ +package im.zhaojun.qiniu.service; + +import cn.hutool.core.util.URLUtil; +import com.qiniu.common.QiniuException; +import com.qiniu.common.Zone; +import com.qiniu.storage.BucketManager; +import com.qiniu.storage.Configuration; +import com.qiniu.storage.model.FileInfo; +import com.qiniu.storage.model.FileListing; +import com.qiniu.util.Auth; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.StorageConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.StorageConfigService; +import im.zhaojun.common.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class QiniuService implements FileService { + + @Value("${zfile.cache.timeout}") + private Long timeout; + + @Resource + private StorageConfigService storageConfigService; + + private static final String BUCKET_NAME_KEY = "bucket-name"; + + private static final String ACCESS_KEY = "accessKey"; + + private static final String SECRET_KEY = "secretKey"; + + private static final String DOMAIN_KEY = "domain"; + + private BucketManager bucketManager; + + private Auth auth; + + private String bucketName; + + private String domain; + + private boolean isPrivate; + + public void initMethod() throws QiniuException { + Map stringStorageConfigMap = + storageConfigService.selectStorageConfigMapByKey(StorageTypeEnum.QINIU); + String accessKey = stringStorageConfigMap.get(ACCESS_KEY).getValue(); + String secretKey = stringStorageConfigMap.get(SECRET_KEY).getValue(); + + Configuration cfg = new Configuration(Zone.autoZone()); + auth = Auth.create(accessKey, secretKey); + bucketManager = new BucketManager(auth, cfg); + bucketName = stringStorageConfigMap.get(BUCKET_NAME_KEY).getValue(); + domain = stringStorageConfigMap.get(DOMAIN_KEY).getValue(); + + isPrivate = bucketManager.getBucketInfo(bucketName).getPrivate() == 1; + } + + @Override + public List fileList(String path) throws Exception { + path = StringUtils.removeFirstSeparator(path); + List fileItemList = new ArrayList<>(); + + // 每次迭代的长度限制, 最大1000, 推荐值 1000 + int limit = 1000; + // 指定目录分隔符, 列出所有公共前缀(模拟列出目录效果). 缺省值为空字符串 + String delimiter = "/"; + // 列举空间文件列表 + FileListing fileListing = bucketManager.listFilesV2(bucketName, path, "", limit, delimiter); + for (FileInfo item : fileListing.items) { + String fileKey = item.key; + String fileName = fileKey.substring(path.length()); + + FileItem fileItem = new FileItem(); + fileItem.setName(fileName); + fileItem.setSize(item.fsize); + fileItem.setTime(new Date(item.putTime / 1000)); + fileItem.setType(FileTypeEnum.FILE); + fileItemList.add(fileItem); + } + + String[] commonPrefixes = fileListing.commonPrefixes; + + for (String commonPrefix : commonPrefixes) { + FileItem fileItem = new FileItem(); + fileItem.setName(commonPrefix.substring(0, commonPrefix.length() - 1)); + fileItem.setType(FileTypeEnum.FOLDER); + fileItemList.add(fileItem); + } + + return fileItemList; + } + + @Override + public String getDownloadUrl(String path) { + String url = URLUtil.complateUrl(domain, path); + if (isPrivate) { + url = auth.privateDownloadUrl(url, timeout); + } + return url; + } +} diff --git a/src/main/java/im/zhaojun/tencent/TencentService.java b/src/main/java/im/zhaojun/tencent/TencentService.java new file mode 100644 index 0000000..99586b8 --- /dev/null +++ b/src/main/java/im/zhaojun/tencent/TencentService.java @@ -0,0 +1,103 @@ +package im.zhaojun.tencent; + + +import cn.hutool.core.util.URLUtil; +import com.qcloud.cos.COSClient; +import com.qcloud.cos.ClientConfig; +import com.qcloud.cos.auth.BasicCOSCredentials; +import com.qcloud.cos.auth.COSCredentials; +import com.qcloud.cos.model.COSObjectSummary; +import com.qcloud.cos.model.ListObjectsRequest; +import com.qcloud.cos.model.ObjectListing; +import com.qcloud.cos.region.Region; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.StorageConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.StorageConfigService; +import im.zhaojun.common.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class TencentService implements FileService { + + @Resource + private StorageConfigService storageConfigService; + + private static final String BUCKET_NAME_KEY = "bucket-name"; + + private static final String SECRET_ID_KEY = "secretId"; + + private static final String SECRET_KEY = "secretKey"; + + private static final String DOMAIN_KEY = "domain"; + + private static final String ENDPOINT_KEY = "endPoint"; + + @Value("${zfile.cache.timeout}") + private Long timeout; + + private String bucketName; + + private String domain; + + private COSClient cosClient; + + @Override + public void initMethod() { + Map stringStorageConfigMap = + storageConfigService.selectStorageConfigMapByKey(StorageTypeEnum.TENCENT); + String secretId = stringStorageConfigMap.get(SECRET_ID_KEY).getValue(); + String secretKey = stringStorageConfigMap.get(SECRET_KEY).getValue(); + String endPoint = stringStorageConfigMap.get(ENDPOINT_KEY).getValue(); + bucketName = stringStorageConfigMap.get(BUCKET_NAME_KEY).getValue(); + domain = stringStorageConfigMap.get(DOMAIN_KEY).getValue(); + + COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); + Region region = new Region("ap-shanghai"); + ClientConfig clientConfig = new ClientConfig(region); +// clientConfig.setSignExpired(); + cosClient = new COSClient(cred, clientConfig); + } + + @Override + public List fileList(String path) { + path = StringUtils.removeFirstSeparator(path); + + List fileItemList = new ArrayList<>(); + ObjectListing objectListing = cosClient.listObjects(new ListObjectsRequest().withBucketName(bucketName).withDelimiter("/").withPrefix(path)); + for (COSObjectSummary s : objectListing.getObjectSummaries()) { + FileItem fileItem = new FileItem(); + fileItem.setName(s.getKey().substring(path.length())); + fileItem.setSize(s.getSize()); + fileItem.setTime(s.getLastModified()); + fileItem.setType(FileTypeEnum.FILE); + fileItemList.add(fileItem); + } + + for (String commonPrefix : objectListing.getCommonPrefixes()) { + FileItem fileItem = new FileItem(); + fileItem.setName(commonPrefix.substring(path.length(), commonPrefix.length() - 1)); + fileItem.setType(FileTypeEnum.FOLDER); + fileItemList.add(fileItem); + } + + return fileItemList; + } + + @Override + public String getDownloadUrl(String path) { + Date expirationDate = new Date(new Date().getTime() + timeout * 1000); + URL url = cosClient.generatePresignedUrl(bucketName, path, expirationDate); + return URLUtil.complateUrl(domain, url.getFile()); + } +} diff --git a/src/main/java/im/zhaojun/upyun/service/UpYunService.java b/src/main/java/im/zhaojun/upyun/service/UpYunService.java new file mode 100644 index 0000000..eedffb8 --- /dev/null +++ b/src/main/java/im/zhaojun/upyun/service/UpYunService.java @@ -0,0 +1,76 @@ +package im.zhaojun.upyun.service; + +import cn.hutool.core.util.URLUtil; +import com.UpYun; +import com.upyun.UpException; +import im.zhaojun.common.enums.FileTypeEnum; +import im.zhaojun.common.enums.StorageTypeEnum; +import im.zhaojun.common.model.FileItem; +import im.zhaojun.common.model.StorageConfig; +import im.zhaojun.common.service.FileService; +import im.zhaojun.common.service.StorageConfigService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +public class UpYunService implements FileService { + + @Resource + private StorageConfigService storageConfigService; + + private static final String BUCKET_NAME_KEY = "bucket-name"; + + private static final String USERNAME_KEY = "username"; + + private static final String PASSWORD_KEY = "password"; + + private static final String DOMAIN_KEY = "domain"; + + private String domain; + + private UpYun upYun; + + public void initMethod() { + Map stringStorageConfigMap = + storageConfigService.selectStorageConfigMapByKey(StorageTypeEnum.UPYUN); + String bucketName = stringStorageConfigMap.get(BUCKET_NAME_KEY).getValue(); + String username = stringStorageConfigMap.get(USERNAME_KEY).getValue(); + String password = stringStorageConfigMap.get(PASSWORD_KEY).getValue(); + domain = stringStorageConfigMap.get(DOMAIN_KEY).getValue(); + upYun = new UpYun(bucketName, username, password); + } + + public List fileList(String path) throws IOException, UpException { + ArrayList fileItems = new ArrayList<>(); + + List folderItems = upYun.readDir(path, null); + + if (folderItems != null) { + for (UpYun.FolderItem folderItem : folderItems) { + FileItem fileItem = new FileItem(); + fileItem.setName(folderItem.name); + fileItem.setSize(folderItem.size); + fileItem.setTime(folderItem.date); + + if ("Folder".equals(folderItem.type)) { + fileItem.setType(FileTypeEnum.FOLDER); + } else if ("File".equals(folderItem.type)) { + fileItem.setType(FileTypeEnum.FILE); + } + fileItems.add(fileItem); + } + } + return fileItems; + } + + @Override + public String getDownloadUrl(String path) { + return URLUtil.complateUrl(domain, path); + } + +} diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..5065d1f --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,8 @@ +{ + "properties": [ + { + "name": "zfile.cache.timeout", + "type": "java.lang.Long", + "description": "目录缓存过期时间 和 下载地址过期时间. 单位为秒." + } + ] } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..aa5bc67 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,17 @@ +# \u76EE\u5F55\u7F13\u5B58\u8FC7\u671F\u65F6\u95F4 \u548C \u4E0B\u8F7D\u5730\u5740\u8FC7\u671F\u65F6\u95F4. \u5355\u4F4D\u4E3A\u79D2. +zfile.cache.timeout = 300 + +spring.cache.type=caffeine + +logging.level.im.zhaojun = debug +logging.level.com.obs.services = error +logging.level.org.springframework.cache = debug + +spring.jackson.date-format=yyyy-MM-dd HH:mm +spring.jackson.time-zone=GMT+8 + +spring.datasource.url=jdbc:sqlite:E:/sqlite/1.db +spring.datasource.driver-class-name=org.sqlite.JDBC + +mybatis.configuration.map-underscore-to-camel-case=true +mybatis.mapper-locations=classpath:mapper/*.xml \ No newline at end of file diff --git a/src/main/resources/config.json b/src/main/resources/config.json new file mode 100644 index 0000000..c487b48 --- /dev/null +++ b/src/main/resources/config.json @@ -0,0 +1,16 @@ +{ + "disableSidebar": false, + "hidden": ["^\\.", "^_h5ai"], + "modes": "details", + "info": { + "enabled": true, + "show": true, + "qrCode": true, + "qrFill": "#999", + "qrBack": "#fff" + }, + "search": { + "enabled": true, + "ignoreCase": true + } +} \ No newline at end of file diff --git a/src/main/resources/mapper/StorageConfigMapper.xml b/src/main/resources/mapper/StorageConfigMapper.xml new file mode 100644 index 0000000..ebc6e41 --- /dev/null +++ b/src/main/resources/mapper/StorageConfigMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + id, "type", "key", title, "value" + + + + + + delete from storage_config + where id = #{id,jdbcType=INTEGER} + + + + + insert into storage_config ("key", "type", title, + "value") + values (#{key,jdbcType=LONGVARCHAR}, #{type,jdbcType=LONGVARCHAR}, #{title,jdbcType=LONGVARCHAR}, + #{value,jdbcType=LONGVARCHAR}) + + + + + insert into storage_config + + + "key", + + + "type", + + + title, + + + "value", + + + + + #{key,jdbcType=LONGVARCHAR}, + + + #{type,jdbcType=LONGVARCHAR}, + + + #{title,jdbcType=LONGVARCHAR}, + + + #{value,jdbcType=LONGVARCHAR}, + + + + + + + update storage_config + + + "key" = #{key,jdbcType=LONGVARCHAR}, + + + "type" = #{type,jdbcType=LONGVARCHAR}, + + + title = #{title,jdbcType=LONGVARCHAR}, + + + "value" = #{value,jdbcType=LONGVARCHAR}, + + + where id = #{id,jdbcType=INTEGER} + + + + + update storage_config + set "key" = #{key,jdbcType=LONGVARCHAR}, + "type" = #{type,jdbcType=LONGVARCHAR}, + title = #{title,jdbcType=LONGVARCHAR}, + "value" = #{value,jdbcType=LONGVARCHAR} + where id = #{id,jdbcType=INTEGER} + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/SystemConfigMapper.xml b/src/main/resources/mapper/SystemConfigMapper.xml new file mode 100644 index 0000000..e4bd338 --- /dev/null +++ b/src/main/resources/mapper/SystemConfigMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + id, site_name, "mode", sidebarEnable, infoEnable, searchEnable, searchIgnoreCase, storageStrategy + + + + insert into system_config (site_name, "mode", sidebarEnable, + infoEnable, searchEnable, searchIgnoreCase, storage_strategy, timeout + ) + values (#{siteName,jdbcType=LONGVARCHAR}, #{mode,jdbcType=NUMERIC}, #{sidebarenable,jdbcType=NUMERIC}, + #{infoenable,jdbcType=NUMERIC}, #{searchenable,jdbcType=NUMERIC}, #{searchignorecase,jdbcType=NUMERIC}, + #{storageStrategy,jdbcType=LONGVARCHAR} + ) + + + + update system_config + + + site_name = #{siteName,jdbcType=LONGVARCHAR}, + + + "mode" = #{mode,jdbcType=NUMERIC}, + + + sidebarEnable = #{sidebarEnable,jdbcType=NUMERIC}, + + + infoEnable = #{infoEnable,jdbcType=NUMERIC}, + + + searchEnable = #{searchEnable,jdbcType=NUMERIC}, + + + searchIgnoreCase = #{searchIgnoreCase,jdbcType=NUMERIC}, + + + storageStrategy = #{storageStrategy,jdbcType=LONGVARCHAR}, + + + where id = #{id,jdbcType=INTEGER} + + + + update system_config + set site_name = #{siteName,jdbcType=LONGVARCHAR}, + "mode" = #{mode,jdbcType=NUMERIC}, + sidebarEnable = #{sidebarEnable,jdbcType=NUMERIC}, + infoEnable = #{infoEnable,jdbcType=NUMERIC}, + searchEnable = #{searchEnable,jdbcType=NUMERIC}, + searchIgnoreCase = #{searchIgnoreCase,jdbcType=NUMERIC}, + storageStrategy = #{storageStrategy,jdbcType=NUMERIC} + where id = #{id,jdbcType=INTEGER} + + + + \ No newline at end of file diff --git a/src/main/resources/rebel.xml b/src/main/resources/rebel.xml new file mode 100644 index 0000000..8eda257 --- /dev/null +++ b/src/main/resources/rebel.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/static/DPlayer/DPlayer.min.css b/src/main/resources/static/DPlayer/DPlayer.min.css new file mode 100644 index 0000000..5886b0a --- /dev/null +++ b/src/main/resources/static/DPlayer/DPlayer.min.css @@ -0,0 +1,4 @@ +button[data-balloon]{overflow:visible}[data-balloon]{position:relative;cursor:pointer}[data-balloon]:after{font-family:sans-serif!important;font-weight:400!important;font-style:normal!important;text-shadow:none!important;font-size:12px!important;background:hsla(0,0%,7%,.9);border-radius:4px;color:#fff;content:attr(data-balloon);padding:.5em 1em;white-space:nowrap}[data-balloon]:after,[data-balloon]:before{filter:alpha(opactiy=0);-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";-moz-opacity:0;-khtml-opacity:0;opacity:0;pointer-events:none;transition:all .18s ease-out .18s;position:absolute;z-index:10}[data-balloon]:before{background:no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M2.658 0h32.004c-6 0-11.627 12.002-16.002 12.002C14.285 12.002 8.594 0 2.658 0z'/%3E%3C/svg%3E");background-size:100% auto;width:18px;height:6px;content:""}[data-balloon]:hover:after,[data-balloon]:hover:before,[data-balloon][data-balloon-visible]:after,[data-balloon][data-balloon-visible]:before{filter:alpha(opactiy=100);-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";-moz-opacity:1;-khtml-opacity:1;opacity:1;pointer-events:auto}[data-balloon].font-awesome:after{font-family:FontAwesome}[data-balloon][data-balloon-break]:after{white-space:pre}[data-balloon][data-balloon-blunt]:after,[data-balloon][data-balloon-blunt]:before{transition:none}[data-balloon][data-balloon-pos=up]:after{margin-bottom:11px}[data-balloon][data-balloon-pos=up]:after,[data-balloon][data-balloon-pos=up]:before{bottom:100%;left:50%;-webkit-transform:translate(-50%,10px);transform:translate(-50%,10px);-webkit-transform-origin:top;transform-origin:top}[data-balloon][data-balloon-pos=up]:before{margin-bottom:5px}[data-balloon][data-balloon-pos=up]:hover:after,[data-balloon][data-balloon-pos=up]:hover:before,[data-balloon][data-balloon-pos=up][data-balloon-visible]:after,[data-balloon][data-balloon-pos=up][data-balloon-visible]:before{-webkit-transform:translate(-50%);transform:translate(-50%)}[data-balloon][data-balloon-pos=up-left]:after{left:0;margin-bottom:11px}[data-balloon][data-balloon-pos=up-left]:after,[data-balloon][data-balloon-pos=up-left]:before{bottom:100%;-webkit-transform:translateY(10px);transform:translateY(10px);-webkit-transform-origin:top;transform-origin:top}[data-balloon][data-balloon-pos=up-left]:before{left:5px;margin-bottom:5px}[data-balloon][data-balloon-pos=up-left]:hover:after,[data-balloon][data-balloon-pos=up-left]:hover:before,[data-balloon][data-balloon-pos=up-left][data-balloon-visible]:after,[data-balloon][data-balloon-pos=up-left][data-balloon-visible]:before{-webkit-transform:translate(0);transform:translate(0)}[data-balloon][data-balloon-pos=up-right]:after{right:0;margin-bottom:11px}[data-balloon][data-balloon-pos=up-right]:after,[data-balloon][data-balloon-pos=up-right]:before{bottom:100%;-webkit-transform:translateY(10px);transform:translateY(10px);-webkit-transform-origin:top;transform-origin:top}[data-balloon][data-balloon-pos=up-right]:before{right:5px;margin-bottom:5px}[data-balloon][data-balloon-pos=up-right]:hover:after,[data-balloon][data-balloon-pos=up-right]:hover:before,[data-balloon][data-balloon-pos=up-right][data-balloon-visible]:after,[data-balloon][data-balloon-pos=up-right][data-balloon-visible]:before{-webkit-transform:translate(0);transform:translate(0)}[data-balloon][data-balloon-pos=down]:after{margin-top:11px}[data-balloon][data-balloon-pos=down]:after,[data-balloon][data-balloon-pos=down]:before{left:50%;top:100%;-webkit-transform:translate(-50%,-10px);transform:translate(-50%,-10px)}[data-balloon][data-balloon-pos=down]:before{background:no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M33.342 12H1.338c6 0 11.627-12.002 16.002-12.002C21.715-.002 27.406 12 33.342 12z'/%3E%3C/svg%3E");background-size:100% auto;width:18px;height:6px;margin-top:5px}[data-balloon][data-balloon-pos=down]:hover:after,[data-balloon][data-balloon-pos=down]:hover:before,[data-balloon][data-balloon-pos=down][data-balloon-visible]:after,[data-balloon][data-balloon-pos=down][data-balloon-visible]:before{-webkit-transform:translate(-50%);transform:translate(-50%)}[data-balloon][data-balloon-pos=down-left]:after{left:0;margin-top:11px;top:100%;-webkit-transform:translateY(-10px);transform:translateY(-10px)}[data-balloon][data-balloon-pos=down-left]:before{background:no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M33.342 12H1.338c6 0 11.627-12.002 16.002-12.002C21.715-.002 27.406 12 33.342 12z'/%3E%3C/svg%3E");background-size:100% auto;width:18px;height:6px;left:5px;margin-top:5px;top:100%;-webkit-transform:translateY(-10px);transform:translateY(-10px)}[data-balloon][data-balloon-pos=down-left]:hover:after,[data-balloon][data-balloon-pos=down-left]:hover:before,[data-balloon][data-balloon-pos=down-left][data-balloon-visible]:after,[data-balloon][data-balloon-pos=down-left][data-balloon-visible]:before{-webkit-transform:translate(0);transform:translate(0)}[data-balloon][data-balloon-pos=down-right]:after{right:0;margin-top:11px;top:100%;-webkit-transform:translateY(-10px);transform:translateY(-10px)}[data-balloon][data-balloon-pos=down-right]:before{background:no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M33.342 12H1.338c6 0 11.627-12.002 16.002-12.002C21.715-.002 27.406 12 33.342 12z'/%3E%3C/svg%3E");background-size:100% auto;width:18px;height:6px;right:5px;margin-top:5px;top:100%;-webkit-transform:translateY(-10px);transform:translateY(-10px)}[data-balloon][data-balloon-pos=down-right]:hover:after,[data-balloon][data-balloon-pos=down-right]:hover:before,[data-balloon][data-balloon-pos=down-right][data-balloon-visible]:after,[data-balloon][data-balloon-pos=down-right][data-balloon-visible]:before{-webkit-transform:translate(0);transform:translate(0)}[data-balloon][data-balloon-pos=left]:after{margin-right:11px;right:100%;top:50%;-webkit-transform:translate(10px,-50%);transform:translate(10px,-50%)}[data-balloon][data-balloon-pos=left]:before{background:no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M0 33.342V1.338c0 6 12.002 11.627 12.002 16.002C12.002 21.715 0 27.406 0 33.342z'/%3E%3C/svg%3E");background-size:100% auto;width:6px;height:18px;margin-right:5px;right:100%;top:50%;-webkit-transform:translate(10px,-50%);transform:translate(10px,-50%)}[data-balloon][data-balloon-pos=left]:hover:after,[data-balloon][data-balloon-pos=left]:hover:before,[data-balloon][data-balloon-pos=left][data-balloon-visible]:after,[data-balloon][data-balloon-pos=left][data-balloon-visible]:before{-webkit-transform:translateY(-50%);transform:translateY(-50%)}[data-balloon][data-balloon-pos=right]:after{left:100%;margin-left:11px;top:50%;-webkit-transform:translate(-10px,-50%);transform:translate(-10px,-50%)}[data-balloon][data-balloon-pos=right]:before{background:no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M12 2.658v32.004c0-6-12.002-11.627-12.002-16.002C-.002 14.285 12 8.594 12 2.658z'/%3E%3C/svg%3E");background-size:100% auto;width:6px;height:18px;left:100%;margin-left:5px;top:50%;-webkit-transform:translate(-10px,-50%);transform:translate(-10px,-50%)}[data-balloon][data-balloon-pos=right]:hover:after,[data-balloon][data-balloon-pos=right]:hover:before,[data-balloon][data-balloon-pos=right][data-balloon-visible]:after,[data-balloon][data-balloon-pos=right][data-balloon-visible]:before{-webkit-transform:translateY(-50%);transform:translateY(-50%)}[data-balloon][data-balloon-length=small]:after{white-space:normal;width:80px}[data-balloon][data-balloon-length=medium]:after{white-space:normal;width:150px}[data-balloon][data-balloon-length=large]:after{white-space:normal;width:260px}[data-balloon][data-balloon-length=xlarge]:after{white-space:normal;width:380px}@media screen and (max-width:768px){[data-balloon][data-balloon-length=xlarge]:after{white-space:normal;width:90vw}}[data-balloon][data-balloon-length=fit]:after{white-space:normal;width:100%} +@-webkit-keyframes my-face{2%{-webkit-transform:translateY(1.5px) rotate(1.5deg);transform:translateY(1.5px) rotate(1.5deg)}4%{-webkit-transform:translateY(-1.5px) rotate(-.5deg);transform:translateY(-1.5px) rotate(-.5deg)}6%{-webkit-transform:translateY(1.5px) rotate(-1.5deg);transform:translateY(1.5px) rotate(-1.5deg)}8%{-webkit-transform:translateY(-1.5px) rotate(-1.5deg);transform:translateY(-1.5px) rotate(-1.5deg)}10%{-webkit-transform:translateY(2.5px) rotate(1.5deg);transform:translateY(2.5px) rotate(1.5deg)}12%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}14%{-webkit-transform:translateY(-1.5px) rotate(1.5deg);transform:translateY(-1.5px) rotate(1.5deg)}16%{-webkit-transform:translateY(-.5px) rotate(-1.5deg);transform:translateY(-.5px) rotate(-1.5deg)}18%{-webkit-transform:translateY(.5px) rotate(-1.5deg);transform:translateY(.5px) rotate(-1.5deg)}20%{-webkit-transform:translateY(-1.5px) rotate(2.5deg);transform:translateY(-1.5px) rotate(2.5deg)}22%{-webkit-transform:translateY(.5px) rotate(-1.5deg);transform:translateY(.5px) rotate(-1.5deg)}24%{-webkit-transform:translateY(1.5px) rotate(1.5deg);transform:translateY(1.5px) rotate(1.5deg)}26%{-webkit-transform:translateY(.5px) rotate(.5deg);transform:translateY(.5px) rotate(.5deg)}28%{-webkit-transform:translateY(.5px) rotate(1.5deg);transform:translateY(.5px) rotate(1.5deg)}30%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}32%{-webkit-transform:translateY(1.5px) rotate(-.5deg);transform:translateY(1.5px) rotate(-.5deg)}34%{-webkit-transform:translateY(1.5px) rotate(-.5deg);transform:translateY(1.5px) rotate(-.5deg)}36%{-webkit-transform:translateY(-1.5px) rotate(2.5deg);transform:translateY(-1.5px) rotate(2.5deg)}38%{-webkit-transform:translateY(1.5px) rotate(-1.5deg);transform:translateY(1.5px) rotate(-1.5deg)}40%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}42%{-webkit-transform:translateY(2.5px) rotate(-1.5deg);transform:translateY(2.5px) rotate(-1.5deg)}44%{-webkit-transform:translateY(1.5px) rotate(.5deg);transform:translateY(1.5px) rotate(.5deg)}46%{-webkit-transform:translateY(-1.5px) rotate(2.5deg);transform:translateY(-1.5px) rotate(2.5deg)}48%{-webkit-transform:translateY(-.5px) rotate(.5deg);transform:translateY(-.5px) rotate(.5deg)}50%{-webkit-transform:translateY(.5px) rotate(.5deg);transform:translateY(.5px) rotate(.5deg)}52%{-webkit-transform:translateY(2.5px) rotate(2.5deg);transform:translateY(2.5px) rotate(2.5deg)}54%{-webkit-transform:translateY(-1.5px) rotate(1.5deg);transform:translateY(-1.5px) rotate(1.5deg)}56%{-webkit-transform:translateY(2.5px) rotate(2.5deg);transform:translateY(2.5px) rotate(2.5deg)}58%{-webkit-transform:translateY(.5px) rotate(2.5deg);transform:translateY(.5px) rotate(2.5deg)}60%{-webkit-transform:translateY(2.5px) rotate(2.5deg);transform:translateY(2.5px) rotate(2.5deg)}62%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}64%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}66%{-webkit-transform:translateY(1.5px) rotate(-.5deg);transform:translateY(1.5px) rotate(-.5deg)}68%{-webkit-transform:translateY(-1.5px) rotate(-.5deg);transform:translateY(-1.5px) rotate(-.5deg)}70%{-webkit-transform:translateY(1.5px) rotate(.5deg);transform:translateY(1.5px) rotate(.5deg)}72%{-webkit-transform:translateY(2.5px) rotate(1.5deg);transform:translateY(2.5px) rotate(1.5deg)}74%{-webkit-transform:translateY(-.5px) rotate(.5deg);transform:translateY(-.5px) rotate(.5deg)}76%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}78%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}80%{-webkit-transform:translateY(1.5px) rotate(1.5deg);transform:translateY(1.5px) rotate(1.5deg)}82%{-webkit-transform:translateY(-.5px) rotate(.5deg);transform:translateY(-.5px) rotate(.5deg)}84%{-webkit-transform:translateY(1.5px) rotate(2.5deg);transform:translateY(1.5px) rotate(2.5deg)}86%{-webkit-transform:translateY(-1.5px) rotate(-1.5deg);transform:translateY(-1.5px) rotate(-1.5deg)}88%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}90%{-webkit-transform:translateY(2.5px) rotate(-.5deg);transform:translateY(2.5px) rotate(-.5deg)}92%{-webkit-transform:translateY(.5px) rotate(-.5deg);transform:translateY(.5px) rotate(-.5deg)}94%{-webkit-transform:translateY(2.5px) rotate(.5deg);transform:translateY(2.5px) rotate(.5deg)}96%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}98%{-webkit-transform:translateY(-1.5px) rotate(-.5deg);transform:translateY(-1.5px) rotate(-.5deg)}0%,to{-webkit-transform:translate(0) rotate(0deg);transform:translate(0) rotate(0deg)}}@keyframes my-face{2%{-webkit-transform:translateY(1.5px) rotate(1.5deg);transform:translateY(1.5px) rotate(1.5deg)}4%{-webkit-transform:translateY(-1.5px) rotate(-.5deg);transform:translateY(-1.5px) rotate(-.5deg)}6%{-webkit-transform:translateY(1.5px) rotate(-1.5deg);transform:translateY(1.5px) rotate(-1.5deg)}8%{-webkit-transform:translateY(-1.5px) rotate(-1.5deg);transform:translateY(-1.5px) rotate(-1.5deg)}10%{-webkit-transform:translateY(2.5px) rotate(1.5deg);transform:translateY(2.5px) rotate(1.5deg)}12%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}14%{-webkit-transform:translateY(-1.5px) rotate(1.5deg);transform:translateY(-1.5px) rotate(1.5deg)}16%{-webkit-transform:translateY(-.5px) rotate(-1.5deg);transform:translateY(-.5px) rotate(-1.5deg)}18%{-webkit-transform:translateY(.5px) rotate(-1.5deg);transform:translateY(.5px) rotate(-1.5deg)}20%{-webkit-transform:translateY(-1.5px) rotate(2.5deg);transform:translateY(-1.5px) rotate(2.5deg)}22%{-webkit-transform:translateY(.5px) rotate(-1.5deg);transform:translateY(.5px) rotate(-1.5deg)}24%{-webkit-transform:translateY(1.5px) rotate(1.5deg);transform:translateY(1.5px) rotate(1.5deg)}26%{-webkit-transform:translateY(.5px) rotate(.5deg);transform:translateY(.5px) rotate(.5deg)}28%{-webkit-transform:translateY(.5px) rotate(1.5deg);transform:translateY(.5px) rotate(1.5deg)}30%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}32%{-webkit-transform:translateY(1.5px) rotate(-.5deg);transform:translateY(1.5px) rotate(-.5deg)}34%{-webkit-transform:translateY(1.5px) rotate(-.5deg);transform:translateY(1.5px) rotate(-.5deg)}36%{-webkit-transform:translateY(-1.5px) rotate(2.5deg);transform:translateY(-1.5px) rotate(2.5deg)}38%{-webkit-transform:translateY(1.5px) rotate(-1.5deg);transform:translateY(1.5px) rotate(-1.5deg)}40%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}42%{-webkit-transform:translateY(2.5px) rotate(-1.5deg);transform:translateY(2.5px) rotate(-1.5deg)}44%{-webkit-transform:translateY(1.5px) rotate(.5deg);transform:translateY(1.5px) rotate(.5deg)}46%{-webkit-transform:translateY(-1.5px) rotate(2.5deg);transform:translateY(-1.5px) rotate(2.5deg)}48%{-webkit-transform:translateY(-.5px) rotate(.5deg);transform:translateY(-.5px) rotate(.5deg)}50%{-webkit-transform:translateY(.5px) rotate(.5deg);transform:translateY(.5px) rotate(.5deg)}52%{-webkit-transform:translateY(2.5px) rotate(2.5deg);transform:translateY(2.5px) rotate(2.5deg)}54%{-webkit-transform:translateY(-1.5px) rotate(1.5deg);transform:translateY(-1.5px) rotate(1.5deg)}56%{-webkit-transform:translateY(2.5px) rotate(2.5deg);transform:translateY(2.5px) rotate(2.5deg)}58%{-webkit-transform:translateY(.5px) rotate(2.5deg);transform:translateY(.5px) rotate(2.5deg)}60%{-webkit-transform:translateY(2.5px) rotate(2.5deg);transform:translateY(2.5px) rotate(2.5deg)}62%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}64%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}66%{-webkit-transform:translateY(1.5px) rotate(-.5deg);transform:translateY(1.5px) rotate(-.5deg)}68%{-webkit-transform:translateY(-1.5px) rotate(-.5deg);transform:translateY(-1.5px) rotate(-.5deg)}70%{-webkit-transform:translateY(1.5px) rotate(.5deg);transform:translateY(1.5px) rotate(.5deg)}72%{-webkit-transform:translateY(2.5px) rotate(1.5deg);transform:translateY(2.5px) rotate(1.5deg)}74%{-webkit-transform:translateY(-.5px) rotate(.5deg);transform:translateY(-.5px) rotate(.5deg)}76%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}78%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}80%{-webkit-transform:translateY(1.5px) rotate(1.5deg);transform:translateY(1.5px) rotate(1.5deg)}82%{-webkit-transform:translateY(-.5px) rotate(.5deg);transform:translateY(-.5px) rotate(.5deg)}84%{-webkit-transform:translateY(1.5px) rotate(2.5deg);transform:translateY(1.5px) rotate(2.5deg)}86%{-webkit-transform:translateY(-1.5px) rotate(-1.5deg);transform:translateY(-1.5px) rotate(-1.5deg)}88%{-webkit-transform:translateY(-.5px) rotate(2.5deg);transform:translateY(-.5px) rotate(2.5deg)}90%{-webkit-transform:translateY(2.5px) rotate(-.5deg);transform:translateY(2.5px) rotate(-.5deg)}92%{-webkit-transform:translateY(.5px) rotate(-.5deg);transform:translateY(.5px) rotate(-.5deg)}94%{-webkit-transform:translateY(2.5px) rotate(.5deg);transform:translateY(2.5px) rotate(.5deg)}96%{-webkit-transform:translateY(-.5px) rotate(1.5deg);transform:translateY(-.5px) rotate(1.5deg)}98%{-webkit-transform:translateY(-1.5px) rotate(-.5deg);transform:translateY(-1.5px) rotate(-.5deg)}0%,to{-webkit-transform:translate(0) rotate(0deg);transform:translate(0) rotate(0deg)}}.dplayer{position:relative;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:1}.dplayer *{box-sizing:content-box}.dplayer svg{width:100%;height:100%}.dplayer svg circle,.dplayer svg path{fill:#fff}.dplayer:-webkit-full-screen{width:100%;height:100%;background:#000;position:fixed;z-index:100000;left:0;top:0}.dplayer:-webkit-full-screen .dplayer-danmaku .dplayer-danmaku-bottom.dplayer-danmaku-move,.dplayer:-webkit-full-screen .dplayer-danmaku .dplayer-danmaku-top.dplayer-danmaku-move{-webkit-animation:danmaku-center 6s linear;animation:danmaku-center 6s linear;-webkit-animation-play-state:inherit;animation-play-state:inherit}.dplayer:-webkit-full-screen .dplayer-danmaku .dplayer-danmaku-right.dplayer-danmaku-move{-webkit-animation:danmaku 8s linear;animation:danmaku 8s linear;-webkit-animation-play-state:inherit;animation-play-state:inherit}.dplayer.dplayer-live .dplayer-bar-wrap,.dplayer.dplayer-live.dplayer-no-danmaku .dplayer-setting,.dplayer.dplayer-live .dplayer-setting-loop,.dplayer.dplayer-live .dplayer-setting-speed,.dplayer.dplayer-live .dplayer-time,.dplayer.dplayer-no-danmaku .dplayer-controller .dplayer-icons .dplayer-comment,.dplayer.dplayer-no-danmaku .dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box .dplayer-setting-danmaku,.dplayer.dplayer-no-danmaku .dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box .dplayer-setting-danunlimit,.dplayer.dplayer-no-danmaku .dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box .dplayer-setting-showdan,.dplayer.dplayer-no-danmaku .dplayer-danmaku{display:none}.dplayer.dplayer-arrow .dplayer-danmaku{font-size:18px}.dplayer.dplayer-arrow .dplayer-icon{margin:0 -3px}.dplayer.dplayer-playing .dplayer-danmaku .dplayer-danmaku-move{-webkit-animation-play-state:running;animation-play-state:running}@media (min-width:900px){.dplayer.dplayer-playing .dplayer-controller,.dplayer.dplayer-playing .dplayer-controller-mask{opacity:0}.dplayer.dplayer-playing:hover .dplayer-controller,.dplayer.dplayer-playing:hover .dplayer-controller-mask{opacity:1}}.dplayer.dplayer-loading .dplayer-bezel .diplayer-loading-icon{display:block}.dplayer.dplayer-loading .dplayer-danmaku,.dplayer.dplayer-loading .dplayer-danmaku-move,.dplayer.dplayer-paused .dplayer-danmaku,.dplayer.dplayer-paused .dplayer-danmaku-move{-webkit-animation-play-state:paused;animation-play-state:paused}.dplayer.dplayer-hide-controller{cursor:none}.dplayer.dplayer-hide-controller .dplayer-controller,.dplayer.dplayer-hide-controller .dplayer-controller-mask{opacity:0;-webkit-transform:translateY(100%);transform:translateY(100%)}.dplayer.dplayer-show-controller .dplayer-controller,.dplayer.dplayer-show-controller .dplayer-controller-mask{opacity:1}.dplayer.dplayer-fulled{position:fixed;z-index:100000;left:0;top:0;width:100%;height:100%}.dplayer.dplayer-mobile .dplayer-controller .dplayer-icons .dplayer-camera-icon,.dplayer.dplayer-mobile .dplayer-controller .dplayer-icons .dplayer-volume{display:none}.dplayer.dplayer-mobile .dplayer-controller .dplayer-icons .dplayer-full .dplayer-full-in-icon{position:static;display:inline-block}.dplayer.dplayer-mobile .dplayer-bar-time{display:none}.dplayer-web-fullscreen-fix{position:fixed;top:0;left:0;margin:0;padding:0}[data-balloon]:before{display:none}[data-balloon]:after{padding:.3em .7em;background:hsla(0,0%,7%,.7)}[data-balloon][data-balloon-pos=up]:after{margin-bottom:0}.dplayer-bezel{position:absolute;left:0;right:0;top:0;bottom:0;font-size:22px;color:#fff;pointer-events:none}.dplayer-bezel .dplayer-bezel-icon{position:absolute;top:50%;left:50%;margin:-26px 0 0 -26px;height:52px;width:52px;padding:12px;box-sizing:border-box;background:rgba(0,0,0,.5);border-radius:50%;opacity:0;pointer-events:none}.dplayer-bezel .dplayer-bezel-icon.dplayer-bezel-transition{-webkit-animation:bezel-hide .5s linear;animation:bezel-hide .5s linear}@-webkit-keyframes bezel-hide{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}to{opacity:0;-webkit-transform:scale(2);transform:scale(2)}}@keyframes bezel-hide{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}to{opacity:0;-webkit-transform:scale(2);transform:scale(2)}}.dplayer-bezel .dplayer-danloading{position:absolute;top:50%;margin-top:-7px;width:100%;text-align:center;font-size:14px;line-height:14px;-webkit-animation:my-face 5s infinite ease-in-out;animation:my-face 5s infinite ease-in-out}.dplayer-bezel .diplayer-loading-icon{display:none;position:absolute;top:50%;left:50%;margin:-18px 0 0 -18px;height:36px;width:36px;pointer-events:none}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-hide{display:none}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot{-webkit-animation:diplayer-loading-dot-fade .8s ease infinite;animation:diplayer-loading-dot-fade .8s ease infinite;opacity:0;-webkit-transform-origin:4px 4px;transform-origin:4px 4px}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot.diplayer-loading-dot-7{-webkit-animation-delay:.7s;animation-delay:.7s}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot.diplayer-loading-dot-6{-webkit-animation-delay:.6s;animation-delay:.6s}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot.diplayer-loading-dot-5{-webkit-animation-delay:.5s;animation-delay:.5s}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot.diplayer-loading-dot-4{-webkit-animation-delay:.4s;animation-delay:.4s}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot.diplayer-loading-dot-3{-webkit-animation-delay:.3s;animation-delay:.3s}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot.diplayer-loading-dot-2{-webkit-animation-delay:.2s;animation-delay:.2s}.dplayer-bezel .diplayer-loading-icon .diplayer-loading-dot.diplayer-loading-dot-1{-webkit-animation-delay:.1s;animation-delay:.1s}@-webkit-keyframes diplayer-loading-dot-fade{0%{opacity:.7;-webkit-transform:scale(1.2);transform:scale(1.2)}50%{opacity:.25;-webkit-transform:scale(.9);transform:scale(.9)}to{opacity:.25;-webkit-transform:scale(.85);transform:scale(.85)}}@keyframes diplayer-loading-dot-fade{0%{opacity:.7;-webkit-transform:scale(1.2);transform:scale(1.2)}50%{opacity:.25;-webkit-transform:scale(.9);transform:scale(.9)}to{opacity:.25;-webkit-transform:scale(.85);transform:scale(.85)}}.dplayer-controller-mask{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==) repeat-x bottom;height:98px;width:100%}.dplayer-controller,.dplayer-controller-mask{position:absolute;bottom:0;transition:all .3s ease}.dplayer-controller{left:0;right:0;height:41px;padding:0 20px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.dplayer-controller.dplayer-controller-comment .dplayer-icons{display:none}.dplayer-controller.dplayer-controller-comment .dplayer-icons.dplayer-comment-box{display:block}.dplayer-controller .dplayer-bar-wrap{padding:5px 0;cursor:pointer;position:absolute;bottom:33px;width:calc(100% - 40px);height:3px}.dplayer-controller .dplayer-bar-wrap:hover .dplayer-bar .dplayer-played .dplayer-thumb{-webkit-transform:scale(1);transform:scale(1)}.dplayer-controller .dplayer-bar-wrap .dplayer-bar-preview{position:absolute;background:#fff;pointer-events:none;display:none;background-size:16000px 100%}.dplayer-controller .dplayer-bar-wrap .dplayer-bar-preview-canvas{position:absolute;width:100%;height:100%;z-index:1;pointer-events:none}.dplayer-controller .dplayer-bar-wrap .dplayer-bar-time{position:absolute;left:0;top:-20px;width:30px;border-radius:4px;padding:5px 7px;background-color:rgba(0,0,0,.62);color:#fff;font-size:12px;text-align:center;opacity:1;transition:opacity .1s ease-in-out;word-wrap:normal;word-break:normal;z-index:2;pointer-events:none}.dplayer-controller .dplayer-bar-wrap .dplayer-bar-time.hidden{opacity:0}.dplayer-controller .dplayer-bar-wrap .dplayer-bar{position:relative;height:3px;width:100%;background:hsla(0,0%,100%,.2);cursor:pointer}.dplayer-controller .dplayer-bar-wrap .dplayer-bar .dplayer-loaded{background:hsla(0,0%,100%,.4);transition:all .5s ease}.dplayer-controller .dplayer-bar-wrap .dplayer-bar .dplayer-loaded,.dplayer-controller .dplayer-bar-wrap .dplayer-bar .dplayer-played{position:absolute;left:0;top:0;bottom:0;height:3px;will-change:width}.dplayer-controller .dplayer-bar-wrap .dplayer-bar .dplayer-played .dplayer-thumb{position:absolute;top:0;right:5px;margin-top:-4px;margin-right:-10px;height:11px;width:11px;border-radius:50%;cursor:pointer;transition:all .3s ease-in-out;-webkit-transform:scale(0);transform:scale(0)}.dplayer-controller .dplayer-icons{height:38px;position:absolute;bottom:0}.dplayer-controller .dplayer-icons.dplayer-comment-box{display:none;position:absolute;transition:all .3s ease-in-out;z-index:2;height:38px;bottom:0;left:20px;right:20px;color:#fff}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-icon{padding:7px}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-icon{position:absolute;left:0;top:0}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-send-icon{position:absolute;right:0;top:0}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box{position:absolute;background:rgba(28,28,28,.9);bottom:41px;left:0;box-shadow:0 0 25px rgba(0,0,0,.3);border-radius:4px;padding:10px 10px 16px;font-size:14px;width:204px;transition:all .3s ease-in-out;-webkit-transform:scale(0);transform:scale(0)}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box.dplayer-comment-setting-open{-webkit-transform:scale(1);transform:scale(1)}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box input[type=radio]{display:none}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box label{cursor:pointer}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-title{font-size:13px;color:#fff;line-height:30px}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-type{font-size:0}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-type .dplayer-comment-setting-title{margin-bottom:6px}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-type label:nth-child(2) span{border-radius:4px 0 0 4px}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-type label:nth-child(4) span{border-radius:0 4px 4px 0}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-type span{width:33%;padding:4px 6px;line-height:16px;display:inline-block;font-size:12px;color:#fff;border:1px solid #fff;margin-right:-1px;box-sizing:border-box;text-align:center;cursor:pointer}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-type input:checked+span{background:#e4e4e6;color:#1c1c1c}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-color{font-size:0}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-color label{font-size:0;padding:6px;display:inline-block}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-color span{width:22px;height:22px;display:inline-block;border-radius:50%;box-sizing:border-box;cursor:pointer}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-setting-box .dplayer-comment-setting-color span:hover{-webkit-animation:my-face 5s infinite ease-in-out;animation:my-face 5s infinite ease-in-out}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-input{outline:none;border:none;padding:8px 31px;font-size:14px;line-height:18px;text-align:center;border-radius:4px;background:none;margin:0;height:100%;box-sizing:border-box;width:100%;color:#fff}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-input::-webkit-input-placeholder{color:#fff;opacity:.8}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-input:-ms-input-placeholder,.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-input::-ms-input-placeholder{color:#fff;opacity:.8}.dplayer-controller .dplayer-icons.dplayer-comment-box .dplayer-comment-input::placeholder{color:#fff;opacity:.8}.dplayer-controller .dplayer-icons.dplayer-icons-left .dplayer-icon{padding:7px}.dplayer-controller .dplayer-icons.dplayer-icons-right{right:20px}.dplayer-controller .dplayer-icons.dplayer-icons-right .dplayer-icon{padding:8px}.dplayer-controller .dplayer-icons .dplayer-live-badge,.dplayer-controller .dplayer-icons .dplayer-time{line-height:38px;color:#eee;text-shadow:0 0 2px rgba(0,0,0,.5);vertical-align:middle;font-size:13px;cursor:default}.dplayer-controller .dplayer-icons .dplayer-live-dot{display:inline-block;width:6px;height:6px;vertical-align:4%;margin-right:5px;content:"";border-radius:6px}.dplayer-controller .dplayer-icons .dplayer-icon{width:40px;height:100%;border:none;background-color:transparent;outline:none;cursor:pointer;vertical-align:middle;box-sizing:border-box;display:inline-block}.dplayer-controller .dplayer-icons .dplayer-icon .dplayer-icon-content{transition:all .2s ease-in-out;opacity:.8}.dplayer-controller .dplayer-icons .dplayer-icon:hover .dplayer-icon-content{opacity:1}.dplayer-controller .dplayer-icons .dplayer-icon.dplayer-quality-icon{color:#fff;width:auto;line-height:22px;font-size:14px}.dplayer-controller .dplayer-icons .dplayer-icon.dplayer-comment-icon{padding:10px 9px 9px}.dplayer-controller .dplayer-icons .dplayer-icon.dplayer-setting-icon{padding-top:8.5px}.dplayer-controller .dplayer-icons .dplayer-icon.dplayer-volume-icon{width:43px}.dplayer-controller .dplayer-icons .dplayer-volume{position:relative;display:inline-block;cursor:pointer;height:100%}.dplayer-controller .dplayer-icons .dplayer-volume:hover .dplayer-volume-bar-wrap .dplayer-volume-bar{width:45px}.dplayer-controller .dplayer-icons .dplayer-volume:hover .dplayer-volume-bar-wrap .dplayer-volume-bar .dplayer-volume-bar-inner .dplayer-thumb{-webkit-transform:scale(1);transform:scale(1)}.dplayer-controller .dplayer-icons .dplayer-volume.dplayer-volume-active .dplayer-volume-bar-wrap .dplayer-volume-bar{width:45px}.dplayer-controller .dplayer-icons .dplayer-volume.dplayer-volume-active .dplayer-volume-bar-wrap .dplayer-volume-bar .dplayer-volume-bar-inner .dplayer-thumb{-webkit-transform:scale(1);transform:scale(1)}.dplayer-controller .dplayer-icons .dplayer-volume .dplayer-volume-bar-wrap{display:inline-block;margin:0 10px 0 -5px;vertical-align:middle;height:100%}.dplayer-controller .dplayer-icons .dplayer-volume .dplayer-volume-bar-wrap .dplayer-volume-bar{position:relative;top:17px;width:0;height:3px;background:#aaa;transition:all .3s ease-in-out}.dplayer-controller .dplayer-icons .dplayer-volume .dplayer-volume-bar-wrap .dplayer-volume-bar .dplayer-volume-bar-inner{position:absolute;bottom:0;left:0;height:100%;transition:all .1s ease;will-change:width}.dplayer-controller .dplayer-icons .dplayer-volume .dplayer-volume-bar-wrap .dplayer-volume-bar .dplayer-volume-bar-inner .dplayer-thumb{position:absolute;top:0;right:5px;margin-top:-4px;margin-right:-10px;height:11px;width:11px;border-radius:50%;cursor:pointer;transition:all .3s ease-in-out;-webkit-transform:scale(0);transform:scale(0)}.dplayer-controller .dplayer-icons .dplayer-setting,.dplayer-controller .dplayer-icons .dplayer-subtitle-btn{display:inline-block;height:100%}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box{position:absolute;right:0;bottom:50px;-webkit-transform:scale(0);transform:scale(0);width:150px;border-radius:2px;background:rgba(28,28,28,.9);padding:7px 0;transition:all .3s ease-in-out;overflow:hidden;z-index:2}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box>div{display:none}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box>div.dplayer-setting-origin-panel{display:block}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box.dplayer-setting-box-open{-webkit-transform:scale(1);transform:scale(1)}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box.dplayer-setting-box-narrow{width:70px;height:180px;text-align:center}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box.dplayer-setting-box-speed .dplayer-setting-origin-panel{display:none}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box.dplayer-setting-box-speed .dplayer-setting-speed-panel{display:block}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-item,.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-speed-item{height:30px;padding:5px 10px;box-sizing:border-box;cursor:pointer;position:relative}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-item:hover,.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-speed-item:hover{background-color:hsla(0,0%,100%,.1)}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku{padding:5px 0}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku .dplayer-label{padding:0 10px;display:inline}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku:hover .dplayer-label{display:none}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku:hover .dplayer-danmaku-bar-wrap{display:inline-block}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku.dplayer-setting-danmaku-active .dplayer-label{display:none}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku.dplayer-setting-danmaku-active .dplayer-danmaku-bar-wrap{display:inline-block}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku .dplayer-danmaku-bar-wrap{padding:0 10px;box-sizing:border-box;display:none;vertical-align:middle;height:100%;width:100%}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku .dplayer-danmaku-bar-wrap .dplayer-danmaku-bar{position:relative;top:8.5px;width:100%;height:3px;background:#fff;transition:all .3s ease-in-out}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku .dplayer-danmaku-bar-wrap .dplayer-danmaku-bar .dplayer-danmaku-bar-inner{position:absolute;bottom:0;left:0;height:100%;transition:all .1s ease;background:#aaa;will-change:width}.dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-danmaku .dplayer-danmaku-bar-wrap .dplayer-danmaku-bar .dplayer-danmaku-bar-inner .dplayer-thumb{position:absolute;top:0;right:5px;margin-top:-4px;margin-right:-10px;height:11px;width:11px;border-radius:50%;cursor:pointer;transition:all .3s ease-in-out;background:#aaa}.dplayer-controller .dplayer-icons .dplayer-full{display:inline-block;height:100%;position:relative}.dplayer-controller .dplayer-icons .dplayer-full:hover .dplayer-full-in-icon{display:block}.dplayer-controller .dplayer-icons .dplayer-full .dplayer-full-in-icon{position:absolute;top:-30px;z-index:1;display:none}.dplayer-controller .dplayer-icons .dplayer-quality{position:relative;display:inline-block;height:100%;z-index:2}.dplayer-controller .dplayer-icons .dplayer-quality:hover .dplayer-quality-list,.dplayer-controller .dplayer-icons .dplayer-quality:hover .dplayer-quality-mask{display:block}.dplayer-controller .dplayer-icons .dplayer-quality .dplayer-quality-mask{display:none;position:absolute;bottom:38px;left:-18px;width:80px;padding-bottom:12px}.dplayer-controller .dplayer-icons .dplayer-quality .dplayer-quality-list{display:none;font-size:12px;width:80px;border-radius:2px;background:rgba(28,28,28,.9);padding:5px 0;transition:all .3s ease-in-out;overflow:hidden;color:#fff;text-align:center}.dplayer-controller .dplayer-icons .dplayer-quality .dplayer-quality-item{height:25px;box-sizing:border-box;cursor:pointer;line-height:25px}.dplayer-controller .dplayer-icons .dplayer-quality .dplayer-quality-item:hover{background-color:hsla(0,0%,100%,.1)}.dplayer-controller .dplayer-icons .dplayer-comment{display:inline-block;height:100%}.dplayer-controller .dplayer-icons .dplayer-label{color:#eee;font-size:13px;display:inline-block;vertical-align:middle;white-space:nowrap}.dplayer-controller .dplayer-icons .dplayer-toggle{width:32px;height:20px;text-align:center;font-size:0;vertical-align:middle;position:absolute;top:5px;right:10px}.dplayer-controller .dplayer-icons .dplayer-toggle input{max-height:0;max-width:0;display:none}.dplayer-controller .dplayer-icons .dplayer-toggle input+label{display:inline-block;position:relative;box-shadow:inset 0 0 0 0 #dfdfdf;border:1px solid #dfdfdf;height:20px;width:32px;border-radius:10px;box-sizing:border-box;cursor:pointer;transition:.2s ease-in-out}.dplayer-controller .dplayer-icons .dplayer-toggle input+label:after,.dplayer-controller .dplayer-icons .dplayer-toggle input+label:before{content:"";position:absolute;display:block;height:18px;width:18px;top:0;left:0;border-radius:15px;transition:.2s ease-in-out}.dplayer-controller .dplayer-icons .dplayer-toggle input+label:after{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.4)}.dplayer-controller .dplayer-icons .dplayer-toggle input:checked+label{border-color:hsla(0,0%,100%,.5)}.dplayer-controller .dplayer-icons .dplayer-toggle input:checked+label:before{width:30px;background:hsla(0,0%,100%,.5)}.dplayer-controller .dplayer-icons .dplayer-toggle input:checked+label:after{left:12px}.dplayer-danmaku{position:absolute;left:0;right:0;top:0;bottom:0;font-size:22px;color:#fff}.dplayer-danmaku .dplayer-danmaku-item{display:inline-block;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;white-space:nowrap;text-shadow:.5px .5px .5px rgba(0,0,0,.5)}.dplayer-danmaku .dplayer-danmaku-item--demo{position:absolute;visibility:hidden}.dplayer-danmaku .dplayer-danmaku-right{position:absolute;right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.dplayer-danmaku .dplayer-danmaku-right.dplayer-danmaku-move{will-change:transform;-webkit-animation:danmaku 5s linear;animation:danmaku 5s linear;-webkit-animation-play-state:paused;animation-play-state:paused}@-webkit-keyframes danmaku{0%{-webkit-transform:translateX(100%);transform:translateX(100%)}}@keyframes danmaku{0%{-webkit-transform:translateX(100%);transform:translateX(100%)}}.dplayer-danmaku .dplayer-danmaku-bottom,.dplayer-danmaku .dplayer-danmaku-top{position:absolute;width:100%;text-align:center;visibility:hidden}.dplayer-danmaku .dplayer-danmaku-bottom.dplayer-danmaku-move,.dplayer-danmaku .dplayer-danmaku-top.dplayer-danmaku-move{will-change:visibility;-webkit-animation:danmaku-center 4s linear;animation:danmaku-center 4s linear;-webkit-animation-play-state:paused;animation-play-state:paused}@-webkit-keyframes danmaku-center{0%{visibility:visible}to{visibility:visible}}@keyframes danmaku-center{0%{visibility:visible}to{visibility:visible}}.dplayer-logo{pointer-events:none;position:absolute;left:20px;top:20px;max-width:50px;max-height:50px}.dplayer-logo img{max-width:100%;max-height:100%;background:none}.dplayer-menu{position:absolute;width:170px;border-radius:2px;background:rgba(28,28,28,.85);padding:5px 0;overflow:hidden;z-index:3;display:none}.dplayer-menu.dplayer-menu-show{display:block}.dplayer-menu .dplayer-menu-item{height:30px;box-sizing:border-box;cursor:pointer}.dplayer-menu .dplayer-menu-item:hover{background-color:hsla(0,0%,100%,.1)}.dplayer-menu .dplayer-menu-item a{padding:0 10px;line-height:30px;color:#eee;font-size:13px;display:inline-block;vertical-align:middle;width:100%;box-sizing:border-box;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.dplayer-menu .dplayer-menu-item a:hover{text-decoration:none}.dplayer-notice{opacity:0;position:absolute;bottom:60px;left:20px;font-size:14px;border-radius:2px;background:rgba(28,28,28,.9);padding:7px 20px;transition:all .3s ease-in-out;overflow:hidden;color:#fff;pointer-events:none}.dplayer-subtitle{position:absolute;bottom:40px;width:90%;left:5%;text-align:center;color:#fff;text-shadow:.5px .5px .5px rgba(0,0,0,.5);font-size:20px}.dplayer-subtitle.dplayer-subtitle-hide{display:none}.dplayer-mask{position:absolute;top:0;bottom:0;left:0;right:0;z-index:1;display:none}.dplayer-mask.dplayer-mask-show{display:block}.dplayer-video-wrap{position:relative;background:#000;font-size:0;width:100%;height:100%}.dplayer-video-wrap .dplayer-video{width:100%;height:100%;display:none}.dplayer-video-wrap .dplayer-video-current{display:block}.dplayer-video-wrap .dplayer-video-prepare{display:none}.dplayer-info-panel{position:absolute;top:10px;left:10px;width:400px;background:rgba(28,28,28,.8);padding:10px;color:#fff;font-size:12px;border-radius:2px}.dplayer-info-panel-hide{display:none}.dplayer-info-panel .dplayer-info-panel-close{cursor:pointer;position:absolute;right:10px;top:10px}.dplayer-info-panel .dplayer-info-panel-item>span{display:inline-block;vertical-align:middle;line-height:15px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.dplayer-info-panel .dplayer-info-panel-item-title{width:100px;text-align:right;margin-right:10px}.dplayer-info-panel .dplayer-info-panel-item-data{width:260px} + +/*# sourceMappingURL=DPlayer.min.css.map*/ \ No newline at end of file diff --git a/src/main/resources/static/DPlayer/DPlayer.min.js b/src/main/resources/static/DPlayer/DPlayer.min.js new file mode 100644 index 0000000..0fa2f36 --- /dev/null +++ b/src/main/resources/static/DPlayer/DPlayer.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("DPlayer",[],t):"object"==typeof exports?exports.DPlayer=t():e.DPlayer=t()}(window,function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var a=t[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(i,a,function(t){return e[t]}.bind(null,a));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=52)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=/mobile/i.test(window.navigator.userAgent),a={secondToTime:function(e){var t=Math.floor(e/3600),n=Math.floor((e-3600*t)/60),i=Math.floor(e-3600*t-60*n);return(t>0?[t,n,i]:[n,i]).map(function(e){return e<10?"0"+e:""+e}).join(":")},getElementViewLeft:function(e){var t=e.offsetLeft,n=e.offsetParent,i=document.body.scrollLeft+document.documentElement.scrollLeft;if(document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement)for(;null!==n&&n!==e;)t+=n.offsetLeft,n=n.offsetParent;else for(;null!==n;)t+=n.offsetLeft,n=n.offsetParent;return t-i},getScrollPosition:function(){return{left:window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0,top:window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}},setScrollPosition:function(e){var t=e.left,n=void 0===t?0:t,i=e.top,a=void 0===i?0:i;this.isFirefox?(document.documentElement.scrollLeft=n,document.documentElement.scrollTop=a):window.scrollTo(n,a)},isMobile:i,isFirefox:/firefox/i.test(window.navigator.userAgent),isChrome:/chrome/i.test(window.navigator.userAgent),storage:{set:function(e,t){localStorage.setItem(e,t)},get:function(e){return localStorage.getItem(e)}},cumulativeOffset:function(e){var t=0,n=0;do{t+=e.offsetTop||0,n+=e.offsetLeft||0,e=e.offsetParent}while(e);return{top:t,left:n}},nameMap:{dragStart:i?"touchstart":"mousedown",dragMove:i?"touchmove":"mousemove",dragEnd:i?"touchend":"mouseup"}};t.default=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=b(n(38)),a=b(n(37)),o=b(n(36)),s=b(n(35)),r=b(n(34)),l=b(n(33)),c=b(n(32)),u=b(n(31)),d=b(n(30)),p=b(n(29)),h=b(n(28)),y=b(n(27)),m=b(n(26)),f=b(n(25)),v=b(n(24)),g=b(n(23));function b(e){return e&&e.__esModule?e:{default:e}}var k={play:i.default,pause:a.default,volumeUp:o.default,volumeDown:s.default,volumeOff:r.default,full:l.default,fullWeb:c.default,setting:u.default,right:d.default,comment:p.default,commentOff:h.default,send:y.default,pallette:m.default,camera:f.default,subtitle:v.default,loading:g.default};t.default=k},function(e,t,n){"use strict";var i,a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};i=function(){return this}();try{i=i||Function("return this")()||(0,eval)("this")}catch(e){"object"===("undefined"==typeof window?"undefined":a(window))&&(i=window)}e.exports=i},function(e,t,n){var i=n(4);e.exports=function(e){"use strict";var t="",n=(e=e||{}).enableSubtitle,a=e.subtitle,o=e.current,s=e.pic,r=i.$escape,l=e.screenshot,c=e.preload,u=e.url;n=a&&"webvtt"===a.type;return t+='\n\n ",n&&(t+='\n \n '),t+="\n"}},function(e,t,n){"use strict";e.exports=n(21)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n=n.width?(this.player.template.menu.style.right=n.width-e+"px",this.player.template.menu.style.left="initial"):(this.player.template.menu.style.left=e+"px",this.player.template.menu.style.right="initial"),t+this.player.template.menu.offsetHeight>=n.height?(this.player.template.menu.style.bottom=n.height-t+"px",this.player.template.menu.style.top="initial"):(this.player.template.menu.style.top=t+"px",this.player.template.menu.style.bottom="initial"),this.player.template.mask.classList.add("dplayer-mask-show"),this.player.events.trigger("contextmenu_show")}},{key:"hide",value:function(){this.player.template.mask.classList.remove("dplayer-mask-show"),this.player.template.menu.classList.remove("dplayer-menu-show"),this.player.events.trigger("contextmenu_hide")}}]),e}();t.default=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.default=function e(t){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),t.options.hotkey&&document.addEventListener("keydown",function(e){if(t.focus){var n=document.activeElement.tagName.toUpperCase(),i=document.activeElement.getAttribute("contenteditable");if("INPUT"!==n&&"TEXTAREA"!==n&&""!==i&&"true"!==i){var a=e||window.event,o=void 0;switch(a.keyCode){case 32:a.preventDefault(),t.toggle();break;case 37:a.preventDefault(),t.seek(t.video.currentTime-5),t.controller.setAutoHide();break;case 39:a.preventDefault(),t.seek(t.video.currentTime+5),t.controller.setAutoHide();break;case 38:a.preventDefault(),o=t.volume()+.1,t.volume(o);break;case 40:a.preventDefault(),o=t.volume()-.1,t.volume(o)}}}}),document.addEventListener("keydown",function(e){switch((e||window.event).keyCode){case 27:t.fullScreen.isFullScreen("web")&&t.fullScreen.cancel("web")}})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;ne.player.template.playedBarWrap.offsetWidth)return;var o=e.player.video.duration*(i/e.player.template.playedBarWrap.offsetWidth);a.default.isMobile&&e.thumbnails&&e.thumbnails.show(),e.thumbnails&&e.thumbnails.move(i),e.player.template.playedBarTime.style.left=i-20+"px",e.player.template.playedBarTime.innerText=a.default.secondToTime(o),e.player.template.playedBarTime.classList.remove("hidden")}}),this.player.template.playedBarWrap.addEventListener(a.default.nameMap.dragEnd,function(){a.default.isMobile&&e.thumbnails&&e.thumbnails.hide()}),a.default.isMobile||(this.player.template.playedBarWrap.addEventListener("mouseenter",function(){e.player.video.duration&&(e.thumbnails&&e.thumbnails.show(),e.player.template.playedBarTime.classList.remove("hidden"))}),this.player.template.playedBarWrap.addEventListener("mouseleave",function(){e.player.video.duration&&(e.thumbnails&&e.thumbnails.hide(),e.player.template.playedBarTime.classList.add("hidden"))}))}},{key:"initFullButton",value:function(){var e=this;this.player.template.browserFullButton.addEventListener("click",function(){e.player.fullScreen.toggle("browser")}),this.player.template.webFullButton.addEventListener("click",function(){e.player.fullScreen.toggle("web")})}},{key:"initVolumeButton",value:function(){var e=this,t=function(t){var n=t||window.event,i=((n.clientX||n.changedTouches[0].clientX)-a.default.getElementViewLeft(e.player.template.volumeBarWrap)-5.5)/35;e.player.volume(i)},n=function n(){document.removeEventListener(a.default.nameMap.dragEnd,n),document.removeEventListener(a.default.nameMap.dragMove,t),e.player.template.volumeButton.classList.remove("dplayer-volume-active")};this.player.template.volumeBarWrapWrap.addEventListener("click",function(t){var n=t||window.event,i=((n.clientX||n.changedTouches[0].clientX)-a.default.getElementViewLeft(e.player.template.volumeBarWrap)-5.5)/35;e.player.volume(i)}),this.player.template.volumeBarWrapWrap.addEventListener(a.default.nameMap.dragStart,function(){document.addEventListener(a.default.nameMap.dragMove,t),document.addEventListener(a.default.nameMap.dragEnd,n),e.player.template.volumeButton.classList.add("dplayer-volume-active")}),this.player.template.volumeIcon.addEventListener("click",function(){e.player.video.muted?(e.player.video.muted=!1,e.player.switchVolumeIcon(),e.player.bar.set("volume",e.player.volume(),"width")):(e.player.video.muted=!0,e.player.template.volumeIcon.innerHTML=s.default.volumeOff,e.player.bar.set("volume",0,"width"))})}},{key:"initQualityButton",value:function(){var e=this;this.player.options.video.quality&&this.player.template.qualityList.addEventListener("click",function(t){t.target.classList.contains("dplayer-quality-item")&&e.player.switchQuality(t.target.dataset.index)})}},{key:"initScreenshotButton",value:function(){var e=this;this.player.options.screenshot&&this.player.template.camareButton.addEventListener("click",function(){var t=document.createElement("canvas");t.width=e.player.video.videoWidth,t.height=e.player.video.videoHeight,t.getContext("2d").drawImage(e.player.video,0,0,t.width,t.height);var n=void 0;t.toBlob(function(e){n=URL.createObjectURL(e);var t=document.createElement("a");t.href=n,t.download="DPlayer.png",t.style.display="none",document.body.appendChild(t),t.click(),document.body.removeChild(t),URL.revokeObjectURL(n)}),e.player.events.trigger("screenshot",n)})}},{key:"initSubtitleButton",value:function(){var e=this;this.player.options.subtitle&&(this.player.events.on("subtitle_show",function(){e.player.template.subtitleButton.dataset.balloon=e.player.tran("Hide subtitle"),e.player.template.subtitleButtonInner.style.opacity="",e.player.user.set("subtitle",1)}),this.player.events.on("subtitle_hide",function(){e.player.template.subtitleButton.dataset.balloon=e.player.tran("Show subtitle"),e.player.template.subtitleButtonInner.style.opacity="0.4",e.player.user.set("subtitle",0)}),this.player.template.subtitleButton.addEventListener("click",function(){e.player.subtitle.toggle()}))}},{key:"setAutoHide",value:function(){var e=this;this.show(),clearTimeout(this.autoHideTimer),this.autoHideTimer=setTimeout(function(){!e.player.video.played.length||e.player.paused||e.disableAutoHide||e.hide()},3e3)}},{key:"show",value:function(){this.player.container.classList.remove("dplayer-hide-controller")}},{key:"hide",value:function(){this.player.container.classList.add("dplayer-hide-controller"),this.player.setting.hide(),this.player.comment&&this.player.comment.hide()}},{key:"isShow",value:function(){return!this.player.container.classList.contains("dplayer-hide-controller")}},{key:"toggle",value:function(){this.isShow()?this.hide():this.show()}},{key:"destroy",value:function(){clearTimeout(this.autoHideTimer)}}]),e}();t.default=l},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;nt&&!e.player.video.paused&&(e.player.container.classList.remove("dplayer-loading"),i=!1),t=n)},100)}},{key:"initfpsChecker",value:function(){var e=this;window.requestAnimationFrame(function(){if(e.enablefpsChecker)if(e.initfpsChecker(),e.fpsStart){e.fpsIndex++;var t=new Date;t-e.fpsStart>1e3&&(e.player.infoPanel.fps(e.fpsIndex/(t-e.fpsStart)*1e3),e.fpsStart=new Date,e.fpsIndex=0)}else e.fpsStart=new Date,e.fpsIndex=0;else e.fpsStart=0,e.fpsIndex=0})}},{key:"initinfoChecker",value:function(){var e=this;this.infoChecker=setInterval(function(){e.enableinfoChecker&&e.player.infoPanel.update()},1e3)}},{key:"enable",value:function(e){this["enable"+e+"Checker"]=!0,"fps"===e&&this.initfpsChecker()}},{key:"disable",value:function(e){this["enable"+e+"Checker"]=!1}},{key:"destroy",value:function(){var e=this;this.types.map(function(t){return e["enable"+t+"Checker"]=!1,e[t+"Checker"]&&clearInterval(e[t+"Checker"]),t})}}]),e}();t.default=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:"browser"){case"browser":return document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement;case"web":return this.player.container.classList.contains("dplayer-fulled")}}},{key:"request",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"browser",t="browser"===e?"web":"browser",n=this.isFullScreen(t);switch(n||(this.lastScrollPosition=s.default.getScrollPosition()),e){case"browser":this.player.container.requestFullscreen?this.player.container.requestFullscreen():this.player.container.mozRequestFullScreen?this.player.container.mozRequestFullScreen():this.player.container.webkitRequestFullscreen?this.player.container.webkitRequestFullscreen():this.player.video.webkitEnterFullscreen&&this.player.video.webkitEnterFullscreen();break;case"web":this.player.container.classList.add("dplayer-fulled"),document.body.classList.add("dplayer-web-fullscreen-fix"),this.player.events.trigger("webfullscreen")}n&&this.cancel(t)}},{key:"cancel",value:function(){switch(arguments.length>0&&void 0!==arguments[0]?arguments[0]:"browser"){case"browser":document.cancelFullScreen?document.cancelFullScreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.webkitCancelFullScreen&&document.webkitCancelFullScreen();break;case"web":this.player.container.classList.remove("dplayer-fulled"),document.body.classList.remove("dplayer-web-fullscreen-fix"),this.player.events.trigger("webfullscreen_cancel")}}},{key:"toggle",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"browser";this.isFullScreen(e)?this.cancel(e):this.request(e)}}]),e}();t.default=r},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;nparseFloat(t.time);)n.push(t),t=this.dan[++this.danIndex];this.draw(n)}window.requestAnimationFrame(function(){e.frame()})}},{key:"opacity",value:function(e){if(void 0!==e){for(var t=this.container.getElementsByClassName("dplayer-danmaku-item"),n=0;n'+e[i].text+"":o.innerHTML=e[i].text,o.style.opacity=t._opacity,o.style.color=e[i].color,o.addEventListener("animationend",function(){t.container.removeChild(o)});var s=t._measure(e[i].text),r=void 0;switch(e[i].type){case"right":(r=c(o,e[i].type,s))>=0&&(o.style.width=s+1+"px",o.style.top=n*r+"px",o.style.transform="translateX(-"+a+"px)");break;case"top":(r=c(o,e[i].type))>=0&&(o.style.top=n*r+"px");break;case"bottom":(r=c(o,e[i].type))>=0&&(o.style.bottom=n*r+"px");break;default:console.error("Can't handled danmaku type: "+e[i].type)}r>=0&&(o.classList.add("dplayer-danmaku-move"),u.appendChild(o))},p=0;p=this.options.time()){this.danIndex=e;break}this.danIndex=this.dan.length}}},{key:"clear",value:function(){this.danTunnel={right:{},top:{},bottom:{}},this.danIndex=0,this.options.container.innerHTML="",this.events&&this.events.trigger("danmaku_clear")}},{key:"htmlEncode",value:function(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")}},{key:"resize",value:function(){for(var e=this.container.offsetWidth,t=this.container.getElementsByClassName("dplayer-danmaku-item"),n=0;n]/;a.$escape=function(e){return function(e){var t=""+e,n=o.exec(t);if(!n)return e;var i="",a=void 0,s=void 0,r=void 0;for(a=n.index,s=0;a\n \n '),a+='\n
\n
\n
\n
\n
\n \n ',s.danmaku&&(a+='\n ',a+=r(l("Danmaku is loading")),a+="\n "),a+='\n ',a+=c.loading,a+='\n
\n\n
\n
\n
\n \n
\n
\n
',a+=r(l("Set danmaku color")),a+='
\n \n \n \n \n \n \n
\n
\n
',a+=r(l("Set danmaku type")),a+='
\n \n \n \n
\n
\n \n ',a+=c.send,a+='\n \n
\n
\n \n
\n \n
\n
\n
\n \n
\n
\n
\n
\n \n 0:00 /\n 0:00\n \n ',s.live&&(a+='\n ',a+=r(l("Live")),a+="\n "),a+='\n
\n
\n ',s.video.quality&&(a+='\n
\n \n
\n
\n ',d(s.video.quality,function(e,t){a+='\n
',a+=r(e.name),a+="
\n "}),a+="\n
\n
\n
\n "),a+="\n ",s.screenshot&&(a+='\n
\n
\n "),a+='\n
\n \n
\n ",s.subtitle&&(a+='\n
\n \n
\n "),a+='\n
\n \n
\n
\n
\n ',a+=r(l("Speed")),a+='\n
',a+=c.right,a+='
\n
\n
\n ',a+=r(l("Loop")),a+='\n
\n \n \n
\n
\n
\n ',a+=r(l("Show danmaku")),a+='\n
\n \n \n
\n
\n
\n ',a+=r(l("Unlimited danmaku")),a+='\n
\n \n \n
\n
\n
\n ',a+=r(l("Opacity for danmaku")),a+='\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n 0.5\n
\n
\n 0.75\n
\n
\n ',a+=r(l("Normal")),a+='\n
\n
\n 1.25\n
\n
\n 1.5\n
\n
\n 2\n
\n
\n
\n
\n
\n \n \n
\n
\n
\n \n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
[x]
\n
\n Player version\n \n
\n
\n Player FPS\n \n
\n
\n Video type\n \n
\n
\n Video url\n \n
\n
\n Video resolution\n \n
\n
\n Video duration\n \n
\n ',s.danmaku&&(a+='\n
\n Danamku id\n \n
\n
\n Danamku api\n \n
\n
\n Danamku amount\n \n
\n '),a+='\n
\n
\n ',d(s.contextmenu,function(e,t){a+='\n
\n ',a+=r(l(e.text)),a+="\n
\n "}),a+='\n
\n
'}},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n=200&&o.status<300||304===o.status){var e=JSON.parse(o.responseText);return 0!==e.code?i(o,e):n(o,e)}a(o)}},o.open(null!==t?"POST":"GET",e,!0),o.setRequestHeader("Content-type","application/json; charset=UTF-8"),o.send(null!==t?JSON.stringify(t):null)};t.default={send:function(e,t,n){i(e,t,function(e,t){console.log("Post danmaku: ",t),n&&n()},function(e,t){alert(t.msg)},function(e){console.log("Request was unsuccessful: "+e.status)})},read:function(e,t){i(e,null,function(e,n){t(null,n.danmaku)},function(e,n){t({status:e.status,response:n})},function(e){t({status:e.status,response:null})})}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i,a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o=n(41),s=(i=o)&&i.__esModule?i:{default:i};t.default=function(e){var t={container:e.element||document.getElementsByClassName("dplayer")[0],live:!1,autoplay:!1,theme:"#b7daff",loop:!1,lang:(navigator.language||navigator.browserLanguage).toLowerCase(),screenshot:!1,hotkey:!0,preload:"metadata",volume:.7,apiBackend:s.default,video:{},contextmenu:[],mutex:!0};for(var n in t)t.hasOwnProperty(n)&&!e.hasOwnProperty(n)&&(e[n]=t[n]);return e.video&&!e.video.type&&(e.video.type="auto"),"object"===a(e.danmaku)&&e.danmaku&&!e.danmaku.user&&(e.danmaku.user="DIYgod"),e.subtitle&&(!e.subtitle.type&&(e.subtitle.type="webvtt"),!e.subtitle.fontSize&&(e.subtitle.fontSize="20px"),!e.subtitle.bottom&&(e.subtitle.bottom="40px"),!e.subtitle.color&&(e.subtitle.color="#fff")),e.video.quality&&(e.video.url=e.video.quality[e.video.defaultQuality].url),e.lang&&(e.lang=e.lang.toLowerCase()),e.contextmenu=e.contextmenu.concat([{text:"Video info",click:function(e){e.infoPanel.triggle()}},{text:"About author",link:"https://diygod.me"},{text:"DPlayer v1.23.0",link:"https://github.com/MoePlayer/DPlayer"}]),e}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t=this.constructor;return this.then(function(n){return t.resolve(e()).then(function(){return n})},function(n){return t.resolve(e()).then(function(){return t.reject(n)})})}},function(e,t,n){"use strict";var i,a,o=e.exports={};function s(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function l(e){if(i===setTimeout)return setTimeout(e,0);if((i===s||!i)&&setTimeout)return i=setTimeout,setTimeout(e,0);try{return i(e,0)}catch(t){try{return i.call(null,e,0)}catch(t){return i.call(this,e,0)}}}!function(){try{i="function"==typeof setTimeout?setTimeout:s}catch(e){i=s}try{a="function"==typeof clearTimeout?clearTimeout:r}catch(e){a=r}}();var c,u=[],d=!1,p=-1;function h(){d&&c&&(d=!1,c.length?u=c.concat(u):p=-1,u.length&&y())}function y(){if(!d){var e=l(h);d=!0;for(var t=u.length;t;){for(c=u,u=[];++p1)for(var n=1;n=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(45),t.setImmediate=setImmediate,t.clearImmediate=clearImmediate},function(e,t,n){"use strict";(function(e){Object.defineProperty(t,"__esModule",{value:!0});var i,a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o=n(43),s=(i=o)&&i.__esModule?i:{default:i};var r=setTimeout;function l(){}function c(e){if(!(this instanceof c))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],y(e,this)}function u(e,t){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,c._immediateFn(function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null!==n){var i;try{i=n(e._value)}catch(e){return void p(t.promise,e)}d(t.promise,i)}else(1===e._state?d:p)(t.promise,e._value)})):e._deferreds.push(t)}function d(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"===(void 0===t?"undefined":a(t))||"function"==typeof t)){var n=t.then;if(t instanceof c)return e._state=3,e._value=t,void h(e);if("function"==typeof n)return void y((i=n,o=t,function(){i.apply(o,arguments)}),e)}e._state=1,e._value=t,h(e)}catch(t){p(e,t)}var i,o}function p(e,t){e._state=2,e._value=t,h(e)}function h(e){2===e._state&&0===e._deferreds.length&&c._immediateFn(function(){e._handled||c._unhandledRejectionFn(e._value)});for(var t=0,n=e._deferreds.length;te&&this.notice(this.tran("REW")+" "+(this.video.currentTime-e).toFixed(0)+" "+this.tran("s")),this.video.currentTime=e,this.danmaku&&this.danmaku.seek(),this.bar.set("played",e/this.video.duration,"width"),this.template.ptime.innerHTML=o.default.secondToTime(e)}},{key:"play",value:function(){var e=this;if(this.paused=!1,this.video.paused&&this.bezel.switch(c.default.play),this.template.playButton.innerHTML=c.default.pause,a.default.resolve(this.video.play()).catch(function(){e.pause()}).then(function(){}),this.timer.enable("loading"),this.container.classList.remove("dplayer-paused"),this.container.classList.add("dplayer-playing"),this.danmaku&&this.danmaku.play(),this.options.mutex)for(var t=0;t<_.length;t++)this!==_[t]&&_[t].pause()}},{key:"pause",value:function(){this.paused=!0,this.container.classList.remove("dplayer-loading"),this.video.paused||this.bezel.switch(c.default.pause),this.template.playButton.innerHTML=c.default.play,this.video.pause(),this.timer.disable("loading"),this.container.classList.remove("dplayer-playing"),this.container.classList.add("dplayer-paused"),this.danmaku&&this.danmaku.pause()}},{key:"switchVolumeIcon",value:function(){this.volume()>=.95?this.template.volumeIcon.innerHTML=c.default.volumeUp:this.volume()>0?this.template.volumeIcon.innerHTML=c.default.volumeDown:this.template.volumeIcon.innerHTML=c.default.volumeOff}},{key:"volume",value:function(e,t,n){if(e=parseFloat(e),!isNaN(e)){e=Math.max(e,0),e=Math.min(e,1),this.bar.set("volume",e,"width");var i=(100*e).toFixed(0)+"%";this.template.volumeBarWrapWrap.dataset.balloon=i,t||this.user.set("volume",e),n||this.notice(this.tran("Volume")+" "+(100*e).toFixed(0)+"%"),this.video.volume=e,this.video.muted&&(this.video.muted=!1),this.switchVolumeIcon()}return this.video.volume}},{key:"toggle",value:function(){this.video.paused?this.play():this.pause()}},{key:"on",value:function(e,t){this.events.on(e,t)}},{key:"switchVideo",value:function(e,t){this.pause(),this.video.poster=e.pic?e.pic:"",this.video.src=e.url,this.initMSE(this.video,e.type||"auto"),t&&(this.template.danmakuLoading.style.display="block",this.bar.set("played",0,"width"),this.bar.set("loaded",0,"width"),this.template.ptime.innerHTML="00:00",this.template.danmaku.innerHTML="",this.danmaku&&this.danmaku.reload({id:t.id,address:t.api,token:t.token,maximum:t.maximum,addition:t.addition,user:t.user}))}},{key:"initMSE",value:function(e,t){var n=this;if(this.type=t,this.options.video.customType&&this.options.video.customType[t])"[object Function]"===Object.prototype.toString.call(this.options.video.customType[t])?this.options.video.customType[t](this.video,this):console.error("Illegal customType: "+t);else switch("auto"===this.type&&(/m3u8(#|\?|$)/i.exec(e.src)?this.type="hls":/.flv(#|\?|$)/i.exec(e.src)?this.type="flv":/.mpd(#|\?|$)/i.exec(e.src)?this.type="dash":this.type="normal"),"hls"===this.type&&(e.canPlayType("application/x-mpegURL")||e.canPlayType("application/vnd.apple.mpegURL"))&&(this.type="normal"),this.type){case"hls":if(Hls)if(Hls.isSupported()){var i=new Hls;i.loadSource(e.src),i.attachMedia(e)}else this.notice("Error: Hls is not supported.");else this.notice("Error: Can't find Hls.");break;case"flv":if(flvjs&&flvjs.isSupported())if(flvjs.isSupported()){var a=flvjs.createPlayer({type:"flv",url:e.src});a.attachMediaElement(e),a.load()}else this.notice("Error: flvjs is not supported.");else this.notice("Error: Can't find flvjs.");break;case"dash":dashjs?dashjs.MediaPlayer().create().initialize(e,e.src,!1):this.notice("Error: Can't find dashjs.");break;case"webtorrent":if(WebTorrent)if(WebTorrent.WEBRTC_SUPPORT){this.container.classList.add("dplayer-loading");var o=new WebTorrent,s=e.src;o.add(s,function(e){e.files.find(function(e){return e.name.endsWith(".mp4")}).renderTo(n.video,{autoplay:n.options.autoplay},function(){n.container.classList.remove("dplayer-loading")})})}else this.notice("Error: Webtorrent is not supported.");else this.notice("Error: Can't find Webtorrent.")}}},{key:"initVideo",value:function(e,t){var n=this;this.initMSE(e,t),this.on("durationchange",function(){1!==e.duration&&(n.template.dtime.innerHTML=o.default.secondToTime(e.duration))}),this.on("progress",function(){var t=e.buffered.length?e.buffered.end(e.buffered.length-1)/e.duration:0;n.bar.set("loaded",t,"width")}),this.on("error",function(){n.tran&&n.notice&&(n.type,n.notice(n.tran("This video fails to load"),-1))}),this.on("ended",function(){n.bar.set("played",1,"width"),n.setting.loop?(n.seek(0),e.play()):n.pause(),n.danmaku&&(n.danmaku.danIndex=0)}),this.on("play",function(){n.paused&&n.play()}),this.on("pause",function(){n.paused||n.pause()}),this.on("timeupdate",function(){n.bar.set("played",n.video.currentTime/n.video.duration,"width");var e=o.default.secondToTime(n.video.currentTime);n.template.ptime.innerHTML!==e&&(n.template.ptime.innerHTML=e)});for(var i=function(t){e.addEventListener(n.events.videoEvents[t],function(){n.events.trigger(n.events.videoEvents[t])})},a=0;a1&&void 0!==arguments[1]?arguments[1]:2e3,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:.8;this.template.notice.innerHTML=e,this.template.notice.style.opacity=i,this.noticeTime&&clearTimeout(this.noticeTime),this.events.trigger("notice_show",e),this.noticeTime=setTimeout(function(){t.template.notice.style.opacity=0,t.events.trigger("notice_hide")},n)}},{key:"resize",value:function(){this.danmaku&&this.danmaku.resize(),this.events.trigger("resize")}},{key:"speed",value:function(e){this.video.playbackRate=e}},{key:"destroy",value:function(){_.splice(_.indexOf(this),1),this.pause(),this.controller.destroy(),this.timer.destroy(),this.video.src="",this.container.innerHTML="",this.events.trigger("destroy")}}],[{key:"version",get:function(){return"1.23.0"}}]),e}();t.default=q},,,function(e,t,n){},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(51);var i,a=n(48),o=(i=a)&&i.__esModule?i:{default:i};console.log("\n %c DPlayer v1.23.0 c3dc104 %c http://dplayer.js.org \n\n","color: #fadfa3; background: #030307; padding:5px 0;","background: #fadfa3; padding:5px 0;"),t.default=o.default}]).default}); +//# sourceMappingURL=DPlayer.min.js.map \ No newline at end of file diff --git a/src/main/resources/static/DPlayer/README.md b/src/main/resources/static/DPlayer/README.md new file mode 100644 index 0000000..348100d --- /dev/null +++ b/src/main/resources/static/DPlayer/README.md @@ -0,0 +1,142 @@ +

+ADPlayer +

+

DPlayer

+ +> 🍭 Wow, such a lovely HTML5 danmaku video player + +[![npm](https://img.shields.io/npm/v/dplayer.svg?style=flat-square)](https://www.npmjs.com/package/dplayer) +[![npm](https://img.shields.io/npm/l/dplayer.svg?style=flat-square)](https://github.com/MoePlayer/DPlayer/blob/master/LICENSE) +[![npm](https://img.shields.io/npm/dt/dplayer.svg?style=flat-square)](https://www.npmjs.com/package/dplayer) +[![size](https://badge-size.herokuapp.com/MoePlayer/DPlayer/master/dist/DPlayer.min.js?compression=gzip&style=flat-square)](https://github.com/MoePlayer/DPlayer/tree/master/dist) +[![Travis](https://img.shields.io/travis/MoePlayer/DPlayer.svg?style=flat-square)](https://travis-ci.org/MoePlayer/DPlayer) +[![devDependency Status](https://img.shields.io/david/dev/MoePlayer/dplayer.svg?style=flat-square)](https://david-dm.org/MoePlayer/DPlayer#info=devDependencies) +[![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?style=flat-square)](https://github.com/MoePlayer/DPlayer#donate) + +## Introduction + +![image](http://i.imgur.com/207ch36.jpg) + +DPlayer is a lovely HTML5 danmaku video player to help people build video and danmaku easily. + +**DPlayer supports:** + +- Streaming formats + - [HLS](https://github.com/video-dev/hls.js) + - [FLV](https://github.com/Bilibili/flv.js) + - [MPEG DASH](https://github.com/Dash-Industry-Forum/dash.js) + - [WebTorrent](https://github.com/webtorrent/webtorrent) + - Any other custom streaming formats +- Media formats + - MP4 H.264 + - WebM + - Ogg Theora Vorbis +- Features + - Danmaku + - Screenshot + - Hotkeys + - Quality switching + - Thumbnails + - Subtitle + +Using DPlayer on your project? [Let me know!](https://github.com/DIYgod/DPlayer/issues/31) + +**[Docs](http://dplayer.js.org)** + +**[中文文档](http://dplayer.js.org/#/zh-Hans/)** + +## Join the Discussion + +- [Telegram Group](https://t.me/adplayer) + +## Related Projects + +Feel free to submit yours in [`Let me know!`](https://github.com/MoePlayer/DPlayer/issues/31) + +### Tooling + +- [DPlayer-thumbnails](https://github.com/MoePlayer/DPlayer-thumbnails): generate video thumbnails + +### Danmaku api + +- [DPlayer-node](https://github.com/MoePlayer/DPlayer-node): Node.js +- [laravel-danmaku](https://github.com/MoePlayer/laravel-danmaku): PHP +- [dplayer-live-backend](https://github.com/Izumi-kun/dplayer-live-backend): Node.js, WebSocket live backend +- [RailsGun](https://github.com/MoePlayer/RailsGun): Ruby + +### Plugins + +- [DPlayer-for-typecho](https://github.com/volio/DPlayer-for-typecho): Typecho +- [Hexo-tag-dplayer](https://github.com/NextMoe/hexo-tag-dplayer): Hexo +- [DPlayer_for_Z-BlogPHP](https://github.com/fghrsh/DPlayer_for_Z-BlogPHP): Z-BlogPHP +- [DPlayer for Discuz!](https://coding.net/u/Click_04/p/video/git): Discuz! +- [DPlayer for WordPress](https://github.com/BlueCocoa/DPlayer-WordPress): WordPress +- [DPlayerHandle](https://github.com/kn007/DPlayerHandle): WordPress +- [Vue-DPlayer](https://github.com/sinchang/vue-dplayer): Vue +- [react-dplayer](https://github.com/hnsylitao/react-dplayer): React + +### Other + +- [DPlayer-Lite](https://github.com/kn007/DPlayer-Lite): lite version +- [hlsjs-p2p-engine](https://github.com/cdnbye/hlsjs-p2p-engine) +- Feel free to submit yours in [`Let me know!`](https://github.com/MoePlayer/DPlayer/issues/31) + +## Who use DPlayer? + +- [小红书](https://www.xiaohongshu.com/): 中国最大的生活社区分享平台,同时也是发现全球好物的电商平台 +- [极客时间](https://time.geekbang.org/): 极客邦科技出品的一款 IT 内容知识服务 App +- [嘀哩嘀哩](http://www.dilidili.wang/): 兴趣使然的无名小站(D站) +- [银色子弹](https://www.sbsub.com/): 银色子弹,简称银弹,由多数柯南热爱者聚集在一起的组织 +- [浙江大学CC98论坛](https://zh.wikipedia.org/wiki/CC98%E8%AE%BA%E5%9D%9B): 浙江大学校网内规模最大的论坛,中国各大学中较活跃的BBS之一 +- [纸飞机南航青年网络社区](http://my.nuaa.edu.cn/video-video.html): 南京航空航天大学门户网站 +- [otomads](https://otomads.com/): 专注于音MAD的视频弹幕网站 +- [Cloudreve](https://github.com/HFO4/Cloudreve): 基于ThinkPHP构建的网盘系统 +- Feel free to submit yours in [`Let me know!`](https://github.com/MoePlayer/DPlayer/issues/31) + +## Current Premium Sponsors + +### Special Sponsors + + + + + + + + +### OpenCollective backers + +![](https://opencollective.com/DPlayer/backers.svg?width=890) + +## Contributors + +This project exists thanks to all the people who contribute. + + + +## Donate + +DPlayer is an MIT licensed open source project and completely free to use. However, the amount of effort needed to maintain and develop new features for the project is not sustainable without proper financial backing. + +## One-time Donations + +We accept donations through these channels: + +- [Paypal](https://www.paypal.me/DIYgod) +- [WeChat Pay](https://i.imgur.com/aq6PtWa.png) +- [Alipay](https://i.imgur.com/wv1Pj2k.png) +- Bitcoin: 13CwQLHzPYm2tewNMSJBeArbbRM5NSmCD1 + +## Recurring Pledges + +Recurring pledges come with exclusive perks, e.g. having your name or your company logo listed in the DPlayer GitHub repository and this website. + +- Become a backer or sponsor via [OpenCollective](https://opencollective.com/dplayer) +- E-mail us: i#html.love + +## Author + +**DPlayer** © [DIYgod](https://github.com/DIYgod), Released under the [MIT](./LICENSE) License.
+Authored and maintained by DIYgod with help from contributors ([list](https://github.com/DIYgod/DPlayer/contributors)). + +> [Blog](https://diygod.me) · GitHub [@DIYgod](https://github.com/DIYgod) · Twitter [@DIYgod](https://twitter.com/DIYgod) · Telegram Channel [@awesomeDIYgod](https://t.me/awesomeDIYgod) diff --git a/src/main/resources/static/DPlayer/index.html b/src/main/resources/static/DPlayer/index.html new file mode 100644 index 0000000..a855071 --- /dev/null +++ b/src/main/resources/static/DPlayer/index.html @@ -0,0 +1,22 @@ + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/config.json b/src/main/resources/static/config.json new file mode 100644 index 0000000..76b74b8 --- /dev/null +++ b/src/main/resources/static/config.json @@ -0,0 +1,5 @@ +{ + "SITE_NAME": "H5AI", + "SHOW_STATS": true, + "FOOTER": "

Footer

" +} \ No newline at end of file diff --git a/src/main/resources/static/contextMenu/font/context-menu-icons.eot b/src/main/resources/static/contextMenu/font/context-menu-icons.eot new file mode 100644 index 0000000..c126cb3 Binary files /dev/null and b/src/main/resources/static/contextMenu/font/context-menu-icons.eot differ diff --git a/src/main/resources/static/contextMenu/font/context-menu-icons.ttf b/src/main/resources/static/contextMenu/font/context-menu-icons.ttf new file mode 100644 index 0000000..19794ff Binary files /dev/null and b/src/main/resources/static/contextMenu/font/context-menu-icons.ttf differ diff --git a/src/main/resources/static/contextMenu/font/context-menu-icons.woff b/src/main/resources/static/contextMenu/font/context-menu-icons.woff new file mode 100644 index 0000000..6cec241 Binary files /dev/null and b/src/main/resources/static/contextMenu/font/context-menu-icons.woff differ diff --git a/src/main/resources/static/contextMenu/font/context-menu-icons.woff2 b/src/main/resources/static/contextMenu/font/context-menu-icons.woff2 new file mode 100644 index 0000000..bc07c72 Binary files /dev/null and b/src/main/resources/static/contextMenu/font/context-menu-icons.woff2 differ diff --git a/src/main/resources/static/contextMenu/jquery.contextMenu.css b/src/main/resources/static/contextMenu/jquery.contextMenu.css new file mode 100644 index 0000000..fa4abd8 --- /dev/null +++ b/src/main/resources/static/contextMenu/jquery.contextMenu.css @@ -0,0 +1,309 @@ +@charset "UTF-8"; +/*! + * jQuery contextMenu - Plugin for simple contextMenu handling + * + * Version: v2.8.0 + * + * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://swisnl.github.io/jQuery-contextMenu/ + * + * Copyright (c) 2011-2019 SWIS BV and contributors + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * + * Date: 2019-01-16T15:45:48.418Z + */ +@-webkit-keyframes cm-spin { + 0% { + -webkit-transform: translateY(-50%) rotate(0deg); + transform: translateY(-50%) rotate(0deg); + } + 100% { + -webkit-transform: translateY(-50%) rotate(359deg); + transform: translateY(-50%) rotate(359deg); + } +} +@-o-keyframes cm-spin { + 0% { + -webkit-transform: translateY(-50%) rotate(0deg); + -o-transform: translateY(-50%) rotate(0deg); + transform: translateY(-50%) rotate(0deg); + } + 100% { + -webkit-transform: translateY(-50%) rotate(359deg); + -o-transform: translateY(-50%) rotate(359deg); + transform: translateY(-50%) rotate(359deg); + } +} +@keyframes cm-spin { + 0% { + -webkit-transform: translateY(-50%) rotate(0deg); + -o-transform: translateY(-50%) rotate(0deg); + transform: translateY(-50%) rotate(0deg); + } + 100% { + -webkit-transform: translateY(-50%) rotate(359deg); + -o-transform: translateY(-50%) rotate(359deg); + transform: translateY(-50%) rotate(359deg); + } +} + +@font-face { + font-family: "context-menu-icons"; + font-style: normal; + font-weight: normal; + + src: url("font/context-menu-icons.eot?2lkho"); + src: url("font/context-menu-icons.eot?2lkho#iefix") format("embedded-opentype"), url("font/context-menu-icons.woff2?2lkho") format("woff2"), url("font/context-menu-icons.woff?2lkho") format("woff"), url("font/context-menu-icons.ttf?2lkho") format("truetype"); +} + +.context-menu-icon-add:before { + content: "\EA01"; +} + +.context-menu-icon-copy:before { + content: "\EA02"; +} + +.context-menu-icon-cut:before { + content: "\EA03"; +} + +.context-menu-icon-delete:before { + content: "\EA04"; +} + +.context-menu-icon-edit:before { + content: "\EA05"; +} + +.context-menu-icon-loading:before { + content: "\EA06"; +} + +.context-menu-icon-paste:before { + content: "\EA07"; +} + +.context-menu-icon-quit:before { + content: "\EA08"; +} + +.context-menu-icon::before { + position: absolute; + top: 50%; + left: 0; + width: 2em; + font-family: "context-menu-icons"; + font-size: 1em; + font-style: normal; + font-weight: normal; + line-height: 1; + color: #2980b9; + text-align: center; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -o-transform: translateY(-50%); + transform: translateY(-50%); + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.context-menu-icon.context-menu-hover:before { + color: #fff; +} + +.context-menu-icon.context-menu-disabled::before { + color: #bbb; +} + +.context-menu-icon.context-menu-icon-loading:before { + -webkit-animation: cm-spin 2s infinite; + -o-animation: cm-spin 2s infinite; + animation: cm-spin 2s infinite; +} + +.context-menu-icon.context-menu-icon--fa { + display: list-item; + font-family: inherit; + line-height: inherit; +} +.context-menu-icon.context-menu-icon--fa::before { + position: absolute; + top: 50%; + left: 0; + width: 2em; + font-family: FontAwesome; + font-size: 1em; + font-style: normal; + font-weight: normal; + line-height: 1; + color: #2980b9; + text-align: center; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -o-transform: translateY(-50%); + transform: translateY(-50%); + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.context-menu-icon.context-menu-icon--fa.context-menu-hover:before { + color: #fff; +} +.context-menu-icon.context-menu-icon--fa.context-menu-disabled::before { + color: #bbb; +} + +.context-menu-icon.context-menu-icon--fa5 { + display: list-item; + font-family: inherit; + line-height: inherit; +} +.context-menu-icon.context-menu-icon--fa5 i, .context-menu-icon.context-menu-icon--fa5 svg { + position: absolute; + top: .3em; + left: .5em; + color: #2980b9; +} +.context-menu-icon.context-menu-icon--fa5.context-menu-hover > i, .context-menu-icon.context-menu-icon--fa5.context-menu-hover > svg { + color: #fff; +} +.context-menu-icon.context-menu-icon--fa5.context-menu-disabled i, .context-menu-icon.context-menu-icon--fa5.context-menu-disabled svg { + color: #bbb; +} + +.context-menu-list { + position: absolute; + display: inline-block; + min-width: 13em; + max-width: 26em; + padding: .25em 0; + margin: .3em; + font-family: inherit; + font-size: inherit; + list-style-type: none; + background: #fff; + border: 1px solid #bebebe; + border-radius: .2em; + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5); + box-shadow: 0 2px 5px rgba(0, 0, 0, .5); +} + +.context-menu-item { + position: relative; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + padding: .2em 2em; + color: #2f2f2f; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: #fff; +} + +.context-menu-separator { + padding: 0; + margin: .35em 0; + border-bottom: 1px solid #e6e6e6; +} + +.context-menu-item > label > input, +.context-menu-item > label > textarea { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.context-menu-item.context-menu-hover { + color: #fff; + cursor: pointer; + background-color: #2980b9; +} + +.context-menu-item.context-menu-disabled { + color: #bbb; + cursor: default; + background-color: #fff; +} + +.context-menu-input.context-menu-hover { + color: #2f2f2f; + cursor: default; +} + +.context-menu-submenu:after { + position: absolute; + top: 50%; + right: .5em; + z-index: 1; + width: 0; + height: 0; + content: ''; + border-color: transparent transparent transparent #2f2f2f; + border-style: solid; + border-width: .25em 0 .25em .25em; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -o-transform: translateY(-50%); + transform: translateY(-50%); +} + +/** + * Inputs + */ +.context-menu-item.context-menu-input { + padding: .3em .6em; +} + +/* vertically align inside labels */ +.context-menu-input > label > * { + vertical-align: top; +} + +/* position checkboxes and radios as icons */ +.context-menu-input > label > input[type="checkbox"], +.context-menu-input > label > input[type="radio"] { + position: relative; + top: .12em; + margin-right: .4em; +} + +.context-menu-input > label { + margin: 0; +} + +.context-menu-input > label, +.context-menu-input > label > input[type="text"], +.context-menu-input > label > textarea, +.context-menu-input > label > select { + display: block; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.context-menu-input > label > textarea { + height: 7em; +} + +.context-menu-item > .context-menu-list { + top: .3em; + /* re-positioned by js */ + right: -.3em; + display: none; +} + +.context-menu-item.context-menu-visible > .context-menu-list { + display: block; +} + +.context-menu-accesskey { + text-decoration: underline; +} diff --git a/src/main/resources/static/contextMenu/jquery.contextMenu.js b/src/main/resources/static/contextMenu/jquery.contextMenu.js new file mode 100644 index 0000000..2544173 --- /dev/null +++ b/src/main/resources/static/contextMenu/jquery.contextMenu.js @@ -0,0 +1,2128 @@ +/** + * jQuery contextMenu v2.8.0 - Plugin for simple contextMenu handling + * + * Version: v2.8.0 + * + * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://swisnl.github.io/jQuery-contextMenu/ + * + * Copyright (c) 2011-2019 SWIS BV and contributors + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * + * Date: 2019-01-16T15:45:48.370Z + */ + +// jscs:disable +/* jshint ignore:start */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node / CommonJS + factory(require('jquery')); + } else { + // Browser globals. + factory(jQuery); + } +})(function ($) { + + 'use strict'; + + // TODO: - + // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio + // create structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative + + // determine html5 compatibility + $.support.htmlMenuitem = ('HTMLMenuItemElement' in window); + $.support.htmlCommand = ('HTMLCommandElement' in window); + $.support.eventSelectstart = ('onselectstart' in document.documentElement); + /* // should the need arise, test for css user-select + $.support.cssUserSelect = (function(){ + var t = false, + e = document.createElement('div'); + + $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) { + var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect', + prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select'; + + e.style.cssText = prop + ': text;'; + if (e.style[propCC] == 'text') { + t = true; + return false; + } + + return true; + }); + + return t; + })(); + */ + + + if (!$.ui || !$.widget) { + // duck punch $.cleanData like jQueryUI does to get that remove event + $.cleanData = (function (orig) { + return function (elems) { + var events, elem, i; + for (i = 0; elems[i] != null; i++) { + elem = elems[i]; + try { + // Only trigger remove when necessary to save time + events = $._data(elem, 'events'); + if (events && events.remove) { + $(elem).triggerHandler('remove'); + } + + // Http://bugs.jquery.com/ticket/8235 + } catch (e) { + } + } + orig(elems); + }; + })($.cleanData); + } + /* jshint ignore:end */ + // jscs:enable + + var // currently active contextMenu trigger + $currentTrigger = null, + // is contextMenu initialized with at least one menu? + initialized = false, + // window handle + $win = $(window), + // number of registered menus + counter = 0, + // mapping selector to namespace + namespaces = {}, + // mapping namespace to options + menus = {}, + // custom command type handlers + types = {}, + // default values + defaults = { + // selector of contextMenu trigger + selector: null, + // where to append the menu to + appendTo: null, + // method to trigger context menu ["right", "left", "hover"] + trigger: 'right', + // hide menu when mouse leaves trigger / menu elements + autoHide: false, + // ms to wait before showing a hover-triggered context menu + delay: 200, + // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu + // as long as the trigger happened on one of the trigger-element's child nodes + reposition: true, + // Flag denoting if a second trigger should close the menu, as long as + // the trigger happened on one of the trigger-element's child nodes. + // This overrides the reposition option. + hideOnSecondTrigger: false, + + //ability to select submenu + selectableSubMenu: false, + + // Default classname configuration to be able avoid conflicts in frameworks + classNames: { + hover: 'context-menu-hover', // Item hover + disabled: 'context-menu-disabled', // Item disabled + visible: 'context-menu-visible', // Item visible + notSelectable: 'context-menu-not-selectable', // Item not selectable + + icon: 'context-menu-icon', + iconEdit: 'context-menu-icon-edit', + iconCut: 'context-menu-icon-cut', + iconCopy: 'context-menu-icon-copy', + iconPaste: 'context-menu-icon-paste', + iconDelete: 'context-menu-icon-delete', + iconAdd: 'context-menu-icon-add', + iconQuit: 'context-menu-icon-quit', + iconLoadingClass: 'context-menu-icon-loading' + }, + + // determine position to show menu at + determinePosition: function ($menu) { + // position to the lower middle of the trigger element + if ($.ui && $.ui.position) { + // .position() is provided as a jQuery UI utility + // (...and it won't work on hidden elements) + $menu.css('display', 'block').position({ + my: 'center top', + at: 'center bottom', + of: this, + offset: '0 5', + collision: 'fit' + }).css('display', 'none'); + } else { + // determine contextMenu position + var offset = this.offset(); + offset.top += this.outerHeight(); + offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2; + $menu.css(offset); + } + }, + // position menu + position: function (opt, x, y) { + var offset; + // determine contextMenu position + if (!x && !y) { + opt.determinePosition.call(this, opt.$menu); + return; + } else if (x === 'maintain' && y === 'maintain') { + // x and y must not be changed (after re-show on command click) + offset = opt.$menu.position(); + } else { + // x and y are given (by mouse event) + var offsetParentOffset = opt.$menu.offsetParent().offset(); + offset = {top: y - offsetParentOffset.top, left: x -offsetParentOffset.left}; + } + + // correct offset if viewport demands it + var bottom = $win.scrollTop() + $win.height(), + right = $win.scrollLeft() + $win.width(), + height = opt.$menu.outerHeight(), + width = opt.$menu.outerWidth(); + + if (offset.top + height > bottom) { + offset.top -= height; + } + + if (offset.top < 0) { + offset.top = 0; + } + + if (offset.left + width > right) { + offset.left -= width; + } + + if (offset.left < 0) { + offset.left = 0; + } + + opt.$menu.css(offset); + }, + // position the sub-menu + positionSubmenu: function ($menu) { + if (typeof $menu === 'undefined') { + // When user hovers over item (which has sub items) handle.focusItem will call this. + // but the submenu does not exist yet if opt.items is a promise. just return, will + // call positionSubmenu after promise is completed. + return; + } + if ($.ui && $.ui.position) { + // .position() is provided as a jQuery UI utility + // (...and it won't work on hidden elements) + $menu.css('display', 'block').position({ + my: 'left top-5', + at: 'right top', + of: this, + collision: 'flipfit fit' + }).css('display', ''); + } else { + // determine contextMenu position + var offset = { + top: -9, + left: this.outerWidth() - 5 + }; + $menu.css(offset); + } + }, + // offset to add to zIndex + zIndex: 1, + // show hide animation settings + animation: { + duration: 50, + show: 'slideDown', + hide: 'slideUp' + }, + // events + events: { + preShow: $.noop, + show: $.noop, + hide: $.noop, + activated: $.noop + }, + // default callback + callback: null, + // list of contextMenu items + items: {} + }, + // mouse position for hover activation + hoveract = { + timer: null, + pageX: null, + pageY: null + }, + // determine zIndex + zindex = function ($t) { + var zin = 0, + $tt = $t; + + while (true) { + zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0); + $tt = $tt.parent(); + if (!$tt || !$tt.length || 'html body'.indexOf($tt.prop('nodeName').toLowerCase()) > -1) { + break; + } + } + return zin; + }, + // event handlers + handle = { + // abort anything + abortevent: function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + }, + // contextmenu show dispatcher + contextmenu: function (e) { + var $this = $(this); + + //Show browser context-menu when preShow returns false + if (e.data.events.preShow($this,e) === false) { + return; + } + + // disable actual context-menu if we are using the right mouse button as the trigger + if (e.data.trigger === 'right') { + e.preventDefault(); + e.stopImmediatePropagation(); + } + + // abort native-triggered events unless we're triggering on right click + if ((e.data.trigger !== 'right' && e.data.trigger !== 'demand') && e.originalEvent) { + return; + } + + // Let the current contextmenu decide if it should show or not based on its own trigger settings + if (typeof e.mouseButton !== 'undefined' && e.data) { + if (!(e.data.trigger === 'left' && e.mouseButton === 0) && !(e.data.trigger === 'right' && e.mouseButton === 2)) { + // Mouse click is not valid. + return; + } + } + + // abort event if menu is visible for this trigger + if ($this.hasClass('context-menu-active')) { + return; + } + + if (!$this.hasClass('context-menu-disabled')) { + // theoretically need to fire a show event at + // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus + // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this }); + // e.data.$menu.trigger(evt); + + $currentTrigger = $this; + if (e.data.build) { + var built = e.data.build($currentTrigger, e); + // abort if build() returned false + if (built === false) { + return; + } + + // dynamically build menu on invocation + e.data = $.extend(true, {}, defaults, e.data, built || {}); + + // abort if there are no items to display + if (!e.data.items || $.isEmptyObject(e.data.items)) { + // Note: jQuery captures and ignores errors from event handlers + if (window.console) { + (console.error || console.log).call(console, 'No items specified to show in contextMenu'); + } + + throw new Error('No Items specified'); + } + + // backreference for custom command type creation + e.data.$trigger = $currentTrigger; + + op.create(e.data); + } + op.show.call($this, e.data, e.pageX, e.pageY); + } + }, + // contextMenu left-click trigger + click: function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + $(this).trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY})); + }, + // contextMenu right-click trigger + mousedown: function (e) { + // register mouse down + var $this = $(this); + + // hide any previous menus + if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) { + $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide'); + } + + // activate on right click + if (e.button === 2) { + $currentTrigger = $this.data('contextMenuActive', true); + } + }, + // contextMenu right-click trigger + mouseup: function (e) { + // show menu + var $this = $(this); + if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + $currentTrigger = $this; + $this.trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY})); + } + + $this.removeData('contextMenuActive'); + }, + // contextMenu hover trigger + mouseenter: function (e) { + var $this = $(this), + $related = $(e.relatedTarget), + $document = $(document); + + // abort if we're coming from a menu + if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { + return; + } + + // abort if a menu is shown + if ($currentTrigger && $currentTrigger.length) { + return; + } + + hoveract.pageX = e.pageX; + hoveract.pageY = e.pageY; + hoveract.data = e.data; + $document.on('mousemove.contextMenuShow', handle.mousemove); + hoveract.timer = setTimeout(function () { + hoveract.timer = null; + $document.off('mousemove.contextMenuShow'); + $currentTrigger = $this; + $this.trigger($.Event('contextmenu', { + data: hoveract.data, + pageX: hoveract.pageX, + pageY: hoveract.pageY + })); + }, e.data.delay); + }, + // contextMenu hover trigger + mousemove: function (e) { + hoveract.pageX = e.pageX; + hoveract.pageY = e.pageY; + }, + // contextMenu hover trigger + mouseleave: function (e) { + // abort if we're leaving for a menu + var $related = $(e.relatedTarget); + if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { + return; + } + + try { + clearTimeout(hoveract.timer); + } catch (e) { + } + + hoveract.timer = null; + }, + // click on layer to hide contextMenu + layerClick: function (e) { + var $this = $(this), + root = $this.data('contextMenuRoot'), + button = e.button, + x = e.pageX, + y = e.pageY, + fakeClick = x === undefined, + target, + offset; + + e.preventDefault(); + + setTimeout(function () { + // If the click is not real, things break: https://github.com/swisnl/jQuery-contextMenu/issues/132 + if(fakeClick){ + if (root !== null && typeof root !== 'undefined' && root.$menu !== null && typeof root.$menu !== 'undefined') { + root.$menu.trigger('contextmenu:hide'); + } + return; + } + + var $window; + var triggerAction = ((root.trigger === 'left' && button === 0) || (root.trigger === 'right' && button === 2)); + + // find the element that would've been clicked, wasn't the layer in the way + if (document.elementFromPoint && root.$layer) { + root.$layer.hide(); + target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop()); + + // also need to try and focus this element if we're in a contenteditable area, + // as the layer will prevent the browser mouse action we want + if (target.isContentEditable) { + var range = document.createRange(), + sel = window.getSelection(); + range.selectNode(target); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + } + $(target).trigger(e); + root.$layer.show(); + } + + if (root.hideOnSecondTrigger && triggerAction && root.$menu !== null && typeof root.$menu !== 'undefined') { + root.$menu.trigger('contextmenu:hide'); + return; + } + + if (root.reposition && triggerAction) { + if (document.elementFromPoint) { + if (root.$trigger.is(target)) { + root.position.call(root.$trigger, root, x, y); + return; + } + } else { + offset = root.$trigger.offset(); + $window = $(window); + // while this looks kinda awful, it's the best way to avoid + // unnecessarily calculating any positions + offset.top += $window.scrollTop(); + if (offset.top <= e.pageY) { + offset.left += $window.scrollLeft(); + if (offset.left <= e.pageX) { + offset.bottom = offset.top + root.$trigger.outerHeight(); + if (offset.bottom >= e.pageY) { + offset.right = offset.left + root.$trigger.outerWidth(); + if (offset.right >= e.pageX) { + // reposition + root.position.call(root.$trigger, root, x, y); + return; + } + } + } + } + } + } + + if (target && triggerAction) { + root.$trigger.one('contextmenu:hidden', function () { + $(target).contextMenu({x: x, y: y, button: button}); + }); + } + + if (root !== null && typeof root !== 'undefined' && root.$menu !== null && typeof root.$menu !== 'undefined') { + root.$menu.trigger('contextmenu:hide'); + } + }, 50); + }, + // key handled :hover + keyStop: function (e, opt) { + if (!opt.isInput) { + e.preventDefault(); + } + + e.stopPropagation(); + }, + key: function (e) { + + var opt = {}; + + // Only get the data from $currentTrigger if it exists + if ($currentTrigger) { + opt = $currentTrigger.data('contextMenu') || {}; + } + // If the trigger happen on a element that are above the contextmenu do this + if (typeof opt.zIndex === 'undefined') { + opt.zIndex = 0; + } + var targetZIndex = 0; + var getZIndexOfTriggerTarget = function (target) { + if (target.style.zIndex !== '') { + targetZIndex = target.style.zIndex; + } else { + if (target.offsetParent !== null && typeof target.offsetParent !== 'undefined') { + getZIndexOfTriggerTarget(target.offsetParent); + } + else if (target.parentElement !== null && typeof target.parentElement !== 'undefined') { + getZIndexOfTriggerTarget(target.parentElement); + } + } + }; + getZIndexOfTriggerTarget(e.target); + // If targetZIndex is heigher then opt.zIndex dont progress any futher. + // This is used to make sure that if you are using a dialog with a input / textarea / contenteditable div + // and its above the contextmenu it wont steal keys events + if (opt.$menu && parseInt(targetZIndex,10) > parseInt(opt.$menu.css("zIndex"),10)) { + return; + } + switch (e.keyCode) { + case 9: + case 38: // up + handle.keyStop(e, opt); + // if keyCode is [38 (up)] or [9 (tab) with shift] + if (opt.isInput) { + if (e.keyCode === 9 && e.shiftKey) { + e.preventDefault(); + if (opt.$selected) { + opt.$selected.find('input, textarea, select').blur(); + } + if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { + opt.$menu.trigger('prevcommand'); + } + return; + } else if (e.keyCode === 38 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') { + // checkboxes don't capture this key + e.preventDefault(); + return; + } + } else if (e.keyCode !== 9 || e.shiftKey) { + if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { + opt.$menu.trigger('prevcommand'); + } + return; + } + break; + // omitting break; + // case 9: // tab - reached through omitted break; + case 40: // down + handle.keyStop(e, opt); + if (opt.isInput) { + if (e.keyCode === 9) { + e.preventDefault(); + if (opt.$selected) { + opt.$selected.find('input, textarea, select').blur(); + } + if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { + opt.$menu.trigger('nextcommand'); + } + return; + } else if (e.keyCode === 40 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') { + // checkboxes don't capture this key + e.preventDefault(); + return; + } + } else { + if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { + opt.$menu.trigger('nextcommand'); + } + return; + } + break; + + case 37: // left + handle.keyStop(e, opt); + if (opt.isInput || !opt.$selected || !opt.$selected.length) { + break; + } + + if (!opt.$selected.parent().hasClass('context-menu-root')) { + var $parent = opt.$selected.parent().parent(); + opt.$selected.trigger('contextmenu:blur'); + opt.$selected = $parent; + return; + } + break; + + case 39: // right + handle.keyStop(e, opt); + if (opt.isInput || !opt.$selected || !opt.$selected.length) { + break; + } + + var itemdata = opt.$selected.data('contextMenu') || {}; + if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) { + opt.$selected = null; + itemdata.$selected = null; + itemdata.$menu.trigger('nextcommand'); + return; + } + break; + + case 35: // end + case 36: // home + if (opt.$selected && opt.$selected.find('input, textarea, select').length) { + return; + } else { + (opt.$selected && opt.$selected.parent() || opt.$menu) + .children(':not(.' + opt.classNames.disabled + ', .' + opt.classNames.notSelectable + ')')[e.keyCode === 36 ? 'first' : 'last']() + .trigger('contextmenu:focus'); + e.preventDefault(); + return; + } + break; + + case 13: // enter + handle.keyStop(e, opt); + if (opt.isInput) { + if (opt.$selected && !opt.$selected.is('textarea, select')) { + e.preventDefault(); + return; + } + break; + } + if (typeof opt.$selected !== 'undefined' && opt.$selected !== null) { + opt.$selected.trigger('mouseup'); + } + return; + + case 32: // space + case 33: // page up + case 34: // page down + // prevent browser from scrolling down while menu is visible + handle.keyStop(e, opt); + return; + + case 27: // esc + handle.keyStop(e, opt); + if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { + opt.$menu.trigger('contextmenu:hide'); + } + return; + + default: // 0-9, a-z + var k = (String.fromCharCode(e.keyCode)).toUpperCase(); + if (opt.accesskeys && opt.accesskeys[k]) { + // according to the specs accesskeys must be invoked immediately + opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu ? 'contextmenu:focus' : 'mouseup'); + return; + } + break; + } + // pass event to selected item, + // stop propagation to avoid endless recursion + e.stopPropagation(); + if (typeof opt.$selected !== 'undefined' && opt.$selected !== null) { + opt.$selected.trigger(e); + } + }, + // select previous possible command in menu + prevItem: function (e) { + e.stopPropagation(); + var opt = $(this).data('contextMenu') || {}; + var root = $(this).data('contextMenuRoot') || {}; + + // obtain currently selected menu + if (opt.$selected) { + var $s = opt.$selected; + opt = opt.$selected.parent().data('contextMenu') || {}; + opt.$selected = $s; + } + + var $children = opt.$menu.children(), + $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(), + $round = $prev; + + // skip disabled or hidden elements + while ($prev.hasClass(root.classNames.disabled) || $prev.hasClass(root.classNames.notSelectable) || $prev.is(':hidden')) { + if ($prev.prev().length) { + $prev = $prev.prev(); + } else { + $prev = $children.last(); + } + if ($prev.is($round)) { + // break endless loop + return; + } + } + + // leave current + if (opt.$selected) { + handle.itemMouseleave.call(opt.$selected.get(0), e); + } + + // activate next + handle.itemMouseenter.call($prev.get(0), e); + + // focus input + var $input = $prev.find('input, textarea, select'); + if ($input.length) { + $input.focus(); + } + }, + // select next possible command in menu + nextItem: function (e) { + e.stopPropagation(); + var opt = $(this).data('contextMenu') || {}; + var root = $(this).data('contextMenuRoot') || {}; + + // obtain currently selected menu + if (opt.$selected) { + var $s = opt.$selected; + opt = opt.$selected.parent().data('contextMenu') || {}; + opt.$selected = $s; + } + + var $children = opt.$menu.children(), + $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(), + $round = $next; + + // skip disabled + while ($next.hasClass(root.classNames.disabled) || $next.hasClass(root.classNames.notSelectable) || $next.is(':hidden')) { + if ($next.next().length) { + $next = $next.next(); + } else { + $next = $children.first(); + } + if ($next.is($round)) { + // break endless loop + return; + } + } + + // leave current + if (opt.$selected) { + handle.itemMouseleave.call(opt.$selected.get(0), e); + } + + // activate next + handle.itemMouseenter.call($next.get(0), e); + + // focus input + var $input = $next.find('input, textarea, select'); + if ($input.length) { + $input.focus(); + } + }, + // flag that we're inside an input so the key handler can act accordingly + focusInput: function () { + var $this = $(this).closest('.context-menu-item'), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + root.$selected = opt.$selected = $this; + root.isInput = opt.isInput = true; + }, + // flag that we're inside an input so the key handler can act accordingly + blurInput: function () { + var $this = $(this).closest('.context-menu-item'), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + root.isInput = opt.isInput = false; + }, + // :hover on menu + menuMouseenter: function () { + var root = $(this).data().contextMenuRoot; + root.hovering = true; + }, + // :hover on menu + menuMouseleave: function (e) { + var root = $(this).data().contextMenuRoot; + if (root.$layer && root.$layer.is(e.relatedTarget)) { + root.hovering = false; + } + }, + // :hover done manually so key handling is possible + itemMouseenter: function (e) { + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + root.hovering = true; + + // abort if we're re-entering + if (e && root.$layer && root.$layer.is(e.relatedTarget)) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + + // make sure only one item is selected + (opt.$menu ? opt : root).$menu + .children('.' + root.classNames.hover).trigger('contextmenu:blur') + .children('.hover').trigger('contextmenu:blur'); + + if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) { + opt.$selected = null; + return; + } + + + $this.trigger('contextmenu:focus'); + }, + // :hover done manually so key handling is possible + itemMouseleave: function (e) { + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) { + if (typeof root.$selected !== 'undefined' && root.$selected !== null) { + root.$selected.trigger('contextmenu:blur'); + } + e.preventDefault(); + e.stopImmediatePropagation(); + root.$selected = opt.$selected = opt.$node; + return; + } + + if(opt && opt.$menu && opt.$menu.hasClass('context-menu-visible')){ + return; + } + + $this.trigger('contextmenu:blur'); + }, + // contextMenu item click + itemClick: function (e) { + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot, + key = data.contextMenuKey, + callback; + + // abort if the key is unknown or disabled or is a menu + if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-separator, .' + root.classNames.notSelectable) || ($this.is('.context-menu-submenu') && root.selectableSubMenu === false )) { + return; + } + + e.preventDefault(); + e.stopImmediatePropagation(); + + if ($.isFunction(opt.callbacks[key]) && Object.prototype.hasOwnProperty.call(opt.callbacks, key)) { + // item-specific callback + callback = opt.callbacks[key]; + } else if ($.isFunction(root.callback)) { + // default callback + callback = root.callback; + } else { + // no callback, no action + return; + } + + // hide menu if callback doesn't stop that + if (callback.call(root.$trigger, key, root, e) !== false) { + root.$menu.trigger('contextmenu:hide'); + } else if (root.$menu.parent().length) { + op.update.call(root.$trigger, root); + } + }, + // ignore click events on input elements + inputClick: function (e) { + e.stopImmediatePropagation(); + }, + // hide + hideMenu: function (e, data) { + var root = $(this).data('contextMenuRoot'); + op.hide.call(root.$trigger, root, data && data.force); + }, + // focus + focusItem: function (e) { + e.stopPropagation(); + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) { + return; + } + + $this + .addClass([root.classNames.hover, root.classNames.visible].join(' ')) + // select other items and included items + .parent().find('.context-menu-item').not($this) + .removeClass(root.classNames.visible) + .filter('.' + root.classNames.hover) + .trigger('contextmenu:blur'); + + // remember selected + opt.$selected = root.$selected = $this; + + + if(opt && opt.$node && opt.$node.hasClass('context-menu-submenu')){ + opt.$node.addClass(root.classNames.hover); + } + + // position sub-menu - do after show so dumb $.ui.position can keep up + if (opt.$node) { + root.positionSubmenu.call(opt.$node, opt.$menu); + } + }, + // blur + blurItem: function (e) { + e.stopPropagation(); + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + if (opt.autoHide) { // for tablets and touch screens this needs to remain + $this.removeClass(root.classNames.visible); + } + $this.removeClass(root.classNames.hover); + opt.$selected = null; + } + }, + // operations + op = { + show: function (opt, x, y) { + var $trigger = $(this), + css = {}; + + // hide any open menus + $('#context-menu-layer').trigger('mousedown'); + + // backreference for callbacks + opt.$trigger = $trigger; + + // show event + if (opt.events.show.call($trigger, opt) === false) { + $currentTrigger = null; + return; + } + + // create or update context menu + var hasVisibleItems = op.update.call($trigger, opt); + if (hasVisibleItems === false) { + $currentTrigger = null; + return; + } + + // position menu + opt.position.call($trigger, opt, x, y); + + // make sure we're in front + if (opt.zIndex) { + var additionalZValue = opt.zIndex; + // If opt.zIndex is a function, call the function to get the right zIndex. + if (typeof opt.zIndex === 'function') { + additionalZValue = opt.zIndex.call($trigger, opt); + } + css.zIndex = zindex($trigger) + additionalZValue; + } + + // add layer + op.layer.call(opt.$menu, opt, css.zIndex); + + // adjust sub-menu zIndexes + opt.$menu.find('ul').css('zIndex', css.zIndex + 1); + + // position and show context menu + opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () { + $trigger.trigger('contextmenu:visible'); + + op.activated(opt); + opt.events.activated(opt); + }); + // make options available and set state + $trigger + .data('contextMenu', opt) + .addClass('context-menu-active'); + + // register key handler + $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key); + // register autoHide handler + if (opt.autoHide) { + // mouse position handler + $(document).on('mousemove.contextMenuAutoHide', function (e) { + // need to capture the offset on mousemove, + // since the page might've been scrolled since activation + var pos = $trigger.offset(); + pos.right = pos.left + $trigger.outerWidth(); + pos.bottom = pos.top + $trigger.outerHeight(); + + if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) { + /* Additional hover check after short time, you might just miss the edge of the menu */ + setTimeout(function () { + if (!opt.hovering && opt.$menu !== null && typeof opt.$menu !== 'undefined') { + opt.$menu.trigger('contextmenu:hide'); + } + }, 50); + } + }); + } + }, + hide: function (opt, force) { + var $trigger = $(this); + if (!opt) { + opt = $trigger.data('contextMenu') || {}; + } + + // hide event + if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) { + return; + } + + // remove options and revert state + $trigger + .removeData('contextMenu') + .removeClass('context-menu-active'); + + if (opt.$layer) { + // keep layer for a bit so the contextmenu event can be aborted properly by opera + setTimeout((function ($layer) { + return function () { + $layer.remove(); + }; + })(opt.$layer), 10); + + try { + delete opt.$layer; + } catch (e) { + opt.$layer = null; + } + } + + // remove handle + $currentTrigger = null; + // remove selected + opt.$menu.find('.' + opt.classNames.hover).trigger('contextmenu:blur'); + opt.$selected = null; + // collapse all submenus + opt.$menu.find('.' + opt.classNames.visible).removeClass(opt.classNames.visible); + // unregister key and mouse handlers + // $(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705 + $(document).off('.contextMenuAutoHide').off('keydown.contextMenu'); + // hide menu + if (opt.$menu) { + opt.$menu[opt.animation.hide](opt.animation.duration, function () { + // tear down dynamically built menu after animation is completed. + if (opt.build) { + opt.$menu.remove(); + $.each(opt, function (key) { + switch (key) { + case 'ns': + case 'selector': + case 'build': + case 'trigger': + return true; + + default: + opt[key] = undefined; + try { + delete opt[key]; + } catch (e) { + } + return true; + } + }); + } + + setTimeout(function () { + $trigger.trigger('contextmenu:hidden'); + }, 10); + }); + } + }, + create: function (opt, root) { + if (typeof root === 'undefined') { + root = opt; + } + + // create contextMenu + opt.$menu = $('
    ').addClass(opt.className || '').data({ + 'contextMenu': opt, + 'contextMenuRoot': root + }); + + $.each(['callbacks', 'commands', 'inputs'], function (i, k) { + opt[k] = {}; + if (!root[k]) { + root[k] = {}; + } + }); + + if (!root.accesskeys) { + root.accesskeys = {}; + } + + function createNameNode(item) { + var $name = $(''); + if (item._accesskey) { + if (item._beforeAccesskey) { + $name.append(document.createTextNode(item._beforeAccesskey)); + } + $('') + .addClass('context-menu-accesskey') + .text(item._accesskey) + .appendTo($name); + if (item._afterAccesskey) { + $name.append(document.createTextNode(item._afterAccesskey)); + } + } else { + if (item.isHtmlName) { + // restrict use with access keys + if (typeof item.accesskey !== 'undefined') { + throw new Error('accesskeys are not compatible with HTML names and cannot be used together in the same item'); + } + $name.html(item.name); + } else { + $name.text(item.name); + } + } + return $name; + } + + // create contextMenu items + $.each(opt.items, function (key, item) { + var $t = $('
  • ').addClass(item.className || ''), + $label = null, + $input = null; + + // iOS needs to see a click-event bound to an element to actually + // have the TouchEvents infrastructure trigger the click event + $t.on('click', $.noop); + + // Make old school string seperator a real item so checks wont be + // akward later. + // And normalize 'cm_separator' into 'cm_seperator'. + if (typeof item === 'string' || item.type === 'cm_separator') { + item = {type: 'cm_seperator'}; + } + + item.$node = $t.data({ + 'contextMenu': opt, + 'contextMenuRoot': root, + 'contextMenuKey': key + }); + + // register accesskey + // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that + if (typeof item.accesskey !== 'undefined') { + var aks = splitAccesskey(item.accesskey); + for (var i = 0, ak; ak = aks[i]; i++) { + if (!root.accesskeys[ak]) { + root.accesskeys[ak] = item; + var matched = item.name.match(new RegExp('^(.*?)(' + ak + ')(.*)$', 'i')); + if (matched) { + item._beforeAccesskey = matched[1]; + item._accesskey = matched[2]; + item._afterAccesskey = matched[3]; + } + break; + } + } + } + + if (item.type && types[item.type]) { + // run custom type handler + types[item.type].call($t, item, opt, root); + // register commands + $.each([opt, root], function (i, k) { + k.commands[key] = item; + // Overwrite only if undefined or the item is appended to the root. This so it + // doesn't overwrite callbacks of root elements if the name is the same. + if ($.isFunction(item.callback) && (typeof k.callbacks[key] === 'undefined' || typeof opt.type === 'undefined')) { + k.callbacks[key] = item.callback; + } + }); + } else { + // add label for input + if (item.type === 'cm_seperator') { + $t.addClass('context-menu-separator ' + root.classNames.notSelectable); + } else if (item.type === 'html') { + $t.addClass('context-menu-html ' + root.classNames.notSelectable); + } else if (item.type !== 'sub' && item.type) { + $label = $('').appendTo($t); + createNameNode(item).appendTo($label); + + $t.addClass('context-menu-input'); + opt.hasTypes = true; + $.each([opt, root], function (i, k) { + k.commands[key] = item; + k.inputs[key] = item; + }); + } else if (item.items) { + item.type = 'sub'; + } + + switch (item.type) { + case 'cm_seperator': + break; + + case 'text': + $input = $('') + .attr('name', 'context-menu-input-' + key) + .val(item.value || '') + .appendTo($label); + break; + + case 'textarea': + $input = $('') + .attr('name', 'context-menu-input-' + key) + .val(item.value || '') + .appendTo($label); + + if (item.height) { + $input.height(item.height); + } + break; + + case 'checkbox': + $input = $('') + .attr('name', 'context-menu-input-' + key) + .val(item.value || '') + .prop('checked', !!item.selected) + .prependTo($label); + break; + + case 'radio': + $input = $('') + .attr('name', 'context-menu-input-' + item.radio) + .val(item.value || '') + .prop('checked', !!item.selected) + .prependTo($label); + break; + + case 'select': + $input = $('') + .attr('name', 'context-menu-input-' + key) + .appendTo($label); + if (item.options) { + $.each(item.options, function (value, text) { + $('').val(value).text(text).appendTo($input); + }); + $input.val(item.selected); + } + break; + + case 'sub': + createNameNode(item).appendTo($t); + item.appendTo = item.$node; + $t.data('contextMenu', item).addClass('context-menu-submenu'); + item.callback = null; + + // If item contains items, and this is a promise, we should create it later + // check if subitems is of type promise. If it is a promise we need to create + // it later, after promise has been resolved. + if ('function' === typeof item.items.then) { + // probably a promise, process it, when completed it will create the sub menu's. + op.processPromises(item, root, item.items); + } else { + // normal submenu. + op.create(item, root); + } + break; + + case 'html': + $(item.html).appendTo($t); + break; + + default: + $.each([opt, root], function (i, k) { + k.commands[key] = item; + // Overwrite only if undefined or the item is appended to the root. This so it + // doesn't overwrite callbacks of root elements if the name is the same. + if ($.isFunction(item.callback) && (typeof k.callbacks[key] === 'undefined' || typeof opt.type === 'undefined')) { + k.callbacks[key] = item.callback; + } + }); + createNameNode(item).appendTo($t); + break; + } + + // disable key listener in + if (item.type && item.type !== 'sub' && item.type !== 'html' && item.type !== 'cm_seperator') { + $input + .on('focus', handle.focusInput) + .on('blur', handle.blurInput); + + if (item.events) { + $input.on(item.events, opt); + } + } + + // add icons + if (item.icon) { + if ($.isFunction(item.icon)) { + item._icon = item.icon.call(this, this, $t, key, item); + } else { + if (typeof(item.icon) === 'string' && ( + item.icon.substring(0, 4) === 'fab ' + || item.icon.substring(0, 4) === 'fas ' + || item.icon.substring(0, 4) === 'far ' + || item.icon.substring(0, 4) === 'fal ') + ) { + // to enable font awesome + $t.addClass(root.classNames.icon + ' ' + root.classNames.icon + '--fa5'); + item._icon = $(''); + } else if (typeof(item.icon) === 'string' && item.icon.substring(0, 3) === 'fa-') { + item._icon = root.classNames.icon + ' ' + root.classNames.icon + '--fa fa ' + item.icon; + } else { + item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon; + } + } + + if(typeof(item._icon) === "string"){ + $t.addClass(item._icon); + } else { + $t.prepend(item._icon); + } + } + } + + // cache contained elements + item.$input = $input; + item.$label = $label; + + // attach item to menu + $t.appendTo(opt.$menu); + + // Disable text selection + if (!opt.hasTypes && $.support.eventSelectstart) { + // browsers support user-select: none, + // IE has a special event for text-selection + // browsers supporting neither will not be preventing text-selection + $t.on('selectstart.disableTextSelect', handle.abortevent); + } + }); + // attach contextMenu to (to bypass any possible overflow:hidden issues on parents of the trigger element) + if (!opt.$node) { + opt.$menu.css('display', 'none').addClass('context-menu-root'); + } + opt.$menu.appendTo(opt.appendTo || document.body); + }, + resize: function ($menu, nested) { + var domMenu; + // determine widths of submenus, as CSS won't grow them automatically + // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100; + // kinda sucks hard... + + // determine width of absolutely positioned element + $menu.css({position: 'absolute', display: 'block'}); + // don't apply yet, because that would break nested elements' widths + $menu.data('width', + (domMenu = $menu.get(0)).getBoundingClientRect ? + Math.ceil(domMenu.getBoundingClientRect().width) : + $menu.outerWidth() + 1); // outerWidth() returns rounded pixels + // reset styles so they allow nested elements to grow/shrink naturally + $menu.css({ + position: 'static', + minWidth: '0px', + maxWidth: '100000px' + }); + // identify width of nested menus + $menu.find('> li > ul').each(function () { + op.resize($(this), true); + }); + // reset and apply changes in the end because nested + // elements' widths wouldn't be calculatable otherwise + if (!nested) { + $menu.find('ul').addBack().css({ + position: '', + display: '', + minWidth: '', + maxWidth: '' + }).outerWidth(function () { + return $(this).data('width'); + }); + } + }, + update: function (opt, root) { + var $trigger = this; + if (typeof root === 'undefined') { + root = opt; + op.resize(opt.$menu); + } + + var hasVisibleItems = false; + + // re-check disabled for each item + opt.$menu.children().each(function () { + var $item = $(this), + key = $item.data('contextMenuKey'), + item = opt.items[key], + disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true, + visible; + if ($.isFunction(item.visible)) { + visible = item.visible.call($trigger, key, root); + } else if (typeof item.visible !== 'undefined') { + visible = item.visible === true; + } else { + visible = true; + } + + if (visible) { + hasVisibleItems = true; + } + + $item[visible ? 'show' : 'hide'](); + + // dis- / enable item + $item[disabled ? 'addClass' : 'removeClass'](root.classNames.disabled); + + if ($.isFunction(item.icon)) { + $item.removeClass(item._icon); + var iconResult = item.icon.call(this, $trigger, $item, key, item); + if(typeof(iconResult) === "string"){ + $item.addClass(iconResult); + } else { + $item.prepend(iconResult); + } + } + + if (item.type) { + // dis- / enable input elements + $item.find('input, select, textarea').prop('disabled', disabled); + + // update input states + switch (item.type) { + case 'text': + case 'textarea': + item.$input.val(item.value || ''); + break; + + case 'checkbox': + case 'radio': + item.$input.val(item.value || '').prop('checked', !!item.selected); + break; + + case 'select': + item.$input.val((item.selected === 0 ? "0" : item.selected) || ''); + break; + } + } + + if (item.$menu) { + // update sub-menu + var subMenuHasVisibleItems = op.update.call($trigger, item, root); + if (subMenuHasVisibleItems) { + hasVisibleItems = true; + } + } + }); + return hasVisibleItems; + }, + layer: function (opt, zIndex) { + // add transparent layer for click area + // filter and background for Internet Explorer, Issue #23 + var $layer = opt.$layer = $('
    ') + .css({ + height: $win.height(), + width: $win.width(), + display: 'block', + position: 'fixed', + 'z-index': zIndex, + top: 0, + left: 0, + opacity: 0, + filter: 'alpha(opacity=0)', + 'background-color': '#000' + }) + .data('contextMenuRoot', opt) + .insertBefore(this) + .on('contextmenu', handle.abortevent) + .on('mousedown', handle.layerClick); + + // IE6 doesn't know position:fixed; + if (typeof document.body.style.maxWidth === 'undefined') { // IE6 doesn't support maxWidth + $layer.css({ + 'position': 'absolute', + 'height': $(document).height() + }); + } + + return $layer; + }, + processPromises: function (opt, root, promise) { + // Start + opt.$node.addClass(root.classNames.iconLoadingClass); + + function completedPromise(opt, root, items) { + // Completed promise (dev called promise.resolve). We now have a list of items which can + // be used to create the rest of the context menu. + if (typeof items === 'undefined') { + // Null result, dev should have checked + errorPromise(undefined);//own error object + } + finishPromiseProcess(opt, root, items); + } + + function errorPromise(opt, root, errorItem) { + // User called promise.reject() with an error item, if not, provide own error item. + if (typeof errorItem === 'undefined') { + errorItem = { + "error": { + name: "No items and no error item", + icon: "context-menu-icon context-menu-icon-quit" + } + }; + if (window.console) { + (console.error || console.log).call(console, 'When you reject a promise, provide an "items" object, equal to normal sub-menu items'); + } + } else if (typeof errorItem === 'string') { + errorItem = {"error": {name: errorItem}}; + } + finishPromiseProcess(opt, root, errorItem); + } + + function finishPromiseProcess(opt, root, items) { + if (typeof root.$menu === 'undefined' || !root.$menu.is(':visible')) { + return; + } + opt.$node.removeClass(root.classNames.iconLoadingClass); + opt.items = items; + op.create(opt, root, true); // Create submenu + op.update(opt, root); // Correctly update position if user is already hovered over menu item + root.positionSubmenu.call(opt.$node, opt.$menu); // positionSubmenu, will only do anything if user already hovered over menu item that just got new subitems. + } + + // Wait for promise completion. .then(success, error, notify) (we don't track notify). Bind the opt + // and root to avoid scope problems + promise.then(completedPromise.bind(this, opt, root), errorPromise.bind(this, opt, root)); + }, + // operation that will run after contextMenu showed on screen + activated: function(opt){ + var $menu = opt.$menu; + var $menuOffset = $menu.offset(); + var winHeight = $(window).height(); + var winScrollTop = $(window).scrollTop(); + var menuHeight = $menu.height(); + if(menuHeight > winHeight){ + $menu.css({ + 'height' : winHeight + 'px', + 'overflow-x': 'hidden', + 'overflow-y': 'auto', + 'top': winScrollTop + 'px' + }); + } else if(($menuOffset.top < winScrollTop) || ($menuOffset.top + menuHeight > winScrollTop + winHeight)){ + $menu.css({ + 'top': winScrollTop + 'px' + }); + } + } + }; + + // split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key + function splitAccesskey(val) { + var t = val.split(/\s+/); + var keys = []; + + for (var i = 0, k; k = t[i]; i++) { + k = k.charAt(0).toUpperCase(); // first character only + // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it. + // a map to look up already used access keys would be nice + keys.push(k); + } + + return keys; + } + +// handle contextMenu triggers + $.fn.contextMenu = function (operation) { + var $t = this, $o = operation; + if (this.length > 0) { // this is not a build on demand menu + if (typeof operation === 'undefined') { + this.first().trigger('contextmenu'); + } else if (typeof operation.x !== 'undefined' && typeof operation.y !== 'undefined') { + this.first().trigger($.Event('contextmenu', { + pageX: operation.x, + pageY: operation.y, + mouseButton: operation.button + })); + } else if (operation === 'hide') { + var $menu = this.first().data('contextMenu') ? this.first().data('contextMenu').$menu : null; + if ($menu) { + $menu.trigger('contextmenu:hide'); + } + } else if (operation === 'destroy') { + $.contextMenu('destroy', {context: this}); + } else if ($.isPlainObject(operation)) { + operation.context = this; + $.contextMenu('create', operation); + } else if (operation) { + this.removeClass('context-menu-disabled'); + } else if (!operation) { + this.addClass('context-menu-disabled'); + } + } else { + $.each(menus, function () { + if (this.selector === $t.selector) { + $o.data = this; + + $.extend($o.data, {trigger: 'demand'}); + } + }); + + handle.contextmenu.call($o.target, $o); + } + + return this; + }; + + // manage contextMenu instances + $.contextMenu = function (operation, options) { + if (typeof operation !== 'string') { + options = operation; + operation = 'create'; + } + + if (typeof options === 'string') { + options = {selector: options}; + } else if (typeof options === 'undefined') { + options = {}; + } + + // merge with default options + var o = $.extend(true, {}, defaults, options || {}); + var $document = $(document); + var $context = $document; + var _hasContext = false; + + if (!o.context || !o.context.length) { + o.context = document; + } else { + // you never know what they throw at you... + $context = $(o.context).first(); + o.context = $context.get(0); + _hasContext = !$(o.context).is(document); + } + + switch (operation) { + + case 'update': + // Updates visibility and such + if(_hasContext){ + op.update($context); + } else { + for(var menu in menus){ + if(menus.hasOwnProperty(menu)){ + op.update(menus[menu]); + } + } + } + break; + + case 'create': + // no selector no joy + if (!o.selector) { + throw new Error('No selector specified'); + } + // make sure internal classes are not bound to + if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) { + throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className'); + } + if (!o.build && (!o.items || $.isEmptyObject(o.items))) { + throw new Error('No Items specified'); + } + counter++; + o.ns = '.contextMenu' + counter; + if (!_hasContext) { + namespaces[o.selector] = o.ns; + } + menus[o.ns] = o; + + // default to right click + if (!o.trigger) { + o.trigger = 'right'; + } + + if (!initialized) { + var itemClick = o.itemClickEvent === 'click' ? 'click.contextMenu' : 'mouseup.contextMenu'; + var contextMenuItemObj = { + // 'mouseup.contextMenu': handle.itemClick, + // 'click.contextMenu': handle.itemClick, + 'contextmenu:focus.contextMenu': handle.focusItem, + 'contextmenu:blur.contextMenu': handle.blurItem, + 'contextmenu.contextMenu': handle.abortevent, + 'mouseenter.contextMenu': handle.itemMouseenter, + 'mouseleave.contextMenu': handle.itemMouseleave + }; + contextMenuItemObj[itemClick] = handle.itemClick; + // make sure item click is registered first + $document + .on({ + 'contextmenu:hide.contextMenu': handle.hideMenu, + 'prevcommand.contextMenu': handle.prevItem, + 'nextcommand.contextMenu': handle.nextItem, + 'contextmenu.contextMenu': handle.abortevent, + 'mouseenter.contextMenu': handle.menuMouseenter, + 'mouseleave.contextMenu': handle.menuMouseleave + }, '.context-menu-list') + .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick) + .on(contextMenuItemObj, '.context-menu-item'); + + initialized = true; + } + + // engage native contextmenu event + $context + .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu); + + if (_hasContext) { + // add remove hook, just in case + $context.on('remove' + o.ns, function () { + $(this).contextMenu('destroy'); + }); + } + + switch (o.trigger) { + case 'hover': + $context + .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter) + .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave); + break; + + case 'left': + $context.on('click' + o.ns, o.selector, o, handle.click); + break; + case 'touchstart': + $context.on('touchstart' + o.ns, o.selector, o, handle.click); + break; + /* + default: + // http://www.quirksmode.org/dom/events/contextmenu.html + $document + .on('mousedown' + o.ns, o.selector, o, handle.mousedown) + .on('mouseup' + o.ns, o.selector, o, handle.mouseup); + break; + */ + } + + // create menu + if (!o.build) { + op.create(o); + } + break; + + case 'destroy': + var $visibleMenu; + if (_hasContext) { + // get proper options + var context = o.context; + $.each(menus, function (ns, o) { + + if (!o) { + return true; + } + + // Is this menu equest to the context called from + if (!$(context).is(o.selector)) { + return true; + } + + $visibleMenu = $('.context-menu-list').filter(':visible'); + if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) { + $visibleMenu.trigger('contextmenu:hide', {force: true}); + } + + try { + if (menus[o.ns].$menu) { + menus[o.ns].$menu.remove(); + } + + delete menus[o.ns]; + } catch (e) { + menus[o.ns] = null; + } + + $(o.context).off(o.ns); + + return true; + }); + } else if (!o.selector) { + $document.off('.contextMenu .contextMenuAutoHide'); + $.each(menus, function (ns, o) { + $(o.context).off(o.ns); + }); + + namespaces = {}; + menus = {}; + counter = 0; + initialized = false; + + $('#context-menu-layer, .context-menu-list').remove(); + } else if (namespaces[o.selector]) { + $visibleMenu = $('.context-menu-list').filter(':visible'); + if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) { + $visibleMenu.trigger('contextmenu:hide', {force: true}); + } + + try { + if (menus[namespaces[o.selector]].$menu) { + menus[namespaces[o.selector]].$menu.remove(); + } + + delete menus[namespaces[o.selector]]; + } catch (e) { + menus[namespaces[o.selector]] = null; + } + + $document.off(namespaces[o.selector]); + } + break; + + case 'html5': + // if and are not handled by the browser, + // or options was a bool true, + // initialize $.contextMenu for them + if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options === 'boolean' && options)) { + $('menu[type="context"]').each(function () { + if (this.id) { + $.contextMenu({ + selector: '[contextmenu=' + this.id + ']', + items: $.contextMenu.fromMenu(this) + }); + } + }).css('display', 'none'); + } + break; + + default: + throw new Error('Unknown operation "' + operation + '"'); + } + + return this; + }; + +// import values into commands + $.contextMenu.setInputValues = function (opt, data) { + if (typeof data === 'undefined') { + data = {}; + } + + $.each(opt.inputs, function (key, item) { + switch (item.type) { + case 'text': + case 'textarea': + item.value = data[key] || ''; + break; + + case 'checkbox': + item.selected = data[key] ? true : false; + break; + + case 'radio': + item.selected = (data[item.radio] || '') === item.value; + break; + + case 'select': + item.selected = data[key] || ''; + break; + } + }); + }; + +// export values from commands + $.contextMenu.getInputValues = function (opt, data) { + if (typeof data === 'undefined') { + data = {}; + } + + $.each(opt.inputs, function (key, item) { + switch (item.type) { + case 'text': + case 'textarea': + case 'select': + data[key] = item.$input.val(); + break; + + case 'checkbox': + data[key] = item.$input.prop('checked'); + break; + + case 'radio': + if (item.$input.prop('checked')) { + data[item.radio] = item.value; + } + break; + } + }); + + return data; + }; + +// find