Compare commits

...
19 Commits
Author SHA1 Message Date
death-claw 3d3eb6aa36 v2.10.5 2020-01-18 22:28:27 +01:00
death-claw e22132255e Bug fixes 2020-01-18 22:24:53 +01:00
death-claw ef041526ec v2.10.4 2020-01-18 13:27:47 +01:00
death-claw 704311dd9a Zero value for total max download will disable the limit
Add system notification
Bug fixes
2020-01-18 13:21:46 +01:00
death-claw 9617a6f20f v2.10.3 2020-01-16 21:39:59 +01:00
death-claw bc17442961 Some bug fixes
Enhance download queue logic
Clear cache button
Fix the partial status
2020-01-16 21:34:09 +01:00
death-claw 574d6818c1 Refactor PostImgHost 2020-01-14 21:03:35 +01:00
death-clawandGitHub 5c416055ee Merge pull request #3 from life-claw/postimg
Added Support for postimg.cc
2020-01-14 19:01:41 +01:00
death-clawandGitHub fa30458dc6 Merge pull request #1 from life-claw/imagevenue
Add support for imagevenue.com
2020-01-14 18:47:47 +01:00
death-clawandGitHub 638af93225 Merge pull request #2 from life-claw/urlSpaces
Sanitize URLs that contain spaces
2020-01-14 18:14:47 +01:00
life-claw 2c6ca741c7 Added PostImgHost 2020-01-07 16:50:11 -06:00
life-claw d7f2df2540 Sanitize URLs with that contain spaces 2020-01-07 16:18:45 -06:00
life-claw b85ade1dba Added ImageVenueHost 2020-01-07 16:04:04 -06:00
death-claw 1c61a0b34e v2.10.2 2019-12-29 11:35:59 +01:00
death-claw 46b9449969 Upgrade electron to v7
Update the retry policy
2019-12-29 11:33:03 +01:00
death-claw cc1bbd1231 v2.10.1 2019-12-27 23:05:10 +01:00
death-claw b29cba2911 Bug fix 2019-12-27 22:58:33 +01:00
death-claw 13bde3aa2c 2.10.0 2019-12-27 21:27:27 +01:00
death-claw 35b4f5fc97 Add support for imgspice
Fix spring dependency bug
2019-12-27 21:25:33 +01:00
47 changed files with 3018 additions and 2733 deletions
+36
View File
@@ -1,5 +1,41 @@
# Changelog
## [2.10.5] - 2020-01-18
### Changed
- Bug fixes
## [2.10.4] - 2020-01-18
### Changed
- Zero value for total max download will disable the limit
- Add system notification
- Bug fixes
## [2.10.3] - 2020-01-16
### Changed
- Some bug fixes
- Enhance download queue logic
- Clear cache button
- Fix the partial status
- Sanitize URLs with spaces
### Added
- Support for Postimg host
- Support for Imagevenue host
## [2.10.2] - 2019-12-29
### Changed
- Upgrade electron to v7
- Bug fix
## [2.10.1] - 2019-12-27
### Changed
- Bug fix
## [2.10.0] - 2019-12-27
### Changed
- Fix spring dependency bug
### Added
- Add support for imgspice
## [2.9.0] - 2019-12-27
### Changed
- Fix build issue
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.9.1</version>
<version>2.10.5</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
+45 -40
View File
@@ -23,6 +23,11 @@ process.on("uncaughtException", err => {
});
function createWindow() {
if (process.platform === 'win32') {
app.setAppUserModelId("tn.mnlr.vripper");
}
let icon;
if(process.platform === "win32") {
icon = __dirname + '/icon.ico';
@@ -59,51 +64,51 @@ function createWindow() {
});
}
getPort().then(port => {
serverPort = port;
ipcMain.on("get-port", event => {
event.reply("port", port);
});
let javaBinPath;
if(appDir !== undefined) {
javaBinPath = path.join(appDir, "java-runtime/bin/java");
} else {
if(process.platform === 'darwin') {
javaBinPath = path.join(app.getPath('exe'), "../../java-runtime/bin/java");
} else {
javaBinPath = path.join(app.getPath('exe'), "../java-runtime/bin/java");
}
}
let jarPath;
if(appDir !== undefined) {
jarPath = path.join(appDir, "bin/vripper-server.jar");
} else {
if(process.platform === 'darwin') {
jarPath = path.join(app.getPath('exe'), "../../bin/vripper-server.jar");
} else {
jarPath = path.join(app.getPath('exe'), "../bin/vripper-server.jar");
}
}
vripperServer = spawn(javaBinPath, [
"-Xms256m",
"-Dvripper.server.port=" + port,
"-jar",
jarPath
], {
stdio: 'ignore'
});
vripperServer.on('exit', (code, signal) => {
console.log(`vripper server terminated, code = ${code}, signal = ${signal}`);
terminated = true;
app.quit();
});
});
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
getPort().then(port => {
serverPort = port;
ipcMain.on("get-port", event => {
event.reply("port", port);
});
let javaBinPath;
if(appDir !== undefined) {
javaBinPath = path.join(appDir, "java-runtime/bin/java");
} else {
if(process.platform === 'darwin') {
javaBinPath = path.join(app.getPath('exe'), "../../java-runtime/bin/java");
} else {
javaBinPath = path.join(app.getPath('exe'), "../java-runtime/bin/java");
}
}
let jarPath;
if(appDir !== undefined) {
jarPath = path.join(appDir, "bin/vripper-server.jar");
} else {
if(process.platform === 'darwin') {
jarPath = path.join(app.getPath('exe'), "../../bin/vripper-server.jar");
} else {
jarPath = path.join(app.getPath('exe'), "../bin/vripper-server.jar");
}
}
vripperServer = spawn(javaBinPath, [
"-Xms256m",
"-Dvripper.server.port=" + port,
"-jar",
jarPath
], {
stdio: 'ignore'
});
vripperServer.on('exit', (code, signal) => {
console.log(`vripper server terminated, code = ${code}, signal = ${signal}`);
terminated = true;
app.quit();
});
});
app.on("second-instance", (event, commandLine, workingDirectory) => {
if (win) {
if (win.isMinimized()) win.restore();
+865 -1244
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.9.1",
"version": "2.10.5",
"description": "A ripper for vipergirls.to built using web technolgies",
"main": "main.js",
"author": "death-claw <53543762+death-claw@users.noreply.github.com>",
@@ -70,8 +70,8 @@
"dist": "node pre-build.js && electron-builder"
},
"devDependencies": {
"electron": "^6.0.9",
"electron-builder": "^21.2.0"
"electron": "^7.1.9",
"electron-builder": "^22.2.0"
},
"dependencies": {
"axios": "^0.19.0",
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.9.1</version>
<version>2.10.5</version>
</parent>
<artifactId>vripper-electron</artifactId>
<name>vripper-electron</name>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.9.1</version>
<version>2.10.5</version>
</parent>
<artifactId>vripper-server</artifactId>
<name>vripper-server</name>
@@ -0,0 +1,24 @@
package tn.mnlr.vripper;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import tn.mnlr.vripper.services.PersistenceService;
@Component
public class EventListenerBean {
@Autowired
private PersistenceService persistenceService;
@Getter
private static boolean init = false;
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
init = true;
persistenceService.restore();
}
}
@@ -23,3 +23,4 @@ public class VripperApplication {
}
}
}
@@ -120,7 +120,7 @@ abstract public class Host {
throw new DownloadException(String.format("Server returned code %d", response.getStatusLine().getStatusCode()));
}
File outputFile = new File(destinationFolder.getPath() + File.separator + imageFileData.getImageName() + ".tmp");
File outputFile = new File(destinationFolder.getPath() + File.separator + String.format("%03d_", image.getIndex()) + imageFileData.getImageName() + ".tmp");
try (InputStream downloadStream = response.getEntity().getContent(); FileOutputStream fos = new FileOutputStream(outputFile)) {
image.setTotal(response.getEntity().getContentLength());
logger.debug(String.format("%s length is %d", imageFileData.getImageUrl(), image.getTotal()));
@@ -0,0 +1,79 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 java.net.URI;
@Service
public class ImageVenueHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(ImageVenueHost.class);
private static final String host = "imagevenue.com";
private static final String CONTINUE_BUTTON_XPATH = "//a[@title='Continue to your image']";
private static final String IMG_XPATH = "//img[@id='thepic']";
@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 {
//Sadly, they do not support https.
//If they add such support in the future,
//then we should automatically adapt the URL here as done elsewhere.
Response resp = getResponse(url, context);
Document doc = resp.getDocument();
try {
logger.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url));
if(xpathService.getAsNode(doc, CONTINUE_BUTTON_XPATH) != null) {
//Button detected. No need to actually click it, just make the call again.
resp = getResponse(url, context);
doc = resp.getDocument();
}
} catch (XpathException e) {
throw new HostException(e);
}
Node imgNode;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
if (imgNode == null) {
throw new HostException("Failed to locate image");
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
URI baseUri = new URI(url);
imageFileData.setImageUrl(new URI(baseUri.getScheme(), baseUri.getHost(), '/'+imgUrl, null).toString());
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
} catch(Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -0,0 +1,62 @@
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 ImgSpiceHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(ImgSpiceHost.class);
private static final String host = "imgspice.com";
private static final String IMG_XPATH = "//img[@id='imgpreview']";
@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;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
imageFileData.setImageUrl(imgUrl);
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -0,0 +1,61 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 java.util.Optional;
@Service
public class PostImgHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(PostImgHost.class);
private static final String host = "postimg.cc";
private static final String TITLE_XPATH = "//span[contains(@class,'imagename')]";
private static final String IMG_XPATH = "//a[@id='download']";
@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://");
Document doc = getResponse(url, context).getDocument();
Node urlNode, titleNode;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url));
titleNode = xpathService.getAsNode(doc, TITLE_XPATH);
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
urlNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = Optional.ofNullable(titleNode).map(node -> node.getTextContent().trim()).orElseGet(() -> getDefaultImageName(url));
imageFileData.setImageUrl(urlNode.getAttributes().getNamedItem("href").getTextContent().trim());
imageFileData.setImageName(imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -12,6 +12,7 @@ import tn.mnlr.vripper.services.AppStateService;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingDeque;
@@ -45,18 +46,19 @@ public class DownloadQ {
appStateService.newDownloadJob(downloadJob);
}
public void rePut(final DownloadJob downloadJob) throws InterruptedException {
downloadQ.get(downloadJob.getImage().getHost()).putFirst(downloadJob);
public void remove(final DownloadJob downloadJob) {
downloadQ.get(downloadJob.getImage().getHost()).remove(downloadJob);
}
public List<DownloadJob> take() throws Exception {
if (hosts.size() == 0) {
throw new Exception("No host available in th application");
}
public List<DownloadJob> peek() {
List<DownloadJob> downloadJobs = new ArrayList<>();
if (hosts.size() == 0) {
return downloadJobs;
}
for (Host host : hosts) {
Iterator<DownloadJob> it = downloadQ.get(host).iterator();
for (int i = 0; i < appSettingsService.getMaxThreads(); i++) {
DownloadJob downloadJob = downloadQ.get(host).pollFirst();
DownloadJob downloadJob = it.hasNext() ? it.next() : null;
if (downloadJob != null) {
downloadJobs.add(downloadJob);
}
@@ -51,24 +51,29 @@ public class ExecutionService {
private Thread executionThread;
BlockingQueue<DownloadJob> queue = new LinkedBlockingQueue<>();
private RetryPolicy<Object> retryPolicy;
private List<DownloadJob> running = Collections.synchronizedList(new ArrayList<>());
private Map<String, Future<?>> futures = new ConcurrentHashMap<>();
private Thread pollThread;
@PostConstruct
private void init() {
retryPolicy = new RetryPolicy<>()
.handleIf(e -> !(e instanceof InterruptedException))
.withDelay(1, 3, ChronoUnit.SECONDS)
.withBackoff(1, 10, ChronoUnit.SECONDS)
.withMaxDuration(Duration.of(10, ChronoUnit.SECONDS))
.withMaxRetries(2)
.withMaxAttempts(3)
.abortOn(InterruptedException.class)
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
executionThread = new Thread(this::start, "Executor thread");
pollThread = new Thread(this::poll, "Polling thread");
pollThread.start();
executionThread.start();
}
@@ -165,7 +170,6 @@ public class ExecutionService {
stopRunning(postId);
}
public synchronized void stop(String postId) {
try {
if (FINISHED.contains(appStateService.getPost(postId).getStatus())) {
@@ -189,13 +193,13 @@ public class ExecutionService {
}
}
private synchronized boolean canRun(Host host) {
private boolean canRun(Host host) {
boolean canRun;
AtomicInteger count = threadCount.get(host);
if (count == null) {
threadCount.put(host, new AtomicInteger(0));
}
canRun = threadCount.get(host).get() < settings.getMaxThreads() && threadCount.values().stream().mapToInt(AtomicInteger::get).sum() < settings.getMaxTotalThreads();
canRun = threadCount.get(host).get() < settings.getMaxThreads() && (settings.getMaxTotalThreads() == 0 || threadCount.values().stream().mapToInt(AtomicInteger::get).sum() < settings.getMaxTotalThreads());
if (canRun && notPauseQ) {
threadCount.get(host).incrementAndGet();
return true;
@@ -203,14 +207,14 @@ public class ExecutionService {
return false;
}
public void start() {
public void poll() {
while (!Thread.interrupted()) {
List<DownloadJob> take;
try {
take = downloadQ.take();
for (DownloadJob downloadJob : take) {
if (!push(downloadJob)) {
downloadQ.rePut(downloadJob);
List<DownloadJob> peek = downloadQ.peek();
for (DownloadJob downloadJob : peek) {
if (canRun(downloadJob.getImage().getHost())) {
queue.offer(downloadJob);
downloadQ.remove(downloadJob);
}
}
synchronized (threadCount) {
@@ -219,6 +223,17 @@ public class ExecutionService {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
public void start() {
while (!Thread.interrupted()) {
try {
push(queue.take());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
logger.error("Execution Service failed", e);
break;
@@ -226,43 +241,38 @@ public class ExecutionService {
}
}
private boolean push(DownloadJob take) {
if (canRun(take.getImage().getHost())) {
Runnable task = () -> {
running.add(take);
private void push(DownloadJob take) {
Runnable task = () -> {
running.add(take);
Failsafe.with(retryPolicy)
.onFailure(e -> {
if (e.getFailure() instanceof InterruptedException || (e.getFailure() instanceof FailsafeException && e.getFailure().getCause() instanceof InterruptedException)) {
logger.debug("Job successfully interrupted");
return;
}
logger.error(String.format("Failed to download %s after %d tries", take.getImage().getUrl(), e.getAttemptCount()), e.getFailure());
take.getImage().setStatus(Image.Status.ERROR);
})
.onComplete(e -> {
appStateService.doneDownloadJob(take.getImage());
logger.debug(String.format("Finished downloading %s", take.getImage().getUrl()));
if (appSettingsService.isViewPhotos()) {
VripperApplication.commonExecutor.submit(
() -> thumbnailGenerator.getThumbnails()
.get(new ThumbnailGenerator.CacheKey(take.getImage().getPostId(), take.getImageFileData().getFileName())));
}
threadCount.get(take.getImage().getHost()).decrementAndGet();
running.remove(take);
futures.remove(take.getImage().getUrl());
synchronized (threadCount) {
threadCount.notify();
}
})
.get(take::call);
};
logger.debug(String.format("Scheduling a job for %s", take.getImage().getUrl()));
futures.put(take.getImage().getUrl(), executor.submit(task));
return true;
} else {
return false;
}
Failsafe.with(retryPolicy)
.onFailure(e -> {
if (e.getFailure() instanceof InterruptedException || (e.getFailure() instanceof FailsafeException && e.getFailure().getCause() instanceof InterruptedException)) {
logger.debug("Job successfully interrupted");
return;
}
logger.error(String.format("Failed to download %s after %d tries", take.getImage().getUrl(), e.getAttemptCount()), e.getFailure());
take.getImage().setStatus(Image.Status.ERROR);
})
.onComplete(e -> {
appStateService.doneDownloadJob(take.getImage());
logger.debug(String.format("Finished downloading %s", take.getImage().getUrl()));
if (appSettingsService.isViewPhotos()) {
VripperApplication.commonExecutor.submit(
() -> thumbnailGenerator.getThumbnails()
.get(new ThumbnailGenerator.CacheKey(take.getImage().getPostId(), take.getImageFileData().getFileName())));
}
threadCount.get(take.getImage().getHost()).decrementAndGet();
running.remove(take);
futures.remove(take.getImage().getUrl());
synchronized (threadCount) {
threadCount.notify();
}
})
.get(take::call);
};
logger.debug(String.format("Scheduling a job for %s", take.getImage().getUrl()));
futures.put(take.getImage().getUrl(), executor.submit(task));
}
public int runningCount() {
@@ -49,15 +49,15 @@ public class AppSettingsService {
private final String CLEAR = "CLEAR";
private final String DARK_THEME = "DARK_THEME";
private final String VIEW_PHOTOS = "VIEW_PHOTOS";
private String downloadPath;
private int maxThreads;
private final String NOTIFICATION = "NOTIFICATION";
@PostConstruct
private void init() {
restore();
}
private String downloadPath;
private int maxThreads;
private boolean autoStart;
private boolean vLogin;
private String vUsername;
@@ -70,6 +70,7 @@ public class AppSettingsService {
private boolean clearCompleted;
private boolean darkTheme;
private boolean viewPhotos;
private boolean notificationEnabled;
public void setVPassword(String vPassword) {
if(vPassword.isEmpty()) {
@@ -83,7 +84,7 @@ public class AppSettingsService {
downloadPath = prefs.get(DOWNLOAD_PATH, defaultDownloadPath);
maxThreads = prefs.getInt(MAX_THREADS, 4);
maxTotalThreads = prefs.getInt(MAX_TOTAL_THREADS, 8);
maxTotalThreads = prefs.getInt(MAX_TOTAL_THREADS, 0);
autoStart = prefs.getBoolean(AUTO_START, true);
vLogin = prefs.getBoolean(V_LOGIN, false);
vUsername = prefs.get(V_USERNAME, "");
@@ -96,6 +97,7 @@ public class AppSettingsService {
clearCompleted = prefs.getBoolean(CLEAR, false);
darkTheme = prefs.getBoolean(DARK_THEME, false);
viewPhotos = prefs.getBoolean(VIEW_PHOTOS, false);
notificationEnabled = prefs.getBoolean(NOTIFICATION, false);
}
@PreDestroy
@@ -116,6 +118,7 @@ public class AppSettingsService {
prefs.putBoolean(CLEAR, clearCompleted);
prefs.putBoolean(DARK_THEME, darkTheme);
prefs.putBoolean(VIEW_PHOTOS, viewPhotos);
prefs.putBoolean(NOTIFICATION, notificationEnabled);
try {
prefs.sync();
@@ -138,7 +141,7 @@ public class AppSettingsService {
throw new ValidationException(String.format("%s is not a directory", settings.getDownloadPath()));
}
if (settings.getMaxTotalThreads() < 1) {
if (settings.getMaxTotalThreads() < 0) {
throw new ValidationException(String.format("Invalid max global concurrent download settings, values must be in greater than %d", 1));
}
@@ -199,8 +202,10 @@ public class AppSettingsService {
private boolean clearCompleted;
@JsonProperty("viewPhotos")
private boolean viewPhotos;
@JsonProperty("notification")
private boolean notification;
public Settings(String downloadPath, int maxThreads, int maxTotalThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder, boolean subLocation, boolean threadSubLocation, boolean clearCompleted, boolean viewPhotos) {
public Settings(String downloadPath, int maxThreads, int maxTotalThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder, boolean subLocation, boolean threadSubLocation, boolean clearCompleted, boolean viewPhotos, boolean notification) {
this.downloadPath = downloadPath;
this.maxThreads = maxThreads;
this.maxTotalThreads = maxTotalThreads;
@@ -215,6 +220,7 @@ public class AppSettingsService {
this.threadSubLocation = threadSubLocation;
this.clearCompleted = clearCompleted;
this.viewPhotos = viewPhotos;
this.notification = notification;
}
}
}
@@ -67,7 +67,7 @@ public class AppStateService {
public synchronized void postDownloadingUpdate(String postId) {
Post post = currentPosts.get(postId);
if (!post.getStatus().equals(Post.Status.DOWNLOADING)) {
if (!post.getStatus().equals(Post.Status.DOWNLOADING) && !post.getStatus().equals(Post.Status.PARTIAL)) {
post.setStatus(Post.Status.DOWNLOADING);
livePostsState.onNext(post);
}
@@ -54,13 +54,13 @@ public class ConnectionManager {
}
public HttpGet buildHttpGet(String url) {
HttpGet httpGet = new HttpGet(url);
HttpGet httpGet = new HttpGet(url.replace(" ", "+"));
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36");
return httpGet;
}
public HttpPost buildHttpPost(String url) {
HttpPost httpPost = new HttpPost(url);
HttpPost httpPost = new HttpPost(url.replace(" ", "+"));
httpPost.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36");
return httpPost;
}
@@ -13,7 +13,7 @@ public class DownloadSpeed {
}
private String formatSI(long bytes) {
return humanReadableByteCount(bytes, true);
return humanReadableByteCount(bytes, false);
}
private String humanReadableByteCount(long bytes, boolean si) {
@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.EventListenerBean;
import tn.mnlr.vripper.SpringContext;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
@@ -88,8 +89,6 @@ public class PersistenceService {
logger.error("Unable to create data file", e);
SpringContext.close();
}
} else {
restore();
}
}
@@ -98,7 +97,9 @@ public class PersistenceService {
this.subscription.dispose();
logger.info(String.format("Destroying %s", PersistenceService.class.getSimpleName()));
logger.info("Persisting data before destroying");
this.persist(stateService.getCurrentPosts());
if (EventListenerBean.isInit()) {
this.persist(stateService.getCurrentPosts());
}
}
private void persist(Map<String, Post> currentPosts) {
@@ -10,23 +10,35 @@ import org.imgscalr.Scalr;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class ThumbnailGenerator {
@Getter
private LoadingCache<CacheKey, byte[]> thumbnails;
@Value("${base.dir}")
private String baseDir;
@Autowired
private PathService pathService;
@Getter
private File cacheFolder;
CacheLoader<CacheKey, byte[]> loader = new CacheLoader<>() {
@Override
@@ -51,6 +63,52 @@ public class ThumbnailGenerator {
.build(loader);
}
public void clearCache() {
thumbnails.invalidateAll();
for (File file : Optional.ofNullable(cacheFolder.listFiles()).orElse(new File[]{})) {
FileSystemUtils.deleteRecursively(file);
}
}
public long cacheSize() {
final AtomicLong size = new AtomicLong(0);
try {
Files.walkFileTree(cacheFolder.toPath(), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
size.addAndGet(attrs.size());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.out.println("skipped: " + file + " (" + exc + ")");
// Skip folders that can't be traversed
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
if (exc != null)
System.out.println("had trouble traversing: " + dir + " (" + exc + ")");
else
size.addAndGet(dir.toFile().length());
// Ignore errors traversing a folder
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
throw new AssertionError("walkFileTree will not throw IOException if the FileVisitor does not");
}
return size.get();
}
private File generateThumbnail(File inputFile, String postId) throws Exception {
if (!inputFile.exists()) {
throw new Exception(String.format("Input file %s does not exist", inputFile.toString()));
@@ -57,21 +57,18 @@ public class VGHandler {
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()));
} else {
appStateService.getGrabQueue().put(queuedVGLink.getLink(), queuedVGLink);
appStateService.getLiveGrabQueue().onNext(queuedVGLink);
}
return null;
};
VripperApplication.commonExecutor.submit(cl);
}
}
@@ -93,6 +93,7 @@ public class GalleryEndpoint {
return ResponseEntity.ok(
Arrays.stream(Objects.requireNonNull(destinationFolder.listFiles()))
.filter(f -> !f.getName().endsWith("tmp"))
.filter(f -> f.getName().toLowerCase().endsWith(".jpg") || f.getName().toLowerCase().endsWith(".jpeg"))
.sorted(Comparator.comparing(File::getName))
.map(GalleryImage::fromFile)
.filter(Objects::nonNull)
@@ -101,6 +102,39 @@ public class GalleryEndpoint {
}
return new ResponseEntity("Gallery does not exist in download location, you probably removed it", HttpStatus.BAD_REQUEST);
}
@GetMapping("/gallery/cache")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<CacheSize> getCacheSize() {
return ResponseEntity.ok(new CacheSize(humanReadableByteCount(thumbnailGenerator.cacheSize(), false)));
}
@GetMapping("/gallery/cache/clear")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<CacheSize> clearCache() {
thumbnailGenerator.clearCache();
return ResponseEntity.ok(new CacheSize(humanReadableByteCount(thumbnailGenerator.cacheSize(), false)));
}
private String humanReadableByteCount(long bytes, boolean si) {
int unit = si ? 1000 : 1024;
if (bytes < unit) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(unit));
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}
}
@Getter
@Setter
@NoArgsConstructor
class CacheSize {
private String size;
public CacheSize(String size) {
this.size = size;
}
}
@Getter
@@ -13,7 +13,10 @@ import tn.mnlr.vripper.q.ExecutionService;
import tn.mnlr.vripper.services.*;
import java.io.File;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -48,7 +51,6 @@ public class PostRestEndpoint {
@Autowired
private ExecutionService executionService;
// private DownloadQ downloadQ;
@Autowired
@@ -61,6 +63,7 @@ public class PostRestEndpoint {
return new ResponseEntity("Failed to process empty request", HttpStatus.BAD_REQUEST);
}
List<String> urls = Arrays.stream(_url.getUrl().split("\\r?\\n")).map(String::trim).filter(e -> !e.isEmpty()).collect(Collectors.toList());
ArrayList<QueuedVGLink> queuedVGLinks = new ArrayList<>();
for (String url : urls) {
logger.debug(String.format("Starting to process thread: %s", url));
if (!url.startsWith("https://vipergirls.to")) {
@@ -80,8 +83,9 @@ public class PostRestEndpoint {
} 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)));
queuedVGLinks.add(new QueuedVGLink(url, threadId, postId));
}
vgHandler.handle(queuedVGLinks);
return ResponseEntity.ok().build();
}
@@ -81,6 +81,7 @@ public class SettingsRestEndpoint {
this.settings.setThreadSubLocation(settings.isThreadSubLocation());
this.settings.setClearCompleted(settings.isClearCompleted());
this.settings.setViewPhotos(settings.isViewPhotos());
this.settings.setNotificationEnabled(settings.isNotification());
this.settings.save();
@@ -107,7 +108,8 @@ public class SettingsRestEndpoint {
settings.isSubLocation(),
settings.isThreadSubLocation(),
settings.isClearCompleted(),
settings.isViewPhotos()
settings.isViewPhotos(),
settings.isNotificationEnabled()
);
}
@@ -6,5 +6,5 @@ server.port=${vripper.server.port:8080}
management.endpoints.web.exposure.include=shutdown
management.endpoint.shutdown.enabled=true
endpoints.shutdown.enabled=true
spring.profiles.active=portable
#spring.profiles.active=installer
#spring.profiles.active=portable
spring.profiles.active=installer
+1461 -1311
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "2.9.1",
"version": "2.10.5",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -35,7 +35,7 @@
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.803.21",
"@angular-devkit/build-angular": "^0.803.23",
"@angular/cli": "~8.3.20",
"@angular/compiler-cli": "^8.2.14",
"@angular/language-service": "~8.2.14",
@@ -43,7 +43,7 @@
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.5.0",
"electron": "^6.0.9",
"electron": "^7.1.7",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^4.2.0",
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.9.1</version>
<version>2.10.5</version>
</parent>
<artifactId>vripper-ui</artifactId>
<name>vripper-ui</name>
-2
View File
@@ -26,8 +26,6 @@ export class AppService {
private _settings: Settings;
private _renderer: Renderer2;
// private settings$: Subject<Settings> = new Subject();
get settings(): Settings {
return { ...this._settings };
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Subject, Observable } from 'rxjs';
import { ElectronService } from 'ngx-electron';
import { Injectable, OnInit } from '@angular/core';
import { Injectable } from '@angular/core';
import { Clipboard } from 'electron';
import { HttpClient } from '@angular/common/http';
import { Settings } from './common/settings.model';
+2 -1
View File
@@ -9,4 +9,5 @@ export interface Settings {
vThanks: boolean;
desktopClipboard: boolean;
viewPhotos: boolean;
}
notification: boolean;
}
@@ -4,23 +4,37 @@
</div>
<mat-dialog-content fxFlex="grow">
<div class="container">
<div class="masonry ">
<ng-container *ngFor="let img of images | async; let i=index">
<figure class="masonry-brick " fxLayout="row" fxLayoutAlign="center center"
style="position: relative; cursor: pointer;">
<mat-progress-spinner *ngIf="!img._initialized" mode="indeterminate"
style="position: absolute;"></mat-progress-spinner>
<img (click)="openSlideshow(i)" (load)="loaded(img)" [src]="img.msrc"/>
<div class="masonry">
<ng-container *ngFor="let img of images | async; let i = index">
<figure class="masonry-brick">
<img
[style.opacity]="img._initialized ? 1 : 0"
[style.visibility]="img._initialized ? 'visible' : 'hidden'"
(click)="openSlideshow(i)"
(load)="loaded(img)"
[src]="img.msrc"
/>
</figure>
</ng-container>
<app-photo-swipe #photoSwipe></app-photo-swipe>
</div>
</div>
</mat-dialog-content>
<div style="height: 5px; width: 100%; margin-top: 8px">
<div *ngIf="loadProgress | async as progress">
<mat-progress-bar
*ngIf="!progress.done"
[mode]="progress.loading ? 'determinate' : 'indeterminate'"
[value]="progress.progress"
></mat-progress-bar>
</div>
</div>
<mat-dialog-actions align="end" fxFlex="nogrow">
<button (click)="refresh()" color="primary" mat-raised-button>
<button (click)="refresh()" [disabled]="disableRefresh | async" color="primary" mat-raised-button>
<mat-icon>refresh</mat-icon>
<span>Refresh</span></button>
<span>Refresh</span>
</button>
<button mat-dialog-close mat-raised-button>Close</button>
</mat-dialog-actions>
</div>
@@ -3,23 +3,22 @@
}
.masonry {
display: flex;
flex-flow: row wrap;
margin-left: -8px;
width: 100%;
}
.masonry-brick {
flex: auto;
height: 250px;
min-width: 150px;
margin: 0 8px 8px 0;
width: 250px;
display: inline-block;
cursor: pointer;
margin: 0 3px;
}
img {
opacity: 0;
visibility: hidden;
transition: opacity 0.5s ease-in, visibility 0.5s;
object-fit: cover;
height: 250px;
width: auto;
}
figure {
width: 250px;
}
@@ -19,6 +19,14 @@ class Image extends IImage {
}
}
interface Progress {
loading: boolean;
progress: number;
done: boolean;
}
const initialProgressState: Progress = { loading: false, progress: 0, done: false };
@Component({
selector: 'app-gallery',
templateUrl: './gallery.component.html',
@@ -37,11 +45,23 @@ export class GalleryComponent implements OnInit, OnDestroy {
images: Subject<Image[]> = new BehaviorSubject([]);
_images: Image[] = [];
disableRefresh: Subject<boolean> = new BehaviorSubject(true);
loadProgress: Subject<Progress> = new BehaviorSubject(initialProgressState);
loadedImages = 0;
@ViewChild('photoSwipe', { static: true }) photoSwipe: PhotoSwipeComponent;
loaded(img: Image) {
img._initialized = true;
this.loadedImages++;
this.loadProgress.next({
loading: true,
progress: Math.floor((this.loadedImages / this._images.length) * 100),
done: this.loadedImages === this._images.length
});
if (this.loadedImages === this._images.length) {
this.disableRefresh.next(false);
}
}
ngOnInit() {
@@ -69,6 +89,9 @@ export class GalleryComponent implements OnInit, OnDestroy {
}
refresh() {
this.loadProgress.next(initialProgressState);
this.loadedImages = 0;
this.disableRefresh.next(true);
this.httpClient.get<Image[]>(this.serverService.baseUrl + '/gallery/' + this.dialogData.postId).subscribe(
response => {
this.ngZone.run(() => {
@@ -4,6 +4,7 @@ import { GrabQueueDataSource } from './grab-queue.datasource';
import { Component, OnInit, NgZone, ChangeDetectionStrategy } from '@angular/core';
import { GridOptions } from 'ag-grid-community';
import { WsConnectionService } from '../ws-connection.service';
import { NotificationService } from '../notification.service';
@Component({
selector: 'app-grab-queue',
@@ -12,7 +13,11 @@ import { WsConnectionService } from '../ws-connection.service';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GrabQueueComponent implements OnInit {
constructor(private wsConnection: WsConnectionService, private zone: NgZone, private linkCollectorService: LinkCollectorService) {
constructor(
private wsConnection: WsConnectionService,
private zone: NgZone,
private linkCollectorService: LinkCollectorService,
private notificationService: NotificationService) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
@@ -35,7 +40,7 @@ export class GrabQueueComponent implements OnInit {
getRowNodeId: data => data['link'],
onGridReady: () => {
this.gridOptions.api.sizeColumnsToFit();
this.dataSource = new GrabQueueDataSource(this.wsConnection, this.gridOptions, this.zone);
this.dataSource = new GrabQueueDataSource(this.wsConnection, this.gridOptions, this.zone, this.notificationService);
this.dataSource.connect();
},
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
@@ -3,6 +3,7 @@ import { CMD } from './../common/cmd.enum';
import { WSMessage } from './../common/ws-message.model';
import { Subscription } from 'rxjs';
import { WsConnectionService } from '../ws-connection.service';
import { NotificationService } from '../notification.service';
import { GridOptions } from 'ag-grid-community';
import { WsHandler } from '../ws-handler';
import { NgZone } from '@angular/core';
@@ -11,7 +12,8 @@ export class GrabQueueDataSource {
constructor(
private wsConnectionService: WsConnectionService,
private gridOptions: GridOptions,
private zone: NgZone
private zone: NgZone,
private notificationService: NotificationService
) {
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
}
@@ -43,6 +45,13 @@ export class GrabQueueDataSource {
}
});
this.gridOptions.api.updateRowData({ update: toUpdate, add: toAdd, remove: toRemove });
const count = this.gridOptions.api.getDisplayedRowCount();
if (count > 0 && toAdd.length > 0) {
this.notificationService.notifyFromGrabQueue(
'Link Collector',
`You have ${count} ${count > 1 ? 'threads' : 'thread'} waiting in the link collector`
);
}
});
})
);
@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { AppService } from './app.service';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
constructor(private appService: AppService) {}
notifyFromGrabQueue(title: string, body: string) {
if (!this.appService.settings.notification) {
return;
}
if (!('Notification' in window)) {
return;
} else if (Notification.permission === 'granted') {
const notification = new Notification(title, { body: body, icon: 'assets/icon.png' });
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
const notification = new Notification(title, { body: body, icon: 'assets/icon.png' });
}
});
}
}
}
@@ -13,7 +13,7 @@
}
app-details-cell .error {
background-color: mat-color($red, 50);
background-color: mat-color($red, 100);
& .progress-bar .mat-progress-bar-fill::after {
background-color: mat-color($red, 300);
}
@@ -3,7 +3,8 @@
<div
[ngClass]="{
error: postState.status === 'ERROR',
downloading: postState.status === 'DOWNLOADING' || postState.status === 'PARTIAL',
downloading: postState.status === 'DOWNLOADING',
partial: postState.status === 'PARTIAL',
complete: postState.status === 'COMPLETE',
stopped: postState.status === 'STOPPED'
}"
@@ -4,6 +4,7 @@
$red: mat-palette($mat-red);
$grey: mat-palette($mat-grey);
$green: mat-palette($mat-green);
$orange: mat-palette($mat-orange);
app-progress-cell .progress-bar .mat-progress-bar-fill::after {
background-color: mat-color($grey, 400);
@@ -14,7 +15,7 @@
}
app-progress-cell .error {
background-color: mat-color($red, 50);
background-color: mat-color($red, 100);
& .progress-bar .mat-progress-bar-fill::after {
background-color: mat-color($red, 300);
}
@@ -23,6 +24,16 @@
}
}
app-progress-cell .partial {
background-color: mat-color($orange, 100);
& .progress-bar .mat-progress-bar-fill::after {
background-color: mat-color($orange, 300);
}
& .progress-bar .mat-progress-bar-buffer {
background-color: mat-color($orange, 100);
}
}
app-progress-cell .downloading {
background-color: mat-color($green, 50);
& .progress-bar .mat-progress-bar-fill::after {
@@ -44,6 +55,6 @@
}
app-progress-cell .stopped {
background-color: mat-color($grey, 100);
background-color: mat-color($grey, 300);
}
}
@@ -43,7 +43,7 @@
<input
formControlName="maxTotalThreads"
matInput
min="1"
min="0"
name="maxTotalThreads"
placeholder="Max global concurrent downloads"
required
@@ -80,6 +80,13 @@
<mat-checkbox color="primary" formControlName="viewPhotos" name="viewPhotos">
Enable photo gallery
</mat-checkbox>
<div style="margin-left: 15px">
Cache size: {{(cacheSize | async).size}}
<button (click)="clearCache()" [disabled]="(cacheClearLoading | async)" color="primary"
mat-stroked-button>
<mat-icon>clear</mat-icon>
<span>Clear</span></button>
</div>
<mat-slide-toggle color="primary" formControlName="vLogin" name="vLogin">
ViperGirls Authentication
@@ -106,10 +113,17 @@
</section>
</form>
</mat-tab>
<mat-tab label="Desktop Integration" *ngIf="electronService.isElectronApp">
<mat-tab label="Desktop Integration">
<form [formGroup]="desktopSettingsForm" autocomplete="off">
<mat-checkbox color="primary" formControlName="desktopClipboard" name="desktopClipboard"
>Monitor Clipboard</mat-checkbox
<mat-checkbox *ngIf="electronService.isElectronApp" color="primary" formControlName="desktopClipboard"
name="desktopClipboard"
>Monitor Clipboard
</mat-checkbox
>
<mat-checkbox color="primary" formControlName="notification" name="notification"
>Enable system notifications
</mat-checkbox
>
</form>
</mat-tab>
@@ -9,6 +9,11 @@ import { ServerService } from '../server-service';
import { ElectronService } from 'ngx-electron';
import { Settings } from '../common/settings.model';
import { OpenDialogReturnValue } from 'electron';
import { Subject, BehaviorSubject } from 'rxjs';
interface CacheSize {
size: string;
}
@Component({
selector: 'app-settings',
@@ -45,10 +50,13 @@ export class SettingsComponent implements OnInit {
});
desktopSettingsForm = new FormGroup({
desktopClipboard: new FormControl(false)
desktopClipboard: new FormControl(false),
notification: new FormControl(false)
});
darkTheme = false;
cacheSize: Subject<CacheSize> = new BehaviorSubject({size: '0'});
cacheClearLoading: Subject<boolean> = new BehaviorSubject(false);
updateTheme() {
this.appService.updateTheme(this.darkTheme);
@@ -60,17 +68,36 @@ export class SettingsComponent implements OnInit {
ngOnInit() {
this.darkTheme = this.appService.darkTheme;
this.httpClient.get<Settings>(this.serverService.baseUrl + '/settings').subscribe(
data => {
this.generalSettingsForm.reset(data);
this.desktopSettingsForm.reset(data);
},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
this.httpClient.get<Settings>(this.serverService.baseUrl + '/settings')
.subscribe(data => {
this.generalSettingsForm.reset(data);
this.desktopSettingsForm.reset(data);
}, error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
});
this.httpClient.get<CacheSize>(this.serverService.baseUrl + '/gallery/cache')
.subscribe(data => {
this.cacheSize.next(data);
}, error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
});
}
clearCache() {
this.cacheClearLoading.next(true);
this.httpClient.get<CacheSize>(this.serverService.baseUrl + '/gallery/cache/clear')
.pipe(finalize(() => this.cacheClearLoading.next(false)))
.subscribe(data => {
this.cacheSize.next(data);
}, error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
})
}
browse() {
@@ -27,6 +27,10 @@ export class WsConnectionService {
private wsHandler: WsHandler;
constructor(private electronService: ElectronService, private serverService: ServerService) {
this.init();
}
init() {
this.wsHandlerPromise = new Promise((resolve, reject) => {
if (this.wsHandler != null) {
return this.wsHandler;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,5 +1,5 @@
export const environment = {
production: true,
localhost: '',
version: '2.9.1'
version: '2.10.5'
};
+1 -1
View File
@@ -5,7 +5,7 @@
export const environment = {
production: false,
localhost: 'http://localhost:8080',
version: '2.9.1'
version: '2.10.5'
};
/*