Compare commits
24 Commits
Author | SHA1 | Date |
---|---|---|
|
b3618714ca | |
|
2ce6bcbece | |
|
5dcdd9a483 | |
|
3dc0531331 | |
|
6de66abea1 | |
|
429aff3fb7 | |
|
e9c3087f19 | |
|
8ad01a1de4 | |
|
899df66c91 | |
|
a74fc50002 | |
|
8003d5614d | |
|
c09f44642e | |
|
27e759009b | |
|
911d2acff3 | |
|
15d99481af | |
|
3e4c52f4e2 | |
|
6225038626 | |
|
17931dd7b6 | |
|
93d4d581ba | |
|
c001cc26f4 | |
|
aafce5afd9 | |
|
7d952e42d6 | |
|
366114e55e | |
|
e9825868b3 |
|
@ -7,3 +7,4 @@
|
||||||
.project
|
.project
|
||||||
*/.factorypath
|
*/.factorypath
|
||||||
**/.antlr/
|
**/.antlr/
|
||||||
|
*.code-workspace
|
17
CHANGELOG.md
|
@ -1,3 +1,20 @@
|
||||||
|
5.3.4-J62
|
||||||
|
========================
|
||||||
|
Current Good working copy after chaturbate reverting their changes. Chaturbate Video and audio currently working.
|
||||||
|
* jre bundled (in releases) (fixed permissions) (linux and macos)
|
||||||
|
* updated ffmpeg (in releases) (fixed permissions) (windows, linux and macos)
|
||||||
|
* fixed run script permissions (linux and macos)
|
||||||
|
* bump ver
|
||||||
|
* all builds checked
|
||||||
|
|
||||||
|
5.3.3-J62
|
||||||
|
========================
|
||||||
|
Reverted for chaturbate reverting their changes. Chaturbate Video and audio currently working.
|
||||||
|
* jre bundled (in releases)
|
||||||
|
* flirt4free fix - "topic" no longer sent, replaced with empty string
|
||||||
|
* bump ver
|
||||||
|
* all builds checked
|
||||||
|
|
||||||
5.3.0
|
5.3.0
|
||||||
========================
|
========================
|
||||||
* Added menu entry to force recording of models without changing the prio
|
* Added menu entry to force recording of models without changing the prio
|
||||||
|
|
26
README.md
|
@ -1,12 +1,30 @@
|
||||||
# CTB Recorder
|
# CTB Recorder
|
||||||
|

|
||||||
|
## Current State Version 5.3.3
|
||||||
|
Reverted for chaturbate reverting their changes. Chaturbate Video and audio currently working.<br>
|
||||||
|
|
||||||
## Current State Version 5.3.2-a
|
- jre bundled<br>
|
||||||
|
- f4f fix<br>
|
||||||
|
- bump ver<br>
|
||||||
|
- all builds checked<br>
|
||||||
|
|
||||||
## updates to fix chaturbate broken audio coming soon
|
### ABOUT This Repo, server and domain hosted and maintained by J62
|
||||||
|
I (j62) now have a fully set up development environment again(have been here and assisted in different ways since 2020), allowing me to handle all builds with ease. I’ve also set up a domain, Git server, and hosting—at my own expense—to provide a stable and maintained platform for the foreseeable future.<br>
|
||||||
|
|
||||||
A free recording software for different camsites. Currently supported: BongaCams, Cam4, CamSoda, Chaturbate, FC2Live, LiveJasmin, MyFreeCams, Streamate
|
I took this step after witnessing the chaotic distribution of zip files in Discord, where multiple developers were scrambling to fix the Chaturbate issue. It became clear that there was a need (once again) for a centralized collaboration and download hub.<br>
|
||||||
|
|
||||||

|
Ideally, everyone should utilize the Git server and create their own forks(of this repo). This way, we can manage changes efficiently through pull requests between forks.<br>
|
||||||
|
|
||||||
|
Support me:<br>
|
||||||
|
|
||||||
|
Bitcoin: bc1q7fvtkx8wklvd4zttsec7sfgxqh9zadk0x236lt <br>
|
||||||
|
Ether: 0x2e687A5628ff16c8f9624A914C1f727000089C3A <br>
|
||||||
|
Solana: Z5YwNPkLheSHuaSJjyHhg3L8UxjpJPt5WU6vu4hFsNR <br>
|
||||||
|
Monero: 47tjD1z63wu3FEnDCvWnFaRAZbpDKc3Ys1WCbgzvB2Gg8XbqU8bARpcCC37mWzuWBAeZPu2UGY4TAcYGhb6fptoTR8X9vjc
|
||||||
|
|
||||||
|
## A free recording software for different camsites. Currently supported: BongaCams, Cam4, CamSoda, Chaturbate, FC2Live, LiveJasmin, MyFreeCams, Streamate
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
If you ever wanted to record a cam girl show to watch it later or if your favorite model lives in another timezone and is never online when you are, CTB Recorder is the solution for you.
|
If you ever wanted to record a cam girl show to watch it later or if your favorite model lives in another timezone and is never online when you are, CTB Recorder is the solution for you.
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>5.3.2</version>
|
<version>5.3.4</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -268,7 +268,8 @@ public class CamrecApplication extends Application {
|
||||||
tabPane.getTabs().add(new RecentlyWatchedTab(recorder, sites));
|
tabPane.getTabs().add(new RecentlyWatchedTab(recorder, sites));
|
||||||
}
|
}
|
||||||
tabPane.getTabs().add(new SettingsTab(sites, recorder));
|
tabPane.getTabs().add(new SettingsTab(sites, recorder));
|
||||||
tabPane.getTabs().add(new NewsTab(config));
|
//tabPane.getTabs().add(new NewsTab(config));
|
||||||
|
tabPane.getTabs().add(new NewsTab(config, getHostServices()));
|
||||||
tabPane.getTabs().add(new DonateTabFx());
|
tabPane.getTabs().add(new DonateTabFx());
|
||||||
tabPane.getTabs().add(new HelpTab());
|
tabPane.getTabs().add(new HelpTab());
|
||||||
tabPane.getTabs().add(new LoggingTab());
|
tabPane.getTabs().add(new LoggingTab());
|
||||||
|
|
|
@ -6,17 +6,22 @@ import ctbrec.GlobalThreadPool;
|
||||||
import ctbrec.Version;
|
import ctbrec.Version;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.io.json.ObjectMapperFactory;
|
import ctbrec.io.json.ObjectMapperFactory;
|
||||||
import ctbrec.ui.CamrecApplication;
|
|
||||||
import ctbrec.ui.controls.Dialogs;
|
import ctbrec.ui.controls.Dialogs;
|
||||||
import ctbrec.ui.tabs.TabSelectionListener;
|
import ctbrec.ui.tabs.TabSelectionListener;
|
||||||
|
import javafx.application.HostServices;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Hyperlink;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -27,16 +32,17 @@ import static ctbrec.io.HttpConstants.USER_AGENT;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class NewsTab extends Tab implements TabSelectionListener {
|
public class NewsTab extends Tab implements TabSelectionListener {
|
||||||
private static final String ACCESS_TOKEN = "a2804d73a89951a22e0f8483a6fcec8943afd88b7ba17c459c095aa9e6f94fd0";
|
private static final String URL = "https://git.ctbrec.com/api/v1/repos/j62/ctbrec/releases";
|
||||||
private static final String URL = "https://mastodon.cloud/api/v1/accounts/480960/statuses?limit=20&exclude_replies=true";
|
|
||||||
private final Config config;
|
private final Config config;
|
||||||
|
private final HostServices hostServices;
|
||||||
private final VBox layout = new VBox();
|
private final VBox layout = new VBox();
|
||||||
|
|
||||||
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
|
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
|
||||||
|
private final OkHttpClient httpClient = new OkHttpClient();
|
||||||
|
|
||||||
public NewsTab(Config config) {
|
public NewsTab(Config config, HostServices hostServices) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
setText("News");
|
this.hostServices = hostServices;
|
||||||
|
setText("Releases");
|
||||||
layout.setMaxWidth(800);
|
layout.setMaxWidth(800);
|
||||||
layout.setAlignment(Pos.CENTER);
|
layout.setAlignment(Pos.CENTER);
|
||||||
setContent(new ScrollPane(layout));
|
setContent(new ScrollPane(layout));
|
||||||
|
@ -44,24 +50,21 @@ public class NewsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void selected() {
|
public void selected() {
|
||||||
GlobalThreadPool.submit(this::loadToots);
|
GlobalThreadPool.submit(this::loadReleases);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadToots() {
|
private void loadReleases() {
|
||||||
try {
|
try {
|
||||||
var request = new Request.Builder()
|
var request = new Request.Builder()
|
||||||
.url(URL)
|
.url(URL)
|
||||||
.header("Authorization", "Bearer " + ACCESS_TOKEN)
|
|
||||||
.header(USER_AGENT, "ctbrec " + Version.getVersion())
|
.header(USER_AGENT, "ctbrec " + Version.getVersion())
|
||||||
.build();
|
.build();
|
||||||
try (var response = CamrecApplication.httpClient.execute(request)) {
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
var body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
|
var body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
|
||||||
log.debug(body);
|
log.debug(body);
|
||||||
if (body.startsWith("[")) {
|
if (body.startsWith("[")) {
|
||||||
onSuccess(body);
|
onSuccess(body);
|
||||||
} else if (body.startsWith("{")) {
|
|
||||||
onError(body);
|
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Unexpected response: " + body);
|
throw new IOException("Unexpected response: " + body);
|
||||||
}
|
}
|
||||||
|
@ -70,30 +73,53 @@ public class NewsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.info("Error while loading news", e);
|
log.info("Error while loading releases", e);
|
||||||
Dialogs.showError(getTabPane().getScene(), "News", "Couldn't load news from mastodon", e);
|
Dialogs.showError(getTabPane().getScene(), "Releases", "Couldn't load release information", e);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onError(String body) throws IOException {
|
|
||||||
var json = new JSONObject(body);
|
|
||||||
if (json.has("error")) {
|
|
||||||
throw new IOException("Request not successful: " + json.getString("error"));
|
|
||||||
} else {
|
|
||||||
throw new IOException("Unexpected response: " + body);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSuccess(String body) throws IOException {
|
private void onSuccess(String body) throws IOException {
|
||||||
Status[] statusArray = mapper.readValue(body, Status[].class);
|
JSONArray releases = new JSONArray(body);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
layout.getChildren().clear();
|
layout.getChildren().clear();
|
||||||
for (Status status : statusArray) {
|
for (int i = 0; i < releases.length(); i++) {
|
||||||
if (status.getInReplyToId() == null && !Objects.equals("direct", status.getVisibility())) {
|
JSONObject release = releases.getJSONObject(i);
|
||||||
var stp = new StatusPane(status, config.getDateTimeFormatter());
|
String tagName = release.optString("tag_name", "Unknown Version");
|
||||||
layout.getChildren().add(stp);
|
String releaseName = release.optString("name", "No Name");
|
||||||
VBox.setMargin(stp, new Insets(10));
|
String description = release.optString("body", "No description available.");
|
||||||
|
JSONArray assets = release.optJSONArray("assets");
|
||||||
|
|
||||||
|
var releasePane = new VBox();
|
||||||
|
releasePane.setPadding(new Insets(10));
|
||||||
|
releasePane.setSpacing(5);
|
||||||
|
|
||||||
|
Label versionLabel = new Label("Version: " + tagName);
|
||||||
|
versionLabel.setStyle("-fx-font-weight: bold;");
|
||||||
|
|
||||||
|
Label nameLabel = new Label("Release: " + releaseName);
|
||||||
|
Label descLabel = new Label(description);
|
||||||
|
|
||||||
|
releasePane.getChildren().addAll(versionLabel, nameLabel, descLabel);
|
||||||
|
|
||||||
|
if (assets != null) {
|
||||||
|
Label assetsLabel = new Label("Assets:");
|
||||||
|
releasePane.getChildren().add(assetsLabel);
|
||||||
|
for (int j = 0; j < assets.length(); j++) {
|
||||||
|
JSONObject asset = assets.getJSONObject(j);
|
||||||
|
String assetName = asset.optString("name", "Unknown File");
|
||||||
|
String downloadUrl = asset.optString("browser_download_url", "#");
|
||||||
|
int size = asset.optInt("size", 0);
|
||||||
|
int downloads = asset.optInt("download_count", 0);
|
||||||
|
|
||||||
|
Hyperlink assetLink = new Hyperlink(assetName + " (" + size / 1024 / 1024 + " MB, " + downloads + " downloads)");
|
||||||
|
assetLink.setOnAction(event -> hostServices.showDocument(downloadUrl));
|
||||||
|
|
||||||
|
releasePane.getChildren().add(assetLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layout.getChildren().add(releasePane);
|
||||||
|
VBox.setMargin(releasePane, new Insets(10));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ public class Flirt4FreeUpdateService extends PaginatedScheduledService {
|
||||||
var name = modelData.getString("model_seo_name");
|
var name = modelData.getString("model_seo_name");
|
||||||
Flirt4FreeModel model = (Flirt4FreeModel) flirt4Free.createModel(name);
|
Flirt4FreeModel model = (Flirt4FreeModel) flirt4Free.createModel(name);
|
||||||
model.setDisplayName(Entities.unescape(modelData.getString("display")));
|
model.setDisplayName(Entities.unescape(modelData.getString("display")));
|
||||||
model.setDescription(modelData.getString("topic"));
|
model.setDescription("");
|
||||||
model.setUrl(Flirt4Free.BASE_URI + "/rooms/" + model.getName() + '/');
|
model.setUrl(Flirt4Free.BASE_URI + "/rooms/" + model.getName() + '/');
|
||||||
model.setNew(modelData.optString("is_new", "0").equals("1"));
|
model.setNew(modelData.optString("is_new", "0").equals("1"));
|
||||||
var videoHost = modelData.getString("video_host");
|
var videoHost = modelData.getString("video_host");
|
||||||
|
|
|
@ -24,9 +24,9 @@ public class DonateTabFx extends Tab {
|
||||||
|
|
||||||
var headerVbox = new VBox(10);
|
var headerVbox = new VBox(10);
|
||||||
headerVbox.setAlignment(Pos.CENTER);
|
headerVbox.setAlignment(Pos.CENTER);
|
||||||
var beer = new Label("Buy me some beer?!");
|
var beer = new Label("Buy me a drink?!");
|
||||||
beer.setFont(new Font(36));
|
beer.setFont(new Font(36));
|
||||||
var desc = new Label("If you like this software and want to buy me some beer or pizza, here are some possibilities!");
|
var desc = new Label("If you like this software and want to buy me some MtDew or Energy Drinks(boost ctbrec dev productivity! :), Beer or Pizza, here are some possibilities!");
|
||||||
desc.setFont(new Font(24));
|
desc.setFont(new Font(24));
|
||||||
headerVbox.getChildren().addAll(beer, desc);
|
headerVbox.getChildren().addAll(beer, desc);
|
||||||
var header = new HBox();
|
var header = new HBox();
|
||||||
|
@ -36,7 +36,7 @@ public class DonateTabFx extends Tab {
|
||||||
container.setTop(header);
|
container.setTop(header);
|
||||||
|
|
||||||
var prefWidth = 360;
|
var prefWidth = 360;
|
||||||
var bitcoinAddress = new TextField("15sLWZon8diPqAX4UdPQU1DcaPuvZs2GgA");
|
var bitcoinAddress = new TextField("bc1q7fvtkx8wklvd4zttsec7sfgxqh9zadk0x236lt");
|
||||||
bitcoinAddress.setEditable(false);
|
bitcoinAddress.setEditable(false);
|
||||||
bitcoinAddress.setPrefWidth(prefWidth);
|
bitcoinAddress.setPrefWidth(prefWidth);
|
||||||
var bitcoinQrCode = new ImageView(getClass().getResource("/html/bitcoin-address.png").toString());
|
var bitcoinQrCode = new ImageView(getClass().getResource("/html/bitcoin-address.png").toString());
|
||||||
|
@ -46,7 +46,7 @@ public class DonateTabFx extends Tab {
|
||||||
bitcoinBox.setAlignment(Pos.TOP_CENTER);
|
bitcoinBox.setAlignment(Pos.TOP_CENTER);
|
||||||
bitcoinBox.getChildren().addAll(bitcoinLabel, bitcoinAddress, bitcoinQrCode);
|
bitcoinBox.getChildren().addAll(bitcoinLabel, bitcoinAddress, bitcoinQrCode);
|
||||||
|
|
||||||
var ethereumAddress = new TextField("0x996041638eEAE7E31f39Ef6e82068d69bA7C090e");
|
var ethereumAddress = new TextField("0x2e687A5628ff16c8f9624A914C1f727000089C3A");
|
||||||
ethereumAddress.setEditable(false);
|
ethereumAddress.setEditable(false);
|
||||||
ethereumAddress.setPrefWidth(prefWidth);
|
ethereumAddress.setPrefWidth(prefWidth);
|
||||||
var ethereumQrCode = new ImageView(getClass().getResource("/html/ethereum-address.png").toString());
|
var ethereumQrCode = new ImageView(getClass().getResource("/html/ethereum-address.png").toString());
|
||||||
|
@ -56,7 +56,7 @@ public class DonateTabFx extends Tab {
|
||||||
ethereumBox.setAlignment(Pos.TOP_CENTER);
|
ethereumBox.setAlignment(Pos.TOP_CENTER);
|
||||||
ethereumBox.getChildren().addAll(ethereumLabel, ethereumAddress, ethereumQrCode);
|
ethereumBox.getChildren().addAll(ethereumLabel, ethereumAddress, ethereumQrCode);
|
||||||
|
|
||||||
var moneroAddress = new TextField("871K7xaLR2X8E84CUBi7D88diXgKjbhjZHTEFfJv9ec9eo4NVPCQ2UsGxkroseCcKQbZsHMgW3kg6HR4tfct3fX2HoFDzK6");
|
var moneroAddress = new TextField("47tjD1z63wu3FEnDCvWnFaRAZbpDKc3Ys1WCbgzvB2Gg8XbqU8bARpcCC37mWzuWBAeZPu2UGY4TAcYGhb6fptoTR8X9vjc");
|
||||||
moneroAddress.setEditable(false);
|
moneroAddress.setEditable(false);
|
||||||
moneroAddress.setPrefWidth(prefWidth);
|
moneroAddress.setPrefWidth(prefWidth);
|
||||||
var moneroQrCode = new ImageView(getClass().getResource("/html/monero-address.png").toString());
|
var moneroQrCode = new ImageView(getClass().getResource("/html/monero-address.png").toString());
|
||||||
|
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 4.9 KiB |
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>5.3.2</version>
|
<version>5.3.4</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -311,7 +311,7 @@ public class ChaturbateModel extends AbstractModel {
|
||||||
String content = response.body().string();
|
String content = response.body().string();
|
||||||
log.trace("Raw stream info for model {}: {}", getName(), content);
|
log.trace("Raw stream info for model {}: {}", getName(), content);
|
||||||
streamInfo = mapper.readValue(content, StreamInfo.class);
|
streamInfo = mapper.readValue(content, StreamInfo.class);
|
||||||
streamInfo.url = streamInfo.url.replaceAll("live-hls", "live-c-fhls").replaceAll("playlist\\.m3u8", "playlist_sfm4s.m3u8");
|
//streamInfo.url = streamInfo.url.replaceAll("live-hls", "live-c-fhls").replaceAll("playlist\\.m3u8", "playlist_sfm4s.m3u8");
|
||||||
return streamInfo;
|
return streamInfo;
|
||||||
} else {
|
} else {
|
||||||
int code = response.code();
|
int code = response.code();
|
||||||
|
|
1082
docs/index.html
|
@ -6,7 +6,7 @@
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<version>5.3.2</version>
|
<version>5.3.4</version>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>../common</module>
|
<module>../common</module>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>5.3.2</version>
|
<version>5.3.4</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
After Width: | Height: | Size: 9.4 KiB |