From 3774b06096b3ba28d959b4fe0fb7ddad3797e7f4 Mon Sep 17 00:00:00 2001 From: Apostolof Date: Sat, 7 Jan 2017 16:40:14 +0200 Subject: [PATCH] Board implementation adn other fixes --- .../activities/board/BoardActivity.java | 207 +++++++++++++++++- .../mthmmy/activities/board/BoardAdapter.java | 180 +++++++++++++++ .../mthmmy/activities/main/MainActivity.java | 4 +- .../activities/main/forum/ForumFragment.java | 4 +- .../activities/main/recent/RecentAdapter.java | 3 +- .../activities/profile/ProfileActivity.java | 8 +- .../latestPosts/LatestPostsAdapter.java | 18 +- .../latestPosts/LatestPostsFragment.java | 51 ++--- .../mthmmy/activities/topic/TopicAdapter.java | 1 - .../main/java/gr/thmmy/mthmmy/data/Board.java | 35 ++- .../gr/thmmy/mthmmy/data/PostSummary.java | 30 +++ .../main/java/gr/thmmy/mthmmy/data/Topic.java | 42 ++++ .../gr/thmmy/mthmmy/data/TopicSummary.java | 30 +-- .../mthmmy/utils/FileManager/ThmmyFile.java | 14 +- .../main/res/layout-v21/activity_profile.xml | 2 +- .../layout-v21/activity_topic_post_row.xml | 8 +- app/src/main/res/layout/activity_board.xml | 57 +---- .../res/layout/activity_board_sub_board.xml | 48 ++++ .../main/res/layout/activity_board_topic.xml | 60 +++++ app/src/main/res/layout/activity_profile.xml | 2 +- app/src/main/res/layout/activity_topic.xml | 10 +- .../res/layout/activity_topic_post_row.xml | 8 +- .../main/res/layout/fragment_recent_row.xml | 88 ++++---- app/src/main/res/values/strings.xml | 83 ++++--- 24 files changed, 754 insertions(+), 239 deletions(-) create mode 100644 app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/data/PostSummary.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/data/Topic.java create mode 100644 app/src/main/res/layout/activity_board_sub_board.xml create mode 100644 app/src/main/res/layout/activity_board_topic.xml diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java index a6fe5f3c..bb620809 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java @@ -1,16 +1,39 @@ package gr.thmmy.mthmmy.activities.board; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.widget.ProgressBar; +import android.widget.Toast; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.Objects; + +import javax.net.ssl.SSLHandshakeException; import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.data.Board; +import gr.thmmy.mthmmy.data.Topic; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; -public class BoardActivity extends BaseActivity { +public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMoreListener{ /** * Debug Tag for logging debug output to LogCat */ + @SuppressWarnings("unused") private static final String TAG = "BoardActivity"; /** * The key to use when putting board's url String to {@link BoardActivity}'s Bundle. @@ -22,7 +45,17 @@ public class BoardActivity extends BaseActivity { public static final String BUNDLE_BOARD_TITLE = "BOARD_TITLE"; private MaterialProgressBar progressBar; - private String boardTitle; + private BoardTask boardTask; + private RecyclerView mainContent; + private BoardAdapter boardAdapter; + private final ArrayList parsedSubBoards = new ArrayList<>(); + private final ArrayList parsedTopics = new ArrayList<>(); + private String boardUrl; + private int numberOfPages = -1; + private int pagesLoaded = 0; + private boolean isLoadingMore; + private static final int visibleThreshold = 5; + private int lastVisibleItem, totalItemCount; @Override protected void onCreate(Bundle savedInstanceState) { @@ -30,7 +63,8 @@ public class BoardActivity extends BaseActivity { setContentView(R.layout.activity_board); Bundle extras = getIntent().getExtras(); - boardTitle = extras.getString("BOARD_TITLE"); + final String boardTitle = extras.getString(BUNDLE_BOARD_TITLE); + boardUrl = extras.getString(BUNDLE_BOARD_URL); //Initializes graphics toolbar = (Toolbar) findViewById(R.id.toolbar); @@ -43,6 +77,173 @@ public class BoardActivity extends BaseActivity { createDrawer(); + boardAdapter = new BoardAdapter(getApplicationContext(), parsedSubBoards, parsedTopics); + mainContent = (RecyclerView) findViewById(R.id.board_recycler_view); + mainContent.setAdapter(boardAdapter); + final LinearLayoutManager layoutManager = new LinearLayoutManager(this); + mainContent.setLayoutManager(layoutManager); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mainContent.getContext(), + layoutManager.getOrientation()); + mainContent.addItemDecoration(dividerItemDecoration); + + mainContent.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + totalItemCount = layoutManager.getItemCount(); + lastVisibleItem = layoutManager.findLastVisibleItemPosition(); + + if (!isLoadingMore && totalItemCount <= (lastVisibleItem + visibleThreshold)) { + isLoadingMore = true; + onLoadMore(); + } + } + }); + progressBar = (MaterialProgressBar) findViewById(R.id.progressBar); + + boardTask = new BoardTask(); + boardTask.execute(boardUrl); + } + + @Override + public void onLoadMore() { + if (pagesLoaded < numberOfPages) { + parsedTopics.add(null); + boardAdapter.notifyItemInserted(parsedSubBoards.size() + parsedTopics.size() - 1); + + //Load data + boardTask = new BoardTask(); + boardTask.execute(boardUrl.substring(0, boardUrl.lastIndexOf(".")) + "." + pagesLoaded * 20); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (boardTask != null && boardTask.getStatus() != AsyncTask.Status.RUNNING) + boardTask.cancel(true); + } + + /** + * An {@link AsyncTask} that handles asynchronous fetching of a board page and parsing it's content. + *

BoardTask's {@link AsyncTask#execute execute} method needs a boards's url as String + * parameter!

+ */ + public class BoardTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "BoardTask"; //Separate tag for AsyncTask + + protected void onPreExecute() { + if (!isLoadingMore) progressBar.setVisibility(ProgressBar.VISIBLE); + } + + @Override + protected Boolean doInBackground(String... boardUrl) { + Log.d(TAG, boardUrl[0]); + Request request = new Request.Builder() + .url(boardUrl[0]) + .build(); + try { + Response response = BaseActivity.getClient().newCall(request).execute(); + return parseBoard(Jsoup.parse(response.body().string())); + } catch (SSLHandshakeException e) { + Report.w(TAG, "Certificate problem (please switch to unsafe connection)."); + } catch (Exception e) { + Report.e("TAG", "ERROR", e); + } + return false; + } + + protected void onPostExecute(Boolean result) { + if (!result) { //Parse failed! + Report.d(TAG, "Parse failed!"); + Toast.makeText(getApplicationContext() + , "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show(); + finish(); + } + ++pagesLoaded; + //Parse was successful + progressBar.setVisibility(ProgressBar.INVISIBLE); + boardAdapter = new BoardAdapter(getApplicationContext(), parsedSubBoards, parsedTopics); + mainContent.swapAdapter(boardAdapter, false); + isLoadingMore = false; + } + + private boolean parseBoard(Document boardPage) { + //Removes loading item + if (isLoadingMore) { + parsedTopics.remove(parsedTopics.size() - 1); + } + + //Finds number of pages + if (numberOfPages == -1) { + Elements pages = boardPage.select("table.tborder td.catbg[height=30]").first() + .select("a.navPages"); + numberOfPages = 1; + if (pages != null && !pages.isEmpty()) { + for (Element page : pages) { + if (Integer.parseInt(page.text()) > numberOfPages) + numberOfPages = Integer.parseInt(page.text()); + } + } + } + { //Finds sub boards + Elements subBoardRows = boardPage.select("div.tborder>table>tbody>tr"); + if (subBoardRows != null && !subBoardRows.isEmpty()) { + for (Element subBoardRow : subBoardRows) { + if (!Objects.equals(subBoardRow.className(), "titlebg")) { + String pUrl = "", pTitle = "", pMods = "", pStats = "", pLastPost = ""; + Elements subBoardColumns = subBoardRow.select(">td"); + for (Element subBoardCol : subBoardColumns) { + if (Objects.equals(subBoardCol.className(), "windowbg")) + pStats = subBoardCol.text(); + else if (Objects.equals(subBoardCol.className(), "smalltext")) + pLastPost = subBoardCol.text(); + else { + pUrl = subBoardCol.select("a").first().attr("href"); + pTitle = subBoardCol.select("a").first().text(); + pMods = subBoardCol.select("div.smalltext").first().text(); + } + } + parsedSubBoards.add(new Board(pUrl, pTitle, pMods, pStats, pLastPost)); + } + } + } + } + { //Finds topics + Elements topicRows = boardPage.select("table.bordercolor>tbody>tr"); + if (topicRows != null && !topicRows.isEmpty()) { + for (Element topicRow : topicRows) { + if (!Objects.equals(topicRow.className(), "titlebg")) { + String pTopicUrl, pSubject, pStartedBy, pLastPost, pStats; + boolean pLocked = false, pSticky = false; + Elements topicColumns = topicRow.select(">td"); + if (topicColumns.size() != 7) return false; + { + Element column = topicColumns.get(2); + Element tmp = column.select("span[id^=msg_] a").first(); + pTopicUrl = tmp.attr("href"); + pSubject = tmp.text(); + if (topicColumns.get(1).select("img[id^=stickyicon]").first() != null) + pSticky = true; + if (topicColumns.get(1).select("img[id^=lockicon]").first() != null) + pLocked = true; + } + pStartedBy = topicColumns.get(3).text(); + pStats = "Replies " + topicColumns.get(4).text() + ", Views " + topicColumns.get(5).text(); + pLastPost = topicColumns.last().text(); + parsedTopics.add(new Topic(pTopicUrl, pSubject, pStartedBy, pLastPost, + pStats, pLocked, pSticky)); + } + } + } + } + return true; + } } } diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java new file mode 100644 index 00000000..c7b871ed --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java @@ -0,0 +1,180 @@ +package gr.thmmy.mthmmy.activities.board; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.topic.TopicActivity; +import gr.thmmy.mthmmy.data.Board; +import gr.thmmy.mthmmy.data.Topic; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_TITLE; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_URL; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; + +/** + * {@link RecyclerView.Adapter} that can display a {@link gr.thmmy.mthmmy.data.Board}. + */ +class BoardAdapter extends RecyclerView.Adapter { + private static final String TAG = "BoardAdapter"; + private final int VIEW_TYPE_SUB_BOARD = 0; + private final int VIEW_TYPE_TOPIC = 1; + private final int VIEW_TYPE_LOADING = 2; + + private final Context context; + private ArrayList parsedSubBoards = new ArrayList<>(); + private ArrayList parsedTopics = new ArrayList<>(); + + BoardAdapter(Context context, ArrayList parsedSubBoards, ArrayList parsedTopics) { + this.context = context; + this.parsedSubBoards = parsedSubBoards; + this.parsedTopics = parsedTopics; + } + + interface OnLoadMoreListener { + void onLoadMore(); + } + + @Override + public int getItemViewType(int position) { + if (position < parsedSubBoards.size()) + return VIEW_TYPE_SUB_BOARD; + else if (parsedTopics.get(position - parsedSubBoards.size()) != null) + return VIEW_TYPE_TOPIC; + else return VIEW_TYPE_LOADING; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_SUB_BOARD) { + View view = LayoutInflater.from(parent.getContext()). + inflate(R.layout.activity_board_sub_board, parent, false); + return new SubBoardViewHolder(view); + } else if (viewType == VIEW_TYPE_TOPIC) { + View view = LayoutInflater.from(parent.getContext()). + inflate(R.layout.activity_board_topic, parent, false); + return new TopicViewHolder(view); + } else if (viewType == VIEW_TYPE_LOADING) { + View view = LayoutInflater.from(parent.getContext()). + inflate(R.layout.recycler_loading_item, parent, false); + return new LoadingViewHolder(view); + } + return null; + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { + if (holder instanceof SubBoardViewHolder) { + Board subBoard = parsedSubBoards.get(position); + final SubBoardViewHolder subBoardViewHolder = (SubBoardViewHolder) holder; + + subBoardViewHolder.boardRow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(context, BoardActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_BOARD_URL, parsedSubBoards.get(holder. + getAdapterPosition()).getUrl()); + extras.putString(BUNDLE_BOARD_TITLE, parsedSubBoards.get(holder. + getAdapterPosition()).getTitle()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + subBoardViewHolder.boardTitle.setText(subBoard.getTitle()); + subBoardViewHolder.boardMods.setText(context.getString(R.string.child_board_mods, subBoard.getMods())); + subBoardViewHolder.boardStats.setText(subBoard.getStats()); + subBoardViewHolder.boardLastPost.setText(subBoard.getLastPost()); + } else if (holder instanceof TopicViewHolder) { + Topic topic = parsedTopics.get(position - parsedSubBoards.size()); + final TopicViewHolder topicViewHolder = (TopicViewHolder) holder; + + topicViewHolder.topicRow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(context, TopicActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_TOPIC_URL, parsedTopics.get(holder. + getAdapterPosition()).getUrl()); + extras.putString(BUNDLE_TOPIC_TITLE, parsedTopics.get(holder. + getAdapterPosition()).getSubject()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + topicViewHolder.topicSubject.setText(topic.getSubject()); + String lockedSticky = ""; + if (topic.isLocked()) + lockedSticky += context.getResources().getString(R.string.fa_lock); + if (topic.isSticky()) + lockedSticky += context.getResources().getString(R.string.fa_sticky); + topicViewHolder.topicLockedSticky.setText(lockedSticky); + topicViewHolder.topicStartedBy.setText(context.getString(R.string.topic_started_by, topic.getStarter())); + topicViewHolder.topicStats.setText(topic.getStats()); + topicViewHolder.topicLastPost.setText(context.getString(R.string.topic_last_post, topic.getLastPost())); + } else if (holder instanceof LoadingViewHolder) { + LoadingViewHolder loadingViewHolder = (LoadingViewHolder) holder; + loadingViewHolder.progressBar.setIndeterminate(true); + } + } + + @Override + public int getItemCount() { + if (parsedSubBoards == null && parsedTopics == null) return 0; + else if (parsedSubBoards == null) return parsedTopics.size(); + else if (parsedTopics == null) return parsedSubBoards.size(); + else return parsedSubBoards.size() + parsedTopics.size(); + } + + private static class SubBoardViewHolder extends RecyclerView.ViewHolder { + final LinearLayout boardRow; + final TextView boardTitle, boardMods, boardStats, boardLastPost; + + SubBoardViewHolder(View itemView) { + super(itemView); + boardRow = (LinearLayout) itemView.findViewById(R.id.child_board_row); + boardTitle = (TextView) itemView.findViewById(R.id.child_board_title); + boardMods = (TextView) itemView.findViewById(R.id.child_board_mods); + boardStats = (TextView) itemView.findViewById(R.id.child_board_stats); + boardLastPost = (TextView) itemView.findViewById(R.id.child_board_last_post); + } + } + + private static class TopicViewHolder extends RecyclerView.ViewHolder { + final LinearLayout topicRow; + final TextView topicSubject, topicLockedSticky, topicStartedBy, topicStats, topicLastPost; + + TopicViewHolder(View itemView) { + super(itemView); + topicRow = (LinearLayout) itemView.findViewById(R.id.topic_row_linear); + topicSubject = (TextView) itemView.findViewById(R.id.topic_subject); + topicLockedSticky = (TextView) itemView.findViewById(R.id.topic_locked_sticky); + topicStartedBy = (TextView) itemView.findViewById(R.id.topic_started_by); + topicStats = (TextView) itemView.findViewById(R.id.topic_stats); + topicLastPost = (TextView) itemView.findViewById(R.id.topic_last_post); + } + } + + private static class LoadingViewHolder extends RecyclerView.ViewHolder { + final MaterialProgressBar progressBar; + + LoadingViewHolder(View itemView) { + super(itemView); + progressBar = (MaterialProgressBar) itemView.findViewById(R.id.recycler_progress_bar); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java index 0bb5df1d..95e1268e 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java @@ -89,14 +89,14 @@ public class MainActivity extends BaseActivity implements RecentFragment.RecentF public void onRecentFragmentInteraction(TopicSummary topicSummary) { Intent i = new Intent(MainActivity.this, TopicActivity.class); i.putExtra(BUNDLE_TOPIC_URL, topicSummary.getTopicUrl()); - i.putExtra(BUNDLE_TOPIC_TITLE, topicSummary.getTitle()); + i.putExtra(BUNDLE_TOPIC_TITLE, topicSummary.getSubject()); startActivity(i); } @Override public void onForumFragmentInteraction(Board board) { Intent i = new Intent(MainActivity.this, BoardActivity.class); - i.putExtra(BUNDLE_BOARD_URL, board.getBoardURL()); + i.putExtra(BUNDLE_BOARD_URL, board.getUrl()); i.putExtra(BUNDLE_BOARD_TITLE, board.getTitle()); startActivity(i); } diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java index 9ad75ed4..28cf90c5 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java @@ -156,7 +156,7 @@ public class ForumFragment extends BaseFragment private HttpUrl forumUrl = SessionManager.forumUrl; //may change upon collapse/expand private Document document; - private List fetchedCategories; + private final List fetchedCategories; ForumTask() { fetchedCategories = new ArrayList<>(); @@ -214,7 +214,7 @@ public class ForumFragment extends BaseFragment category.setExpanded(true); Elements boardsElements = categoryBlock.select("b [name]"); for(Element boardElement: boardsElements) { - Board board = new Board(boardElement.text(), boardElement.attr("href")); + Board board = new Board(boardElement.attr("href"), boardElement.text(), null, null, null); category.getBoards().add(board); } } diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java index 48b2e9a2..8e039862 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java @@ -41,8 +41,7 @@ class RecentAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(final ViewHolder holder, final int position) { - - holder.mTitleView.setText(recentList.get(position).getTitle()); + holder.mTitleView.setText(recentList.get(position).getSubject()); holder.mDateTimeView.setText(recentList.get(position).getDateTimeModified()); holder.mUserView.setText(context.getString(R.string.byUser, recentList.get(position).getLastUser())); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java index a740af10..1659665b 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java @@ -38,7 +38,7 @@ import gr.thmmy.mthmmy.activities.profile.latestPosts.LatestPostsFragment; import gr.thmmy.mthmmy.activities.profile.stats.StatsFragment; import gr.thmmy.mthmmy.activities.profile.summary.SummaryFragment; import gr.thmmy.mthmmy.activities.topic.TopicActivity; -import gr.thmmy.mthmmy.data.TopicSummary; +import gr.thmmy.mthmmy.data.PostSummary; import gr.thmmy.mthmmy.utils.CircleTransform; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import mthmmy.utils.Report; @@ -169,10 +169,10 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment } @Override - public void onLatestPostsFragmentInteraction(TopicSummary topicSummary) { + public void onLatestPostsFragmentInteraction(PostSummary postSummary) { Intent i = new Intent(ProfileActivity.this, TopicActivity.class); - i.putExtra(BUNDLE_TOPIC_URL, topicSummary.getTopicUrl()); - i.putExtra(BUNDLE_TOPIC_TITLE, topicSummary.getTitle().substring(topicSummary.getTitle(). + i.putExtra(BUNDLE_TOPIC_URL, postSummary.getTopicUrl()); + i.putExtra(BUNDLE_TOPIC_TITLE, postSummary.getTitle().substring(postSummary.getTitle(). lastIndexOf("/ ") + 2)); startActivity(i); } diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java index 0f0f05c5..4e861de9 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java @@ -8,25 +8,33 @@ import android.webkit.WebView; import android.widget.RelativeLayout; import android.widget.TextView; +import java.util.ArrayList; + import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.activities.base.BaseFragment; +import gr.thmmy.mthmmy.data.PostSummary; import gr.thmmy.mthmmy.data.TopicSummary; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; -import static gr.thmmy.mthmmy.activities.profile.latestPosts.LatestPostsFragment.parsedTopicSummaries; - /** * {@link RecyclerView.Adapter} that can display a {@link TopicSummary} and makes a call to the * specified {@link LatestPostsFragment.LatestPostsFragmentInteractionListener}. */ class LatestPostsAdapter extends RecyclerView.Adapter { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") private static final String TAG = "LatestPostsAdapter"; private final int VIEW_TYPE_ITEM = 0; private final int VIEW_TYPE_LOADING = 1; final private LatestPostsFragment.LatestPostsFragmentInteractionListener interactionListener; + private final ArrayList parsedTopicSummaries; - LatestPostsAdapter(BaseFragment.FragmentInteractionListener interactionListener){ + LatestPostsAdapter(BaseFragment.FragmentInteractionListener interactionListener, + ArrayList parsedTopicSummaries) { this.interactionListener = (LatestPostsFragment.LatestPostsFragmentInteractionListener) interactionListener; + this.parsedTopicSummaries = parsedTopicSummaries; } interface OnLoadMoreListener { @@ -55,11 +63,11 @@ class LatestPostsAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { if (holder instanceof LatestPostViewHolder) { - TopicSummary topic = parsedTopicSummaries.get(position); + PostSummary topic = parsedTopicSummaries.get(position); final LatestPostViewHolder latestPostViewHolder = (LatestPostViewHolder) holder; latestPostViewHolder.postTitle.setText(topic.getTitle()); - latestPostViewHolder.postDate.setText(topic.getDateTimeModified()); + latestPostViewHolder.postDate.setText(topic.getDateTime()); latestPostViewHolder.post.loadDataWithBaseURL("file:///android_asset/" , topic.getPost(), "text/html", "UTF-8", null); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java index 5b811550..6daf5f11 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java @@ -23,7 +23,7 @@ import javax.net.ssl.SSLHandshakeException; import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.activities.base.BaseActivity; import gr.thmmy.mthmmy.activities.base.BaseFragment; -import gr.thmmy.mthmmy.data.TopicSummary; +import gr.thmmy.mthmmy.data.PostSummary; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import mthmmy.utils.Report; import okhttp3.Request; @@ -32,7 +32,7 @@ import okhttp3.Response; /** * Use the {@link LatestPostsFragment#newInstance} factory method to create an instance of this fragment. */ -public class LatestPostsFragment extends BaseFragment { +public class LatestPostsFragment extends BaseFragment implements LatestPostsAdapter.OnLoadMoreListener{ /** * Debug Tag for logging debug output to LogCat */ @@ -43,10 +43,11 @@ public class LatestPostsFragment extends BaseFragment { */ private static final String PROFILE_URL = "PROFILE_URL"; /** - * {@link ArrayList} of {@link TopicSummary} objects used to hold profile's latest posts. Data + * {@link ArrayList} of {@link PostSummary} objects used to hold profile's latest posts. Data * are added in {@link LatestPostsTask}. */ - static ArrayList parsedTopicSummaries; + private ArrayList parsedTopicSummaries; + private RecyclerView mainContent; private LatestPostsAdapter latestPostsAdapter; private int numberOfPages = -1; private int pagesLoaded = 0; @@ -87,8 +88,8 @@ public class LatestPostsFragment extends BaseFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View rootView = inflater.inflate(R.layout.profile_fragment_latest_posts, container, false); - latestPostsAdapter = new LatestPostsAdapter(fragmentInteractionListener); - RecyclerView mainContent = (RecyclerView) rootView.findViewById(R.id.profile_latest_posts_recycler); + latestPostsAdapter = new LatestPostsAdapter(fragmentInteractionListener, parsedTopicSummaries); + mainContent = (RecyclerView) rootView.findViewById(R.id.profile_latest_posts_recycler); mainContent.setAdapter(latestPostsAdapter); final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); mainContent.setLayoutManager(layoutManager); @@ -96,21 +97,6 @@ public class LatestPostsFragment extends BaseFragment { layoutManager.getOrientation()); mainContent.addItemDecoration(dividerItemDecoration); - final LatestPostsAdapter.OnLoadMoreListener onLoadMoreListener = new LatestPostsAdapter.OnLoadMoreListener() { - @Override - public void onLoadMore() { - if (pagesLoaded < numberOfPages) { - parsedTopicSummaries.add(null); - latestPostsAdapter.notifyItemInserted(parsedTopicSummaries.size() - 1); - - //Load data - profileLatestPostsTask = new LatestPostsTask(); - profileLatestPostsTask.execute(profileUrl + ";sa=showPosts;start=" + pagesLoaded * 15); - ++pagesLoaded; - } - } - }; - //latestPostsAdapter.setOnLoadMoreListener(); mainContent.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -121,7 +107,7 @@ public class LatestPostsFragment extends BaseFragment { if (!isLoadingMore && totalItemCount <= (lastVisibleItem + visibleThreshold)) { isLoadingMore = true; - onLoadMoreListener.onLoadMore(); + onLoadMore(); } } }); @@ -129,6 +115,19 @@ public class LatestPostsFragment extends BaseFragment { return rootView; } + @Override + public void onLoadMore() { + if (pagesLoaded < numberOfPages) { + parsedTopicSummaries.add(null); + latestPostsAdapter.notifyItemInserted(parsedTopicSummaries.size() - 1); + + //Load data + profileLatestPostsTask = new LatestPostsTask(); + profileLatestPostsTask.execute(profileUrl + ";sa=showPosts;start=" + pagesLoaded * 15); + ++pagesLoaded; + } + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -148,7 +147,7 @@ public class LatestPostsFragment extends BaseFragment { } public interface LatestPostsFragmentInteractionListener extends FragmentInteractionListener { - void onLatestPostsFragmentInteraction(TopicSummary topicSummary); + void onLatestPostsFragmentInteraction(PostSummary postSummary); } /** @@ -193,7 +192,9 @@ public class LatestPostsFragment extends BaseFragment { } //Parse was successful progressBar.setVisibility(ProgressBar.INVISIBLE); - latestPostsAdapter.notifyDataSetChanged(); + latestPostsAdapter = new LatestPostsAdapter(fragmentInteractionListener, parsedTopicSummaries); + mainContent.swapAdapter(latestPostsAdapter, false); + //latestPostsAdapter.notifyDataSetChanged(); isLoadingMore = false; } @@ -259,7 +260,7 @@ public class LatestPostsFragment extends BaseFragment { //style.css pPost = ("" + pPost); - parsedTopicSummaries.add(new TopicSummary(pTopicUrl, pTopicTitle, "", pDateTime, pPost)); + parsedTopicSummaries.add(new PostSummary(pTopicUrl, pTopicTitle, pDateTime, pPost)); } } return true; diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java index cb4616f4..bebfb695 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java @@ -319,7 +319,6 @@ class TopicAdapter extends RecyclerView.Adapter { public void onClick(View v) { //Clicking an expanded header starts profile activity if (viewProperties.get(holder.getAdapterPosition())[isUserExtraInfoVisibile]) { - Intent intent = new Intent(context, ProfileActivity.class); Bundle extras = new Bundle(); extras.putString(BUNDLE_PROFILE_URL, currentPost.getProfileURL()); diff --git a/app/src/main/java/gr/thmmy/mthmmy/data/Board.java b/app/src/main/java/gr/thmmy/mthmmy/data/Board.java index 35bf9986..591187a4 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/data/Board.java +++ b/app/src/main/java/gr/thmmy/mthmmy/data/Board.java @@ -1,34 +1,33 @@ package gr.thmmy.mthmmy.data; -import java.util.ArrayList; - public class Board { - private final String title; - private final String boardURL; - - private ArrayList subBoards; - private ArrayList topicSummaries; + private final String url, title, mods, stats, lastPost; - public Board(String title, String boardURL) { + public Board(String url, String title, String mods, String stats, String lastPost) { + this.url = url; this.title = title; - this.boardURL = boardURL; - subBoards = new ArrayList<>(); - topicSummaries = new ArrayList<>(); + this.mods = mods; + this.stats = stats; + this.lastPost = lastPost; + } + + public String getUrl() { + return url; } public String getTitle() { return title; } - public String getBoardURL() { - return boardURL; + public String getMods() { + return mods; } - public ArrayList getSubBoards() { - return subBoards; + public String getStats() { + return stats; } - public ArrayList getTopicSummaries() { - return topicSummaries; + public String getLastPost() { + return lastPost; } -} +} \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/data/PostSummary.java b/app/src/main/java/gr/thmmy/mthmmy/data/PostSummary.java new file mode 100644 index 00000000..1259eb74 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/data/PostSummary.java @@ -0,0 +1,30 @@ +package gr.thmmy.mthmmy.data; + +public class PostSummary { + private final String topicUrl; + private final String title; + private final String dateTime; + private final String post; + + public PostSummary(String topicUrl, String title, String dateTime, + String post) { + this.topicUrl = topicUrl; + this.title = title; + this.dateTime = dateTime; + this.post = post; + } + + public String getTopicUrl() { + return topicUrl; + } + + public String getTitle() { + return title; + } + + public String getDateTime() { + return dateTime; + } + + public String getPost(){ return post;} +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/data/Topic.java b/app/src/main/java/gr/thmmy/mthmmy/data/Topic.java new file mode 100644 index 00000000..0041e422 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/data/Topic.java @@ -0,0 +1,42 @@ +package gr.thmmy.mthmmy.data; + +public class Topic extends TopicSummary { + private final String stats; + private final boolean locked, sticky; + + public Topic(String topicUrl, String subject, String starter, String lastPost, + String stats, boolean locked, boolean sticky) { + super(topicUrl, subject, starter, lastPost); + this.stats = stats; + this.locked = locked; + this.sticky = sticky; + } + + public String getSubject() { + return subject; + } + + public String getStarter() { + return lastUser; + } + + public boolean isLocked() { + return locked; + } + + public boolean isSticky() { + return sticky; + } + + public String getUrl() { + return topicUrl; + } + + public String getLastPost() { + return dateTimeModified; + } + + public String getStats() { + return stats; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/data/TopicSummary.java b/app/src/main/java/gr/thmmy/mthmmy/data/TopicSummary.java index 41ea18b3..486d7ac2 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/data/TopicSummary.java +++ b/app/src/main/java/gr/thmmy/mthmmy/data/TopicSummary.java @@ -1,36 +1,24 @@ package gr.thmmy.mthmmy.data; public class TopicSummary { - private final String topicUrl; - private final String title; - private final String lastUser; - private final String dateTimeModified; - private final String post; + final String topicUrl; + final String subject; + final String lastUser; + final String dateTimeModified; - - public TopicSummary(String topicUrl, String title, String lastUser, String dateTimeModified) { + public TopicSummary(String topicUrl, String subject, String lastUser, String dateTimeModified) { this.topicUrl = topicUrl; - this.title = title; + this.subject = subject; this.lastUser = lastUser; this.dateTimeModified = dateTimeModified; - this.post = ""; - } - - public TopicSummary(String topicUrl, String title, String username, String dateTimeModified, - String post) { - this.topicUrl = topicUrl; - this.title = title; - this.lastUser = username; - this.dateTimeModified = dateTimeModified; - this.post = post; } public String getTopicUrl() { return topicUrl; } - public String getTitle() { - return title; + public String getSubject() { + return subject; } public String getLastUser() { @@ -40,6 +28,4 @@ public class TopicSummary { public String getDateTimeModified() { return dateTimeModified; } - - public String getPost(){ return post;} } diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java b/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java index 2b9e34d8..abe8ce9c 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java @@ -21,17 +21,13 @@ import static gr.thmmy.mthmmy.activities.base.BaseActivity.getClient; *

Class has one constructor:

  • {@link #ThmmyFile(URL, String, String)}
* and the methods:
  • getters
  • *
  • {@link #download()}
  • - *
  • {@link #download(String)}

+ *
  • {@link #download()}
  • */ public class ThmmyFile { /** * Debug Tag for logging debug output to LogCat */ private static final String TAG = "ThmmyFile"; - /** - * Folder name used when downloading files without a package name. - */ - private static final String NO_PACKAGE_FOLDER_NAME = "Other"; private final URL fileUrl; private final String filename, fileInfo; private String extension, filePath; @@ -39,7 +35,7 @@ public class ThmmyFile { /** * This constructor only creates a ThmmyFile object and does not download the file. To download - * the file use {@link #download(String)} or {@link #download()}! + * the file use {@link #download()} or {@link #download()}! * * @param fileUrl {@link URL} object with file's url * @param filename {@link String} with desired file name @@ -67,7 +63,7 @@ public class ThmmyFile { } /** - * This is null until {@link #download(String)} or {@link #download()} is called and has succeeded. + * This is null until {@link #download()} or {@link #download()} is called and has succeeded. * * @return String with file's extension or null */ @@ -77,7 +73,7 @@ public class ThmmyFile { } /** - * This is null until {@link #download(String)} or {@link #download()} is called and has succeeded. + * This is null until {@link #download()} or {@link #download()} is called and has succeeded. * * @return String with file's path or null */ @@ -87,7 +83,7 @@ public class ThmmyFile { } /** - * This is null until {@link #download(String)} or {@link #download()} is called and has succeeded. + * This is null until {@link #download()} or {@link #download()} is called and has succeeded. * * @return {@link File} or null */ diff --git a/app/src/main/res/layout-v21/activity_profile.xml b/app/src/main/res/layout-v21/activity_profile.xml index adb8ad6b..288ac7ba 100644 --- a/app/src/main/res/layout-v21/activity_profile.xml +++ b/app/src/main/res/layout-v21/activity_profile.xml @@ -39,7 +39,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:adjustViewBounds="true" - android:contentDescription="@string/thumbnail" + android:contentDescription="@string/post_thumbnail" android:fitsSystemWindows="true" android:src="@drawable/ic_default_user_thumbnail" android:transitionName="user_thumbnail" diff --git a/app/src/main/res/layout-v21/activity_topic_post_row.xml b/app/src/main/res/layout-v21/activity_topic_post_row.xml index 36d7638d..8b745727 100644 --- a/app/src/main/res/layout-v21/activity_topic_post_row.xml +++ b/app/src/main/res/layout-v21/activity_topic_post_row.xml @@ -89,7 +89,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:adjustViewBounds="true" - android:contentDescription="@string/thumbnail" + android:contentDescription="@string/post_thumbnail" android:maxHeight="@dimen/thumbnail_size" android:maxWidth="@dimen/thumbnail_size" android:src="@drawable/ic_default_user_thumbnail" @@ -104,7 +104,7 @@ android:layout_toEndOf="@+id/thumbnail_holder" android:ellipsize="end" android:maxLines="1" - android:text="@string/username" + android:text="@string/post_author" android:textColor="@color/primary_text" android:textStyle="bold"/> @@ -116,7 +116,7 @@ android:layout_toEndOf="@+id/thumbnail_holder" android:ellipsize="end" android:maxLines="1" - android:text="@string/subject" + android:text="@string/post_subject" /> @@ -128,7 +128,7 @@ android:layout_marginTop="9dp" android:background="@color/card_background" android:clickable="true" - android:contentDescription="@string/quote" + android:contentDescription="@string/post_quote_button" android:src="@drawable/ic_format_quote_unchecked"/> diff --git a/app/src/main/res/layout/activity_board.xml b/app/src/main/res/layout/activity_board.xml index cfc80bdd..4d7a758a 100644 --- a/app/src/main/res/layout/activity_board.xml +++ b/app/src/main/res/layout/activity_board.xml @@ -36,60 +36,6 @@ tools:context="gr.thmmy.mthmmy.activities.topic.TopicActivity"> - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_board_sub_board.xml b/app/src/main/res/layout/activity_board_sub_board.xml new file mode 100644 index 00000000..fda3490c --- /dev/null +++ b/app/src/main/res/layout/activity_board_sub_board.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_board_topic.xml b/app/src/main/res/layout/activity_board_topic.xml new file mode 100644 index 00000000..d02ee6fb --- /dev/null +++ b/app/src/main/res/layout/activity_board_topic.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml index 1f7115b3..51b3db32 100644 --- a/app/src/main/res/layout/activity_profile.xml +++ b/app/src/main/res/layout/activity_profile.xml @@ -39,7 +39,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:adjustViewBounds="true" - android:contentDescription="@string/thumbnail" + android:contentDescription="@string/post_thumbnail" android:fitsSystemWindows="true" android:src="@drawable/ic_default_user_thumbnail" app:layout_collapseMode="parallax"/> diff --git a/app/src/main/res/layout/activity_topic.xml b/app/src/main/res/layout/activity_topic.xml index 28872028..b3b2eaca 100644 --- a/app/src/main/res/layout/activity_topic.xml +++ b/app/src/main/res/layout/activity_topic.xml @@ -50,7 +50,7 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_weight="0.8" - android:contentDescription="@string/text_first" + android:contentDescription="@string/button_first" app:srcCompat="@drawable/page_first"/> @@ -78,7 +78,7 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_weight="0.8" - android:contentDescription="@string/text_next" + android:contentDescription="@string/button_next" app:srcCompat="@drawable/page_next"/> diff --git a/app/src/main/res/layout/activity_topic_post_row.xml b/app/src/main/res/layout/activity_topic_post_row.xml index db0179e2..6c56547e 100644 --- a/app/src/main/res/layout/activity_topic_post_row.xml +++ b/app/src/main/res/layout/activity_topic_post_row.xml @@ -88,7 +88,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:adjustViewBounds="true" - android:contentDescription="@string/thumbnail" + android:contentDescription="@string/post_thumbnail" android:maxHeight="@dimen/thumbnail_size" android:maxWidth="@dimen/thumbnail_size" android:src="@drawable/ic_default_user_thumbnail"/> @@ -102,7 +102,7 @@ android:layout_toEndOf="@+id/thumbnail_holder" android:ellipsize="end" android:maxLines="1" - android:text="@string/username" + android:text="@string/post_author" android:textColor="@color/primary_text" android:textStyle="bold"/> @@ -114,7 +114,7 @@ android:layout_toEndOf="@+id/thumbnail_holder" android:ellipsize="end" android:maxLines="1" - android:text="@string/subject" + android:text="@string/post_subject" /> @@ -126,7 +126,7 @@ android:layout_marginTop="9dp" android:background="@color/card_background" android:clickable="true" - android:contentDescription="@string/quote" + android:contentDescription="@string/post_quote_button" android:src="@drawable/ic_format_quote_unchecked"/> diff --git a/app/src/main/res/layout/fragment_recent_row.xml b/app/src/main/res/layout/fragment_recent_row.xml index 930b3dd4..8f256f2e 100644 --- a/app/src/main/res/layout/fragment_recent_row.xml +++ b/app/src/main/res/layout/fragment_recent_row.xml @@ -1,54 +1,54 @@ - + android:layout_height="wrap_content" + android:orientation="vertical"> - + + - - + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:textAppearance="?attr/textAppearanceListItem" + android:textColor="@color/primary_text"/> - + - + - - + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fae91cda..2963f672 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,32 +1,68 @@ mTHMMY + + Login + Authenticating… + Logout + Home + + thmmy.gr Username Password LOGIN Don\'t have an account? Continue as guest! + + by %1$s + + + Child Board + Moderators: %1$s + Stats + Last post + Subject + Locked/Sticky + Started By: %1$s + Stats + Last post on: %1$s + + + Post author + Post subject + Post + Thumbnail + Quote button + #%1$d + first + previous + Page + next + last + + + General Statistics + Statistics + Posting Activity + Most Popular Boards By Posts + Most Popular Boards By Activity + + About v%1$s Logo You should watch a funny pic! - Login - Authenticating… - Logout - Username should be here… - Subject should be here… - Post should be here… - Thumbnail should be here… - Quote button should be here… - first - previous - Page - next - last - Home + + Libraries + mTHMMY uses the following open-source libraries: + Apache v2.0 License libraries + The MIT License libraries + Contact + Do not hesitate to contact us for any matter either by email at thmmynolife@gmail.com, or by joining our discord server at https://discord.gg/CVt3yrn. + @@ -37,21 +73,6 @@ - - - #%1$d - - by %1$s - - General Statistics - Statistics - Posting Activity - Most Popular Boards By Posts - Most Popular Boards By Activity - Libraries - mTHMMY uses the following open-source libraries: - Apache v2.0 License libraries - The MIT License libraries - Contact - Do not hesitate to contact us for any matter either by email at thmmynolife@gmail.com, or by joining our discord server at https://discord.gg/CVt3yrn. + +