diff --git a/.travis.yml b/.travis.yml index 5e3f5d44..eff5dbdf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ android: - platform-tools - tools - extra-android-m2repository - - build-tools-27.0.3 + - build-tools-28.0.2 - android-27 script: cd Clover && ./gradlew build --console plain -x lint diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle index 201a6518..68a28c64 100644 --- a/Clover/app/build.gradle +++ b/Clover/app/build.gradle @@ -15,7 +15,7 @@ def getCommitHash = { -> android { compileSdkVersion 27 // update the travis config when changing this - buildToolsVersion '27.0.3' + buildToolsVersion '28.0.2' defaultConfig { minSdkVersion 15 diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index 1b6fdc88..fe98acc0 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -95,6 +95,7 @@ public class ChanSettings { public static final StringSetting fontSize; public static final BooleanSetting fontCondensed; public static final BooleanSetting openLinkConfirmation; + public static final BooleanSetting openLinkBrowser; public static final BooleanSetting autoRefreshThread; // public static final BooleanSetting imageAutoLoad; public static final OptionsSetting imageAutoLoadNetwork; @@ -108,7 +109,6 @@ public class ChanSettings { public static final StringSetting postDefaultName; public static final BooleanSetting postPinThread; - public static final BooleanSetting postNewCaptcha; public static final BooleanSetting developer; @@ -132,6 +132,7 @@ public class ChanSettings { public static final BooleanSetting controllerSwipeable; public static final BooleanSetting saveBoardFolder; public static final BooleanSetting videoDefaultMuted; + public static final BooleanSetting videoAutoLoop; public static final BooleanSetting watchEnabled; public static final BooleanSetting watchCountdown; @@ -170,6 +171,7 @@ public class ChanSettings { fontSize = new StringSetting(p, "preference_font", tablet ? "16" : "14"); fontCondensed = new BooleanSetting(p, "preference_font_condensed", false); openLinkConfirmation = new BooleanSetting(p, "preference_open_link_confirmation", false); + openLinkBrowser = new BooleanSetting(p, "preference_open_link_browser", false); autoRefreshThread = new BooleanSetting(p, "preference_auto_refresh_thread", true); // imageAutoLoad = new BooleanSetting(p, "preference_image_auto_load", true); imageAutoLoadNetwork = new OptionsSetting<>(p, "preference_image_auto_load_network", MediaAutoLoadMode.class, MediaAutoLoadMode.WIFI); @@ -183,7 +185,6 @@ public class ChanSettings { postDefaultName = new StringSetting(p, "preference_default_name", ""); postPinThread = new BooleanSetting(p, "preference_pin_on_post", false); - postNewCaptcha = new BooleanSetting(p, "preference_new_captcha", true); developer = new BooleanSetting(p, "preference_developer", false); @@ -209,6 +210,7 @@ public class ChanSettings { controllerSwipeable = new BooleanSetting(p, "preference_controller_swipeable", true); saveBoardFolder = new BooleanSetting(p, "preference_save_subboard", false); videoDefaultMuted = new BooleanSetting(p, "preference_video_default_muted", true); + videoAutoLoop = new BooleanSetting(p, "preference_video_loop", true); watchEnabled = new BooleanSetting(p, "preference_watch_enabled", false); watchEnabled.addCallback((setting, value) -> diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteRegistry.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteRegistry.java index d0dc8583..dc793adb 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/SiteRegistry.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteRegistry.java @@ -24,6 +24,7 @@ import org.floens.chan.core.site.sites.lainchan.Lainchan; import org.floens.chan.core.site.sites.chan8.Chan8; import org.floens.chan.core.site.sites.arisuchan.Arisuchan; import org.floens.chan.core.site.sites.sushichan.Sushichan; +import org.floens.chan.core.site.sites.dvach.Dvach; import java.util.ArrayList; import java.util.List; @@ -41,6 +42,7 @@ public class SiteRegistry { URL_HANDLERS.add(Lainchan.URL_HANDLER); URL_HANDLERS.add(Arisuchan.URL_HANDLER); URL_HANDLERS.add(Sushichan.URL_HANDLER); + URL_HANDLERS.add(Dvach.URL_HANDLER); } static { @@ -53,5 +55,6 @@ public class SiteRegistry { SITE_CLASSES.put(2, Lainchan.class); SITE_CLASSES.put(3, Arisuchan.class); SITE_CLASSES.put(4, Sushichan.class); + SITE_CLASSES.put(5, Dvach.class); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/Dvach.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/Dvach.java new file mode 100644 index 00000000..fad5bf0b --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/Dvach.java @@ -0,0 +1,213 @@ +package org.floens.chan.core.site.sites.dvach; + +import android.support.annotation.Nullable; + +import org.floens.chan.core.model.Post; +import org.floens.chan.core.model.orm.Board; +import org.floens.chan.core.model.orm.Loadable; +import org.floens.chan.core.settings.OptionsSetting; +import org.floens.chan.core.site.Boards; +import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.SiteAuthentication; +import org.floens.chan.core.site.SiteIcon; +import org.floens.chan.core.site.SiteSetting; +import org.floens.chan.core.site.common.CommonReplyHttpCall; +import org.floens.chan.core.site.common.CommonSite; +import org.floens.chan.core.site.common.MultipartHttpCall; +import org.floens.chan.core.site.common.vichan.VichanActions; +import org.floens.chan.core.site.common.vichan.VichanCommentParser; +import org.floens.chan.core.site.common.vichan.VichanEndpoints; +import org.floens.chan.core.site.http.DeleteRequest; +import org.floens.chan.core.site.http.HttpCall; +import org.floens.chan.core.site.http.Reply; +import org.floens.chan.core.site.sites.chan4.Chan4; +import org.floens.chan.utils.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import okhttp3.HttpUrl; + +import static android.support.constraint.Constraints.TAG; + +public class Dvach extends CommonSite { + public static final CommonSiteUrlHandler URL_HANDLER = new CommonSiteUrlHandler() { + @Override + public Class getSiteClass() { + return Dvach.class; + } + + @Override + public HttpUrl getUrl() { + return HttpUrl.parse("https:/2ch.hk"); + } + + @Override + public String[] getNames() { + return new String[]{"dvach", "2ch"}; + } + + @Override + public String desktopUrl(Loadable loadable, @Nullable Post post) { + if (loadable.isCatalogMode()) { + return getUrl().newBuilder().addPathSegment(loadable.boardCode).toString(); + } else if (loadable.isThreadMode()) { + return getUrl().newBuilder() + .addPathSegment(loadable.boardCode).addPathSegment("res") + .addPathSegment(String.valueOf(loadable.no) + ".html") + .toString(); + } else { + return getUrl().toString(); + } + } + }; + + static final String CAPTCHA_KEY = "6LeQYz4UAAAAAL8JCk35wHSv6cuEV5PyLhI6IxsM"; + + private OptionsSetting captchaType; + + @Override + public void initializeSettings() { + super.initializeSettings(); + captchaType = new OptionsSetting<>(settingsProvider, + "preference_captcha_type", + Chan4.CaptchaType.class, Chan4.CaptchaType.V2JS); + } + + @Override + public List settings() { + return Arrays.asList( + SiteSetting.forOption( + captchaType, + "Captcha type", + Arrays.asList("Javascript", "Noscript")) + ); + } + + @Override + public void setup() { + setName("2ch.hk"); + setIcon(SiteIcon.fromFavicon(HttpUrl.parse("https://2ch.hk/favicon.ico"))); + setBoardsType(BoardsType.DYNAMIC); + + setResolvable(URL_HANDLER); + + setConfig(new CommonConfig() { + @Override + public boolean feature(Feature feature) { + return feature == Feature.POSTING; + } + }); + + setEndpoints(new VichanEndpoints(this, + "https://2ch.hk", + "https://2ch.hk") { + @Override + public HttpUrl imageUrl(Post.Builder post, Map arg) { + return root.builder().s(arg.get("path")).url(); + } + + @Override + public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map arg) { + return root.builder().s(arg.get("thumbnail")).url(); + } + + @Override + public HttpUrl boards() { + return new HttpUrl.Builder().scheme("https").host("2ch.hk").addPathSegment("boards.json").build(); + } + + @Override + public HttpUrl reply(Loadable loadable) { + return new HttpUrl.Builder().scheme("https").host("2ch.hk").addPathSegment("makaba").addPathSegment("posting.fcgi").addQueryParameter("json", "1").build(); + } + }); + + setActions(new VichanActions(this) { + + @Override + public void setupPost(Reply reply, MultipartHttpCall call) { + super.setupPost(reply, call); + + if (reply.loadable.isThreadMode()) { + // "thread" is already added in VichanActions. + call.parameter("post", "New Reply"); + } else { + call.parameter("post", "New Thread"); + call.parameter("page", "1"); + } + } + + @Override + public boolean requirePrepare() { + return false; + } + + @Override + public void post(Reply reply, final PostListener postListener) { + httpCallManager.makeHttpCall(new DvachReplyCall(Dvach.this, reply), new HttpCall.HttpCallback() { + @Override + public void onHttpSuccess(CommonReplyHttpCall httpPost) { + postListener.onPostComplete(httpPost, httpPost.replyResponse); + } + + @Override + public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) { + postListener.onPostError(httpPost, e); + } + }); + } + + @Override + public boolean postRequiresAuthentication() { + return !isLoggedIn(); + } + + @Override + public SiteAuthentication postAuthenticate() { + if (isLoggedIn()) { + return SiteAuthentication.fromNone(); + } else { + switch (captchaType.get()) { + case V2JS: + return SiteAuthentication.fromCaptcha2(CAPTCHA_KEY, "https://2ch.hk/api/captcha/recaptcha/mobile"); + case V2NOJS: + return SiteAuthentication.fromCaptcha2nojs(CAPTCHA_KEY, "https://2ch.hk/api/captcha/recaptcha/mobile"); + default: + throw new IllegalArgumentException(); + } + } + } + + @Override + public void delete(DeleteRequest deleteRequest, DeleteListener deleteListener) { + super.delete(deleteRequest, deleteListener); + } + + @Override + public void boards(final BoardsListener listener) { + requestQueue.add(new DvachBoardsRequest(Dvach.this, response -> { + listener.onBoardsReceived(new Boards(response)); + }, (error) -> { + Logger.e(TAG, "Failed to get boards from server", error); + + // API fail, provide some default boards + List list = new ArrayList<>(); + list.add(Board.fromSiteNameCode(Dvach.this, "бред", "b")); + list.add(Board.fromSiteNameCode(Dvach.this, "Видеоигры, general, официальные треды", "vg")); + list.add(Board.fromSiteNameCode(Dvach.this, "новости", "news")); + list.add(Board.fromSiteNameCode(Dvach.this, "политика, новости, ольгинцы, хохлы, либерахи, рептилоиды.. oh shi", "po")); + Collections.shuffle(list); + listener.onBoardsReceived(new Boards(list)); + })); + } + }); + + setApi(new DvachApi(this)); + + setParser(new VichanCommentParser()); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachApi.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachApi.java new file mode 100644 index 00000000..ffcb99bf --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachApi.java @@ -0,0 +1,234 @@ +package org.floens.chan.core.site.sites.dvach; + +import android.util.JsonReader; + +import org.floens.chan.core.model.Post; +import org.floens.chan.core.model.PostImage; +import org.floens.chan.core.site.SiteEndpoints; +import org.floens.chan.core.site.common.CommonSite; +import org.floens.chan.core.site.parser.ChanReaderProcessingQueue; +import org.jsoup.parser.Parser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.floens.chan.core.site.SiteEndpoints.makeArgument; + +public class DvachApi extends CommonSite.CommonApi { + DvachApi(CommonSite commonSite) { + super(commonSite); + } + + @Override + public void loadThread(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception { + reader.beginObject(); // Main object + + while (reader.hasNext()) { + + if (reader.nextName().equals("threads")) { + reader.beginArray(); // Threads array + + while (reader.hasNext()) { + reader.beginObject(); // Posts object + if (reader.nextName().equals("posts")) { + reader.beginArray(); // Posts array + while (reader.hasNext()) { + readPostObject(reader, queue); + } + reader.endArray(); + } + reader.endObject(); + } + + reader.endArray(); + } else { + reader.skipValue(); + } + } + + reader.endObject(); + } + + @Override + public void loadCatalog(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception { + reader.beginObject(); // Main object + + while (reader.hasNext()) { + if (reader.nextName().equals("threads")) { + reader.beginArray(); // Threads array + + while (reader.hasNext()) { + readPostObject(reader, queue); + } + + reader.endArray(); + } else { + reader.skipValue(); + } + } + + reader.endObject(); + } + + @Override + public void readPostObject(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception { + Post.Builder builder = new Post.Builder(); + builder.board(queue.getLoadable().board); + + SiteEndpoints endpoints = queue.getLoadable().getSite().endpoints(); + + List files = new ArrayList<>(); + + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + + switch (key) { + case "name": + builder.name(reader.nextString()); + break; + case "comment": + builder.comment(reader.nextString()); + break; + case "timestamp": + builder.setUnixTimestampSeconds(reader.nextLong()); + break; + case "trip": + builder.tripcode(reader.nextString()); + break; + case "op": + int opId = reader.nextInt(); + builder.op(opId == 0); + builder.opId(opId); + break; + case "sticky": + builder.sticky(reader.nextInt() == 1); + break; + case "closed": + builder.closed(reader.nextInt() == 1); + break; + case "archived": + builder.archived(reader.nextInt() == 1); + break; + case "posts_count": + builder.replies(reader.nextInt() - 1); + break; + case "files_count": + builder.images(reader.nextInt()); + break; + case "lasthit": + builder.lastModified(reader.nextLong()); + break; + case "num": + String num = reader.nextString(); + builder.id(Integer.parseInt(num)); + break; + case "files": + reader.beginArray(); + + while (reader.hasNext()) { + PostImage postImage = readPostImage(reader, builder, endpoints); + if (postImage != null) { + files.add(postImage); + } + } + + reader.endArray(); + break; + default: + // Unknown/ignored key + reader.skipValue(); + break; + } + } + reader.endObject(); + + builder.images(files); + + if (builder.op) { + // Update OP fields later on the main thread + Post.Builder op = new Post.Builder(); + op.closed(builder.closed); + op.archived(builder.archived); + op.sticky(builder.sticky); + op.replies(builder.replies); + op.images(builder.imagesCount); + op.uniqueIps(builder.uniqueIps); + op.lastModified(builder.lastModified); + queue.setOp(op); + } + + Post cached = queue.getCachedPost(builder.id); + if (cached != null) { + // Id is known, use the cached post object. + queue.addForReuse(cached); + return; + } + + queue.addForParse(builder); + } + + private PostImage readPostImage(JsonReader reader, Post.Builder builder, + SiteEndpoints endpoints) throws IOException { + reader.beginObject(); + + String path = null; + long fileSize = 0; + String fileExt = null; + int fileWidth = 0; + int fileHeight = 0; + String fileName = null; + String thumbnail = null; + + while (reader.hasNext()) { + switch (reader.nextName()) { + case "path": + path = reader.nextString(); + break; + case "name": + fileName = reader.nextString(); + break; + case "size": + fileSize = reader.nextLong(); + break; + case "width": + fileWidth = reader.nextInt(); + break; + case "height": + fileHeight = reader.nextInt(); + break; + case "thumbnail": + thumbnail = reader.nextString(); + break; + default: + reader.skipValue(); + break; + } + } + + reader.endObject(); + + if (fileName != null) { + fileExt = fileName.substring(fileName.lastIndexOf('.') + 1); + fileName = fileName.substring(0, fileName.lastIndexOf('.')); + } + + if (path != null && fileName != null) { + Map args = makeArgument("path", path, "thumbnail", thumbnail); + return new PostImage.Builder() + .originalName(fileName) + .thumbnailUrl(endpoints.thumbnailUrl(builder, false, args)) + .spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args)) + .imageUrl(endpoints.imageUrl(builder, args)) + .filename(Parser.unescapeEntities(fileName, false)) + .extension(fileExt) + .imageWidth(fileWidth) + .imageHeight(fileHeight) + .size(fileSize) + .build(); + } + return null; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachBoardsRequest.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachBoardsRequest.java new file mode 100644 index 00000000..b34af8b6 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachBoardsRequest.java @@ -0,0 +1,108 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.core.site.sites.dvach; + +import android.util.JsonReader; + +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; + +import org.floens.chan.core.model.orm.Board; +import org.floens.chan.core.net.JsonReaderRequest; +import org.floens.chan.core.site.Site; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class DvachBoardsRequest extends JsonReaderRequest> { + private final Site site; + + DvachBoardsRequest(Site site, Listener> listener, ErrorListener errorListener) { + super(site.endpoints().boards().toString(), listener, errorListener); + this.site = site; + } + + @Override + public List readJson(JsonReader reader) throws Exception { + List list = new ArrayList<>(); + + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (key.equals("boards")) { + reader.beginArray(); + + while (reader.hasNext()) { + Board board = readBoardEntry(reader); + if (board != null) { + list.add(board); + } + } + + reader.endArray(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + return list; + } + + private Board readBoardEntry(JsonReader reader) throws IOException { + reader.beginObject(); + + Board board = new Board(); + board.siteId = site.id(); + board.site = site; + + while (reader.hasNext()) { + String key = reader.nextName(); + + switch (key) { + case "name": + board.name = reader.nextString(); + break; + case "id": + board.code = reader.nextString(); + break; + case "bump_limit": + board.bumpLimit = reader.nextInt(); + break; + case "info": + board.description = reader.nextString(); + break; + case "category": + board.workSafe = !"Взрослым".equals(reader.nextString()); + break; + default: + reader.skipValue(); + break; + } + } + + reader.endObject(); + + if (!board.finish()) { + // Invalid data, ignore + return null; + } + return board; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachReplyCall.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachReplyCall.java new file mode 100644 index 00000000..e9da6955 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachReplyCall.java @@ -0,0 +1,101 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.core.site.sites.dvach; + +import android.text.TextUtils; + +import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.common.CommonReplyHttpCall; +import org.floens.chan.core.site.http.Reply; +import org.jsoup.Jsoup; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class DvachReplyCall extends CommonReplyHttpCall { + private static final Pattern ERROR_MESSAGE = Pattern.compile("^\\{\"Error\":-\\d+,\"Reason\":\"(.*)\""); + private static final Pattern POST_MESSAGE = Pattern.compile("^\\{\"Error\":null,\"Status\":\"OK\",\"Num\":(\\d+)"); + private static final Pattern THREAD_MESSAGE = Pattern.compile("^\\{\"Error\":null,\"Status\":\"Redirect\",\"Target\":(\\d+)"); + private static final String PROBABLY_BANNED_TEXT = "banned"; + + DvachReplyCall(Site site, Reply reply) { + super(site, reply); + } + + @Override + public void addParameters(MultipartBody.Builder formBuilder) { + formBuilder.addFormDataPart("task", "post"); + formBuilder.addFormDataPart("board", reply.loadable.boardCode); + formBuilder.addFormDataPart("comment", reply.comment); + formBuilder.addFormDataPart("thread", String.valueOf(reply.loadable.no)); + + formBuilder.addFormDataPart("name", reply.name); + formBuilder.addFormDataPart("email", reply.options); + + if (!reply.loadable.isThreadMode() && !TextUtils.isEmpty(reply.subject)) { + formBuilder.addFormDataPart("subject", reply.subject); + } + + + if (reply.captchaResponse != null) { + formBuilder.addFormDataPart("captcha_type", "recaptcha"); + formBuilder.addFormDataPart("captcha_key", Dvach.CAPTCHA_KEY); + + if (reply.captchaChallenge != null) { + formBuilder.addFormDataPart("recaptcha_challenge_field", reply.captchaChallenge); + formBuilder.addFormDataPart("recaptcha_response_field", reply.captchaResponse); + } else { + formBuilder.addFormDataPart("g-recaptcha-response", reply.captchaResponse); + } + } + + if (reply.file != null) { + formBuilder.addFormDataPart("image", reply.fileName, RequestBody.create( + MediaType.parse("application/octet-stream"), reply.file + )); + } + } + + @Override + public void process(Response response, String result) throws IOException { + Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result); + if (errorMessageMatcher.find()) { + replyResponse.errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text(); + replyResponse.probablyBanned = replyResponse.errorMessage.contains(PROBABLY_BANNED_TEXT); + } else { + replyResponse.posted = true; + Matcher postMessageMatcher = POST_MESSAGE.matcher(result); + if (postMessageMatcher.find()) { + replyResponse.postNo = Integer.parseInt(postMessageMatcher.group(1)); + } else { + Matcher threadMessageMatcher = THREAD_MESSAGE.matcher(result); + if (threadMessageMatcher.find()) { + int threadNo = Integer.parseInt(threadMessageMatcher.group(1)); + replyResponse.threadNo = threadNo; + replyResponse.postNo = threadNo; + } + } + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java index 9d5d93cd..148f935a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java @@ -99,6 +99,7 @@ public class PostCell extends LinearLayout implements PostCellInterface { private PostIcons icons; private TextView comment; private FastTextView replies; + private View repliesAdditionalArea; private ImageView options; private View divider; private View filterMatchColor; @@ -153,6 +154,7 @@ public class PostCell extends LinearLayout implements PostCellInterface { icons = findViewById(R.id.icons); comment = findViewById(R.id.comment); replies = findViewById(R.id.replies); + repliesAdditionalArea = findViewById(R.id.replies_additional_area); options = findViewById(R.id.options); divider = findViewById(R.id.divider); filterMatchColor = findViewById(R.id.filter_match_color); @@ -186,18 +188,21 @@ public class PostCell extends LinearLayout implements PostCellInterface { dividerParams.rightMargin = paddingPx; divider.setLayoutParams(dividerParams); - replies.setOnClickListener(v -> { - if (threadMode) { - int repliesFromSize; - synchronized (post.repliesFrom) { - repliesFromSize = post.repliesFrom.size(); - } + OnClickListener repliesClickListener = v -> { + if (replies.getVisibility() != VISIBLE || !threadMode) { + return; + } + int repliesFromSize; + synchronized (post.repliesFrom) { + repliesFromSize = post.repliesFrom.size(); + } - if (repliesFromSize > 0) { - callback.onShowPostReplies(post); - } + if (repliesFromSize > 0) { + callback.onShowPostReplies(post); } - }); + }; + replies.setOnClickListener(repliesClickListener); + repliesAdditionalArea.setOnClickListener(repliesClickListener); options.setOnClickListener(v -> { List items = new ArrayList<>(); @@ -321,6 +326,7 @@ public class PostCell extends LinearLayout implements PostCellInterface { setPostLinkableListener(post, true); replies.setClickable(threadMode); + repliesAdditionalArea.setClickable(threadMode); if (!threadMode) { replies.setBackgroundResource(0); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java index 1208e8d6..1651eab4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java @@ -45,7 +45,7 @@ public class BehaviourSettingsController extends SettingsController { public void onCreate() { super.onCreate(); - navigation.setTitle(R.string.settings_screen_behaviour); + navigation.setTitle(R.string.settings_screen_behavior); setupLayout(); @@ -79,11 +79,6 @@ public class BehaviourSettingsController extends SettingsController { { SettingsGroup reply = new SettingsGroup(R.string.settings_group_reply); - reply.add(new BooleanSettingView(this, - ChanSettings.postNewCaptcha, - R.string.setting_use_new_captcha, - R.string.setting_use_new_captcha_description)); - reply.add(new BooleanSettingView(this, ChanSettings.postPinThread, R.string.setting_post_pin, 0)); @@ -133,6 +128,9 @@ public class BehaviourSettingsController extends SettingsController { post.add(new BooleanSettingView(this, ChanSettings.openLinkConfirmation, R.string.setting_open_link_confirmation, 0)); + post.add(new BooleanSettingView(this, + ChanSettings.openLinkBrowser, + R.string.setting_open_link_browser, 0)); groups.add(post); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java index a929ac51..d1ef97c4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java @@ -135,7 +135,7 @@ public class BrowseController extends ThreadController implements overflowBuilder .withSubItem(ARCHIVE_ID, R.string.thread_view_archive, this::archiveClicked) - .withSubItem(R.string.action_order, this::orderClicked) + .withSubItem(R.string.action_sort, this::orderClicked) .withSubItem(R.string.action_open_browser, this::openBrowserClicked) .withSubItem(R.string.action_share, this::shareClicked) .build() @@ -196,7 +196,7 @@ public class BrowseController extends ThreadController implements } private void orderClicked(ToolbarMenuSubItem item) { - handleOrder(threadLayout.getPresenter()); + handleSorting(threadLayout.getPresenter()); } private void openBrowserClicked(ToolbarMenuSubItem item) { @@ -270,7 +270,7 @@ public class BrowseController extends ThreadController implements threadLayout.setPostViewMode(postViewMode); } - private void handleOrder(final ThreadPresenter presenter) { + private void handleSorting(final ThreadPresenter presenter) { List items = new ArrayList<>(); for (PostsFilter.Order order : PostsFilter.Order.values()) { int nameId = 0; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java index a9183de1..c0d4ddd2 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java @@ -138,7 +138,7 @@ public class MainSettingsController extends SettingsController implements Settin new AppearanceSettingsController(context)))); general.add(new LinkSettingView(this, - R.string.settings_behaviour, R.string.settings_behaviour_description, + R.string.settings_behavior, R.string.settings_behavior_description, v -> navigationController.pushController( new BehaviourSettingsController(context)))); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/MediaSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/MediaSettingsController.java index ec6253cc..06f2d093 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/MediaSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/MediaSettingsController.java @@ -127,6 +127,11 @@ public class MediaSettingsController extends SettingsController { setupMediaLoadTypesSetting(loading); + loading.add(new BooleanSettingView(this, + ChanSettings.videoAutoLoop, + R.string.setting_video_auto_loop, + R.string.setting_video_auto_loop_description)); + groups.add(loading); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java index 5fbd27aa..2bbfe838 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java @@ -88,8 +88,8 @@ public class ViewThreadController extends ThreadController implements ThreadLayo .withSubItem(R.string.action_reload, this::reloadClicked) .withSubItem(R.string.action_open_browser, this::openBrowserClicked) .withSubItem(R.string.action_share, this::shareClicked) - .withSubItem(R.string.action_up, this::upClicked) - .withSubItem(R.string.action_down, this::downClicked) + .withSubItem(R.string.action_scroll_to_top, this::upClicked) + .withSubItem(R.string.action_scroll_to_bottom, this::downClicked) .build() .build(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index 0e4e6668..018226aa 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java @@ -301,12 +301,20 @@ public class ThreadLayout extends CoordinatorLayout implements .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - AndroidUtils.openLinkInBrowser((Activity) getContext(), link); + openLinkConfirmed(link); } }) .setTitle(R.string.open_link_confirmation) .setMessage(link) .show(); + } else { + openLinkConfirmed(link); + } + } + + public void openLinkConfirmed(final String link) { + if (ChanSettings.openLinkBrowser.get()) { + AndroidUtils.openLink(link); } else { AndroidUtils.openLinkInBrowser((Activity) getContext(), link); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java index c1bc9b74..15863af4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java @@ -391,7 +391,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa if (query != null) { int size = postAdapter.getDisplayList().size(); searchStatus.setText(getContext().getString(R.string.search_results, - size, getContext().getResources().getQuantityString(R.plurals.posts, size, size), query)); + getContext().getResources().getQuantityString(R.plurals.posts, size, size), + query)); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java index 8b5a63f6..5aff97d4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java @@ -406,7 +406,7 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener videoView.setOnPreparedListener(mp -> { mediaPlayer = mp; - mp.setLooping(true); + mp.setLooping(ChanSettings.videoAutoLoop.get()); mp.setVolume(0f, 0f); onModeLoaded(Mode.MOVIE, videoView); callback.onVideoLoaded(this, hasMediaPlayerAudioTracks(mp)); diff --git a/Clover/app/src/main/res/layout/cell_post.xml b/Clover/app/src/main/res/layout/cell_post.xml index 9fd6a16f..f7a2ec9d 100644 --- a/Clover/app/src/main/res/layout/cell_post.xml +++ b/Clover/app/src/main/res/layout/cell_post.xml @@ -83,6 +83,15 @@ along with this program. If not, see . app:singleLine="true" app:textColor="?attr/text_color_secondary" /> + + . Grant Later - Clover is up to date + Clover is up-to-date Failed to check for updates. Install Downloading update Download failed Failed to move downloaded file to the Download directory. Retry update - Clover was not updated yet. Click retry to retry the install. + Clover was not updated yet. Tap \"retry\" to retry the install. retry Storage permission required @@ -52,8 +52,8 @@ Re-enable this permission in the app settings if you permanently disabled it." - post - posts + %d post + %d posts @@ -94,22 +94,22 @@ Re-enable this permission in the app settings if you permanently disabled it."%1$dR %2$dI Reload - Pin - Open in browser + Bookmark + Open in a browser Share Download album Search Image search Catalog mode Board mode - Top - Bottom - Order - Rename pin + Top + Bottom + Sort + Rename bookmark Rename Reply - Clover cannot open this link, opening in your browser. + Clover cannot open this link; it will open in your browser instead. Bump order Reply count @@ -119,12 +119,12 @@ Re-enable this permission in the app settings if you permanently disabled it."Latest reply Search - Found %1$d %2$s for "%3$s" + Found %1$s for \"%2$s\" Search subjects, comments, names and filenames - Open link? + Open this link? Open this thread? - No applications found to open link + No applications were found to open this link Confirm exit @@ -132,7 +132,7 @@ Re-enable this permission in the app settings if you permanently disabled it."404 Failed to show image Failed to show image, out of memory - Deepzoom loading failed + Failed to show image, deep-zoom loading failed Image not found Failed to open image Spoiler image @@ -145,7 +145,7 @@ Re-enable this permission in the app settings if you permanently disabled it."404 not found Tap to refresh Loading - Loading in %1$d + Loading in %1$ds Retry Archived Closed @@ -168,7 +168,7 @@ Re-enable this permission in the app settings if you permanently disabled it."No sites added Add a site here Add site - Add a site by its url or name + Add a site by its URL or name URL or name http://example.com %s added @@ -186,16 +186,16 @@ Re-enable this permission in the app settings if you permanently disabled it."Configure boards of %s No boards added - Removed \'%s\' + Removed \"%s\" %s added Select all - all boards + All boards Enabled Filter Action Pattern - Test pattern + Test the pattern Pattern Boards Types @@ -212,29 +212,29 @@ Re-enable this permission in the app settings if you permanently disabled it."Pick color Filter help - - If the pattern matches then the post can be hidden or highlighted.
- -

For tripcodes, names and IDs:

-

- It will match the given pattern exact.
- !Ep8pui8Vw2 will match the tripcode !Ep8pui8Vw2 but not Ep8pu. -

- -

For comments, subjects and filenames:

-

- These filters are pattern based, and have three modes:
-
- 1. The pattern foo bar will match text that has any of the words in it. It will match foo or bar, but not foobar. - Placing a * allows any character to be filled in: f*o will match both foo, foooo but not foobar
-
- 2. Quoting your pattern with \" like \"foo bar\" will match the text exact. - foo bar matches but foo does not.
-
- 3. Regular expressions. /^>implying/ for example. -

- ]]> + +If the pattern matches then the post can be hidden or highlighted.
+ +

For tripcodes, names and IDs:

+

+ It will match the given pattern exact.
+ !Ep8pui8Vw2 will match the tripcode !Ep8pui8Vw2 but not Ep8pu. +

+ +

For comments, subjects and filenames:

+

+ These filters are pattern based, and have three modes:
+
+ 1. The pattern foo bar will match text that has any of the words in it. It will match foo or bar, but not foobar. + Placing a * allows any character to be filled in: f*o will match both foo, foooo but not foobar
+
+ 2. Quoting your pattern with \" like \"foo bar\" will match the text exactly. + foo bar matches but foo does not.
+
+ 3. Regular expressions. /^>implying/ for example. +

+]]>
Tripcode @@ -261,7 +261,7 @@ Re-enable this permission in the app settings if you permanently disabled it."Error loading archive Bookmarked threads - Removed \'%1$s\' + Removed \"%1$s\" %1$s cleared No bookmarks cleared. No bookmarks cleared. Hold to remove all. @@ -309,7 +309,12 @@ Re-enable this permission in the app settings if you permanently disabled it."Error deleting post WebM video error - The WebM video failed to play. This might be a problem with the hardware of your phone.\n\nYou can install an external media player that has a software decoder and enable \"Open videos external\" in the settings so that the video opens externally in a software decoding app. + +"The WebM video failed to play. This might be a problem with the hardware of your phone. + +A workaround is to use an external media player with a software decoder. +Enable \"Play videos with external player\" in the settings to play videos with an external player." + Don\'t show again Stop watching @@ -348,7 +353,7 @@ Re-enable this permission in the app settings if you permanently disabled it."Downloading images Tap to cancel Image saved - Saved as %1$s + Saved as \"%1$s\" Saving image failed Setup sites @@ -363,23 +368,23 @@ Re-enable this permission in the app settings if you permanently disabled it."Sites Appearance Theme, layout, fonts, etc. - Behaviour - Thread refresh, captcha, etc. + Behavior + Thread refresh, captcha, etc. Media Save location, auto loading, etc. Filters - Watching pinned threads + Watching bookmarked threads Off Thread watcher settings Settings - To watch pins for new posts, turn the thread watcher on. + To watch bookmarks for new posts, turn the thread watcher on. Enable in the background - Watch pins when Clover is in the background + Watch bookmarks when Clover is placed in the background Background update interval - The time between updates in the background + The interval between updates when placed in the background Notify about All posts @@ -390,8 +395,8 @@ Re-enable this permission in the app settings if you permanently disabled it."All posts Only posts quoting you - Heads-up notification on mentions - Show a heads-up notification on mentions + Heads-up notification on quotes + Show a heads-up notification when quoted Notification light None @@ -408,9 +413,11 @@ Re-enable this permission in the app settings if you permanently disabled it."About Check for updates Report crashes - Crash reporting creates reports of errors in Clover. - Crash reports do not collect any personally identifiable information. - Setting will be applied on next app start. + +"Crash reporting creates reports of errors in Clover. +Crash reports do not collect any personally identifiable information." + + Setting will be applied on the next app start. Released under the GNU GPLv3 license Tap to see license Open Source Licenses @@ -430,12 +437,12 @@ Re-enable this permission in the app settings if you permanently disabled it."Phone layout Slide mode Split mode - Catalog mode columns + Catalog mode column count Auto %1$d columns Never hide the toolbar - Enable the reply FAB - Disabling replaces it with a menu option + Enable the reply button + Disabling replaces the round button with a menu option Post @@ -447,10 +454,10 @@ Re-enable this permission in the app settings if you permanently disabled it."Show file info on posts Show filename on posts - - Behaviour + + Behavior - + General Auto refresh threads Confirm before exit @@ -458,28 +465,27 @@ Re-enable this permission in the app settings if you permanently disabled it."Clear all thread hides Cleared all thread hides - + Reply - Use the new captcha - Enable to use the newer recaptcha for thread replies. - Pin thread on post + Bookmark thread on post Default post name - + Text only mode Hide images when in board and thread view Reveal text spoilers - Makes the spoiler text appear clicked + Makes the spoiler text appear tapped Make everyone Anonymous Hide IDs - Show Anonymous username + Always show \"Anonymous\" name Reply buttons on the bottom Volume keys scroll content Tap the post number to reply Ask before opening links + Always open links in external browser - + HTTP Proxy Enable proxy Proxy server address @@ -495,26 +501,31 @@ Re-enable this permission in the app settings if you permanently disabled it."Save images in a board folder Create a folder for each board to store images in Save original filename - Save the image with the filename the site assigned. - If disabled, save the image with the filename from the uploader. + +"Save the image with the filename the site assigned. +If disabled, save the image with the filename the uploader assigned." + Start videos muted If a video has audio, mute it by default. - Open videos external - Open videos in an external media player - Share url to image - Share the url to the image instead of the image itself + Play videos with external player + Play videos in an external media player app + Share URL to image + Share the URL to the image instead of the image itself Reveal image spoilers Always reveal spoiler thumbnails and images. Media loading - Auto load images + Automatically load images Always Wi-Fi only Never - Auto load videos + Automatically load videos + + Enable automatic video-looping + Automatically loop video content @@ -523,14 +534,18 @@ Re-enable this permission in the app settings if you permanently disabled it." "Permission to access storage is required for browsing files. -Re-enable this permission in the app settings if you permanently disabled it." +Re-enable this permission in the app settings if you permanently disabled it." + Choose Up - Swipe to change the theme.\nTap the toolbar menu to change its color.\n - Click here to change the FAB color + +"Swipe to change the theme. +Tap the toolbar menu to change its color." + + Tap here to change the FAB color View logs @@ -548,13 +563,13 @@ Re-enable this permission in the app settings if you permanently disabled it."Loading… Connection error " - - Go here to reset your PIN.
-
- Don't have a 4chan Pass?
- Click here to learn more. - ]]> + +Go here to reset your PIN.
+
+Don't have a 4chan Pass?
+Tap here to learn more. +]]> "
Themes diff --git a/Clover/build.gradle b/Clover/build.gradle index b32f870a..551e9f07 100644 --- a/Clover/build.gradle +++ b/Clover/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.2.0' } } diff --git a/Clover/gradle/wrapper/gradle-wrapper.properties b/Clover/gradle/wrapper/gradle-wrapper.properties index 288a78a3..fedcafd2 100644 --- a/Clover/gradle/wrapper/gradle-wrapper.properties +++ b/Clover/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Mar 28 12:15:28 CEST 2018 +#Sat Oct 06 14:30:14 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/README.md b/README.md index abb6beb4..5bc038c5 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ For first-time contributors, the issues with the label [good first issue](https: See the [Clover setup guide](https://github.com/Floens/Clover/wiki/Building-Clover) for a guide on building Clover. -We have a spacial guide for [making new themes](https://github.com/Floens/Clover/wiki/Adding-a-new-theme) +We have a special guide for [making new themes.](https://github.com/Floens/Clover/wiki/Adding-a-new-theme) ## Translations