diff --git a/.travis.yml b/.travis.yml index fce9c94b..5e3f5d44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,3 +11,6 @@ android: - android-27 script: cd Clover && ./gradlew build --console plain -x lint + +notifications: + email: false diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml index b0ce7abb..c0c9ddd2 100644 --- a/Clover/app/src/main/AndroidManifest.xml +++ b/Clover/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . --> @@ -23,7 +24,9 @@ along with this program. If not, see . - + * All {@code final} fields are thread-safe. @@ -99,12 +97,13 @@ public class Post { public final List repliesFrom = new ArrayList<>(); // These members may only mutate on the main thread. - private boolean sticky = false; - private boolean closed = false; - private boolean archived = false; - private int replies = -1; - private int imagesCount = -1; - private int uniqueIps = -1; + private boolean sticky; + private boolean closed; + private boolean archived; + private int replies; + private int imagesCount; + private int uniqueIps; + private long lastModified; private String title = ""; private Post(Builder builder) { @@ -116,6 +115,7 @@ public class Post { replies = builder.replies; imagesCount = builder.imagesCount; uniqueIps = builder.uniqueIps; + lastModified = builder.lastModified; sticky = builder.sticky; closed = builder.closed; archived = builder.archived; @@ -214,6 +214,16 @@ public class Post { this.uniqueIps = uniqueIps; } + @MainThread + public long getLastModified() { + return lastModified; + } + + @MainThread + public void setLastModified(long lastModified) { + this.lastModified = lastModified; + } + @MainThread public String getTitle() { return title; @@ -246,19 +256,16 @@ public class Post { public boolean sticky; public boolean closed; public boolean archived; + public long lastModified = -1L; public String subject = ""; public String name = ""; public CharSequence comment = ""; public String tripcode = ""; - public long unixTimestampSeconds = -1; + public long unixTimestampSeconds = -1L; public List images; - public String countryCode; - public String countryName; - public HttpUrl countryUrl; - public List httpIcons; public String posterId = ""; @@ -324,6 +331,11 @@ public class Post { return this; } + public Builder lastModified(long lastModified) { + this.lastModified = lastModified; + return this; + } + public Builder closed(boolean closed) { this.closed = closed; return this; diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java index 534a51f6..e05ac5fb 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java @@ -1,8 +1,8 @@ package org.floens.chan.core.presenter; import org.floens.chan.core.database.DatabaseManager; -import org.floens.chan.core.settings.Setting; import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.SiteSetting; import java.util.List; @@ -29,7 +29,7 @@ public class SiteSetupPresenter { callback.showLogin(); } - List> settings = site.settings(); + List settings = site.settings(); if (!settings.isEmpty()) { callback.showSettings(settings); } @@ -57,6 +57,6 @@ public class SiteSetupPresenter { void setIsLoggedIn(boolean isLoggedIn); - void showSettings(List> settings); + void showSettings(List settings); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java index a40557a6..a85acd1d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java @@ -21,12 +21,11 @@ import org.floens.chan.core.model.Post; import org.floens.chan.core.model.json.site.SiteConfig; import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Loadable; -import org.floens.chan.core.settings.Setting; import org.floens.chan.core.settings.json.JsonSettings; -import org.floens.chan.core.site.parser.ChanReader; import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.LoginRequest; import org.floens.chan.core.site.http.Reply; +import org.floens.chan.core.site.parser.ChanReader; import java.util.List; @@ -150,7 +149,7 @@ public interface Site { boolean boardFeature(BoardFeature boardFeature, Board board); - List> settings(); + List settings(); SiteEndpoints endpoints(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java index 6042e8cb..454627a0 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java @@ -25,7 +25,6 @@ import org.floens.chan.core.database.LoadableProvider; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.json.site.SiteConfig; import org.floens.chan.core.model.orm.Board; -import org.floens.chan.core.settings.Setting; import org.floens.chan.core.settings.SettingProvider; import org.floens.chan.core.settings.json.JsonSettings; import org.floens.chan.core.settings.json.JsonSettingsProvider; @@ -99,7 +98,7 @@ public abstract class SiteBase implements Site { } @Override - public List> settings() { + public List settings() { return new ArrayList<>(); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteSetting.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteSetting.java new file mode 100644 index 00000000..17b94c21 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteSetting.java @@ -0,0 +1,51 @@ +/* + * 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; + +import org.floens.chan.core.settings.OptionsSetting; +import org.floens.chan.core.settings.Setting; + +import java.util.List; + +/** + * Hacky stuff to give the site settings a good UI. + */ +public class SiteSetting { + public enum Type { + OPTIONS + } + + public final String name; + public final Type type; + public final Setting setting; + + public List optionNames; + + private SiteSetting(String name, Type type, Setting setting) { + this.name = name; + this.type = type; + this.setting = setting; + } + + public static SiteSetting forOption(OptionsSetting options, String name, + List optionNames) { + SiteSetting setting = new SiteSetting(name, Type.OPTIONS, options); + setting.optionNames = optionNames; + return setting; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java index 82563f27..29851612 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java @@ -193,6 +193,9 @@ public class FutabaChanReader implements ChanReader { case "unique_ips": builder.uniqueIps(reader.nextInt()); break; + case "last_modified": + builder.lastModified(reader.nextLong()); + break; case "id": builder.posterId(reader.nextString()); break; @@ -253,6 +256,7 @@ public class FutabaChanReader implements ChanReader { op.replies(builder.replies); op.images(builder.imagesCount); op.uniqueIps(builder.uniqueIps); + op.lastModified(builder.lastModified); queue.setOp(op); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/vichan/VichanApi.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/vichan/VichanApi.java index 29261fb2..382147ba 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/vichan/VichanApi.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/vichan/VichanApi.java @@ -171,6 +171,9 @@ public class VichanApi extends CommonSite.CommonApi { case "unique_ips": builder.uniqueIps(reader.nextInt()); break; + case "last_modified": + builder.lastModified(reader.nextLong()); + break; case "id": builder.posterId(reader.nextString()); break; @@ -228,6 +231,7 @@ public class VichanApi extends CommonSite.CommonApi { op.replies(builder.replies); op.images(builder.imagesCount); op.uniqueIps(builder.uniqueIps); + op.lastModified(builder.lastModified); queue.setOp(op); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java b/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java index 90910abd..a197639d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java @@ -310,6 +310,7 @@ public class ChanThreadLoader implements Response.ErrorListener, Response.Listen realOp.setReplies(fakeOp.replies); realOp.setImagesCount(fakeOp.imagesCount); realOp.setUniqueIps(fakeOp.uniqueIps); + realOp.setLastModified(fakeOp.lastModified); } else { Logger.e(TAG, "Thread has no op!"); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java index 50aa614d..fc4a3ed3 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java @@ -26,7 +26,6 @@ import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.settings.OptionSettingItem; import org.floens.chan.core.settings.OptionsSetting; -import org.floens.chan.core.settings.Setting; import org.floens.chan.core.settings.SettingProvider; import org.floens.chan.core.settings.SharedPreferencesSettingProvider; import org.floens.chan.core.settings.StringSetting; @@ -38,6 +37,7 @@ import org.floens.chan.core.site.SiteBase; import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteIcon; import org.floens.chan.core.site.SiteRequestModifier; +import org.floens.chan.core.site.SiteSetting; import org.floens.chan.core.site.SiteUrlHandler; import org.floens.chan.core.site.common.CommonReplyHttpCall; import org.floens.chan.core.site.common.FutabaChanReader; @@ -478,14 +478,18 @@ public class Chan4 extends SiteBase { public void initializeSettings() { super.initializeSettings(); - captchaType = new OptionsSetting<>(settingsProvider, "preference_captcha_type", + captchaType = new OptionsSetting<>(settingsProvider, + "preference_captcha_type", CaptchaType.class, CaptchaType.V2NOJS); } @Override - public List> settings() { + public List settings() { return Arrays.asList( - captchaType + SiteSetting.forOption( + captchaType, + "Captcha type", + Arrays.asList("Javascript", "Noscript")) ); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostsFilter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostsFilter.java index 3c5bb1cf..be0fafc3 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostsFilter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostsFilter.java @@ -35,33 +35,20 @@ import javax.inject.Inject; import static org.floens.chan.Chan.inject; public class PostsFilter { - public static final Comparator IMAGE_COMPARATOR = new Comparator() { - @Override - public int compare(Post lhs, Post rhs) { - return rhs.getImagesCount() - lhs.getImagesCount(); - } - }; + private static final Comparator IMAGE_COMPARATOR = + (lhs, rhs) -> rhs.getImagesCount() - lhs.getImagesCount(); - public static final Comparator REPLY_COMPARATOR = new Comparator() { - @Override - public int compare(Post lhs, Post rhs) { - return rhs.getReplies() - lhs.getReplies(); - } - }; + private static final Comparator REPLY_COMPARATOR = + (lhs, rhs) -> rhs.getReplies() - lhs.getReplies(); - public static final Comparator NEWEST_COMPARATOR = new Comparator() { - @Override - public int compare(Post lhs, Post rhs) { - return (int) (rhs.time - lhs.time); - } - }; + private static final Comparator NEWEST_COMPARATOR = + (lhs, rhs) -> (int) (rhs.time - lhs.time); - public static final Comparator OLDEST_COMPARATOR = new Comparator() { - @Override - public int compare(Post lhs, Post rhs) { - return (int) (lhs.time - rhs.time); - } - }; + private static final Comparator OLDEST_COMPARATOR = + (lhs, rhs) -> (int) (lhs.time - rhs.time); + + private static final Comparator MODIFIED_COMPARATOR = + (lhs, rhs) -> (int) (rhs.getLastModified() - lhs.getLastModified()); @Inject DatabaseManager databaseManager; @@ -99,6 +86,9 @@ public class PostsFilter { case OLDEST: Collections.sort(posts, OLDEST_COMPARATOR); break; + case MODIFIED: + Collections.sort(posts, MODIFIED_COMPARATOR); + break; } } @@ -149,7 +139,8 @@ public class PostsFilter { REPLY("reply"), IMAGE("image"), NEWEST("newest"), - OLDEST("oldest"); + OLDEST("oldest"), + MODIFIED("modified"); public String name; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ArchiveController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ArchiveController.java index 3ffae763..9c97c786 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ArchiveController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ArchiveController.java @@ -115,6 +115,7 @@ public class ArchiveController extends Controller implements ArchivePresenter.Ca @Override public void onSearchEntered(String entered) { + presenter.onSearchEntered(entered); } @Override 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 7e9e6799..a929ac51 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 @@ -290,6 +290,9 @@ public class BrowseController extends ThreadController implements case OLDEST: nameId = R.string.order_oldest; break; + case MODIFIED: + nameId = R.string.order_modified; + break; } String name = getString(nameId); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java index a7a88e37..ec686253 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java @@ -18,12 +18,13 @@ package org.floens.chan.ui.controller; import android.content.Context; +import android.support.annotation.NonNull; import org.floens.chan.R; import org.floens.chan.core.presenter.SiteSetupPresenter; import org.floens.chan.core.settings.OptionsSetting; -import org.floens.chan.core.settings.Setting; import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.SiteSetting; import org.floens.chan.ui.settings.LinkSettingView; import org.floens.chan.ui.settings.ListSettingView; import org.floens.chan.ui.settings.SettingsController; @@ -98,21 +99,25 @@ public class SiteSetupController extends SettingsController implements SiteSetup } @Override - public void showSettings(List> settings) { + public void showSettings(List settings) { SettingsGroup group = new SettingsGroup("Additional settings"); - for (Setting setting : settings) { - if (setting instanceof OptionsSetting) { - OptionsSetting optionsSetting = (OptionsSetting) setting; + for (SiteSetting setting : settings) { + if (setting.type == SiteSetting.Type.OPTIONS) { + + // Turn the SiteSetting for a list of options into a proper setting with a + // name and a list of options, both given in the SiteSetting. + OptionsSetting optionsSetting = (OptionsSetting) setting.setting; List> items = new ArrayList<>(); - for (Enum anEnum : optionsSetting.getItems()) { - items.add(new ListSettingView.Item<>(anEnum.name(), anEnum)); + Enum[] settingItems = optionsSetting.getItems(); + for (int i = 0; i < settingItems.length; i++) { + String name = setting.optionNames.get(i); + Enum anEnum = settingItems[i]; + items.add(new ListSettingView.Item<>(name, anEnum)); } - String name = optionsSetting.getItems()[0].getDeclaringClass().getSimpleName(); - ListSettingView v = new ListSettingView(this, - optionsSetting, name, items); + ListSettingView v = getListSettingView(setting, optionsSetting, items); group.add(v); } @@ -121,6 +126,17 @@ public class SiteSetupController extends SettingsController implements SiteSetup groups.add(group); } + @SuppressWarnings("unchecked") + @NonNull + private ListSettingView getListSettingView( + SiteSetting setting, + OptionsSetting optionsSetting, + List> items) { + // we know it's an enum + return (ListSettingView) new ListSettingView(this, + optionsSetting, setting.name, items); + } + @Override public void showLogin() { SettingsGroup login = new SettingsGroup(R.string.setup_site_group_login); 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 e8065498..0e4e6668 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 @@ -472,8 +472,8 @@ public class ThreadLayout extends CoordinatorLayout implements public void showNewPostsNotification(boolean show, int more) { if (show) { if (!threadListLayout.scrolledToBottom()) { - String text = getContext().getString(R.string.thread_new_posts, - more, getContext().getResources().getQuantityString(R.plurals.posts, more, more)); + String text = getContext().getResources() + .getQuantityString(R.plurals.thread_new_posts, more, more); newPostsNotification = Snackbar.make(this, text, Snackbar.LENGTH_LONG); newPostsNotification.setAction(R.string.thread_new_posts_goto, new OnClickListener() { diff --git a/Clover/app/src/main/res/layout/controller_save_location.xml b/Clover/app/src/main/res/layout/controller_save_location.xml index a1a0ccd3..bf19677c 100644 --- a/Clover/app/src/main/res/layout/controller_save_location.xml +++ b/Clover/app/src/main/res/layout/controller_save_location.xml @@ -55,7 +55,7 @@ along with this program. If not, see . android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_vertical" - android:text="@string/up" + android:text="@string/setting_folder_navigate_up" android:textSize="18sp" /> diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index f41ab367..4b4cf5d5 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -20,7 +20,6 @@ along with this program. If not, see . Add Close Back - Up OK Exit Delete @@ -117,6 +116,7 @@ Re-enable this permission in the app settings if you permanently disabled it."Image count Newest Oldest + Latest reply Search Found %1$d %2$s for "%3$s" @@ -149,7 +149,10 @@ Re-enable this permission in the app settings if you permanently disabled it."Retry Archived Closed - %1$d new %2$s + + %d new post + %d new posts + View Please select a thread Scroll to top/bottom @@ -296,7 +299,7 @@ Re-enable this permission in the app settings if you permanently disabled it."Error posting: %s Post successful Type the text - > + > [s] Delete your post? @@ -522,6 +525,7 @@ Re-enable this permission in the app settings if you permanently disabled it." Choose + Up diff --git a/README.md b/README.md index 3a0d9487..abb6beb4 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,31 @@ -# Clover - 4chan browser for Android +# Clover - imageboard browser for Android -Clover is a fast Android app for browsing [4chan](https://www.4chan.org/). -Clover adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. Clover is licensed under the GPL and will always be free. +[![Build Status](https://travis-ci.org/Floens/Clover.svg?branch=dev)](https://travis-ci.org/Floens/Clover) +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/clover/localized.svg)](https://crowdin.com/project/clover) -[Join the Slack!](https://join.slack.com/t/uchan/shared_invite/enQtMjkyOTM3NDczNTcxLTNkMzljNDUyNjkzNjEwOTNkZTljZWQ3ZDNmNWUyMTY2YTAwNzBhNmI3YTg1YmNjMDQxZTgzMTM2YzE2YzRhMGI) -[IRC](https://webchat.freenode.net/?url=irc:///#Clover) -[Clover website](http://floens.github.io/Clover/) -[Clover dev builds](https://floens.github.io/Clover/#dev) -[F-Droid](https://floens.github.io/Clover/#fdroid) -[APK releases](https://floens.github.io/Clover/#releases) +[Website](http://floens.github.io/Clover/) +[APK releases](https://floens.github.io/Clover/#releases) | [Development APK releases](https://floens.github.io/Clover/#dev) | [F-Droid](https://floens.github.io/Clover/#fdroid) +[Telegram](https://t.me/cloverapp) | [Slack](https://join.slack.com/t/uchan/shared_invite/enQtMjkyOTM3NDczNTcxLTNkMzljNDUyNjkzNjEwOTNkZTljZWQ3ZDNmNWUyMTY2YTAwNzBhNmI3YTg1YmNjMDQxZTgzMTM2YzE2YzRhMGI) | [IRC](https://webchat.freenode.net/?url=irc:///#Clover) [Donate](https://floens.github.io/Clover/#donate) +Clover is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. Clover is licensed under the GPL and will always be free. ## Issues and features -Issues can be reported at the [Slack](https://join.slack.com/t/uchan/shared_invite/enQtMjkyOTM3NDczNTcxLTNkMzljNDUyNjkzNjEwOTNkZTljZWQ3ZDNmNWUyMTY2YTAwNzBhNmI3YTg1YmNjMDQxZTgzMTM2YzE2YzRhMGI) or here at the [Github Issues page](https://github.com/Floens/Clover/issues). Please search before reporting an issue to avoid duplicates! - -Also take a look at the [Trello board](https://trello.com/b/V6gclKvM/clover) to see if your issue or feature has already been considered. +Issues can be reported at one of the contact places noted above or here at the [Issues page](https://github.com/Floens/Clover/issues). Please search before reporting an issue to avoid duplicates! ## Contributing -Contributing to Clover is appreciated, there's always stuff to do or bugs to fix. I keep the todo list of Clover at the -[Clover Trello board](https://trello.com/b/V6gclKvM/clover). -[Make a new theme](https://github.com/Floens/Clover/wiki/Adding-a-new-theme) - +For first-time contributors, the issues with the label [good first issue](https://github.com/Floens/Clover/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are a great place to start. -## Building Clover 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) + + +## Translations +We use crowdin for crowdsourcing translations of the English strings to other languages. +[Help us with translating at crowdin.com here](https://crowdin.com/project/clover) + ## License -* Clover is [GPLv3](https://github.com/Floens/Clover/blob/master/COPYING.txt) -* [Licenses of the used libraries](https://github.com/Floens/Clover/blob/dev/Clover/app/src/main/assets/html/licenses.html). +Clover is [GPLv3](https://github.com/Floens/Clover/blob/master/COPYING.txt), [licenses of the used libraries](https://github.com/Floens/Clover/blob/dev/Clover/app/src/main/assets/html/licenses.html).