Update to testing version 1.3.2 (check release for changelog)

This commit is contained in:
jely2002
2020-04-04 20:19:32 +02:00
parent 81b8058585
commit 91f58282cb
15 changed files with 332 additions and 62 deletions

View File

@@ -36,7 +36,14 @@ body {
object-fit: cover;
}
.title, .channel, .duration {
.thumbnail-settings {
border-radius: 3px;
width: 100%;
height: 18vw;
object-fit: cover;
}
.title, .channel, .duration, .size, #directoryInputLabel {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@@ -82,6 +89,10 @@ body {
background-color: #424242;
}
#directoryInput {
cursor: default;
}
/* CUSTOM CHECKMARK SPINNER */
.circle-loader {
margin-bottom: 3.5em;

View File

@@ -4,13 +4,16 @@ const {remote} = require('electron')
let selectedURL
let availableFormats = []
let availableFormatCodes = []
let formatSizes = []
let audioSize;
let ffmpegLoc
let timings
if(process.platform === "darwin") {
let appPath = remote.app.getAppPath().slice(0, -8)
youtubedl.setYtdlBinary(appPath + "youtube-dl")
ffmpegLoc = appPath + "ffmpeg"
youtubedl.setYtdlBinary(appPath + "youtube-dl-darwin")
ffmpegLoc = appPath + "ffmpeg-darwin"
} else {
youtubedl.setYtdlBinary("resources/youtube-dl.exe")
ffmpegLoc = "resources/ffmpeg.exe"
@@ -26,6 +29,9 @@ function url_entered() {
let url = $("#url").val()
if(validate(url)) {
availableFormats = []
formatSizes = []
availableFormatCodes = []
audioSize = []
showInfo(url)
$('#url').addClass("is-valid").removeClass("is-invalid")
} else {
@@ -44,6 +50,7 @@ function showInfo(url) {
youtubedl.getInfo(url, function(err, info) {
if (err) showError(err)
$(".thumbnail").attr("src", info.thumbnail)
$(".thumbnail-settings").attr("src", info.thumbnail)
$(".title").html("<strong>Title:</strong> " + info.title)
$(".channel").html("<strong>Channel:</strong> " + info.uploader)
$(".duration").html("<strong>Duration:</strong> " + info.duration)
@@ -55,27 +62,37 @@ function showInfo(url) {
if (err) showError(err)
output.splice(0,3)
console.log(output)
let audio
output.forEach(function(entry) {
if(entry.includes("audio only") && entry.includes("m4a")) audio = entry
if(!(entry.includes('mp4') || entry.includes('webm'))) return
if(entry.includes('4320p')) addFormat("4320p", entry)
if(entry.includes('2160p')) addFormat("2160p", entry)
if(entry.includes('1440p')) addFormat("1440p", entry)
if(entry.includes('1080p')) addFormat("1080p", entry)
if(entry.includes('720p')) addFormat("720p", entry)
if(entry.includes('480p')) addFormat("480p", entry)
if(entry.includes('360p')) addFormat("360p", entry)
if(entry.includes('240p')) addFormat("240p", entry)
if(entry.includes('144p')) addFormat("144p", entry)
if(entry.includes('4320p')) addFormat("4320p", entry, audio)
if(entry.includes('2160p')) addFormat("2160p", entry, audio)
if(entry.includes('1440p')) addFormat("1440p", entry, audio)
if(entry.includes('1080p')) addFormat("1080p", entry, audio)
if(entry.includes('720p')) addFormat("720p", entry, audio)
if(entry.includes('480p')) addFormat("480p", entry, audio)
if(entry.includes('360p')) addFormat("360p", entry, audio)
if(entry.includes('240p')) addFormat("240p", entry, audio)
if(entry.includes('144p')) addFormat("144p", entry, audio)
})
})
}
function downloadAudio(quality) {
console.log(quality)
let realQuality = 0
if(quality === "worst") {
realQuality = '9'
}
const options = [
'-f', quality + 'audio[ext=m4a]',
'--extract-audio', '--audio-quality', realQuality,
'--audio-format', 'mp3',
'--ffmpeg-location', ffmpegLoc, '--hls-prefer-ffmpeg',
'-o', remote.app.getPath('music').replace(/\\/g, "/") + '/' + '%(title)s.%(ext)s'
'--embed-thumbnail',
'-o', downloadPath.replace(/\\/g, "/") + '/' + '%(title)s.%(ext)s'
]
console.log(options)
youtubedl.exec(selectedURL, options, {}, function(err, output) {
if (err) showError(err)
downloadFinished()
@@ -85,21 +102,12 @@ function downloadAudio(quality) {
function downloadVideo(quality) {
let downloadSubs = $('#subtitles').prop('checked')
let fps = quality.substr(-2)
let qualityOption
let height
if(fps === "60" || fps === "50") {
height = quality.slice(0,-3);
qualityOption = 'bestvideo[height<='+ height + '][fps=' + fps + ']+bestaudio[ext=m4a]/best[height<=' + height + '][fps=' + fps + ']'
} else {
height = quality.slice(0,-1);
qualityOption = 'bestvideo[height<='+ height + '][fps<50]+bestaudio[ext=m4a]/best[height<=' + height + '][fps<50]'
}
let qualityOption = quality + "+bestaudio[ext=m4a]/best"
const options = [
'-f', qualityOption,
'--ffmpeg-location', ffmpegLoc, '--hls-prefer-ffmpeg',
'--merge-output-format', 'mp4',
'-o', remote.app.getPath('videos').replace(/\\/g, "/") + '/' + '%(title)s.%(ext)s'
'-o', downloadPath.replace(/\\/g, "/") + '/' + '%(title)s-(%(height)sp%(fps)s).%(ext)s'
]
if(downloadSubs) {
options.push("--all-subs")
@@ -117,7 +125,7 @@ function downloadVideo(quality) {
function download() {
let quality = $('#quality').val()
timings = setTimeout(showWarning, 60000)
timings = setTimeout(showWarning, 90000)
stepper.next()
if($('input[name=type-select]:checked').val() === "video") {
downloadVideo(quality)
@@ -133,24 +141,61 @@ function downloadFinished() {
$('#reset-btn').html("Download another video").prop("disabled", false)
}
function addFormat(quality, entry) {
if(entry.includes(quality + "60")) {
if(!availableFormats.includes(quality + "60")) availableFormats.push(quality + "60")
} else if(entry.includes(quality + "50")) {
if(!availableFormats.includes(quality + "50")) availableFormats.push(quality + "50")
} else {
if(!availableFormats.includes(quality)) availableFormats.push(quality)
function addSize(entry, audio) {
let vidSizeUnformat = entry.split(' ').pop()
let vidUnit = vidSizeUnformat.slice(-3)
let vidSize = parseFloat(vidSizeUnformat.slice(0, -3))
let audSizeUnformat = audio.split(' ').pop()
let audUnit = audSizeUnformat.slice(-3)
let audSize = parseFloat(audSizeUnformat.slice(0, -3))
if(vidUnit === audUnit) {
let mib = audSize + vidSize
formatSizes.push(mib.toFixed(1) + "MB")
} else if(audUnit === "MiB") {
let mib = audSize + (vidSize/1024)
formatSizes.push(mib.toFixed(1) + "MB")
} else if(audUnit === "KiB") {
let mib = (audSize/1024 + vidSize)
formatSizes.push(mib.toFixed(1) + "MB")
}
audioSize = "~" + audSize.toFixed(1) + "MB"
}
function addFormat(quality, entry, audio) {
if(entry.includes(quality + "60")) {
if(!availableFormats.includes(quality + "60")) {
availableFormats.push(quality + "60")
availableFormatCodes.push(entry.slice(0,3))
addSize(entry, audio)
}
} else if(entry.includes(quality + "50")) {
if(!availableFormats.includes(quality + "50")) {
availableFormats.push(quality + "50")
availableFormatCodes.push(entry.slice(0,3))
addSize(entry, audio)
}
} else {
if(!availableFormats.includes(quality)) {
availableFormats.push(quality)
availableFormatCodes.push(entry.slice(0,3))
addSize(entry, audio)
}
}
}
function resetSteps() {
selectedURL = ""
availableFormats = []
formatSizes = []
availableFormatCodes = []
audioSize = []
$('#url').removeClass("is-valid").removeClass("is-invalid")
$(".thumbnail").attr("src", "https://via.placeholder.com/640x360?text=%20")
$(".title").html("<strong>Title:</strong> --")
$(".channel").html("<strong>Channel:</strong> --")
$(".duration").html("<strong>Duration:</strong> --")
$(".size").html("<strong>Download size:</strong> --")
$('.main-input').trigger('reset')
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
@@ -159,6 +204,7 @@ function resetSteps() {
$('#subtitles').prop("disabled", true).prop("checked", false)
$('#quality').empty().append(new Option("Select quality", "quality")).prop("disabled", true).val("quality")
$("#download-btn").prop("disabled", true)
$("#directoryInput").prop("disabled", true)
stepper.reset()
}
@@ -169,6 +215,8 @@ function resetBack() {
$('#subtitles').prop("disabled", true).prop("checked", false)
$('#quality').empty().append(new Option("Select quality", "quality")).prop("disabled", true).val("quality")
$("#download-btn").prop("disabled", true)
$("#directoryInput").prop("disabled", true)
$(".size").html("<strong>Download size:</strong> --")
}
function showWarning() {

View File

@@ -49,7 +49,7 @@
<div class="spinner-border text-dark" role="status">
<span class="sr-only">Loading...</span>
</div>
<img src="img/waiting-for-link.png" class="img-fluid thumbnail" alt="Thumbnail of the specified video">
<img src="web-resources/waiting-for-link.png" class="img-fluid thumbnail" alt="Thumbnail of the specified video">
</div>
<div class="col-md-6 text-left">
<p class="title"><strong>Title:</strong> --</p>
@@ -67,10 +67,12 @@
<div id="settings-part" class="content fade" role="tabpanel" aria-labelledby="settings-part-trigger">
<form class="main-input">
<div class="row">
<div class="col-md-6">
<img src="img/waiting-for-link.png" class="img-fluid thumbnail" alt="Thumbnail of the specified video">
<div class="col-md-5 text-left">
<img src="web-resources/waiting-for-link.png" class="img-fluid thumbnail-settings" alt="Thumbnail of the specified video">
<p class="size mt-3"><strong>Download size:</strong> --</p>
<p class="duration mb-0"><strong>Duration:</strong> --</p>
</div>
<div class="col-md-6 text-left">
<div class="col-md-7 text-left">
<div class="form-group">
<form id="settings">
<div class="custom-control custom-radio custom-control-inline">
@@ -93,6 +95,12 @@
<label class="custom-control-label" for="subtitles">Add subtitles (if available)</label>
</div>
</div>
<div class="form-group">
<div class="custom-file">
<input type="text" onclick="setDirectory()" class="custom-file-input" id="directoryInput" disabled>
<label class="custom-file-label" id="directoryInputLabel" for="directoryInput">Choose download directory</label>
</div>
</div>
</form>
</div>
<button type="button" onclick="resetBack()" class="btn btn-dark">Back</button>
@@ -130,6 +138,15 @@
An error has occured, please report this to the author.
</div>
</div>
<div class="toast" id="connection" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-danger">
<i class="fas mr-3 fa-wifi"></i>
<strong class="mr-auto">No internet connection found!</strong>
</div>
<div class="toast-body">
Please connect to the internet and restart the app. This app will not work without an internet connection.
</div>
</div>
<div class="toast warning-toast" id="warning" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-warning">
<i class="fas mr-3 fa-exclamation-triangle"></i>
@@ -139,12 +156,14 @@
</button>
</div>
<div class="toast-body">
The download is taking longer than usual, this might be because you are downloading a large file. Otherwise please restart the program.
The download is taking longer than usual, this might be because you're downloading a large file, have a slow connection, or are downloading subtitles. Otherwise please restart the program.
</div>
</div>
<script src="downloader.js"></script>
<script src="plugins.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bs-stepper@1.7.0/dist/js/bs-stepper.min.js" integrity="sha256-INfYp5owpb0btFquNHGlhSxgGYrFlGYRU2oN/3jWGeM=" crossorigin="anonymous"></script>
<script src="downloader.js"></script>
<script src="metrics.js"></script>
<script src="updater.js"></script>
</body>
</html>

View File

@@ -12,7 +12,7 @@ function createWindow () {
resizable: false,
maximizable: false,
titleBarStyle: "hidden",
icon: "img/icon-light.png",
icon: "web-resources/icon-light.png",
webPreferences: {
nodeIntegration: true
}
@@ -25,7 +25,7 @@ function createWindow () {
resizable: false,
maximizable: false,
frame: false,
icon: "img/icon-light.png",
icon: "web-resources/icon-light.png",
webPreferences: {
nodeIntegration: true
}
@@ -33,7 +33,9 @@ function createWindow () {
}
win.removeMenu()
//win.webContents.openDevTools()
if(process.argv[2] === '--dev') {
win.webContents.openDevTools()
}
win.loadFile('index.html')
win.on('closed', () => {
win = null

63
metrics.js Normal file
View File

@@ -0,0 +1,63 @@
const os = require('os')
const fs = require('fs')
let Platform = process.platform
let Version = remote.app.getVersion()
let ram = (process.getSystemMemoryInfo().total / 1.074e+6).toFixed(0)
let cpuModel = os.cpus()[0].model
let cpuCores = os.cpus().length
let country = remote.app.getLocaleCountryCode()
let metricsID;
let appTrimPath
startMetrics()
function startMetrics() {
if(process.platform === "darwin") {
appTrimPath = remote.app.getAppPath().slice(0, -8)
} else {
appTrimPath = "resources/"
}
fs.access(appTrimPath + "metricsID.txt", fs.F_OK, function(err) {
if (err) {
metricsID = generateUUID();
fs.writeFile(appTrimPath + "metricsID.txt", metricsID, 'utf-8', function(err) {
if(err) console.log(err)
sendInitialMetrics()
})
} else {
fs.readFile(appTrimPath + "metricsID.txt", 'utf-8', function (err, data) {
if (err) console.log(err)
metricsID = data
sendInitialMetrics()
})
}
})
}
function sendInitialMetrics() {
$.ajax({
type: 'POST',
url: 'http://backend.jelleglebbeek.com/youtubedl/metrics.php',
dataType: 'json',
data: {
uuid: metricsID,
version: Version,
platform: Platform,
memory: ram,
cpumodel: cpuModel,
cpucores: cpuCores,
countrycode: country
},
success: function() {
console.log("Metrics send.");
}
});
}
function generateUUID() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}

30
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-gui",
"version": "1.1.3",
"version": "1.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -817,6 +817,15 @@
"ms": "2.0.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1384,12 +1393,9 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"mkdirp": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
"integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==",
"requires": {
"minimist": "^1.2.5"
}
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"ms": {
"version": "2.1.2",
@@ -2348,6 +2354,16 @@
"request": "~2.88.0",
"streamify": "~0.2.9",
"universalify": "~0.1.2"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"zero-fill": {

View File

@@ -4,7 +4,7 @@
"description": "A GUI for the YouTube-dl library",
"main": "main.js",
"scripts": {
"start": "electron .",
"start": "electron . --dev",
"dist": "electron-builder"
},
"keywords": [],
@@ -12,7 +12,8 @@
"license": "GPL-3.0-only",
"devDependencies": {
"electron": "^8.2.0",
"electron-builder": "^22.4.1"
"electron-builder": "^22.4.1",
"request": "^2.88.2"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
@@ -20,6 +21,7 @@
"custom-electron-titlebar": "^3.2.2-hotfix62",
"fs": "0.0.1-security",
"jquery": "^3.4.1",
"mkdirp": "^1.0.4",
"popper.js": "^1.16.1",
"youtube-dl": "^3.0.2"
},
@@ -41,7 +43,10 @@
{
"from": "resources",
"to": "./",
"filter": ["ffmpeg.exe", "youtube-dl.exe"]
"filter": [
"ffmpeg.exe",
"youtube-dl.exe"
]
}
]
},
@@ -53,12 +58,12 @@
{
"from": "resources",
"to": "./",
"filter": ["ffmpeg", "youtube-dl"]
"filter": [
"ffmpeg",
"youtube-dl"
]
}
]
},
"linux": {
"target": "AppImage"
}
}
}

View File

@@ -3,15 +3,17 @@ window.$ = window.jQuery = require('jquery')
const customTitlebar = require('custom-electron-titlebar')
let stepper
let downloadPath = remote.app.getPath('downloads');
let downloadMode
if(process.platform === "darwin") {
new customTitlebar.Titlebar({
backgroundColor: customTitlebar.Color.fromHex('#000000'),
backgroundColor: customTitlebar.Color.fromHex('#212121'),
maximizable: false,
shadow: true,
shadow: false,
titleHorizontalAlignment: "center",
enableMnemonics: false,
icon: "img/icon-light.png"
icon: "web-resources/icon-light.png"
})
} else {
new customTitlebar.Titlebar({
@@ -20,7 +22,7 @@ if(process.platform === "darwin") {
shadow: true,
titleHorizontalAlignment: "left",
enableMnemonics: false,
icon: "img/icon-light.png"
icon: "web-resources/icon-light.png"
})
}
@@ -37,18 +39,48 @@ $(document).ready(function () {
autohide: false,
animation: true
})
$('#connection').toast({
autohide: false,
animation: true
})
$("#directoryInputLabel").html(remote.app.getPath('downloads'))
$("#quality").on('change', function() {
if(downloadMode === "audio") return
let index = availableFormats.indexOf(document.getElementById("quality").options[document.getElementById("quality").selectedIndex].text)
$('.size').html('<b>Download size: </b>' + formatSizes[index])
})
})
function setDirectory() {
$('#directoryInput').blur();
let path = remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
defaultPath: downloadPath,
properties: [
'openDirectory',
'createDirectory'
]
}).then(result => {
$('#directoryInputLabel').html(result.filePaths[0])
downloadPath = result.filePaths[0]
})
}
function setType(type) {
$("#download-btn").prop("disabled", false)
$("#directoryInput").prop("disabled", false)
if(type === "audio") {
downloadMode = "audio"
$('#quality').empty().append(new Option("Best", "best")).append(new Option("Worst", "worst")).prop("disabled", false).val("best")
$('.size').html('<b>Download size: </b>' + audioSize)
} else if(type === "video") {
downloadMode = "video"
$('#quality').empty()
availableFormats.forEach(function(quality) {
$('#quality').append(new Option(quality, quality)).prop("disabled", false)
$('#quality').append(new Option(quality, availableFormatCodes[availableFormats.indexOf(quality)])).prop("disabled", false)
$('#subtitles').prop("disabled", false)
})
$('#quality').val(availableFormats[availableFormats.length-1])
$('#quality').val(availableFormatCodes[availableFormatCodes.length-1])
let index = availableFormats.indexOf(document.getElementById("quality").options[document.getElementById("quality").selectedIndex].text)
$('.size').html('<b>Download size: </b>' + formatSizes[index])
}
}

1
resources/details Normal file
View File

@@ -0,0 +1 @@
{"version":"2020.03.24","path":"resources/youtube-dl.exe"}

0
resources/ffmpeg → resources/ffmpeg-darwin Executable file → Normal file
View File

0
resources/youtube-dl → resources/youtube-dl-darwin Executable file → Normal file
View File

73
updater.js Normal file
View File

@@ -0,0 +1,73 @@
'use strict'
const request = require('request')
let defaultBin;
let defaultPath;
let filePath
let url = 'https://yt-dl.org/downloads/latest/youtube-dl'
if(process.platform === "darwin") {
defaultBin = remote.app.getAppPath().slice(0, -8)
defaultPath = defaultBin + 'details'
filePath = defaultBin + "youtube-dl-darwin"
} else {
defaultPath = "resources/details"
filePath = "resources/youtube-dl.exe"
url = "https://yt-dl.org/downloads/latest/youtube-dl.exe"
}
update()
function update() {
request.get(url, { followRedirect: false }, function (err, res) {
if (err) {
console.log(err)
if(err.toString().includes('ENOTFOUND')) {
$('#connection').toast('show')
$('#url').prop("disabled", true).attr("placeholder", "Please connect to the internet and restart this app")
}
return
}
if (res.statusCode !== 302) {
return console.log('Did not get redirect for the latest version link. Status: ' + res.statusCode)
}
const newUrl = res.headers.location
const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
console.log("Latest release: " + newVersion)
if(newVersion === getCurrentVersion()) {
console.log("Binaries were already up-to-date!")
} else {
console.log("New version found! Updating...")
const downloadFile = request.get(newUrl)
downloadFile.on('response', function response(res) {
if (res.statusCode !== 200) {
console.log('Response Error: ' + res.statusCode)
return
}
downloadFile.pipe(fs.createWriteStream(filePath, {mode: 493}))
})
downloadFile.on('error', function error(err) {
console.log(err)
})
downloadFile.on('end', function end() {
console.log("New youtube-dl version downloaded: " + newVersion)
console.log("Writing version data...")
fs.writeFileSync(
defaultPath,
JSON.stringify({
version: newVersion,
path: filePath
}),
'utf8'
)
})
}
})
}
function getCurrentVersion() {
let details = JSON.parse(fs.readFileSync(defaultPath, 'utf-8'))
console.log("Current version: " + details.version)
return details.version;
}

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB