feat: automatically fill in copied URL's into the url box (#94)

This commit is contained in:
Jelle Glebbeek
2021-06-01 00:01:37 +02:00
parent 434211f1a8
commit cd603901f8
9 changed files with 181 additions and 16 deletions

View File

@@ -7,10 +7,12 @@ const BinaryUpdater = require("./modules/BinaryUpdater");
const AppUpdater = require("./modules/AppUpdater");
const TaskList = require("./modules/persistence/TaskList");
const DoneAction = require("./modules/DoneAction");
const ClipboardWatcher = require("./modules/ClipboardWatcher");
let win
let env
let queryManager
let clipboardWatcher
let taskList
let appStarting = true;
@@ -34,6 +36,8 @@ function startCriticalHandlers(env) {
shell.openExternal(url);
});
clipboardWatcher = new ClipboardWatcher(win, env);
queryManager = new QueryManager(win, env);
taskList = new TaskList(env.paths, queryManager)
@@ -44,6 +48,7 @@ function startCriticalHandlers(env) {
binaryUpdater.checkUpdate().finally(() => {
win.webContents.send("binaryLock", {lock: false});
taskList.load();
clipboardWatcher.startPolling();
});
} else if(env.settings.taskList) {
taskList.load();

View File

@@ -0,0 +1,39 @@
const { clipboard } = require('electron');
class ClipboardWatcher {
constructor(win, env) {
this.win = win;
this.env = env;
this.urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)?/gi;
}
startPolling() {
this.poll();
this.pollId = setInterval(() => this.poll(), 1000);
}
resetPlaceholder() {
const standard = "Enter a video/playlist URL to add to the queue";
this.win.webContents.send("updateLinkPlaceholder", {text: standard, copied: false});
}
poll() {
if(this.env.settings.autoFillClipboard) {
const text = clipboard.readText();
if (text != null) {
if (this.previous != null && this.previous === text) return;
this.previous = text;
const isURL = text.match(this.urlRegex);
if (isURL) {
this.win.webContents.send("updateLinkPlaceholder", {text: text, copied: true});
} else {
this.resetPlaceholder();
}
} else {
this.resetPlaceholder();
}
}
}
}
module.exports = ClipboardWatcher;

View File

@@ -2,11 +2,12 @@ const os = require("os");
const fs = require("fs").promises;
class Settings {
constructor(paths, env, outputFormat, proxy, spoofUserAgent, validateCertificate, taskList, nameFormat, nameFormatMode, sizeMode, splitMode, maxConcurrent, updateBinary, updateApplication, cookiePath, statSend, downloadMetadata, downloadThumbnail, keepUnmerged, calculateTotalSize, theme) {
constructor(paths, env, outputFormat, proxy, autoFillClipboard, spoofUserAgent, validateCertificate, taskList, nameFormat, nameFormatMode, sizeMode, splitMode, maxConcurrent, updateBinary, updateApplication, cookiePath, statSend, downloadMetadata, downloadThumbnail, keepUnmerged, calculateTotalSize, theme) {
this.paths = paths;
this.env = env
this.outputFormat = outputFormat == null ? "none" : outputFormat;
this.proxy = proxy == null ? "" : proxy;
this.autoFillClipboard = autoFillClipboard == null ? true : autoFillClipboard;
this.spoofUserAgent = spoofUserAgent == null ? true : spoofUserAgent;
this.validateCertificate = validateCertificate == null ? false : validateCertificate;
this.taskList = taskList == null ? true : taskList;
@@ -30,7 +31,7 @@ class Settings {
try {
let result = await fs.readFile(paths.settings, "utf8");
let data = JSON.parse(result);
return new Settings(paths, env, data.outputFormat, data.proxy, data.spoofUserAgent, data.validateCertificate, data.taskList, data.nameFormat, data.nameFormatMode, data.sizeMode, data.splitMode, data.maxConcurrent, data.updateBinary, data.updateApplication, data.cookiePath, data.statSend, data.downloadMetadata, data.downloadThumbnail, data.keepUnmerged, data.calculateTotalSize, data.theme);
return new Settings(paths, env, data.outputFormat, data.proxy, data.autoFillClipboard, data.spoofUserAgent, data.validateCertificate, data.taskList, data.nameFormat, data.nameFormatMode, data.sizeMode, data.splitMode, data.maxConcurrent, data.updateBinary, data.updateApplication, data.cookiePath, data.statSend, data.downloadMetadata, data.downloadThumbnail, data.keepUnmerged, data.calculateTotalSize, data.theme);
} catch(err) {
console.log(err);
let settings = new Settings(paths, env);
@@ -43,6 +44,7 @@ class Settings {
update(settings) {
this.outputFormat = settings.outputFormat;
this.proxy = settings.proxy;
this.autoFillClipboard = settings.autoFillClipboard;
this.spoofUserAgent = settings.spoofUserAgent;
this.validateCertificate = settings.validateCertificate;
this.taskList = settings.taskList;
@@ -57,8 +59,7 @@ class Settings {
if(this.maxConcurrent !== settings.maxConcurrent) {
this.maxConcurrent = settings.maxConcurrent;
this.env.changeMaxConcurrent(settings.maxConcurrent);
}
this.updateBinary = settings.updateBinary;
}this.updateBinary = settings.updateBinary;
this.updateApplication = settings.updateApplication;
this.theme = settings.theme;
this.save();
@@ -71,6 +72,7 @@ class Settings {
return {
outputFormat: this.outputFormat,
proxy: this.proxy,
autoFillClipboard: this.autoFillClipboard,
spoofUserAgent: this.spoofUserAgent,
validateCertificate: this.validateCertificate,
taskList: this.taskList,

View File

@@ -36,6 +36,7 @@ contextBridge.exposeInMainWorld(
"maximized",
"videoAction",
"updateGlobalButtons",
"updateLinkPlaceholder",
"totalSize",
"binaryLock"
];

View File

@@ -17,7 +17,7 @@
<div class="container url-input">
<form id="url-form" class="row mx-auto">
<div class="input-group">
<input type="text" class="form-control" id="add-url" placeholder="Enter a video/playlist URL to add to the queue" required>
<input type="text" class="form-control" id="add-url" placeholder="Enter a video/playlist URL to add to the queue">
<div class="input-group-append">
<button type="button" id="add-url-btn" class="btn btn-dark" title="Add video to queue"><i class="bi bi-plus"></i></button>
</div>
@@ -256,6 +256,10 @@
<input type="range" class="form-range w-50 d-block align-middle" min="1" max="32" id="maxConcurrent">
<button type="button" class="btn btn-dark" id="defaultConcurrent">Reset to default</button>
</div>
<div class="mb-1">
<input class="check-input" type="checkbox" value="" id="autoFillClipboard">
<label class="check-label" for="autoFillClipboard">Automatically fill in copied links</label>
</div>
<hr/>
<h3>Appearance</h3>
<div class="mb-4">

View File

@@ -1,4 +1,5 @@
let platform;
let linkCopied = false;
let progressCooldown = [];
let sizeCooldown = [];
let sizeCache = [];
@@ -31,6 +32,11 @@ async function init() {
window.main.invoke('titlebarClick', "maximize")
})
//Updates the placeholder to a copied link
window.main.receive("updateLinkPlaceholder", (args) => {
$('#add-url').prop("placeholder", args.text);
linkCopied = args.copied;
});
//Init the when done dropdown
$('.dropdown-toggle').dropdown();
@@ -82,10 +88,7 @@ async function init() {
//Add url when user presses enter, but prevent default behavior
$(document).on("keydown", "form", function(event) {
if(event.key == "Enter") {
if ($('#url-form')[0].checkValidity()) {
parseURL($('#add-url').val());
$('#url-form').trigger('reset');
}
verifyURL();
return false;
}
return true
@@ -93,10 +96,7 @@ async function init() {
//Add url when user press on the + button
$('#add-url-btn').on('click', () => {
if($('#url-form')[0].checkValidity()) {
parseURL($('#add-url').val());
$('#url-form').trigger('reset');
}
verifyURL();
});
$('body').on('click', '#install-btn', () => {
@@ -150,6 +150,7 @@ async function init() {
let settings = {
updateBinary: $('#updateBinary').prop('checked'),
updateApplication: $('#updateApplication').prop('checked'),
autoFillClipboard: $('#autoFillClipboard').prop('checked'),
outputFormat: $('#outputFormat').val(),
proxy: $('#proxySetting').val(),
spoofUserAgent: $('#spoofUserAgent').prop('checked'),
@@ -191,6 +192,7 @@ async function init() {
$('#spoofUserAgent').prop('checked', settings.spoofUserAgent);
$('#validateCertificate').prop('checked', settings.validateCertificate);
$('#taskList').prop('checked', settings.taskList);
$('#autoFillClipboard').prop('checked', settings.autoFillClipboard);
$('#proxySetting').val(settings.proxy);
$('#nameFormatCustom').val(settings.nameFormat);
$('#nameFormat').val(settings.nameFormatMode);
@@ -397,6 +399,19 @@ async function init() {
});
}
function verifyURL() {
if(linkCopied) {
parseURL($('#add-url').prop('placeholder'));
$('#url-form').trigger('reset');
} else if($('#url-form')[0].checkValidity()) {
const value = $('#add-url').val()
if(value != null && value.length > 0) {
parseURL(value);
$('#url-form').trigger('reset');
}
}
}
function toggleWhiteMode(setting) {
const value = setting === "light";
$('body').toggleClass("white-mode", value);

View File

@@ -0,0 +1,99 @@
const ClipboardWatcher = require("../modules/ClipboardWatcher");
const { clipboard } = require('electron');
jest.mock('electron', () => ({
clipboard: {
readText: jest.fn()
}
}));
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
});
describe('poll', () => {
it('reads the text from the clipboard', () => {
clipboard.readText.mockReturnValue("https://i.am.a.url.com");
const instance = instanceBuilder(true);
const resetMock = jest.spyOn(instance, "resetPlaceholder").mockImplementation(() => {});
instance.poll();
expect(clipboard.readText).toBeCalledTimes(1);
expect(resetMock).toBeCalledTimes(0);
});
it('resets when it is not a URL', () => {
clipboard.readText.mockReturnValue("im not a url");
const instance = instanceBuilder(true);
const resetMock = jest.spyOn(instance, "resetPlaceholder").mockImplementation(() => {});
instance.poll();
expect(resetMock).toBeCalledTimes(1);
});
it('resets when the copied text is null', () => {
clipboard.readText.mockReturnValue(null);
const instance = instanceBuilder(true);
const resetMock = jest.spyOn(instance, "resetPlaceholder").mockImplementation(() => {});
instance.poll();
expect(resetMock).toBeCalledTimes(1);
});
it('sends the URL to renderer if it is one', () => {
clipboard.readText.mockReturnValue("https://i.am.a.url.com");
const instance = instanceBuilder(true);
const resetMock = jest.spyOn(instance, "resetPlaceholder").mockImplementation(() => {});
instance.poll();
expect(instance.win.webContents.send).toBeCalledWith("updateLinkPlaceholder", {text: "https://i.am.a.url.com", copied: true})
expect(instance.win.webContents.send).toBeCalledTimes(1);
expect(resetMock).toBeCalledTimes(0);
});
it('doesnt poll when it is disabled in settings', () => {
clipboard.readText.mockReturnValue("https://i.am.a.url.com");
const instance = instanceBuilder(false);
const resetMock = jest.spyOn(instance, "resetPlaceholder").mockImplementation(() => {});
instance.poll();
expect(clipboard.readText).toBeCalledTimes(0);
expect(resetMock).toBeCalledTimes(0);
expect(instance.win.webContents.send).toBeCalledTimes(0);
});
it('does nothing when the previous copied text matches the new one', () => {
clipboard.readText.mockReturnValue("https://i.am.a.url.com");
const instance = instanceBuilder(true);
const resetMock = jest.spyOn(instance, "resetPlaceholder").mockImplementation(() => {});
instance.poll();
instance.poll();
expect(clipboard.readText).toBeCalledTimes(2);
expect(resetMock).toBeCalledTimes(0);
expect(instance.win.webContents.send).toBeCalledTimes(1);
});
});
describe('resetPlaceholder', () => {
it('sends the standard placeholder to renderer', () => {
const instance = instanceBuilder(true);
instance.resetPlaceholder();
expect(instance.win.webContents.send).toBeCalledTimes(1);
expect(instance.win.webContents.send.mock.calls[0][1].copied).toBeFalsy();
});
});
describe('startPolling', () => {
it('Polls one time', () => {
const instance = instanceBuilder(true);
const pollMock = jest.spyOn(instance, "poll").mockImplementation(() => {});
instance.startPolling()
expect(pollMock).toBeCalledTimes(1);
});
it('Starts a polling loop', () => {
const loops = 5;
const instance = instanceBuilder(true);
const pollMock = jest.spyOn(instance, "poll").mockImplementation(() => {});
instance.startPolling()
jest.advanceTimersByTime(loops * 1000);
expect(pollMock).toBeCalledTimes(loops + 1);
});
});
function instanceBuilder(enabled) {
const env = {settings: {autoFillClipboard: enabled}};
const win = {webContents: {send: jest.fn()}};
return new ClipboardWatcher(win, env);
}

View File

@@ -2,8 +2,8 @@ const fs = require('fs').promises;
const os = require("os");
const Settings = require('../modules/persistence/Settings');
const env = {version: "2.0.0-test1"};
const defaultSettingsInstance = new Settings({settings: "tests/test-settings.json"}, env, "none", "", true, false, true, "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "click", "49", 8, true, true, "C:\\Users\\user\\cookies.txt", false, true, false, false, true, "dark");
const defaultSettings = "{\"outputFormat\":\"none\",\"proxy\":\"\",\"spoofUserAgent\":true,\"validateCertificate\":false,\"taskList\":true,\"nameFormat\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"nameFormatMode\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"sizeMode\":\"click\",\"splitMode\":\"49\",\"maxConcurrent\":8,\"defaultConcurrent\":8,\"updateBinary\":true,\"updateApplication\":true,\"statSend\":false,\"downloadMetadata\":true,\"downloadThumbnail\":false,\"keepUnmerged\":false,\"calculateTotalSize\":true,\"theme\":\"dark\",\"version\":\"2.0.0-test1\"}"
const defaultSettingsInstance = new Settings({settings: "tests/test-settings.json"}, env, "none", "", true, true, false, true, "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "click", "49", 8, true, true, "C:\\Users\\user\\cookies.txt", false, true, false, false, true, "dark");
const defaultSettings = "{\"outputFormat\":\"none\",\"proxy\":\"\",\"autoFillClipboard\":true,\"spoofUserAgent\":true,\"validateCertificate\":false,\"taskList\":true,\"nameFormat\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"nameFormatMode\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"sizeMode\":\"click\",\"splitMode\":\"49\",\"maxConcurrent\":8,\"defaultConcurrent\":8,\"updateBinary\":true,\"updateApplication\":true,\"statSend\":false,\"downloadMetadata\":true,\"downloadThumbnail\":false,\"keepUnmerged\":false,\"calculateTotalSize\":true,\"theme\":\"dark\",\"version\":\"2.0.0-test1\"}"
describe('Load settings from file', () => {
beforeEach(() => {

View File

@@ -1 +1 @@
{"outputFormat":"none","proxy": "","spoofUserAgent":true,"validateCertificate": false,"taskList":true,"nameFormat":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","nameFormatMode":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","sizeMode":"click","splitMode":"49","maxConcurrent":8,"defaultConcurrent":8,"updateBinary":true,"updateApplication":true,"cookiePath":"C:\\Users\\user\\cookies.txt","statSend":false,"downloadMetadata":true,"downloadThumbnail":false,"keepUnmerged":false,"calculateTotalSize":true,"theme": "dark","version":"2.0.0-test1"}
{"outputFormat":"none","proxy": "","autoFillClipboard":true,"spoofUserAgent":true,"validateCertificate": false,"taskList":true,"nameFormat":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","nameFormatMode":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","sizeMode":"click","splitMode":"49","maxConcurrent":8,"defaultConcurrent":8,"updateBinary":true,"updateApplication":true,"cookiePath":"C:\\Users\\user\\cookies.txt","statSend":false,"downloadMetadata":true,"downloadThumbnail":false,"keepUnmerged":false,"calculateTotalSize":true,"theme": "dark","version":"2.0.0-test1"}