mirror of
https://github.com/dev-claw/vripper-project.git
synced 2026-08-19 08:35:41 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab80e2f3b3 | ||
|
|
2ef082f87e | ||
|
|
a1b97e76a7 | ||
|
|
185a078b4d | ||
|
|
28a4c944ee | ||
|
|
23d3553d99 | ||
|
|
fb09e4c46b | ||
|
|
4feb6f5fe1 | ||
|
|
df1b9efbbb | ||
|
|
553924fa1d | ||
|
|
a90dbd568f | ||
|
|
ca7ca1ce8e | ||
|
|
b3ab239a1e | ||
|
|
6e212c791c |
@@ -1,5 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## [2.5.1] - 2019-12-09
|
||||
### Changed
|
||||
- Disable download button when nothing selected in Link collector grab menu
|
||||
### Added
|
||||
- Add hosts column in Link collector grab menu
|
||||
|
||||
## [2.5.0] - 2019-12-08
|
||||
### Changed
|
||||
- Change loading screen
|
||||
- UI Fixes
|
||||
- Fix bug with previews
|
||||
### Added
|
||||
- Add link collector feature
|
||||
- Add support for multiple links at a time
|
||||
|
||||
## [2.4.1] - 2019-11-26
|
||||
### Changed
|
||||
- Increase attempts for establishing ws connection between front and back end
|
||||
- Change default download folder to Home folder (installer), App folder (portable)
|
||||
|
||||
## [2.4.0] - 2019-11-23
|
||||
### Changed
|
||||
- Pimpandhost Host support
|
||||
|
||||
## [2.3.2] - 2019-11-23
|
||||
### Changed
|
||||
- Fix a bug with the scan component
|
||||
|
||||
## [2.3.1] - 2019-11-22
|
||||
### Changed
|
||||
- Fix connexion reset issue
|
||||
- Fix Post Status when autostart option is selected
|
||||
- Add stop all button
|
||||
|
||||
## [2.3.0] - 2019-11-21
|
||||
### Changed
|
||||
- UI enhancements and overhaul
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.3.0</version>
|
||||
<version>2.5.1</version>
|
||||
<packaging>pom</packaging>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
Generated
+1
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vripper-electron",
|
||||
"version": "2.3.0",
|
||||
"version": "2.5.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -644,11 +644,6 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"command-exists": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.8.tgz",
|
||||
"integrity": "sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vripper-electron",
|
||||
"version": "2.3.0",
|
||||
"version": "2.5.1",
|
||||
"description": "A ripper for vipergirls.to built using web technolgies",
|
||||
"main": "main.js",
|
||||
"author": "death-claw <53543762+death-claw@users.noreply.github.com>",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.3.0</version>
|
||||
<version>2.5.1</version>
|
||||
</parent>
|
||||
<artifactId>vripper-electron</artifactId>
|
||||
<name>vripper-electron</name>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.3.0</version>
|
||||
<version>2.5.1</version>
|
||||
</parent>
|
||||
<artifactId>vripper-server</artifactId>
|
||||
<name>vripper-server</name>
|
||||
@@ -67,6 +67,11 @@
|
||||
<artifactId>failsafe</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>28.1-jre</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper-ui</artifactId>
|
||||
|
||||
@@ -27,6 +27,8 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Iterator;
|
||||
|
||||
@@ -196,6 +198,20 @@ abstract public class Host {
|
||||
}
|
||||
}
|
||||
|
||||
protected String appendUri(String uri, String appendQuery) throws URISyntaxException {
|
||||
URI oldUri = new URI(uri);
|
||||
|
||||
String newQuery = oldUri.getQuery();
|
||||
if (newQuery == null) {
|
||||
newQuery = appendQuery;
|
||||
} else {
|
||||
newQuery += "&" + appendQuery;
|
||||
}
|
||||
|
||||
return new URI(oldUri.getScheme(), oldUri.getAuthority(),
|
||||
oldUri.getPath(), newQuery, oldUri.getFragment()).toString();
|
||||
}
|
||||
|
||||
private void setImageRequest(final ImageFileData imageFileData) {
|
||||
HttpGet httpGet = cm.buildHttpGet(imageFileData.getImageUrl());
|
||||
httpGet.addHeader("Referer", imageFileData.getPageUrl());
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package tn.mnlr.vripper.host;
|
||||
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import tn.mnlr.vripper.exception.HostException;
|
||||
import tn.mnlr.vripper.exception.XpathException;
|
||||
import tn.mnlr.vripper.q.ImageFileData;
|
||||
import tn.mnlr.vripper.services.ConnectionManager;
|
||||
|
||||
@Service
|
||||
public class PimpandhostHost extends Host {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PimpandhostHost.class);
|
||||
|
||||
private static final String host = "pimpandhost.com";
|
||||
private static final String IMG_XPATH = "//img[contains(@class, 'original')]";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLookup() {
|
||||
return host;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setNameAndUrl(final String _url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
|
||||
|
||||
String url;
|
||||
try {
|
||||
url = appendUri(_url.replace("http://", "https://"), "size=original");
|
||||
} catch (Exception e) {
|
||||
throw new HostException(e);
|
||||
}
|
||||
|
||||
Response resp = getResponse(url, context);
|
||||
Document doc = resp.getDocument();
|
||||
|
||||
Node imgNode;
|
||||
try {
|
||||
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
|
||||
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
|
||||
} catch (XpathException e) {
|
||||
throw new HostException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(String.format("Resolving name and image url for %s", url));
|
||||
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
|
||||
String imgUrl = "https:" + imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
|
||||
|
||||
imageFileData.setImageUrl(imgUrl);
|
||||
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
|
||||
} catch (Exception e) {
|
||||
throw new HostException("Unexpected error occurred", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import lombok.Setter;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tn.mnlr.vripper.exception.ValidationException;
|
||||
|
||||
@@ -23,6 +24,9 @@ import java.util.prefs.Preferences;
|
||||
@Setter
|
||||
public class AppSettingsService {
|
||||
|
||||
@Value("${base.dir}")
|
||||
private String defaultDownloadPath;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AppSettingsService.class);
|
||||
|
||||
private Preferences prefs = Preferences.userNodeForPackage(AppSettingsService.class);
|
||||
@@ -65,7 +69,7 @@ public class AppSettingsService {
|
||||
|
||||
public void restore() {
|
||||
|
||||
downloadPath = prefs.get(DOWNLOAD_PATH, System.getProperty("user.dir"));
|
||||
downloadPath = prefs.get(DOWNLOAD_PATH, defaultDownloadPath);
|
||||
maxThreads = prefs.getInt(MAX_THREADS, 4);
|
||||
autoStart = prefs.getBoolean(AUTO_START, true);
|
||||
vLogin = prefs.getBoolean(V_LOGIN, false);
|
||||
|
||||
@@ -29,6 +29,10 @@ public class AppStateService {
|
||||
|
||||
private Map<String, AtomicInteger> runningPosts = new ConcurrentHashMap<>();
|
||||
|
||||
private Map<String, QueuedVGLink> grabQueue = new ConcurrentHashMap<>();
|
||||
|
||||
private PublishProcessor<QueuedVGLink> liveGrabQueue = PublishProcessor.create();
|
||||
|
||||
private PublishProcessor<Image> liveImageUpdates = PublishProcessor.create();
|
||||
|
||||
private PublishProcessor<Post> livePostsState = PublishProcessor.create();
|
||||
|
||||
@@ -8,12 +8,15 @@ import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@EnableScheduling
|
||||
public class ConnectionManager {
|
||||
|
||||
private ConnectionManager() {
|
||||
@@ -34,6 +37,11 @@ public class ConnectionManager {
|
||||
pcm = new PoolingHttpClientConnectionManager();
|
||||
pcm.setMaxTotal(50);
|
||||
pcm.setDefaultMaxPerRoute(4);
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 1000)
|
||||
private void idleConnectionMonitoring() {
|
||||
pcm.closeExpiredConnections();
|
||||
pcm.closeIdleConnections(30, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ public class PostParser {
|
||||
if (appSettingsService.isAutoStart()) {
|
||||
logger.debug("Auto start downloads option is enabled");
|
||||
logger.debug(String.format("Starting to enqueue %d jobs for %s", post.getImages().size(), post.getUrl()));
|
||||
post.setStatus(Post.Status.PENDING);
|
||||
try {
|
||||
downloadQ.enqueue(post);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -70,11 +71,12 @@ public class PostParser {
|
||||
}
|
||||
logger.debug(String.format("Done enqueuing jobs for %s", post.getUrl()));
|
||||
} else {
|
||||
post.setStatus(Post.Status.STOPPED);
|
||||
logger.debug("Auto start downloads option is disabled");
|
||||
}
|
||||
}
|
||||
|
||||
public VRThreadParser createVRThreadParser(String threadId) {
|
||||
return new VRThreadParser(threadId, cm, vipergirlsAuthService);
|
||||
return new VRThreadParser(threadId, cm, vipergirlsAuthService, supportedHosts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class QueuedVGLink {
|
||||
|
||||
private final String type = "grabQueue";
|
||||
private final String link;
|
||||
private final String threadId;
|
||||
private final String postId;
|
||||
@Setter
|
||||
boolean removed;
|
||||
|
||||
public QueuedVGLink(String link, String threadId, String postId) {
|
||||
this.link = link;
|
||||
this.threadId = threadId;
|
||||
this.postId = postId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
QueuedVGLink that = (QueuedVGLink) o;
|
||||
return Objects.equals(link, that.link);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(link);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tn.mnlr.vripper.VripperApplication;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class VGHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VGHandler.class);
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
|
||||
@Autowired
|
||||
private PostParser postParser;
|
||||
|
||||
@Getter
|
||||
private LoadingCache<String, List<VRPostState>> cache;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
|
||||
CacheLoader<String, List<VRPostState>> loader = new CacheLoader<>() {
|
||||
@Override
|
||||
public List<VRPostState> load(String threadId) throws Exception {
|
||||
VRThreadParser vrThreadParser = postParser.createVRThreadParser(threadId);
|
||||
List<VRPostState> posts = new ArrayList<>();
|
||||
vrThreadParser.parse();
|
||||
vrThreadParser.getPostPublishProcessor()
|
||||
.onBackpressureBuffer()
|
||||
.blockingSubscribe(posts::add);
|
||||
return posts;
|
||||
}
|
||||
};
|
||||
|
||||
cache = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(30, TimeUnit.MINUTES)
|
||||
.build(loader);
|
||||
}
|
||||
|
||||
public void handle(List<QueuedVGLink> queuedVGLinks) throws Exception {
|
||||
for (QueuedVGLink queuedVGLink : queuedVGLinks) {
|
||||
if (queuedVGLink.getPostId() != null) {
|
||||
postParser.addPost(queuedVGLink.getPostId(), queuedVGLink.getThreadId());
|
||||
} else {
|
||||
appStateService.getGrabQueue().put(queuedVGLink.getLink(), queuedVGLink);
|
||||
appStateService.getLiveGrabQueue().onNext(queuedVGLink);
|
||||
|
||||
Callable<Void> cl = () -> {
|
||||
List<VRPostState> vrPostStates = cache.get(queuedVGLink.getThreadId());
|
||||
logger.debug(String.format("%d found for %s", vrPostStates.size(), queuedVGLink.getLink()));
|
||||
if (vrPostStates.size() == 1) {
|
||||
postParser.addPost(vrPostStates.get(0).getPostId(), vrPostStates.get(0).getThreadId());
|
||||
remove(queuedVGLink.getLink());
|
||||
logger.debug(String.format("threadId %s, postId %s is added automatically for download", queuedVGLink.getThreadId(), queuedVGLink.getPostId()));
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
VripperApplication.commonExecutor.submit(cl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(String url) {
|
||||
Optional.ofNullable(appStateService.getGrabQueue().remove(url)).ifPresent(e -> {
|
||||
e.setRemoved(true);
|
||||
appStateService.getLiveGrabQueue().onNext(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -99,49 +99,22 @@ class VRPostParser {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
class VRPostParseState extends VRPostState {
|
||||
|
||||
private final String type = "postParse";
|
||||
private String postId;
|
||||
private int number;
|
||||
private String title;
|
||||
private int imageCount;
|
||||
private String url;
|
||||
private List<String> previews;
|
||||
|
||||
|
||||
VRPostParseState(String threadId, String postId, int number, String title, int imageCount, String url, List<String> previews) {
|
||||
super(threadId);
|
||||
this.postId = postId;
|
||||
this.number = number;
|
||||
this.title = title;
|
||||
this.imageCount = imageCount;
|
||||
this.previews = previews;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
class VRPostHandler extends DefaultHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VRPostHandler.class);
|
||||
|
||||
private final List<Host> supportedHosts;
|
||||
|
||||
private final String threadId;
|
||||
private final String postId;
|
||||
private List<Image> images = new ArrayList<>();
|
||||
private List<String> previews = new ArrayList<>();
|
||||
private String threadTitle;
|
||||
|
||||
private String postTitle;
|
||||
private String forum;
|
||||
private int previewCounter = 0;
|
||||
private int index = 0;
|
||||
|
||||
private String postTitle;
|
||||
private int imageCount;
|
||||
|
||||
private String forum;
|
||||
|
||||
private List<String> previews = new ArrayList<>();
|
||||
private List<Image> images = new ArrayList<>();
|
||||
@Getter
|
||||
private Post parsedPost;
|
||||
|
||||
@@ -172,7 +145,7 @@ class VRPostHandler extends DefaultHandler {
|
||||
case "image":
|
||||
index++;
|
||||
if (previewCounter++ < 4) {
|
||||
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(thumbUrl -> previews.add(thumbUrl));
|
||||
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(previews::add);
|
||||
}
|
||||
|
||||
String mainUrl = Optional.ofNullable(attributes.getValue("main_url")).map(String::trim).orElse(null);
|
||||
|
||||
@@ -2,11 +2,28 @@ package tn.mnlr.vripper.services;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public abstract class VRPostState {
|
||||
private final String threadId;
|
||||
import java.util.List;
|
||||
|
||||
VRPostState(String threadId) {
|
||||
@Getter
|
||||
public class VRPostState {
|
||||
private final String type = "postParse";
|
||||
private final String threadId;
|
||||
private String postId;
|
||||
private int number;
|
||||
private String title;
|
||||
private int imageCount;
|
||||
private String url;
|
||||
private List<String> previews;
|
||||
private String hosts;
|
||||
|
||||
VRPostState(String threadId, String postId, int number, String title, int imageCount, String url, List<String> previews, String hosts) {
|
||||
this.threadId = threadId;
|
||||
this.postId = postId;
|
||||
this.number = number;
|
||||
this.title = title;
|
||||
this.imageCount = imageCount;
|
||||
this.previews = previews;
|
||||
this.url = url;
|
||||
this.hosts = hosts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
import io.reactivex.processors.ReplayProcessor;
|
||||
import lombok.Getter;
|
||||
import net.jodah.failsafe.Failsafe;
|
||||
import net.jodah.failsafe.RetryPolicy;
|
||||
@@ -15,16 +15,17 @@ import org.xml.sax.Attributes;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
import tn.mnlr.vripper.exception.DownloadException;
|
||||
import tn.mnlr.vripper.exception.PostParseException;
|
||||
import tn.mnlr.vripper.host.Host;
|
||||
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static tn.mnlr.vripper.services.PostParser.VR_API;
|
||||
|
||||
@@ -40,16 +41,18 @@ public class VRThreadParser {
|
||||
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
|
||||
private static SAXParserFactory factory = SAXParserFactory.newInstance();
|
||||
@Getter
|
||||
private final PublishProcessor<VRPostState> postPublishProcessor = PublishProcessor.create();
|
||||
private final ReplayProcessor<VRPostState> postPublishProcessor = ReplayProcessor.create();
|
||||
private final String threadId;
|
||||
private final ConnectionManager cm;
|
||||
private final VipergirlsAuthService vipergirlsAuthService;
|
||||
private final List<Host> hosts;
|
||||
|
||||
|
||||
VRThreadParser(String threadId, ConnectionManager cm, VipergirlsAuthService vipergirlsAuthService) {
|
||||
VRThreadParser(String threadId, ConnectionManager cm, VipergirlsAuthService vipergirlsAuthService, List<Host> hosts) {
|
||||
this.threadId = threadId;
|
||||
this.cm = cm;
|
||||
this.vipergirlsAuthService = vipergirlsAuthService;
|
||||
this.hosts = hosts;
|
||||
}
|
||||
|
||||
public Void parse() throws PostParseException {
|
||||
@@ -64,7 +67,7 @@ public class VRThreadParser {
|
||||
throw new PostParseException(e);
|
||||
}
|
||||
|
||||
VRThreadHandler handler = new VRThreadHandler(threadId, postPublishProcessor);
|
||||
VRThreadHandler handler = new VRThreadHandler(threadId, postPublishProcessor, hosts);
|
||||
AtomicReference<Throwable> thr = new AtomicReference<>();
|
||||
logger.debug(String.format("Requesting %s", httpGet));
|
||||
Failsafe.with(retryPolicy)
|
||||
@@ -97,23 +100,25 @@ public class VRThreadParser {
|
||||
class VRThreadHandler extends DefaultHandler {
|
||||
|
||||
private final String threadId;
|
||||
private final PublishProcessor<VRPostState> vrPostPublishProcessor;
|
||||
private final ReplayProcessor<VRPostState> vrPostPublishProcessor;
|
||||
private final List<Host> supportedHosts;
|
||||
private final Map<Host, AtomicInteger> hostMap = new HashMap<>();
|
||||
private List<String> previews = new ArrayList<>();
|
||||
private String threadTitle;
|
||||
private String postId;
|
||||
private String postTitle;
|
||||
private int imageCount;
|
||||
private int postCounter;
|
||||
private int previewCounter = 0;
|
||||
private List<String> previews = new ArrayList<>();
|
||||
|
||||
VRThreadHandler(String threadId, PublishProcessor<VRPostState> vrPostPublishProcessor) {
|
||||
VRThreadHandler(String threadId, ReplayProcessor<VRPostState> vrPostPublishProcessor, List<Host> supportedHosts) {
|
||||
this.vrPostPublishProcessor = vrPostPublishProcessor;
|
||||
this.threadId = threadId;
|
||||
this.supportedHosts = supportedHosts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDocument() {
|
||||
vrPostPublishProcessor.onNext(new VRThreadParseState(threadId, "START"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,8 +135,12 @@ class VRThreadHandler extends DefaultHandler {
|
||||
postTitle = Optional.ofNullable(attributes.getValue("title")).map(e -> e.trim().isEmpty() ? null : e.trim()).orElse(threadTitle);
|
||||
break;
|
||||
case "image":
|
||||
Optional.ofNullable(attributes.getValue("main_url"))
|
||||
.map(String::trim)
|
||||
.flatMap(mainUrl -> supportedHosts.stream().filter(host -> host.isSupported(mainUrl)).findFirst())
|
||||
.ifPresent(host -> Optional.ofNullable(hostMap.get(host)).ifPresentOrElse(AtomicInteger::incrementAndGet, () -> hostMap.put(host, new AtomicInteger(0))));
|
||||
if (previewCounter++ < 4) {
|
||||
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(thumbUrl -> previews.add(thumbUrl));
|
||||
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(previews::add);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -141,36 +150,25 @@ class VRThreadHandler extends DefaultHandler {
|
||||
public void endElement(String uri, String localName, String qName) {
|
||||
if ("post".equals(qName.toLowerCase())) {
|
||||
if (imageCount != 0) {
|
||||
vrPostPublishProcessor.onNext(new VRPostParseState(
|
||||
vrPostPublishProcessor.onNext(new VRPostState(
|
||||
threadId,
|
||||
postId,
|
||||
postCounter,
|
||||
postTitle,
|
||||
imageCount,
|
||||
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId),
|
||||
previews
|
||||
previews,
|
||||
hostMap.entrySet().stream().map(e -> e.getKey().getHost() + " (" + e.getValue().get() + ")").collect(Collectors.joining(", "))
|
||||
));
|
||||
}
|
||||
previewCounter = 0;
|
||||
previews = new ArrayList<>();
|
||||
hostMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endDocument() {
|
||||
vrPostPublishProcessor.onNext(new VRThreadParseState(threadId, "END"));
|
||||
vrPostPublishProcessor.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
class VRThreadParseState extends VRPostState {
|
||||
|
||||
private final String type = "threadParseState";
|
||||
private final String state;
|
||||
|
||||
VRThreadParseState(String threadId, String state) {
|
||||
super(threadId);
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,6 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*").withSockJS();
|
||||
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*").withSockJS().setClientLibraryUrl("../../assets/sockjs.min.js");
|
||||
}
|
||||
}
|
||||
|
||||
+81
-40
@@ -1,9 +1,6 @@
|
||||
package tn.mnlr.vripper.web.restendpoints;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -13,16 +10,13 @@ import org.springframework.web.bind.annotation.*;
|
||||
import tn.mnlr.vripper.VripperApplication;
|
||||
import tn.mnlr.vripper.exception.PostParseException;
|
||||
import tn.mnlr.vripper.q.DownloadQ;
|
||||
import tn.mnlr.vripper.services.AppSettingsService;
|
||||
import tn.mnlr.vripper.services.AppStateService;
|
||||
import tn.mnlr.vripper.services.PathService;
|
||||
import tn.mnlr.vripper.services.PostParser;
|
||||
import tn.mnlr.vripper.services.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@CrossOrigin(value = "*")
|
||||
@@ -41,8 +35,11 @@ public class PostRestEndpoint {
|
||||
@Autowired
|
||||
private PathService pathService;
|
||||
|
||||
@Autowired
|
||||
private VGHandler vgHandler;
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity handleException(Exception e) {
|
||||
public ResponseEntity<String> handleException(Exception e) {
|
||||
logger.error("Error when process request", e);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@@ -57,27 +54,33 @@ public class PostRestEndpoint {
|
||||
|
||||
@PostMapping("/post")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public ResponseEntity processPost(@RequestBody ThreadUrl url) throws Exception {
|
||||
logger.debug(String.format("Starting to process thread: %s", url.getUrl()));
|
||||
if (url.getUrl() == null || url.getUrl().isEmpty()) {
|
||||
return new ResponseEntity<>("Failed to process empty request", HttpStatus.BAD_REQUEST);
|
||||
} else if (!url.getUrl().startsWith("https://vipergirls.to")) {
|
||||
return new ResponseEntity<>("ViperGirls only links are supported", HttpStatus.BAD_REQUEST);
|
||||
public ResponseEntity processPost(@RequestBody ThreadUrl _url) throws Exception {
|
||||
if (_url.getUrl() == null || _url.getUrl().isEmpty()) {
|
||||
return new ResponseEntity("Failed to process empty request", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String threadId, postId;
|
||||
try {
|
||||
Matcher m = VG_URL_PATTERN.matcher(url.getUrl());
|
||||
if (m.find()) {
|
||||
threadId = m.group(1);
|
||||
postId = m.group(4);
|
||||
} else {
|
||||
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url));
|
||||
List<String> urls = Arrays.stream(_url.getUrl().split("\\r?\\n")).map(String::trim).filter(e -> !e.isEmpty()).collect(Collectors.toList());
|
||||
for (String url : urls) {
|
||||
logger.debug(String.format("Starting to process thread: %s", url));
|
||||
if (!url.startsWith("https://vipergirls.to")) {
|
||||
logger.error(String.format("Unsupported link %s", url));
|
||||
continue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url), e);
|
||||
|
||||
String threadId, postId;
|
||||
try {
|
||||
Matcher m = VG_URL_PATTERN.matcher(url);
|
||||
if (m.find()) {
|
||||
threadId = m.group(1);
|
||||
postId = m.group(4);
|
||||
} else {
|
||||
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url), e);
|
||||
}
|
||||
vgHandler.handle(Collections.singletonList(new QueuedVGLink(url, threadId, postId)));
|
||||
}
|
||||
return ResponseEntity.ok(new PairThreadIdPostId(threadId, postId));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/post/restart")
|
||||
@@ -131,7 +134,7 @@ public class PostRestEndpoint {
|
||||
|
||||
@PostMapping("/post/remove")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized ResponseEntity remove(@RequestBody @NonNull List<PostId> postIds) {
|
||||
public synchronized ResponseEntity<List<RemoveResult>> remove(@RequestBody @NonNull List<PostId> postIds) {
|
||||
List<RemoveResult> result = new ArrayList<>();
|
||||
for (PostId postId : postIds) {
|
||||
downloadQ.stop(postId.getPostId());
|
||||
@@ -143,15 +146,27 @@ public class PostRestEndpoint {
|
||||
|
||||
@PostMapping("/post/clear/all")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public ResponseEntity clearAll() {
|
||||
public ResponseEntity<RemoveAllResult> clearAll() {
|
||||
return ResponseEntity.ok(new RemoveAllResult(appStateService.clearAll()));
|
||||
}
|
||||
|
||||
@PostMapping("/post/remove/all")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public ResponseEntity removeAll() {
|
||||
public ResponseEntity<RemoveAllResult> removeAll() {
|
||||
return ResponseEntity.ok(new RemoveAllResult(appStateService.removeAll()));
|
||||
}
|
||||
|
||||
@GetMapping("/grab/{threadId}")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized ResponseEntity<List<VRPostState>> grab(@PathVariable("threadId") @NonNull ThreadId threadId) throws Exception {
|
||||
return ResponseEntity.ok(vgHandler.getCache().get(threadId.getThreadId()));
|
||||
}
|
||||
|
||||
@PostMapping("/grab/remove")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized void grabRemove(@RequestBody @NonNull ThreadUrl threadUrl) {
|
||||
vgHandler.remove(threadUrl.getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -168,23 +183,21 @@ class ThreadUrl {
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
class PostId {
|
||||
private String postId;
|
||||
class ThreadId {
|
||||
private String threadId;
|
||||
|
||||
public PostId(String postId) {
|
||||
this.postId = postId;
|
||||
public ThreadId(String threadId) {
|
||||
this.threadId = threadId;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
class PairThreadIdPostId {
|
||||
private String threadId;
|
||||
class PostId {
|
||||
private String postId;
|
||||
|
||||
PairThreadIdPostId(String threadId, String postId) {
|
||||
this.threadId = threadId;
|
||||
public PostId(String postId) {
|
||||
this.postId = postId;
|
||||
}
|
||||
}
|
||||
@@ -235,4 +248,32 @@ class DownloadPath {
|
||||
DownloadPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
class PairThreadIdPostId {
|
||||
private String threadId;
|
||||
private String postId;
|
||||
|
||||
public PairThreadIdPostId(String threadId, String postId) {
|
||||
this.threadId = threadId;
|
||||
this.postId = postId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PairThreadIdPostId that = (PairThreadIdPostId) o;
|
||||
return Objects.equals(threadId, that.threadId) &&
|
||||
Objects.equals(postId, that.postId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(threadId, postId);
|
||||
}
|
||||
}
|
||||
+30
-32
@@ -14,7 +14,6 @@ import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
import tn.mnlr.vripper.VripperApplication;
|
||||
import tn.mnlr.vripper.entities.Image;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
import tn.mnlr.vripper.entities.mixin.ui.ImageUIMixin;
|
||||
@@ -56,10 +55,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
private Map<String, Disposable> postsSubscriptions = new ConcurrentHashMap<>();
|
||||
private Map<String, Disposable> postDetailsSubscriptions = new ConcurrentHashMap<>();
|
||||
private Map<String, Disposable> vrPostParserSubscriptions = new ConcurrentHashMap<>();
|
||||
private Map<String, Disposable> stateSubscriptions = new ConcurrentHashMap<>();
|
||||
private Map<String, Disposable> downloadSpeedSubscriptions = new ConcurrentHashMap<>();
|
||||
private Map<String, Disposable> userSubscriptions = new ConcurrentHashMap<>();
|
||||
private Map<String, Disposable> grabQueueSubscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
private Map<String, Future<Void>> threadParseRequests = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -71,6 +70,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
WSMessage wsMessage = om.readValue(message.getPayload(), WSMessage.class);
|
||||
WSMessage.CMD cmd = WSMessage.CMD.valueOf(wsMessage.getCmd());
|
||||
switch (cmd) {
|
||||
case GRAB_QUEUE_SUB:
|
||||
subscribeForGrabQueue(session);
|
||||
break;
|
||||
case GLOBAL_STATE_SUB:
|
||||
subscribeForGlobalState(session);
|
||||
break;
|
||||
@@ -83,9 +85,6 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
case POSTS_SUB:
|
||||
subscribeForPosts(session);
|
||||
break;
|
||||
case THREAD_PARSING_SUB:
|
||||
subscribeForThreadParsing(session, wsMessage.getPayload());
|
||||
break;
|
||||
case POST_DETAILS_SUB:
|
||||
subscribeForPostDetails(session, wsMessage.getPayload());
|
||||
break;
|
||||
@@ -93,11 +92,6 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
logger.debug(String.format("Client %s unsubscribed from post details", session.getId()));
|
||||
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
break;
|
||||
case THREAD_PARSING_UNSUB:
|
||||
logger.debug(String.format("Client %s unsubscribed from thread parsing", session.getId()));
|
||||
Optional.ofNullable(vrPostParserSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
Optional.ofNullable(threadParseRequests.remove(session.getId())).ifPresent(d -> d.cancel(true));
|
||||
break;
|
||||
case POSTS_UNSUB:
|
||||
logger.debug(String.format("Client %s unsubscribed from posts", session.getId()));
|
||||
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
@@ -114,6 +108,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
logger.debug(String.format("Client %s unsubscribed from user info", session.getId()));
|
||||
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
break;
|
||||
case GRAB_QUEUE_UNSUB:
|
||||
logger.debug(String.format("Client %s unsubscribed from grab queue", session.getId()));
|
||||
Optional.ofNullable(grabQueueSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,30 +218,30 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
);
|
||||
}
|
||||
|
||||
private void subscribeForThreadParsing(WebSocketSession session, String threadId) {
|
||||
private void subscribeForGrabQueue(WebSocketSession session) {
|
||||
|
||||
logger.debug(String.format("Client %s subscribed for thread parsing with threadId = %s", session.getId(), threadId));
|
||||
if (vrPostParserSubscriptions.containsKey(session.getId())) {
|
||||
vrPostParserSubscriptions.get(session.getId()).dispose();
|
||||
logger.debug(String.format("Client %s subscribed for grab queue", session.getId()));
|
||||
if (grabQueueSubscriptions.containsKey(session.getId())) {
|
||||
grabQueueSubscriptions.get(session.getId()).dispose();
|
||||
}
|
||||
|
||||
if (threadParseRequests.containsKey(session.getId())) {
|
||||
threadParseRequests.get(session.getId()).cancel(true);
|
||||
try {
|
||||
send(session, new TextMessage(om.writeValueAsString(appStateService.getGrabQueue().values())));
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error occurred", e);
|
||||
}
|
||||
|
||||
VRThreadParser vrThreadParser = postParser.createVRThreadParser(threadId);
|
||||
|
||||
vrPostParserSubscriptions.put(session.getId(), vrThreadParser.getPostPublishProcessor()
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(200)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
grabQueueSubscriptions.put(session.getId(),
|
||||
appStateService.getLiveGrabQueue()
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(2000, TimeUnit.MILLISECONDS, 200)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.map(e -> e.stream().distinct().collect(Collectors.toList()))
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
);
|
||||
|
||||
threadParseRequests.put(session.getId(), VripperApplication.commonExecutor.submit(vrThreadParser::parse));
|
||||
}
|
||||
|
||||
private void subscribeForPostDetails(WebSocketSession session, String postId) {
|
||||
@@ -295,7 +293,7 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
Optional.ofNullable(vrPostParserSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
Optional.ofNullable(grabQueueSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
|
||||
Optional.ofNullable(threadParseRequests.remove(session.getId())).ifPresent(d -> d.cancel(true));
|
||||
|
||||
logger.debug(String.format("Connection closed for client id: %s", session.getId()));
|
||||
@@ -318,10 +316,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
GLOBAL_STATE_UNSUB,
|
||||
SPEED_SUB,
|
||||
SPEED_UNSUB,
|
||||
THREAD_PARSING_SUB,
|
||||
THREAD_PARSING_UNSUB,
|
||||
USER_SUB,
|
||||
USER_UNSUB
|
||||
USER_UNSUB,
|
||||
GRAB_QUEUE_SUB,
|
||||
GRAB_QUEUE_UNSUB
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+5278
-2434
File diff suppressed because it is too large
Load Diff
+21
-21
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vripper-ui",
|
||||
"version": "2.3.0",
|
||||
"version": "2.5.1",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
@@ -13,31 +13,31 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^7.2.3",
|
||||
"@angular/cdk": "^7.3.1",
|
||||
"@angular/common": "~7.2.0",
|
||||
"@angular/compiler": "~7.2.0",
|
||||
"@angular/core": "~7.2.0",
|
||||
"@angular/flex-layout": "^7.0.0-beta.23",
|
||||
"@angular/forms": "~7.2.0",
|
||||
"@angular/material": "^7.3.0",
|
||||
"@angular/platform-browser": "~7.2.0",
|
||||
"@angular/platform-browser-dynamic": "~7.2.0",
|
||||
"@angular/router": "~7.2.0",
|
||||
"ag-grid-angular": "^21.2.1",
|
||||
"ag-grid-community": "^21.2.1",
|
||||
"@angular/animations": "^8.2.14",
|
||||
"@angular/cdk": "^8.2.3",
|
||||
"@angular/common": "~8.2.14",
|
||||
"@angular/compiler": "~8.2.14",
|
||||
"@angular/core": "~8.2.14",
|
||||
"@angular/flex-layout": "^8.0.0-beta.27",
|
||||
"@angular/forms": "~8.2.14",
|
||||
"@angular/material": "^8.2.3",
|
||||
"@angular/platform-browser": "~8.2.14",
|
||||
"@angular/platform-browser-dynamic": "~8.2.14",
|
||||
"@angular/router": "~8.2.14",
|
||||
"ag-grid-angular": "^22.1.0",
|
||||
"ag-grid-community": "^22.1.0",
|
||||
"core-js": "^2.5.4",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ngx-electron": "^2.1.1",
|
||||
"rxjs": "~6.3.3",
|
||||
"ngx-electron": "^2.2.0",
|
||||
"rxjs": "~6.5.3",
|
||||
"tslib": "^1.9.0",
|
||||
"zone.js": "~0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.13.9",
|
||||
"@angular/cli": "~7.2.2",
|
||||
"@angular/compiler-cli": "^7.2.15",
|
||||
"@angular/language-service": "~7.2.0",
|
||||
"@angular-devkit/build-angular": "^0.803.20",
|
||||
"@angular/cli": "~8.3.20",
|
||||
"@angular/compiler-cli": "^8.2.14",
|
||||
"@angular/language-service": "~8.2.14",
|
||||
"@types/jasmine": "~2.8.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
@@ -53,6 +53,6 @@
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"typescript": "~3.2.2"
|
||||
"typescript": "~3.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.3.0</version>
|
||||
<version>2.5.1</version>
|
||||
</parent>
|
||||
<artifactId>vripper-ui</artifactId>
|
||||
<name>vripper-ui</name>
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<div fxLayout="column" *ngIf="connecting()" class="overlay loading" fxLayoutAlign="center center">
|
||||
<mat-spinner></mat-spinner>
|
||||
<h2>Loading...</h2>
|
||||
</div>
|
||||
<div fxLayout="column" *ngIf="noConnectionState()" class="overlay no-connection" fxLayoutAlign="center center">
|
||||
<mat-icon class="overlay-icon">error</mat-icon>
|
||||
<h2>The app seems to be shutdown</h2>
|
||||
</div>
|
||||
<div [ngClass]="{ electron: electronService.isElectronApp }" fxLayout="column" id="app-container">
|
||||
<div id="app-header" fxFlex="nogrow">
|
||||
<app-toolbar></app-toolbar>
|
||||
<ng-container *ngIf="appState | async as state">
|
||||
<div *ngIf="state === 'CONNECTING'" class="overlay loading" fxLayout="column" fxLayoutAlign="center center">
|
||||
<div class="sk-chase">
|
||||
<div class="sk-chase-dot"></div>
|
||||
<div class="sk-chase-dot"></div>
|
||||
<div class="sk-chase-dot"></div>
|
||||
<div class="sk-chase-dot"></div>
|
||||
<div class="sk-chase-dot"></div>
|
||||
<div class="sk-chase-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app-body" fxFlex="grow">
|
||||
<router-outlet></router-outlet>
|
||||
<div *ngIf="state === 'DISCONNECTED'" class="overlay no-connection" fxLayout="column" fxLayoutAlign="center center">
|
||||
<mat-icon class="overlay-icon">error</mat-icon>
|
||||
<h2>Error connecting to daemon, try restarting the application</h2>
|
||||
</div>
|
||||
<div *ngIf="state === 'CONNECTED'" [ngClass]="{ electron: electron | async }" fxLayout="column" id="app-container">
|
||||
<div fxFlex="nogrow" id="app-header">
|
||||
<app-toolbar></app-toolbar>
|
||||
</div>
|
||||
<div fxFlex="grow" id="app-body" style="margin-bottom: 20px;">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<app-status-bar style="width: 100%"></app-status-bar>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,13 +1,76 @@
|
||||
#title {
|
||||
user-select: none
|
||||
}
|
||||
|
||||
.overlay-icon {
|
||||
font-size: 128px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
font-size: 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.no-connection {
|
||||
color: #eb6060;
|
||||
.no-connection, .loading {
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.sk-chase {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
animation: sk-chase 2.5s infinite linear both;
|
||||
}
|
||||
|
||||
.sk-chase-dot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
animation: sk-chase-dot 2.0s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.sk-chase-dot:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 25%;
|
||||
height: 25%;
|
||||
background-color: #fff;
|
||||
border-radius: 100%;
|
||||
animation: sk-chase-dot-before 2.0s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.sk-chase-dot:nth-child(1) { animation-delay: -1.1s; }
|
||||
.sk-chase-dot:nth-child(2) { animation-delay: -1.0s; }
|
||||
.sk-chase-dot:nth-child(3) { animation-delay: -0.9s; }
|
||||
.sk-chase-dot:nth-child(4) { animation-delay: -0.8s; }
|
||||
.sk-chase-dot:nth-child(5) { animation-delay: -0.7s; }
|
||||
.sk-chase-dot:nth-child(6) { animation-delay: -0.6s; }
|
||||
.sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; }
|
||||
.sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; }
|
||||
.sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; }
|
||||
.sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; }
|
||||
.sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; }
|
||||
.sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; }
|
||||
|
||||
@keyframes sk-chase {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes sk-chase-dot {
|
||||
80%, 100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes sk-chase-dot-before {
|
||||
50% {
|
||||
transform: scale(0.4);
|
||||
} 100%, 0% {
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,47 @@
|
||||
import { AppService } from './app.service';
|
||||
import { ClipboardService } from './clipboard.service';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
import { Component, OnInit, OnDestroy, NgZone, AfterViewInit, Renderer2 } from '@angular/core';
|
||||
import { Component, OnDestroy, AfterViewInit, Renderer2, ChangeDetectionStrategy, NgZone } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Subscription, BehaviorSubject, Subject } from 'rxjs';
|
||||
import { WsConnectionService, WSState } from './ws-connection.service';
|
||||
import { WsHandler } from './ws-handler';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
styleUrls: ['./app.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
export class AppComponent implements OnDestroy, AfterViewInit {
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private dialog: MatDialog,
|
||||
private ws: WsConnectionService,
|
||||
public electronService: ElectronService,
|
||||
private clipboardService: ClipboardService,
|
||||
private ngZone: NgZone,
|
||||
private appService: AppService,
|
||||
private renderer: Renderer2
|
||||
) {}
|
||||
|
||||
subscriptions: Subscription[] = [];
|
||||
websocketHandlerPromise: Promise<WsHandler>;
|
||||
currentState: WSState;
|
||||
themeLoaded = false;
|
||||
|
||||
noConnectionState(): boolean {
|
||||
return this.currentState === WSState.CLOSE || this.currentState === WSState.ERROR;
|
||||
private renderer: Renderer2,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
this.electron = new BehaviorSubject(electronService.isElectronApp);
|
||||
}
|
||||
|
||||
connecting(): boolean {
|
||||
return (this.currentState === WSState.INIT || this.currentState === WSState.CONNECTING) && !this.themeLoaded ;
|
||||
}
|
||||
private subscriptions: Subscription[] = [];
|
||||
appState: Subject<string> = new BehaviorSubject('CONNECTING');
|
||||
electron: Subject<boolean>;
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.appService.renderer = this.renderer;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.currentState = WSState.INIT;
|
||||
this.subscriptions.push(this.ws.state.subscribe(e => {
|
||||
this.currentState = e;
|
||||
if (this.currentState === WSState.CLOSE || this.currentState === WSState.ERROR) {
|
||||
this.dialog.closeAll();
|
||||
} else if (this.currentState === WSState.OPEN) {
|
||||
this.subscriptions.push(this.ws.state.subscribe(wsState => {
|
||||
if (wsState === WSState.CLOSE || wsState === WSState.ERROR) {
|
||||
setTimeout(() => this.ngZone.run(() => {
|
||||
this.dialog.closeAll();
|
||||
this.appState.next('DISCONNECTED');
|
||||
}), 500);
|
||||
} else if (wsState === WSState.OPEN) {
|
||||
this.clipboardService.init();
|
||||
this.appService
|
||||
.loadTheme()
|
||||
.subscribe(() => this.ngZone.run(() => this.themeLoaded = true));
|
||||
.subscribe(() => this.ngZone.run(() => this.appState.next('CONNECTED')));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import { ScanComponent } from './scan/scan.component';
|
||||
import { StatusBarComponent } from './status-bar/status-bar.component';
|
||||
import { SelectionService } from './selection-service';
|
||||
import { ToolbarComponent } from './toolbar/toolbar.component';
|
||||
import { GrabQueueComponent } from './grab-queue/grab-queue.component';
|
||||
import { UrlGrabRendererComponent } from './grab-queue/url-renderer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -50,17 +52,20 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
|
||||
AppPreviewComponent,
|
||||
AppPreviewDirective,
|
||||
UrlRendererComponent,
|
||||
UrlGrabRendererComponent,
|
||||
FilterComponent,
|
||||
ScanComponent,
|
||||
StatusBarComponent,
|
||||
ToolbarComponent
|
||||
ToolbarComponent,
|
||||
GrabQueueComponent
|
||||
],
|
||||
entryComponents: [
|
||||
PostDetailComponent,
|
||||
SettingsComponent,
|
||||
ConfirmDialogComponent,
|
||||
AppPreviewComponent,
|
||||
ScanComponent
|
||||
ScanComponent,
|
||||
MultiPostComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
@@ -74,7 +79,8 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
|
||||
AgGridModule.withComponents([
|
||||
PostProgressRendererComponent,
|
||||
PostDetailsProgressRendererComponent,
|
||||
UrlRendererComponent
|
||||
UrlRendererComponent,
|
||||
UrlGrabRendererComponent
|
||||
]),
|
||||
OverlayModule
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, Renderer2, NgZone } from '@angular/core';
|
||||
import { Injectable, Renderer2 } from '@angular/core';
|
||||
import { ServerService } from './server-service';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { ScanComponent } from './scan/scan.component';
|
||||
@@ -23,7 +23,6 @@ export class AppService {
|
||||
}
|
||||
|
||||
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
|
||||
isScanOpen = false;
|
||||
|
||||
updateTheme(darkTheme: boolean) {
|
||||
this.darkTheme = darkTheme;
|
||||
@@ -65,11 +64,8 @@ export class AppService {
|
||||
}
|
||||
});
|
||||
|
||||
scanDialog.afterOpened().subscribe(() => this.isScanOpen = true);
|
||||
|
||||
scanDialog.afterClosed().subscribe(() => {
|
||||
smallDialogSubscription.unsubscribe();
|
||||
this.isScanOpen = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ export enum CMD {
|
||||
SPEED_SUB = 'SPEED_SUB',
|
||||
USER_SUB = 'USER_SUB',
|
||||
THREAD_PARSING_SUB = 'THREAD_PARSING_SUB',
|
||||
GRAB_QUEUE_SUB = 'GRAB_QUEUE_SUB',
|
||||
POSTS_UNSUB = 'POSTS_UNSUB',
|
||||
POST_DETAILS_UNSUB = 'POST_DETAILS_UNSUB',
|
||||
GLOBAL_STATE_UNSUB = 'GLOBAL_STATE_UNSUB',
|
||||
SPEED_UNSUB = 'SPEED_UNSUB',
|
||||
USER_UNSUB = 'USER_UNSUB',
|
||||
THREAD_PARSING_UNSUB = 'THREAD_PARSING_UNSUB'
|
||||
THREAD_PARSING_UNSUB = 'THREAD_PARSING_UNSUB',
|
||||
GRAB_QUEUE_UNSUB = 'GRAB_QUEUE_UNSUB'
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
|
||||
@Component({
|
||||
selector: 'app-preview',
|
||||
template: `
|
||||
<div class="previews" style="display: inline-block; white-space: nowrap;">
|
||||
<div class="previews" style="display: inline-block; white-space: nowrap; height: 200px; max-width: 400px">
|
||||
<ng-container *ngFor="let link of links">
|
||||
<img style="display: inline-block; max-width:150px; min-width:150px; width: auto; height: auto;"
|
||||
<img
|
||||
[@simpleFadeAnimation]="'in'"
|
||||
class="mat-elevation-z8"
|
||||
[src]="link"
|
||||
/>
|
||||
</ng-container>
|
||||
@@ -18,7 +17,7 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
|
||||
`
|
||||
img {
|
||||
max-height: 200px;
|
||||
max-width: 200px;
|
||||
max-width: 400px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AppPreviewComponent } from './preview-tooltip.component';
|
||||
import { Directive, Input, HostListener, ComponentRef, OnInit, ElementRef, NgZone } from '@angular/core';
|
||||
import { Directive, Input, HostListener, ComponentRef, OnInit, ElementRef, NgZone, OnDestroy } from '@angular/core';
|
||||
import { OverlayRef, Overlay, OverlayPositionBuilder } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
|
||||
@Directive({ selector: '[appPreview]' })
|
||||
export class AppPreviewDirective implements OnInit {
|
||||
export class AppPreviewDirective implements OnInit, OnDestroy {
|
||||
private overlayRef: OverlayRef;
|
||||
tooltipPortal = new ComponentPortal(AppPreviewComponent);
|
||||
|
||||
@@ -21,14 +21,25 @@ export class AppPreviewDirective implements OnInit {
|
||||
.withPush(true)
|
||||
.withGrowAfterOpen(true)
|
||||
.withPositions([{
|
||||
originX: 'start',
|
||||
originX: 'center',
|
||||
originY: 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayX: 'center',
|
||||
overlayY: 'top',
|
||||
}, {
|
||||
originX: 'center',
|
||||
originY: 'top',
|
||||
overlayX: 'center',
|
||||
overlayY: 'bottom',
|
||||
}]);
|
||||
this.overlayRef = this.overlay.create({ positionStrategy });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.ngZone.run(() => {
|
||||
this.overlayRef.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('mouseenter')
|
||||
show() {
|
||||
this.ngZone.run(() => {
|
||||
|
||||
@@ -6,10 +6,7 @@ export class VRPostParse {
|
||||
public title: string,
|
||||
public imageCount: number,
|
||||
public url: string,
|
||||
public previews: string[]
|
||||
public previews: string[],
|
||||
public hosts: string
|
||||
) {}
|
||||
}
|
||||
|
||||
export class VRThreadParseState {
|
||||
constructor(public threadId: string, public state: string) {}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
|
||||
</ag-grid-angular>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { UrlGrabRendererComponent } from './url-renderer.component';
|
||||
import { GrabQueueDataSource } from './grab-queue.datasource';
|
||||
import { Component, OnInit, NgZone } from '@angular/core';
|
||||
import { GridOptions } from 'ag-grid-community';
|
||||
import { WsConnectionService } from '../ws-connection.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grab-queue',
|
||||
templateUrl: './grab-queue.component.html',
|
||||
styleUrls: ['./grab-queue.component.scss']
|
||||
})
|
||||
export class GrabQueueComponent implements OnInit {
|
||||
constructor(private wsConnection: WsConnectionService, private zone: NgZone) {
|
||||
this.gridOptions = <GridOptions>{
|
||||
columnDefs: [
|
||||
{
|
||||
headerName: 'Url',
|
||||
field: 'link',
|
||||
sortable: true,
|
||||
sort: 'asc',
|
||||
cellRenderer: 'urlCellRenderer'
|
||||
}
|
||||
],
|
||||
rowHeight: 48,
|
||||
animateRows: true,
|
||||
rowData: [],
|
||||
frameworkComponents: {
|
||||
urlCellRenderer: UrlGrabRendererComponent
|
||||
},
|
||||
overlayLoadingTemplate: '<span></span>',
|
||||
overlayNoRowsTemplate: '<span></span>',
|
||||
getRowNodeId: data => data['link'],
|
||||
onGridReady: () => {
|
||||
this.gridOptions.api.sizeColumnsToFit();
|
||||
this.dataSource = new GrabQueueDataSource(this.wsConnection, this.gridOptions, this.zone);
|
||||
this.dataSource.connect();
|
||||
},
|
||||
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
|
||||
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit()
|
||||
};
|
||||
}
|
||||
|
||||
gridOptions: GridOptions;
|
||||
dataSource: GrabQueueDataSource;
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { GrabQueueState } from './grab-queue.model';
|
||||
import { CMD } from './../common/cmd.enum';
|
||||
import { WSMessage } from './../common/ws-message.model';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { WsConnectionService } from '../ws-connection.service';
|
||||
import { GridOptions } from 'ag-grid-community';
|
||||
import { WsHandler } from '../ws-handler';
|
||||
import { NgZone } from '@angular/core';
|
||||
|
||||
export class GrabQueueDataSource {
|
||||
constructor(
|
||||
private wsConnectionService: WsConnectionService,
|
||||
private gridOptions: GridOptions,
|
||||
private zone: NgZone
|
||||
) {
|
||||
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
|
||||
}
|
||||
|
||||
websocketHandlerPromise: Promise<WsHandler>;
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
connect() {
|
||||
this.websocketHandlerPromise.then((handler: WsHandler) => {
|
||||
console.log('Connecting to grab queue datasource');
|
||||
this.subscriptions.push(
|
||||
handler.subscribeForGrabQueue((e: GrabQueueState[]) => {
|
||||
this.zone.run(() => {
|
||||
const toAdd = [];
|
||||
const toUpdate = [];
|
||||
const toRemove = [];
|
||||
e.forEach(v => {
|
||||
if (v.removed) {
|
||||
if (this.gridOptions.api.getRowNode(v.link) != null) {
|
||||
toRemove.push(this.gridOptions.api.getRowNode(v.link).data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.gridOptions.api.getRowNode(v.link) == null) {
|
||||
toAdd.push(v);
|
||||
} else {
|
||||
toUpdate.push(v);
|
||||
}
|
||||
});
|
||||
this.gridOptions.api.updateRowData({ update: toUpdate, add: toAdd, remove: toRemove });
|
||||
});
|
||||
})
|
||||
);
|
||||
handler.send(new WSMessage(CMD.GRAB_QUEUE_SUB.toString()));
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
console.log('Disconnecting from grab queue datasource');
|
||||
this.subscriptions.forEach(e => e.unsubscribe());
|
||||
this.websocketHandlerPromise.then((handler: WsHandler) => {
|
||||
handler.send(new WSMessage(CMD.GRAB_QUEUE_UNSUB.toString()));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class GrabQueueState {
|
||||
constructor(
|
||||
public type: string,
|
||||
public link: string,
|
||||
public threadId: string,
|
||||
public postId: string,
|
||||
public removed: boolean
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
import { MultiPostComponent } from './../multi-post/multi-post.component';
|
||||
import { OnInit, OnDestroy, Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { AgRendererComponent } from 'ag-grid-angular';
|
||||
import { ICellRendererParams } from 'ag-grid-community';
|
||||
import { GrabQueueState } from './grab-queue.model';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { BreakpointObserver, BreakpointState, Breakpoints } from '@angular/cdk/layout';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ServerService } from '../server-service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-url-cell-grab',
|
||||
template: `
|
||||
<div fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<span><a (click)="goTo()" href="javascript:void(0)">{{ grabQueue.link }}</a></span>
|
||||
<span>
|
||||
<button style="margin-right: 5px" (click)="grab()" color="primary" mat-mini-fab><mat-icon>get_app</mat-icon></button>
|
||||
<button (click)="remove()" color="primary" mat-mini-fab><mat-icon>delete</mat-icon></button>
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class UrlGrabRendererComponent implements OnInit, OnDestroy, AgRendererComponent {
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
private httpClient: HttpClient,
|
||||
private serverService: ServerService,
|
||||
private _snackBar: MatSnackBar,
|
||||
private electronService: ElectronService
|
||||
) {}
|
||||
|
||||
grabQueue: GrabQueueState;
|
||||
params: ICellRendererParams;
|
||||
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
agInit(params: ICellRendererParams): void {
|
||||
this.grabQueue = params.data;
|
||||
}
|
||||
|
||||
refresh(params: ICellRendererParams): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
grab() {
|
||||
const dialogRef = this.dialog.open(MultiPostComponent, {
|
||||
width: '90%',
|
||||
height: '90%',
|
||||
maxWidth: '100vw',
|
||||
maxHeight: '100vh',
|
||||
data: this.grabQueue
|
||||
});
|
||||
|
||||
const smallDialogSubscription = this.isExtraSmall.subscribe(result => {
|
||||
if (result.matches) {
|
||||
dialogRef.updateSize('100%', '100%');
|
||||
} else {
|
||||
dialogRef.updateSize('90%', '90%');
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
if (smallDialogSubscription != null) {
|
||||
smallDialogSubscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goTo() {
|
||||
if (this.electronService.isElectronApp) {
|
||||
this.electronService.shell.openExternal(this.grabQueue.link);
|
||||
} else {
|
||||
window.open(this.grabQueue.link, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.httpClient.post(this.serverService.baseUrl + '/grab/remove', { url: this.grabQueue.link }).subscribe(
|
||||
() => {},
|
||||
error => {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
<div style="height: calc(100% - 20px)">
|
||||
<app-posts style="width: 100%; height: 100%;"></app-posts>
|
||||
</div>
|
||||
<mat-tab-group dynamicHeight style="height: 100%; margin-bottom: 20px; overflow: hidden;">
|
||||
<mat-tab label="Downloads">
|
||||
<app-posts style="width: 100%; height: 100%;"></app-posts>
|
||||
</mat-tab>
|
||||
<mat-tab label="Link Collector">
|
||||
<app-grab-queue style="width: 100%; height: 100%;"></app-grab-queue>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-tab-body-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AppService } from './../app.service';
|
||||
import { ServerService } from './../server-service';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
import { ClipboardService } from './../clipboard.service';
|
||||
import { Component, OnInit, OnDestroy, NgZone, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -11,12 +12,12 @@ import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HomeComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private clipboardService: ClipboardService,
|
||||
public dialog: MatDialog,
|
||||
public electronService: ElectronService,
|
||||
private appService: AppService,
|
||||
private httpClient: HttpClient,
|
||||
private serverService: ServerService,
|
||||
private _snackBar: MatSnackBar,
|
||||
private ngZone: NgZone
|
||||
) {}
|
||||
@@ -24,17 +25,23 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.clipboardService.links.subscribe(e => {
|
||||
this.ngZone.run(() => {
|
||||
if (this.appService.isScanOpen) {
|
||||
this._snackBar.open(e + ' was not processed, the app is busy parsing another thread', null, {
|
||||
duration: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.appService.scan(e);
|
||||
this.httpClient
|
||||
.post<{ threadId: string; postId: string }>(this.serverService.baseUrl + '/post', { url: e })
|
||||
.subscribe(
|
||||
response => {
|
||||
this._snackBar.open('Clipboard successfully scanned', null, {
|
||||
duration: 5000
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatMenuModule,
|
||||
MatDialogModule,
|
||||
MatCardModule,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatMenuModule,
|
||||
MatDialogModule,
|
||||
MatCardModule,
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
<div fxLayout="column" style="height: 100%;">
|
||||
<div class="dialog-container" fxLayout="column" style="height: 100%;">
|
||||
<div fxFlex="nogrow">
|
||||
<form autocomplete="off" fxLayout="row" fxLayoutAlign="left center" fxLayoutGap="20px">
|
||||
<mat-form-field fxFlex="1 1 50%">
|
||||
<input (ngModelChange)="search($event)" matInput name="search" ngModel placeholder="Search"/>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
|
||||
</ag-grid-angular>
|
||||
<div style="height: 4px; width: 100%; padding: 5px 0 0">
|
||||
<mat-progress-bar *ngIf="loading | async" mode="indeterminate"></mat-progress-bar>
|
||||
<h2 class="no-wrap" mat-dialog-title>Select posts</h2>
|
||||
</div>
|
||||
<mat-dialog-content fxFlex="grow" fxLayout="column">
|
||||
<div fxLayout="column" style="height: 100%;">
|
||||
<div fxFlex="nogrow">
|
||||
<form autocomplete="off" fxLayout="row" fxLayoutAlign="left center" fxLayoutGap="20px">
|
||||
<mat-form-field fxFlex="1 1 50%">
|
||||
<input (ngModelChange)="search($event)" matInput name="search" ngModel placeholder="Search"/>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
|
||||
</ag-grid-angular>
|
||||
<div style="height: 4px; width: 100%; padding: 5px 0 0">
|
||||
<mat-progress-bar *ngIf="(loading | async)" mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end" fxFlex="nogrow">
|
||||
<button (click)="onNoClick()" mat-raised-button>Close</button>
|
||||
<button (click)="submit()" [disabled]="(selectedRowsCount | async) === 0" color="primary" mat-raised-button>
|
||||
Download
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { VRPostParse, VRThreadParseState } from './../common/vr-post-parse.model';
|
||||
import { Component, OnInit, NgZone, OnDestroy, Input, Output, EventEmitter, ChangeDetectionStrategy, AfterViewInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { GridOptions } from 'ag-grid-community';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { VRPostParse } from './../common/vr-post-parse.model';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
EventEmitter,
|
||||
ChangeDetectionStrategy,
|
||||
AfterViewInit,
|
||||
Inject
|
||||
} from '@angular/core';
|
||||
import { MatSnackBar, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
import { GridOptions, RowNode } from 'ag-grid-community';
|
||||
import { UrlRendererComponent } from './url-renderer.component';
|
||||
import { WsHandler } from '../ws-handler';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { WSMessage } from '../common/ws-message.model';
|
||||
import { CMD } from '../common/cmd.enum';
|
||||
import { WsConnectionService } from '../ws-connection.service';
|
||||
import { Subscription, concat, merge, BehaviorSubject, Subject } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ServerService } from '../server-service';
|
||||
import { GrabQueueState } from '../grab-queue/grab-queue.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-multi-post',
|
||||
@@ -18,13 +26,6 @@ import { ServerService } from '../server-service';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
@Input()
|
||||
threadId: string;
|
||||
|
||||
@Output()
|
||||
done: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
gridOptions: GridOptions;
|
||||
websocketHandlerPromise: Promise<WsHandler>;
|
||||
subscription: Subscription;
|
||||
@@ -33,12 +34,13 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
constructor(
|
||||
private ngZone: NgZone,
|
||||
private _snackBar: MatSnackBar,
|
||||
private wsConnectionService: WsConnectionService,
|
||||
private httpClient: HttpClient,
|
||||
private serverService: ServerService
|
||||
) {
|
||||
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
|
||||
}
|
||||
private serverService: ServerService,
|
||||
public dialogRef: MatDialogRef<MultiPostComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public dialogData: GrabQueueState
|
||||
) {}
|
||||
|
||||
selectedRowsCount: Subject<number> = new BehaviorSubject(0);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.loading.emit(true);
|
||||
@@ -67,6 +69,11 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
field: 'url',
|
||||
cellClass: 'no-padding',
|
||||
cellRenderer: 'urlCellRenderer'
|
||||
},
|
||||
{
|
||||
headerName: 'Hosts',
|
||||
field: 'hosts',
|
||||
cellClass: 'no-padding'
|
||||
}
|
||||
],
|
||||
defaultColDef: {
|
||||
@@ -86,80 +93,66 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
getRowNodeId: data => data['url'],
|
||||
onGridReady: () => {
|
||||
this.gridOptions.api.sizeColumnsToFit();
|
||||
this.connect();
|
||||
this.grab();
|
||||
},
|
||||
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
|
||||
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit()
|
||||
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit(),
|
||||
onSelectionChanged: () => this.onSelectionChange(this.gridOptions.api.getSelectedNodes())
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribe();
|
||||
onSelectionChange(data: RowNode[]) {
|
||||
this.selectedRowsCount.next(data.length);
|
||||
}
|
||||
|
||||
ngOnDestroy() {}
|
||||
|
||||
search(event) {
|
||||
this.gridOptions.api.setQuickFilter(event);
|
||||
}
|
||||
|
||||
private unsubscribe() {
|
||||
if (this.subscription != null) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
this.websocketHandlerPromise.then((handler: WsHandler) => {
|
||||
handler.send(new WSMessage(CMD.THREAD_PARSING_UNSUB.toString()));
|
||||
});
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.websocketHandlerPromise.then((handler: WsHandler) => {
|
||||
console.log('Connecting to thread parsing');
|
||||
this.subscription = handler.subscribeForThreadParsing(
|
||||
(states: Array<VRThreadParseState>, data: Array<VRPostParse>) => {
|
||||
if (data[0].threadId === this.threadId) {
|
||||
this.gridOptions.api.updateRowData({ add: data });
|
||||
}
|
||||
|
||||
if (states.length > 0 && states[states.length - 1].state === 'END' && states[0].threadId === this.threadId) {
|
||||
this.ngZone.run(() => {
|
||||
this.loading.emit(false);
|
||||
if (this.gridOptions.api.getDisplayedRowCount() === 1) {
|
||||
this.addPosts(
|
||||
[this.gridOptions.api.getDisplayedRowAtIndex(0).data].map(e => ({
|
||||
threadId: e.threadId,
|
||||
postId: e.postId
|
||||
}))
|
||||
);
|
||||
this.done.emit(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
handler.send(new WSMessage(CMD.THREAD_PARSING_SUB.toString(), this.threadId));
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.loading.emit(true);
|
||||
const data = (<VRPostParse[]>this.gridOptions.api.getSelectedRows()).map(e => ({
|
||||
postId: e.postId,
|
||||
threadId: this.threadId
|
||||
threadId: this.dialogData.threadId
|
||||
}));
|
||||
this.addPosts(data);
|
||||
|
||||
merge(
|
||||
this.httpClient.post(this.serverService.baseUrl + '/post/add', data),
|
||||
this.httpClient.post(this.serverService.baseUrl + '/grab/remove', { url: this.dialogData.link })
|
||||
)
|
||||
.pipe(finalize(() => {
|
||||
this.dialogRef.close();
|
||||
this.loading.emit(false);
|
||||
}))
|
||||
.subscribe(
|
||||
() => {},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addPosts(data: { postId: string; threadId: string }[]) {
|
||||
this.httpClient.post(this.serverService.baseUrl + '/post/add', data).subscribe(
|
||||
() => {
|
||||
this._snackBar.open('Adding posts to queue', null, {
|
||||
duration: 5000
|
||||
});
|
||||
this.done.emit();
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
);
|
||||
grab() {
|
||||
this.httpClient
|
||||
.get<VRPostParse[]>(this.serverService.baseUrl + '/grab/' + this.dialogData.threadId)
|
||||
.pipe(finalize(() => this.loading.emit(false)))
|
||||
.subscribe(
|
||||
data => {
|
||||
this.gridOptions.api.updateRowData({ add: data });
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onNoClick() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@ export class PostDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.gridOptions = <GridOptions> {
|
||||
columnDefs: [
|
||||
{
|
||||
headerName: '#',
|
||||
field: 'index',
|
||||
sortable: true,
|
||||
sort: 'asc',
|
||||
cellClass: 'col-white',
|
||||
width: 10
|
||||
},
|
||||
{
|
||||
headerName: 'URL',
|
||||
field: 'url',
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
style="background-color: transparent; width: 100%; height: 100%;"
|
||||
>
|
||||
<div class="progress-bar" style="position: absolute; width: 100%; top: 37px; padding: 0 20px">
|
||||
<mat-progress-bar [value]="details.progress" class="example-margin" color="primary" mode="determinate">
|
||||
<mat-progress-bar *ngIf="(loaded | async)" [value]="details.progress" class="example-margin" color="primary"
|
||||
mode="determinate">
|
||||
</mat-progress-bar>
|
||||
</div>
|
||||
<span fxLayout="row">
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
AfterViewInit
|
||||
} from '@angular/core';
|
||||
import { AgRendererComponent } from 'ag-grid-angular';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Subscription, Subject, BehaviorSubject } from 'rxjs';
|
||||
import { PostDetails } from './post-details.model';
|
||||
import { WsHandler } from '../ws-handler';
|
||||
import { ICellRendererParams } from 'ag-grid-community';
|
||||
@@ -34,6 +34,8 @@ export class PostDetailsProgressRendererComponent implements AgRendererComponent
|
||||
subscription: Subscription;
|
||||
postDetails$: EventEmitter<PostDetails> = new EventEmitter();
|
||||
private postDetails: PostDetails;
|
||||
loaded: Subject<boolean> = new BehaviorSubject(false);
|
||||
loading;
|
||||
|
||||
trunc(value: number): number {
|
||||
return Math.trunc(value);
|
||||
@@ -64,12 +66,14 @@ export class PostDetailsProgressRendererComponent implements AgRendererComponent
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.postDetails$.emit(this.postDetails);
|
||||
this.loading = setTimeout(() => this.loaded.next(true), 100);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.subscription != null) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
clearTimeout(this.loading);
|
||||
}
|
||||
|
||||
agInit(params: ICellRendererParams): void {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export class PostDetails {
|
||||
|
||||
constructor(public postId: string, public postName: string, public url: string, public progress: number, public status: string) {
|
||||
this.postId = postId;
|
||||
this.postName = postName;
|
||||
this.url = url;
|
||||
this.progress = progress;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
constructor(
|
||||
public postId: string,
|
||||
public postName: string,
|
||||
public url: string,
|
||||
public progress: number,
|
||||
public status: string,
|
||||
public index: number
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,110 +1,3 @@
|
||||
<!-- <div class="container" style="height: 100%; background-color: white">
|
||||
<div
|
||||
[ngClass]="{
|
||||
error: postState.status === 'ERROR',
|
||||
downloading: postState.status === 'DOWNLOADING' || postState.status === 'PARTIAL',
|
||||
complete: postState.status === 'COMPLETE',
|
||||
stopped: postState.status === 'STOPPED'
|
||||
}"
|
||||
class="progress-bar-back"
|
||||
style="position: relative; height: 100%; max-height: 48px;"
|
||||
>
|
||||
<div
|
||||
class="progress-foreground"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between"
|
||||
style="background-color: transparent; width: 100%; height: 100%;"
|
||||
>
|
||||
<div class="progress-bar" color="accent" style="position: absolute; width: 100%; top: 37px; padding: 0 50px">
|
||||
<mat-progress-bar [value]="postState.progress" class="example-margin" color="primary" mode="determinate">
|
||||
</mat-progress-bar>
|
||||
</div>
|
||||
<span fxLayout="row">
|
||||
<span (click)="toggleExpand()" class="chevron">
|
||||
<button *ngIf="expanded; else notExpanded" mat-icon-button>
|
||||
<mat-icon>expand_more</mat-icon>
|
||||
</button>
|
||||
<ng-template #notExpanded>
|
||||
<button mat-icon-button>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
</span>
|
||||
<span>{{ postState.title }}</span>
|
||||
<span class="filler" fxFlex="grow"></span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="progress-percentage">{{ postState.done + '/' + postState.total }}</span>
|
||||
<button [matMenuTriggerFor]="menu" mat-icon-button>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button
|
||||
(click)="restart()"
|
||||
*ngIf="
|
||||
(postState.status === 'COMPLETE' && postState.progress !== 100) ||
|
||||
postState.status === 'ERROR' ||
|
||||
postState.status === 'STOPPED'
|
||||
"
|
||||
mat-menu-item
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
<span>Start</span>
|
||||
</button>
|
||||
<button
|
||||
(click)="stop()"
|
||||
*ngIf="
|
||||
postState.status === 'DOWNLOADING' || postState.status === 'PARTIAL' || postState.status === 'PENDING'
|
||||
"
|
||||
mat-menu-item
|
||||
>
|
||||
<ng-container>
|
||||
<mat-icon>stop</mat-icon>
|
||||
<span>Stop</span>
|
||||
</ng-container>
|
||||
</button>
|
||||
<button (click)="seeDetails()" mat-menu-item>
|
||||
<mat-icon>list</mat-icon>
|
||||
<span>Files</span>
|
||||
</button>
|
||||
<button (click)="remove()" mat-menu-item>
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
<button (click)="open()" *ngIf="electronService.isElectronApp" mat-menu-item>
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
<span>Download Location</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<section *ngIf="expanded">
|
||||
<div class="details table">
|
||||
<div class="row" style="display: table-row">
|
||||
<h4 class="cell attribute">Status:</h4>
|
||||
<p class="cell value">
|
||||
{{ postState.status | titlecase }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="row" style="display: table-row">
|
||||
<h4 class="cell attribute">Post URL:</h4>
|
||||
<p class="cell value">
|
||||
<a (click)="goTo()" [appPreview]="postState.previews" href="javascript:void(0)"
|
||||
>https://vipergirls/threads/?p={{ postState.postId }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div style="display: table-row">
|
||||
<h4 class="cell attribute">Host:</h4>
|
||||
<p class="cell value">
|
||||
{{ postState.hosts }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div> -->
|
||||
|
||||
<div class="container" style="height: 100%; background-color: white">
|
||||
<ng-container *ngIf="postState$ | async as postState">
|
||||
<div
|
||||
@@ -124,16 +17,18 @@
|
||||
fxLayoutGap="5px"
|
||||
style="background-color: transparent; width: 100%; height: 100%;"
|
||||
>
|
||||
<div class="checkbox" style="display: inline-block; width: 5px; height: 100%;"></div>
|
||||
<span fxFlex="grow" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<span
|
||||
><a (click)="goTo()" [appPreview]="postState.previews" href="javascript:void(0)">{{
|
||||
<div class="checkbox" fxFlex="nogrow" style="width: 5px; height: 100%;"></div>
|
||||
<span class="row-text" fxFlex="grow" fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="20px">
|
||||
<span [title]="postState.title">
|
||||
<a (click)="goTo()" [appPreview]="postState.previews" href="javascript:void(0)">{{
|
||||
postState.title
|
||||
}}</a></span
|
||||
>
|
||||
<span>{{postState.done + '/' + postState.total}} done from {{postState.hosts}}</span>
|
||||
<span [title]="postState.done + '/' + postState.total + ' from ' + postState.hosts"
|
||||
>{{ postState.done + '/' + postState.total }} from {{ postState.hosts }}</span
|
||||
>
|
||||
</span>
|
||||
<button [matMenuTriggerFor]="menu" mat-icon-button>
|
||||
<button [matMenuTriggerFor]="menu" fxFlex="nogrow" mat-icon-button>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
@@ -148,7 +43,8 @@
|
||||
</mat-menu>
|
||||
</div>
|
||||
<div class="progress-bar" color="accent" style="position: absolute; width: 100%; top: 37px; padding: 0 10px">
|
||||
<mat-progress-bar [value]="postState.progress" color="primary" mode="determinate"></mat-progress-bar>
|
||||
<mat-progress-bar *ngIf="(loaded | async)" [value]="postState.progress" color="primary"
|
||||
mode="determinate"></mat-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
.checkbox {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.row-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row-text > span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
import { AgRendererComponent } from 'ag-grid-angular';
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { Subscription, Observable, BehaviorSubject, Subject } from 'rxjs';
|
||||
import { WsHandler } from '../ws-handler';
|
||||
import { ICellRendererParams } from 'ag-grid-community';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
@@ -51,6 +51,8 @@ export class PostProgressRendererComponent implements AgRendererComponent, OnIni
|
||||
expanded = false;
|
||||
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
|
||||
fs;
|
||||
loaded: Subject<boolean> = new BehaviorSubject(false);
|
||||
loading;
|
||||
|
||||
trunc(value: number): number {
|
||||
return Math.trunc(value);
|
||||
@@ -73,12 +75,14 @@ export class PostProgressRendererComponent implements AgRendererComponent, OnIni
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.postState$.emit(this.postState);
|
||||
this.loading = setTimeout(() => this.loaded.next(true), 100);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.updatesSubscription != null) {
|
||||
this.updatesSubscription.unsubscribe();
|
||||
}
|
||||
clearTimeout(this.loading);
|
||||
}
|
||||
|
||||
agInit(params: ICellRendererParams): void {
|
||||
|
||||
@@ -3,32 +3,24 @@
|
||||
<h2 class="no-wrap" mat-dialog-title>Scan</h2>
|
||||
</div>
|
||||
<mat-dialog-content fxFlex="grow" fxLayout="column">
|
||||
<div *ngIf="!(hideScan | async)">
|
||||
<form
|
||||
#f="ngForm"
|
||||
(ngSubmit)="submit(f)"
|
||||
autocomplete="off"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="center center"
|
||||
fxLayoutGap="20px"
|
||||
>
|
||||
<mat-form-field fxFlex="grow">
|
||||
<input [(ngModel)]="input" matInput name="url" placeholder="Put a vipergirls.to link" required/>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button [disabled]="f.invalid" color="primary" mat-raised-button type="submit">Scan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-container *ngIf="threadId | async as thId">
|
||||
<app-multi-post (done)="done($event)" *ngIf="thId != null" [threadId]="thId"
|
||||
style="height: 100%"></app-multi-post>
|
||||
</ng-container>
|
||||
<form #f="ngForm" autocomplete="off" style="height: 100%;">
|
||||
<mat-form-field fxFlex="grow">
|
||||
<textarea
|
||||
[(ngModel)]="input"
|
||||
mat-autosize="true"
|
||||
matAutosizeMinRows="5"
|
||||
matInput
|
||||
name="url"
|
||||
placeholder="Put each link in a new line"
|
||||
required
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<mat-progress-bar *ngIf="loading | async" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button (click)="close()" mat-raised-button>Close</button>
|
||||
<button (click)="addPosts()" *ngIf="threadId != null" color="primary" mat-raised-button type="submit">
|
||||
Download
|
||||
<mat-dialog-actions align="end" fxFlex="nogrow">
|
||||
<button mat-dialog-close mat-raised-button>Close</button>
|
||||
<button (click)="submit()" [disabled]="f.invalid || (loading | async)" color="primary" mat-raised-button>Scan
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,8 @@ import {
|
||||
Component,
|
||||
OnInit,
|
||||
NgZone,
|
||||
ViewChild,
|
||||
Inject,
|
||||
ChangeDetectionStrategy,
|
||||
EventEmitter,
|
||||
AfterViewInit
|
||||
} from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
@@ -13,7 +11,7 @@ import { MatSnackBar, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { ServerService } from '../server-service';
|
||||
import { MultiPostComponent } from '../multi-post/multi-post.component';
|
||||
import { Subject, BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scan',
|
||||
@@ -31,18 +29,12 @@ export class ScanComponent implements OnInit, AfterViewInit {
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData
|
||||
) {}
|
||||
|
||||
@ViewChild(MultiPostComponent)
|
||||
multipost: MultiPostComponent;
|
||||
|
||||
input: string;
|
||||
threadId: EventEmitter<string> = new EventEmitter();
|
||||
hideScan: EventEmitter<boolean> = new EventEmitter();
|
||||
loading: Subject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
submit(form: NgForm) {
|
||||
submit() {
|
||||
this.ngZone.run(() => {
|
||||
this.hideScan.emit(true);
|
||||
this.threadId.emit(null);
|
||||
this.processUrl(this.input, form);
|
||||
this.processUrl(this.input);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,62 +46,30 @@ export class ScanComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
processUrl(url: string, form?: NgForm) {
|
||||
processUrl(url: string) {
|
||||
this.loading.next(true);
|
||||
this.httpClient
|
||||
.post<{ threadId: string; postId: string }>(this.serverService.baseUrl + '/post', { url: url })
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.ngZone.run(() => {
|
||||
if (form != null) {
|
||||
form.resetForm();
|
||||
this.input = null;
|
||||
}
|
||||
});
|
||||
this.close();
|
||||
this.loading.next(false);
|
||||
})
|
||||
)
|
||||
.subscribe(response => {
|
||||
this.ngZone.run(() => {
|
||||
if (response.postId != null) {
|
||||
this.httpClient
|
||||
.post(this.serverService.baseUrl + '/post/add', [response])
|
||||
.pipe(finalize(() => this.dialogRef.close()))
|
||||
.subscribe(
|
||||
() => {
|
||||
this._snackBar.open('Adding posts to queue', null, {
|
||||
duration: 5000
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.threadId.emit(response.threadId);
|
||||
.subscribe(response => {},
|
||||
error => {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addPosts() {
|
||||
this.ngZone.run(() => {
|
||||
if (this.multipost != null) {
|
||||
this.multipost.submit();
|
||||
}
|
||||
this.dialogRef.close();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ngZone.run(() => {
|
||||
if (this.data.url != null) {
|
||||
this.hideScan.emit(true);
|
||||
this.processUrl(this.data.url);
|
||||
} else {
|
||||
this.hideScan.emit(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export class SettingsComponent implements OnInit {
|
||||
this.clipboardService.init(data);
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
padding: 2px 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
span {
|
||||
|
||||
@@ -38,9 +38,11 @@ export class StatusBarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
selected: EventEmitter<number> = new EventEmitter();
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.selected.emit(0);
|
||||
this.globalState.emit(new GlobalState(0, 0, 0, 0));
|
||||
this.downloadSpeed.emit(new DownloadSpeed('0 B'));
|
||||
this.ngZone.run(() => {
|
||||
this.selected.emit(0);
|
||||
this.globalState.emit(new GlobalState(0, 0, 0, 0));
|
||||
this.downloadSpeed.emit(new DownloadSpeed('0 B'));
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -63,7 +65,9 @@ export class StatusBarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
handler.send(new WSMessage(CMD.GLOBAL_STATE_SUB.toString()));
|
||||
handler.send(new WSMessage(CMD.SPEED_SUB.toString()));
|
||||
});
|
||||
this.subscriptions.push(this.selectionService.selected$.subscribe(selected => this.selected.emit(selected.length)));
|
||||
this.subscriptions.push(
|
||||
this.selectionService.selected$.subscribe(selected => this.ngZone.run(() => this.selected.emit(selected.length)))
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -13,11 +13,17 @@
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
<button (click)="stop()" [disabled]="disableSelection | async" aria-label="Stop" mat-icon-button title="Stop">
|
||||
<mat-icon>stop</mat-icon>
|
||||
<mat-icon>pause</mat-icon>
|
||||
</button>
|
||||
<div class="or-spacer-vertical left">
|
||||
<div class="mask"></div>
|
||||
</div>
|
||||
<button (click)="clear()" aria-label="Clear completed" mat-icon-button title="Clear completed">
|
||||
<mat-icon>clear_all</mat-icon>
|
||||
</button>
|
||||
<button (click)="stopAll()" aria-label="Stop All" mat-icon-button title="Stop All">
|
||||
<mat-icon>stop</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div fxFlex="grow" id="global-search">
|
||||
<form autocomplete="off">
|
||||
|
||||
@@ -22,6 +22,7 @@ import { PostsDataService } from '../posts-data.service';
|
||||
@Component({
|
||||
selector: 'app-toolbar',
|
||||
templateUrl: './toolbar.component.html',
|
||||
styleUrls: ['./tooltip.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@@ -142,7 +143,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this._snackBar.open(`${data.removed} items cleared`, null, { duration: 5000 });
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
@@ -170,7 +171,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this._snackBar.open(`${data.removed} items removed`, null, { duration: 5000 });
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
@@ -185,7 +186,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this._snackBar.open(`Download stopped`, null, { duration: 5000 });
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
@@ -200,7 +201,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this._snackBar.open(`Download started`, null, { duration: 5000 });
|
||||
},
|
||||
error => {
|
||||
this._snackBar.open(error.error, null, {
|
||||
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
@@ -209,8 +210,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.disableSelection.next(true);
|
||||
this.loggedUser.emit(new LoggedUser(null));
|
||||
this.ngZone.run(() => {
|
||||
this.disableSelection.next(true);
|
||||
this.loggedUser.emit(new LoggedUser(null));
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -228,7 +231,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
this.selectionService.selected$.subscribe(selected => {
|
||||
this.selected = selected;
|
||||
this.disableSelection.next(this.selected.length === 0);
|
||||
this.ngZone.run(() => this.disableSelection.next(this.selected.length === 0));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.or-spacer-vertical {
|
||||
display:inline-block;
|
||||
width:1px;
|
||||
position:relative;
|
||||
|
||||
.mask {
|
||||
overflow:hidden; width:10px; height:50px;
|
||||
}
|
||||
&.left .mask:after {
|
||||
content:'';
|
||||
display:block;
|
||||
margin-left:-20px;
|
||||
width:20px;
|
||||
height:100%;
|
||||
border-radius:12px;
|
||||
box-shadow:0 0 3px black;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export enum WSState {
|
||||
OPEN
|
||||
}
|
||||
|
||||
const maxAttemps = 5;
|
||||
const maxAttemps = 15;
|
||||
|
||||
@Injectable()
|
||||
export class WsConnectionService {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { element } from 'protractor';
|
||||
import { GrabQueueState } from './grab-queue/grab-queue.model';
|
||||
import { LoggedUser } from './common/logged-user.model';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { WSMessage } from './common/ws-message.model';
|
||||
@@ -6,7 +8,6 @@ import { map, filter } from 'rxjs/operators';
|
||||
import { PostDetails } from './post-detail/post-details.model';
|
||||
import { GlobalState } from './common/global-state.model';
|
||||
import { DownloadSpeed } from './common/download-speed.model';
|
||||
import { VRPostParse, VRThreadParseState } from './common/vr-post-parse.model';
|
||||
|
||||
export class WsHandler {
|
||||
constructor(private websocket: Subject<any>) {}
|
||||
@@ -111,7 +112,8 @@ export class WsHandler {
|
||||
element.postName,
|
||||
element.url,
|
||||
element.current === 0 && element.total === 0 ? 0 : (element.current / element.total) * 100,
|
||||
element.status
|
||||
element.status,
|
||||
element.index
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -121,37 +123,24 @@ export class WsHandler {
|
||||
.subscribe(e => callback(e));
|
||||
}
|
||||
|
||||
subscribeForThreadParsing(
|
||||
callback: (stateArray: Array<VRThreadParseState>, dataArray: Array<VRPostParse>) => void
|
||||
): Subscription {
|
||||
subscribeForGrabQueue(callback: (grabQueueStream: Array<GrabQueueState>) => void): Subscription {
|
||||
return this.websocket
|
||||
.pipe(
|
||||
map(e => JSON.parse(e)),
|
||||
filter(e => e.length > 0 && (e[0].type === 'postParse' || e[0].type === 'threadParseState')),
|
||||
filter(e => e.length > 0 && e.filter(v => v.type === 'grabQueue').length > 0),
|
||||
map(e => {
|
||||
const stateValues: VRThreadParseState[] = [];
|
||||
const dataValues: VRPostParse[] = [];
|
||||
e.forEach(element => {
|
||||
if (element.type === 'postParse') {
|
||||
dataValues.push(
|
||||
new VRPostParse(
|
||||
element.threadId,
|
||||
element.postId,
|
||||
element.number,
|
||||
element.title,
|
||||
element.imageCount,
|
||||
element.url,
|
||||
element.previews
|
||||
)
|
||||
);
|
||||
} else if (element.type === 'threadParseState') {
|
||||
stateValues.push(new VRThreadParseState(element.threadId, element.state));
|
||||
}
|
||||
const grabQueue: Array<GrabQueueState> = [];
|
||||
(<Array<any>>e).forEach(element => {
|
||||
grabQueue.push(
|
||||
new GrabQueueState(element.type, element.link, element.threadId, element.postId, element.removed)
|
||||
);
|
||||
});
|
||||
return { states: stateValues, data: dataValues };
|
||||
return grabQueue;
|
||||
})
|
||||
)
|
||||
.subscribe(e => callback(e.states, e.data));
|
||||
.subscribe(e => {
|
||||
callback(e);
|
||||
});
|
||||
}
|
||||
|
||||
send(wsMessage: WSMessage) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
$font-family: Fira, sans-serif;
|
||||
$font-size: 14px;
|
||||
$font-weight: 700;
|
||||
$secondary-font-family: Fira, sans-serif;
|
||||
$secondary-font-size: 14px;
|
||||
$secondary-font-weight:700;
|
||||
$ag-font-family: Fira, sans-serif;
|
||||
$ag-font-size: 14px;
|
||||
$ag-font-weight: 700;
|
||||
$ag-secondary-font-family: Fira, sans-serif;
|
||||
$ag-secondary-font-size: 14px;
|
||||
$ag-secondary-font-weight:700;
|
||||
|
||||
@import '~ag-grid-community/src/styles/ag-grid.scss';
|
||||
@import '~ag-grid-community/src/styles/ag-theme-material/sass/ag-theme-material.scss';
|
||||
@@ -26,7 +26,7 @@ $secondary-font-weight:700;
|
||||
background-color: mat-color($background, background) !important;
|
||||
}
|
||||
|
||||
.ag-theme-material .ag-header {
|
||||
.ag-theme-material .ag-header-cell {
|
||||
background-color: mat-color($background, background) !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,26 +75,6 @@ app-root {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.loading {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
.no-connection {
|
||||
background-color: whitesmoke !important;
|
||||
}
|
||||
|
||||
#app-container {
|
||||
position: fixed;
|
||||
display: block;
|
||||
@@ -125,3 +105,7 @@ body a {
|
||||
.ag-row-selected app-progress-cell .checkbox {
|
||||
background-color: orange !important;
|
||||
}
|
||||
|
||||
.col-white {
|
||||
background-color: white;
|
||||
}
|
||||
Reference in New Issue
Block a user