Compare commits

..
14 Commits
Author SHA1 Message Date
death-claw c6ef54e115 v2.7.1 2019-12-21 11:24:03 +01:00
death-claw 441b2831fc Add version to title 2019-12-21 11:16:53 +01:00
death-claw 3cf4d60eca v2.7.0 2019-12-21 09:59:17 +01:00
death-claw d228c89a1c Add support for pixxxels
Add missing icon
2019-12-21 09:56:57 +01:00
death-claw f3b592e81b v2.6.0 2019-12-21 08:39:04 +01:00
death-claw a5ff659fb4 Better quality app icon
Implement context menu on download tab
Add stop remove and start button to context menu
Link will not be removed automatically from link collector tab
Some UI fixes
2019-12-21 08:34:56 +01:00
death-claw ab80e2f3b3 v2.5.1 2019-12-09 20:26:52 +01:00
death-claw 2ef082f87e Add hosts column in Link collector grab menu
Disable download button when nothing selected in Link collector grab menu
2019-12-09 19:56:26 +01:00
death-claw a1b97e76a7 v2.5.0 2019-12-08 20:13:58 +01:00
death-claw 185a078b4d Missing change 2019-12-08 20:08:42 +01:00
death-claw 28a4c944ee Add support for multiple links at a time
Add the Link collector feature
Fix bug with previews
2019-12-08 20:07:13 +01:00
death-claw 23d3553d99 Include startup icon
Fix UI layout
2019-12-06 23:09:57 +01:00
death-claw fb09e4c46b v2.4.1 2019-11-26 20:03:10 +01:00
death-claw 4feb6f5fe1 Increase attempts for establishing ws connection between front and back end
Change default download folder to Home folder (installer), App folder (portable)
2019-11-26 20:01:08 +01:00
82 changed files with 6842 additions and 3211 deletions
+39
View File
@@ -1,5 +1,44 @@
# Changelog
## [2.7.1] - 2019-12-21
### Added
- Add version to title bar
## [2.7.0] - 2019-12-21
### Changed
- Add missing icon
### Added
- Add support for pixxxels
## [2.6.0] - 2019-12-21
### Changed
- Disable download button when nothing selected in Link collector grab menu
- Better quality app icon
- Implement context menu on download tab
- Add stop remove and start button to context menu
- Link will not be removed automatically from link collector tab
- Some UI fixes
## [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
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.4.0</version>
<version>2.7.1</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+1 -1
View File
@@ -86,7 +86,6 @@ getPort().then(port => {
}
vripperServer = spawn(javaBinPath, [
"-Xms256m",
"-Xmx1024m",
"-Dvripper.server.port=" + port,
"-jar",
jarPath
@@ -136,6 +135,7 @@ if (!gotTheLock) {
})
.catch((error) => {
// Terminate immediately
console.log(error);
vripperServer.kill('SIGKILL');
terminated = true;
app.quit();
+60 -35
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.4.0",
"version": "2.7.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -35,6 +35,11 @@
"defer-to-connect": "^1.0.1"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/debug": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
@@ -117,6 +122,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -216,9 +222,9 @@
"dev": true
},
"astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
},
"async-exit-hook": {
"version": "2.0.1",
@@ -519,18 +525,18 @@
"dev": true
},
"cli-truncate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.0.0.tgz",
"integrity": "sha512-C4hp+8GCIFVsUUiXcw+ce+7wexVWImw8rQrgMBFsqerx9LvvcGlwm6sMjQYAEmV/Xb87xc1b5Ttx505MSpZVqg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"requires": {
"slice-ansi": "^2.1.0",
"string-width": "^4.1.0"
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"emoji-regex": {
"version": "8.0.0",
@@ -543,21 +549,21 @@
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"string-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz",
"integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^5.2.0"
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^4.1.0"
"ansi-regex": "^5.0.0"
}
}
}
@@ -626,6 +632,7 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
@@ -633,7 +640,8 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"combined-stream": {
"version": "1.0.8",
@@ -760,11 +768,6 @@
"array-find-index": "^1.0.1"
}
},
"custom-electron-titlebar": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/custom-electron-titlebar/-/custom-electron-titlebar-3.0.10.tgz",
"integrity": "sha512-FVKy7P0NpMTkm5rEpETXkJWQ7w9ufdU8TA5xhfiR1Jgnhy6fBaCNUMmXWLrNFrhG69MVvDXM/lLN21Rxaz78Ig=="
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -2426,19 +2429,41 @@
}
},
"slice-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
"integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"requires": {
"ansi-styles": "^3.2.0",
"astral-regex": "^1.0.0",
"is-fullwidth-code-point": "^2.0.0"
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz",
"integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==",
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
}
}
},
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.4.0",
"version": "2.7.1",
"description": "A ripper for vipergirls.to built using web technolgies",
"main": "main.js",
"author": "death-claw <53543762+death-claw@users.noreply.github.com>",
@@ -8,7 +8,7 @@
"license": "ISC",
"build": {
"appId": "tn.mnlr.vripper",
"productName": "vripper",
"productName": "VRipper",
"files": [
"**/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
@@ -38,7 +38,7 @@
}
],
"win": {
"icon": "icon.png",
"icon": "icon.ico",
"target": [
"nsis"
]
@@ -62,7 +62,7 @@
"target": [
"dmg"
],
"icon": "icon.png"
"icon": "icon.icns"
}
},
"scripts": {
@@ -77,7 +77,6 @@
"axios": "^0.19.0",
"cheerio": "^1.0.0-rc.3",
"copy-dir": "^1.1.0",
"custom-electron-titlebar": "^3.0.10",
"electron-context-menu": "^0.13.0",
"get-port": "^5.0.0",
"rimraf": "^2.6.3"
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.4.0</version>
<version>2.7.1</version>
</parent>
<artifactId>vripper-electron</artifactId>
<name>vripper-electron</name>
+6 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.4.0</version>
<version>2.7.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>
@@ -0,0 +1,67 @@
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 PixxxelsHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(PixxxelsHost.class);
private static final String host = "pixxxels.cc";
private static final String IMG_XPATH = "//*[@id='download']";
private static final String TITLE_XPATH = "//*[contains(@class,'imagename')]";
@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 = _url.replace("http://", "https://");
Response resp = getResponse(url, context);
Document doc = resp.getDocument();
Node imgNode, titleNode;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
logger.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url));
titleNode = xpathService.getAsNode(doc, TITLE_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = titleNode.getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("href").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();
@@ -54,9 +54,7 @@ public class PersistenceService {
om.addMixIn(Post.class, PostPersistanceMixin.class);
subscription = processor
.onBackpressureLatest()
.buffer(10, TimeUnit.SECONDS)
.filter(e -> !e.isEmpty())
.map(e -> e.get(0))
.throttleLatest(1, TimeUnit.SECONDS)
.doOnNext(this::persist)
.subscribe();
}
@@ -77,6 +77,6 @@ public class PostParser {
}
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().filter(v -> v.getValue().get() > 0).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");
}
}
@@ -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 links only 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);
}
}
@@ -56,7 +56,7 @@ public class SettingsRestEndpoint {
} catch (ValidationException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new Response(e.getMessage()));
.body(e.getMessage());
}
this.settings.setDownloadPath(settings.getDownloadPath());
this.settings.setMaxThreads(settings.getMaxThreads());
@@ -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
}
}
+5278 -2434
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "2.4.0",
"version": "2.7.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
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.4.0</version>
<version>2.7.1</version>
</parent>
<artifactId>vripper-ui</artifactId>
<name>vripper-ui</name>
+22 -14
View File
@@ -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>
+72 -9
View File
@@ -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);
}
}
+21 -30
View File
@@ -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')));
}
}));
}
+15 -4
View File
@@ -1,3 +1,5 @@
import { ContextMenuService } from './ctxt-menu.service';
import { PostContextMenuComponent } from './posts/post-context-menu.component';
import { PostsDataService } from './posts-data.service';
import { AppPreviewDirective } from './common/preview-tooltip.directive';
import { WsConnectionService } from './ws-connection.service';
@@ -34,6 +36,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 +54,22 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
AppPreviewComponent,
AppPreviewDirective,
UrlRendererComponent,
UrlGrabRendererComponent,
FilterComponent,
ScanComponent,
StatusBarComponent,
ToolbarComponent
ToolbarComponent,
GrabQueueComponent,
PostContextMenuComponent
],
entryComponents: [
PostDetailComponent,
SettingsComponent,
ConfirmDialogComponent,
AppPreviewComponent,
ScanComponent
ScanComponent,
MultiPostComponent,
PostContextMenuComponent
],
imports: [
BrowserAnimationsModule,
@@ -74,7 +83,8 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
AgGridModule.withComponents([
PostProgressRendererComponent,
PostDetailsProgressRendererComponent,
UrlRendererComponent
UrlRendererComponent,
UrlGrabRendererComponent
]),
OverlayModule
],
@@ -85,7 +95,8 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
ServerService,
SharedService,
SelectionService,
PostsDataService
PostsDataService,
ContextMenuService
],
bootstrap: [AppComponent]
})
+7 -6
View File
@@ -1,11 +1,13 @@
import { environment } from 'src/environments/environment';
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';
import { Observable } from 'rxjs';
import { BreakpointState, Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { Title } from '@angular/platform-browser';
@Injectable()
export class AppService {
@@ -14,7 +16,10 @@ export class AppService {
private serverService: ServerService,
public dialog: MatDialog,
private breakpointObserver: BreakpointObserver,
) {}
private titleService: Title
) {
this.titleService.setTitle('VRipper ' + environment.version);
}
darkTheme = false;
_renderer: Renderer2;
@@ -23,7 +28,6 @@ export class AppService {
}
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
isScanOpen = false;
updateTheme(darkTheme: boolean) {
this.darkTheme = darkTheme;
@@ -65,11 +69,8 @@ export class AppService {
}
});
scanDialog.afterOpened().subscribe(() => this.isScanOpen = true);
scanDialog.afterClosed().subscribe(() => {
smallDialogSubscription.unsubscribe();
this.isScanOpen = false;
});
}
}
+3 -1
View File
@@ -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,32 +1,48 @@
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);
@Input() appPreview: string[];
constructor(private overlayPositionBuilder: OverlayPositionBuilder,
constructor(
private overlayPositionBuilder: OverlayPositionBuilder,
private elementRef: ElementRef,
private overlay: Overlay,
private ngZone: NgZone) {}
private ngZone: NgZone
) {}
ngOnInit() {
const positionStrategy = this.overlayPositionBuilder
.flexibleConnectedTo(this.elementRef)
.withPush(true)
.withGrowAfterOpen(true)
.withPositions([{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top',
}]);
this.overlayRef = this.overlay.create({ positionStrategy });
.flexibleConnectedTo(this.elementRef)
.withPush(true)
.withGrowAfterOpen(true)
.withPositions([
{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
},
{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom'
}
]);
this.overlayRef = this.overlay.create({ positionStrategy, scrollStrategy: this.overlay.scrollStrategies.close() });
}
ngOnDestroy() {
this.ngZone.run(() => {
this.overlayRef.dispose();
});
}
@HostListener('mouseenter')
@@ -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) {}
}
+68
View File
@@ -0,0 +1,68 @@
import { PostState } from './posts/post-state.model';
import { Injectable, ComponentRef } from '@angular/core';
import { filter, take } from 'rxjs/operators';
import { fromEvent, Subscription } from 'rxjs';
import { OverlayPositionBuilder, Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { PostContextMenuComponent } from './posts/post-context-menu.component';
@Injectable()
export class ContextMenuService {
constructor(private overlayPositionBuilder: OverlayPositionBuilder, private overlay: Overlay) {}
postCtxtMenuPortal = new ComponentPortal(PostContextMenuComponent);
private postCtxtMenuOverlayRef: OverlayRef;
sub: Subscription;
openPostCtxtMenu(mouseEvent: MouseEvent, postState: PostState) {
mouseEvent.preventDefault();
this.closePostCtxtMenu();
const positionStrategy = this.overlayPositionBuilder
.flexibleConnectedTo({ x: mouseEvent.x, y: mouseEvent.y })
.withPush(true)
.withGrowAfterOpen(true)
.withPositions([
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom'
}
]);
this.postCtxtMenuOverlayRef = this.overlay.create({
positionStrategy
});
const ref: ComponentRef<PostContextMenuComponent> = this.postCtxtMenuOverlayRef.attach(this.postCtxtMenuPortal);
ref.instance.postState = postState;
ref.instance.contextMenuService = this;
this.sub = fromEvent<MouseEvent>(document, 'click')
.pipe(
filter(event => {
const clickTarget = event.target as HTMLElement;
return !!this.postCtxtMenuOverlayRef && !this.postCtxtMenuOverlayRef.overlayElement.contains(clickTarget);
}),
take(1)
)
.subscribe(() => this.closePostCtxtMenu());
}
closePostCtxtMenu() {
if (this.sub != null) {
this.sub.unsubscribe();
}
if (this.postCtxtMenuOverlayRef) {
this.postCtxtMenuOverlayRef.dispose();
this.postCtxtMenuOverlayRef = null;
}
}
}
export interface Theme {
darkTheme: boolean;
}
@@ -0,0 +1,2 @@
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
</ag-grid-angular>
@@ -0,0 +1,48 @@
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',
suppressMovable: true,
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,98 @@
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 style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
><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
});
}
);
}
}
+8 -3
View File
@@ -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%;
}
+19 -12
View File
@@ -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() {}
}
+4 -4
View File
@@ -7,7 +7,6 @@ import {
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
MatMenuModule,
MatDialogModule,
MatCardModule,
@@ -16,7 +15,8 @@ import {
MatSnackBar,
MatTabsModule,
MatDividerModule,
MatChipsModule
MatChipsModule,
MatListModule
} from '@angular/material';
@NgModule({
@@ -28,7 +28,6 @@ import {
MatIconModule,
MatInputModule,
MatFormFieldModule,
MatProgressSpinnerModule,
MatMenuModule,
MatDialogModule,
MatCardModule,
@@ -36,7 +35,8 @@ import {
MatSnackBarModule,
MatTabsModule,
MatDividerModule,
MatChipsModule
MatChipsModule,
MatListModule
],
providers: [MatSnackBar]
})
@@ -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, 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);
@@ -52,6 +54,7 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
field: 'number',
cellClass: 'no-padding',
maxWidth: 100,
suppressMovable: true,
checkboxSelection: true,
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: false,
@@ -60,13 +63,21 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
{
headerName: 'Title',
field: 'title',
suppressMovable: true,
cellClass: 'no-padding'
},
{
headerName: 'URL',
field: 'url',
cellClass: 'no-padding',
suppressMovable: true,
cellRenderer: 'urlCellRenderer'
},
{
headerName: 'Hosts',
field: 'hosts',
suppressMovable: true,
cellClass: 'no-padding'
}
],
defaultColDef: {
@@ -86,89 +97,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');
let count = 0;
let data_0;
this.subscription = handler.subscribeForThreadParsing(
(states: Array<VRThreadParseState>, data: Array<VRPostParse>) => {
if (data.length > 0 && data[0].threadId === this.threadId) {
this.gridOptions.api.updateRowData({ add: data });
count += data.length;
data_0 = data[0];
}
if (states.length > 0 && states[states.length - 1].state === 'END' && states[0].threadId === this.threadId) {
this.ngZone.run(() => {
this.loading.emit(false);
if (count === 0) {
this._snackBar.open('No posts were found', null, {
duration: 5000
});
this.done.emit(true);
} else if (count === 1) {
this.addPosts(
[data_0].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);
this.httpClient
.post(this.serverService.baseUrl + '/post/add', data)
.pipe(
finalize(() => {
this.dialogRef.close();
this.loading.emit(false);
})
)
.subscribe(
() => {},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', 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,10 +22,20 @@ export class PostDetailComponent implements OnInit, OnDestroy {
this.gridOptions = <GridOptions> {
columnDefs: [
{
headerName: '#',
field: 'index',
sortable: true,
sort: 'asc',
suppressMovable: true,
cellClass: 'col-white',
width: 10
},
{
headerName: 'URL',
field: 'url',
sortable: true,
suppressMovable: true,
cellRenderer: 'progressCellRenderer',
cellClass: 'no-padding'
}
@@ -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
) {}
}
@@ -0,0 +1,213 @@
import { ServerService } from './../server-service';
import { HttpClient } from '@angular/common/http';
import { ElectronService } from 'ngx-electron';
import { Component, ChangeDetectionStrategy, NgZone } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { MatDialog, MatSnackBar } from '@angular/material';
import { PostDetailComponent } from '../post-detail/post-detail.component';
import { Observable } from 'rxjs';
import { BreakpointState, BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { PostState } from './post-state.model';
import { DownloadPath } from '../common/download-path.model';
import { ContextMenuService } from '../ctxt-menu.service';
import { ConfirmDialogComponent } from '../common/confirmation-component/confirmation-dialog';
import { filter, flatMap } from 'rxjs/operators';
import { RemoveResponse } from '../common/remove-response.model';
@Component({
selector: 'app-post-ctx-menu',
template: `
<mat-card>
<mat-action-list>
<button
(click)="restart()"
*ngIf="
(postState.status === 'COMPLETE' && postState.progress !== 100) ||
postState.status === 'ERROR' ||
postState.status === 'STOPPED'
"
mat-list-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-list-item
>
<mat-icon>stop</mat-icon>
<span>Stop</span>
</button>
<button (click)="remove()" mat-list-item>
<mat-icon>delete</mat-icon>
<span>Remove</span>
</button>
<button (click)="seeDetails()" mat-list-item>
<mat-icon>list</mat-icon>
<span>Files</span>
</button>
<button (click)="open()" *ngIf="electronService.isElectronApp" mat-list-item>
<mat-icon>open_in_new</mat-icon>
<span>Download Location</span>
</button>
</mat-action-list>
</mat-card>
`,
styles: [
`
mat-card {
padding: 0 0 8px 0;
}
mat-icon {
margin-right: 5px;
}
`
],
animations: [
trigger('simpleFadeAnimation', [
state('in', style({ opacity: 1 })),
transition(':enter', [style({ opacity: 0 }), animate(300)])
])
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PostContextMenuComponent {
postState: PostState;
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
fs;
contextMenuService: ContextMenuService;
constructor(
public electronService: ElectronService,
private dialog: MatDialog,
private breakpointObserver: BreakpointObserver,
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar,
private ngZone: NgZone
) {
if (this.electronService.isElectronApp) {
this.fs = this.electronService.remote.require('fs');
}
}
restart() {
this.contextMenuService.closePostCtxtMenu();
this.httpClient.post(this.serverService.baseUrl + '/post/restart', [ this.postState.postId ]).subscribe(
() => {
this._snackBar.open('Download started', null, {
duration: 5000
});
},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
remove() {
this.contextMenuService.closePostCtxtMenu();
this.ngZone.run(() => {
this.dialog
.open(ConfirmDialogComponent, {
maxHeight: '100vh',
maxWidth: '100vw',
height: '200px',
width: '60%',
data: { header: 'Confirmation', content: 'Are you sure you want to remove this item ?' }
})
.afterClosed()
.pipe(
filter(e => e === 'yes'),
flatMap(e =>
this.httpClient.post<RemoveResponse>(this.serverService.baseUrl + '/post/remove', [ this.postState.postId ])
)
)
.subscribe(
() => {},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
});
}
stop() {
this.contextMenuService.closePostCtxtMenu();
this.httpClient.post(this.serverService.baseUrl + '/post/stop', [ this.postState.postId ]).subscribe(
() => {
this._snackBar.open('Download stopped', null, {
duration: 5000
});
},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
seeDetails() {
this.contextMenuService.closePostCtxtMenu();
this.ngZone.run(() => {
const dialogRef = this.dialog.open(PostDetailComponent, {
width: '90%',
height: '90%',
maxWidth: '100vw',
maxHeight: '100vh',
data: this.postState
});
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();
}
});
});
}
open() {
this.contextMenuService.closePostCtxtMenu();
if (!this.electronService.isElectronApp) {
console.error('Cannot open downloader folder, not electron app');
return;
}
// Request the server to give the correct file location
this.httpClient.get<DownloadPath>(this.serverService.baseUrl + '/post/path/' + this.postState.postId).subscribe(
path => {
if (this.fs.existsSync(path.path)) {
this.electronService.shell.openItem(path.path);
} else {
if (this.postState.done <= 0) {
this._snackBar.open('Download has not been started yet for this post', null, {
duration: 5000
});
} else {
this._snackBar.open(path.path + ' does not exist, you probably removed it', null, {
duration: 5000
});
}
}
},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
}
@@ -1,111 +1,4 @@
<!-- <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">
<div (contextmenu)="context($event)" class="container" style="height: 100%; background-color: white">
<ng-container *ngIf="postState$ | async as postState">
<div
[ngClass]="{
@@ -124,31 +17,21 @@
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>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button (click)="seeDetails()" mat-menu-item>
<mat-icon>list</mat-icon>
<span>Files</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>
</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,15 @@
.checkbox {
background-color: transparent;
}
.row-text {
white-space: nowrap;
overflow: hidden;
padding-right: 15px;
}
.row-text > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -7,19 +7,15 @@ import {
NgZone,
ChangeDetectionStrategy,
AfterViewInit,
EventEmitter
EventEmitter,
} from '@angular/core';
import { AgRendererComponent } from 'ag-grid-angular';
import { Subscription, Observable } from 'rxjs';
import { Subscription, BehaviorSubject, Subject } from 'rxjs';
import { WsHandler } from '../ws-handler';
import { ICellRendererParams } from 'ag-grid-community';
import { ICellRendererParams, RowNode, GridApi } from 'ag-grid-community';
import { ElectronService } from 'ngx-electron';
import { BreakpointState, Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { MatDialog, MatSnackBar } from '@angular/material';
import { PostDetailComponent } from '../post-detail/post-detail.component';
import { HttpClient } from '@angular/common/http';
import { ServerService } from '../server-service';
import { DownloadPath } from '../common/download-path.model';
import { MatDialog } from '@angular/material';
import { ContextMenuService } from '../ctxt-menu.service';
@Component({
selector: 'app-progress-cell',
@@ -32,16 +28,9 @@ export class PostProgressRendererComponent implements AgRendererComponent, OnIni
private wsConnectionService: WsConnectionService,
private zone: NgZone,
public electronService: ElectronService,
private breakpointObserver: BreakpointObserver,
public dialog: MatDialog,
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar
) {
private contextMenuService: ContextMenuService) {
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
if (this.electronService.isElectronApp) {
this.fs = this.electronService.remote.require('fs');
}
}
websocketHandlerPromise: Promise<WsHandler>;
@@ -49,8 +38,10 @@ export class PostProgressRendererComponent implements AgRendererComponent, OnIni
private postState: PostState;
updatesSubscription: Subscription;
expanded = false;
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
fs;
loaded: Subject<boolean> = new BehaviorSubject(false);
loading;
node: RowNode;
gridApi: GridApi;
trunc(value: number): number {
return Math.trunc(value);
@@ -73,15 +64,19 @@ 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 {
this.node = params.node;
this.gridApi = params.api;
this.postState = params.data;
}
@@ -99,55 +94,9 @@ export class PostProgressRendererComponent implements AgRendererComponent, OnIni
}
}
seeDetails() {
const dialogRef = this.dialog.open(PostDetailComponent, {
width: '90%',
height: '90%',
maxWidth: '100vw',
maxHeight: '100vh',
data: this.postState
});
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();
}
});
}
open() {
if (!this.electronService.isElectronApp) {
console.error('Cannot open downloader folder, not electron app');
return;
}
// Request the server to give the correct file location
this.httpClient.get<DownloadPath>(this.serverService.baseUrl + '/post/path/' + this.postState.postId).subscribe(
path => {
if (this.fs.existsSync(path.path)) {
this.electronService.shell.openItem(path.path);
} else {
if (this.postState.done <= 0) {
this._snackBar.open('Download has not been started yet for this post', null, {
duration: 5000
});
} else {
this._snackBar.open(path.path + ' does not exist, you probably removed it', null, {
duration: 5000
});
}
}
},
error => {
console.error(error);
}
);
context(event: MouseEvent) {
this.gridApi.getSelectedNodes().forEach(e => e.setSelected(false));
this.node.setSelected(true);
this.contextMenuService.openPostCtxtMenu(event, this.postState);
}
}
+7 -2
View File
@@ -6,6 +6,7 @@ import { WsConnectionService } from '../ws-connection.service';
import { GridOptions } from 'ag-grid-community';
import { PostProgressRendererComponent } from './post-progress.renderer.component';
import { Subject } from 'rxjs';
import { ContextMenuService } from '../ctxt-menu.service';
@Component({
selector: 'app-posts',
@@ -17,7 +18,8 @@ export class PostsComponent implements OnInit, OnDestroy, AfterViewInit {
private wsConnection: WsConnectionService,
private zone: NgZone,
private selectionService: SelectionService,
private postsDataService: PostsDataService
private postsDataService: PostsDataService,
private contextMenuService: ContextMenuService
) {
this.gridOptions = <GridOptions>{
columnDefs: [
@@ -28,10 +30,12 @@ export class PostsComponent implements OnInit, OnDestroy, AfterViewInit {
cellRenderer: 'progressCellRenderer',
cellClass: 'no-padding',
sort: 'asc',
suppressMovable: true,
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true
}
],
rowBuffer: 200,
rowHeight: 48,
animateRows: true,
rowSelection: 'multiple',
@@ -50,7 +54,8 @@ export class PostsComponent implements OnInit, OnDestroy, AfterViewInit {
},
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit(),
onSelectionChanged: () => this.selectionService.onSelectionChanged(this.gridOptions.api.getSelectedNodes())
onSelectionChanged: () => this.selectionService.onSelectionChanged(this.gridOptions.api.getSelectedNodes()),
onBodyScroll: () => this.contextMenuService.closePostCtxtMenu()
};
}
+17 -25
View File
@@ -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>
+10 -56
View File
@@ -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,16 +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.processUrl(this.input, form);
this.processUrl(this.input);
});
}
@@ -52,70 +46,30 @@ export class ScanComponent implements OnInit, AfterViewInit {
});
}
processUrl(url: string, form?: NgForm) {
this.hideScan.emit(true);
this.threadId.emit(null);
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.hideScan.emit(false);
this._snackBar.open(error.error, null, {
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);
}
});
}
@@ -57,7 +57,9 @@ export class SettingsComponent implements OnInit {
this.desktopSettingsForm.reset(data);
},
error => {
console.error(error);
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
@@ -94,7 +96,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 {
@@ -28,7 +28,7 @@
<div fxFlex="grow" id="global-search">
<form autocomplete="off">
<mat-form-field style="width: 100%">
<input (ngModelChange)="search($event)" matInput name="search" ngModel/>
<input (ngModelChange)="search($event)" matInput name="search" ngModel placeholder="Search"/>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</form>
+18 -10
View File
@@ -101,7 +101,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.postsDataService.remove(data);
},
error => {
console.error(error);
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
@@ -116,7 +118,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
});
},
error => {
console.error(error);
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
@@ -131,7 +135,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
});
},
error => {
console.error(error);
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
@@ -143,7 +149,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
});
}
@@ -171,7 +177,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
});
}
@@ -186,7 +192,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
});
}
@@ -201,7 +207,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
});
}
@@ -210,8 +216,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() {
@@ -229,7 +237,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));
});
}
@@ -15,4 +15,9 @@
border-radius:12px;
box-shadow:0 0 3px black;
}
}
#global-search {
font-size: 16px;
line-height: 18px;
}
+5 -2
View File
@@ -15,7 +15,7 @@ export enum WSState {
OPEN
}
const maxAttemps = 5;
const maxAttemps = 15;
@Injectable()
export class WsConnectionService {
@@ -32,10 +32,13 @@ export class WsConnectionService {
return this.wsHandler;
}
if (this.electronService.isElectronApp) {
this.electronService.ipcRenderer.send('get-port');
const portRequest = setInterval(() => {
this.electronService.ipcRenderer.send('get-port');
}, 1000);
// wait for MainIPC
this.electronService.ipcRenderer.once('port', (event, port) => {
clearInterval(portRequest);
console.log('server running on port', port);
this.serverService.baseUrl = 'http://localhost:' + port;
this.tryConnect(resolve, reject);
+15 -26
View File
@@ -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) {
+81
View File
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns="http://www.w3.org/2000/svg"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93334"
version="1.1"
id="svg8"
inkscape:export-filename="C:\Users\Marwen\IdeaProjects\vripper-project\vripper-electron\build\icons\1024x1024.png"
inkscape:export-xdpi="96.00956"
inkscape:export-ydpi="96.00956"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="v.svg">
<defs
id="defs2"/>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-200.49762"
inkscape:cy="299.73785"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"/>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-26.06665)">
<circle
id="path3715"
cx="135.46666"
cy="161.53331"
style="fill:#aa0000;fill-opacity:1;stroke-width:0.35248032"
r="135.45317"/>
<flowRoot
xml:space="preserve"
id="flowRoot4522"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40.00005341px;line-height:1.25;font-family:'Fira Code';-inkscape-font-specification:'Fira Code, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none"
transform="matrix(5.7825687,0,0,5.7825687,-859.25089,-2020.8769)"><flowRegion
id="flowRegion4524"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40.00005341px;font-family:'Fira Code';-inkscape-font-specification:'Fira Code, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"><rect
id="rect4526"
width="514.28571"
height="380"
x="160"
y="356.80539"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40.00005341px;font-family:'Fira Code';-inkscape-font-specification:'Fira Code, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1" /></flowRegion>
<flowPara
id="flowPara4534">V</flowPara></flowRoot>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -1,4 +1,5 @@
export const environment = {
production: true,
localhost: ''
localhost: '',
version: '2.7.1'
};
+2 -1
View File
@@ -4,7 +4,8 @@
export const environment = {
production: false,
localhost: 'http://localhost:8080'
localhost: 'http://localhost:8080',
version: '2.7.1'
};
/*
+7 -7
View File
@@ -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;
}
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>V Ripper</title>
<title>VRipper</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
+4 -20
View File
@@ -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;
}