extends LifecyclePresenter {
+
+ private V view;
+
+ public Presenter(V view) {
+ this.view = view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ this.view = null;
+ }
+
+ protected V getView() {
+ return view;
+ }
+
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterActivity.java
new file mode 100644
index 0000000..d235018
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterActivity.java
@@ -0,0 +1,62 @@
+package com.moiseum.wolnelektury.base.mvp;
+
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.base.AbstractActivity;
+
+/**
+ * Created by Piotr Ostrowski on 13.06.2018.
+ */
+public abstract class PresenterActivity extends AbstractActivity {
+
+ private P presenter;
+
+ protected abstract P createPresenter();
+
+ protected P getPresenter() {
+ return presenter;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ presenter = createPresenter();
+ presenter.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ presenter.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ presenter.onStop();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ presenter.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ presenter.onPause();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ presenter.onDestroy();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ presenter.onSaveInstanceState(outState);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterFragment.java
new file mode 100644
index 0000000..b7a0dce
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterFragment.java
@@ -0,0 +1,80 @@
+package com.moiseum.wolnelektury.base.mvp;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.view.View;
+
+import com.moiseum.wolnelektury.base.AbstractFragment;
+
+
+/**
+ * Fragment that creates {@link LifecyclePresenter} and in its lifecycle methods calls corresponding methods of
+ * presenter.
+ *
+ * @param
type of presenter for this fragment.
+ */
+public abstract class PresenterFragment
extends AbstractFragment {
+
+ private P presenter;
+
+ protected abstract P createPresenter();
+
+ protected P getPresenter() {
+ return presenter;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ presenter = createPresenter();
+ presenter.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ presenter.onViewCreated(savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ presenter.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ presenter.onStop();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ presenter.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ presenter.onPause();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ presenter.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ presenter.onDestroy();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ presenter.onSaveInstanceState(outState);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/CheckableRelativeLayout.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/CheckableRelativeLayout.java
new file mode 100644
index 0000000..5a19829
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/CheckableRelativeLayout.java
@@ -0,0 +1,40 @@
+package com.moiseum.wolnelektury.components;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.RelativeLayout;
+
+/**
+ * Relative layout for handling checking/selecting action.
+ */
+public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
+
+ public CheckableRelativeLayout(Context context) {
+ super(context);
+ }
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ super.setSelected(checked);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return super.isSelected();
+ }
+
+ @Override
+ public void toggle() {
+ super.setSelected(!isSelected());
+ }
+}
+
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/ProgressRecyclerView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ProgressRecyclerView.java
new file mode 100644
index 0000000..cccb42a
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ProgressRecyclerView.java
@@ -0,0 +1,141 @@
+package com.moiseum.wolnelektury.components;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter;
+
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+
+/**
+ * @author golonkos
+ */
+
+public class ProgressRecyclerView extends FrameLayout {
+
+ public interface ProgressRecycleViewRetryListener {
+ void onRetryClicked();
+ }
+
+ @BindView(R.id.rvList)
+ RecyclerView rvList;
+ @BindView(R.id.tvEmpty)
+ TextView tvEmpty;
+ @BindView(R.id.pbLoading)
+ ProgressBar pbLoading;
+ @BindView(R.id.ibRetry)
+ ImageButton ibRetry;
+
+ private RecyclerAdapter adapter;
+ private ProgressRecycleViewRetryListener listener;
+
+ public ProgressRecyclerView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public ProgressRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ProgressRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs);
+ }
+
+ private void init(AttributeSet attrs) {
+ View view = LayoutInflater.from(getContext()).inflate(R.layout.progress_recyclerview, this, true);
+ ButterKnife.bind(this, view);
+
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ProgressRecyclerView);
+ try {
+ if (a.hasValue(R.styleable.ProgressRecyclerView_emptyText)) {
+ tvEmpty.setText(a.getString(R.styleable.ProgressRecyclerView_emptyText));
+ }
+ } finally {
+ a.recycle();
+ }
+ }
+
+ public void setAdapter(RecyclerAdapter adapter) {
+ this.adapter = adapter;
+ rvList.setAdapter(adapter);
+ }
+
+ public void setItems(List items) {
+ if (adapter == null) {
+ throw new UnsupportedOperationException("Adapter not set");
+ }
+ adapter.setItems(items);
+ tvEmpty.setVisibility(items.isEmpty() ? VISIBLE : GONE);
+ }
+
+ public void addItems(List items) {
+ if (adapter == null) {
+ throw new UnsupportedOperationException("Adapter not set");
+ }
+ if (items.size() > 0) {
+ adapter.addItems(items);
+ }
+ }
+
+ public void setProgressVisible(boolean visible) {
+ pbLoading.setVisibility(visible ? VISIBLE : GONE);
+ if (visible) {
+ tvEmpty.setVisibility(GONE);
+ }
+ }
+
+ public void showRetryButton(ProgressRecycleViewRetryListener listener) {
+ this.listener = listener;
+ tvEmpty.setVisibility(GONE);
+ ibRetry.setVisibility(VISIBLE);
+ }
+
+ public void setEmptyText(@StringRes int stringResId) {
+ tvEmpty.setText(stringResId);
+ }
+
+ public void setLayoutManager(RecyclerView.LayoutManager layoutManager) {
+ rvList.setLayoutManager(layoutManager);
+ }
+
+ public void setHasFixedSize(boolean fixedSize) {
+ rvList.setHasFixedSize(fixedSize);
+ }
+
+ public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
+ rvList.addOnScrollListener(listener);
+ }
+
+ public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
+ rvList.removeOnScrollListener(listener);
+ }
+
+ @OnClick(R.id.ibRetry)
+ public void retryButtonClick() {
+ if (listener != null) {
+ listener.onRetryClicked();
+ }
+ ibRetry.setVisibility(GONE);
+ }
+
+ public void updateEmptyViewVisibility() {
+ tvEmpty.setVisibility(ibRetry.getVisibility() == GONE && adapter.getItems().isEmpty() ? VISIBLE : GONE);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/ZoomableViewPager.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ZoomableViewPager.java
new file mode 100644
index 0000000..1cfdb52
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ZoomableViewPager.java
@@ -0,0 +1,113 @@
+package com.moiseum.wolnelektury.components;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+
+import it.sephiroth.android.library.imagezoom.ImageViewTouch;
+
+/**
+ * Created by Piotr Ostrowski on 29.06.2017.
+ */
+
+public class ZoomableViewPager extends ViewPager {
+
+ public interface OnItemClickListener {
+ void onItemClick(int position);
+ }
+
+ private class TapGestureListener extends GestureDetector.SimpleOnGestureListener {
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (mOnItemClickListener != null) {
+ mOnItemClickListener.onItemClick(getCurrentItem());
+ }
+ return true;
+ }
+ }
+
+ private OnItemClickListener mOnItemClickListener;
+ private GestureDetector tapGestureDetector;
+
+ public ZoomableViewPager(Context context) {
+ super(context);
+ setup();
+ }
+
+ public ZoomableViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setup();
+ }
+
+ @Override
+ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+ if (v instanceof ImageViewTouch) {
+ ImageViewTouch imageViewTouch = (ImageViewTouch) v;
+ return imageViewTouchCanScroll(imageViewTouch, dx);
+ } else {
+ return super.canScroll(v, checkV, dx, x, y);
+ }
+ }
+
+ /**
+ * Determines whether the ImageViewTouch can be scrolled.
+ *
+ * @param direction - positive direction value means scroll from right to left,
+ * negative value means scroll from left to right
+ * @return true if there is some more place to scroll, false - otherwise.
+ */
+ private boolean imageViewTouchCanScroll(ImageViewTouch imageViewTouch, int direction) {
+ int widthScreen = getWidthScreen();
+
+ RectF bitmapRect = imageViewTouch.getBitmapRect();
+ Rect imageViewRect = new Rect();
+ getGlobalVisibleRect(imageViewRect);
+
+ int widthBitmapViewTouch = (int) bitmapRect.width();
+
+ if (widthBitmapViewTouch < widthScreen) {
+ return false;
+ }
+
+ if (direction < 0) {
+ return Math.abs(bitmapRect.right - imageViewRect.right) > 1.0f;
+ } else {
+ return Math.abs(bitmapRect.left - imageViewRect.left) > 1.0f;
+ }
+
+ }
+
+ private int getWidthScreen() {
+ WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+
+ Point size = new Point();
+ display.getSize(size);
+ return size.x;
+ }
+
+ private void setup() {
+ tapGestureDetector = new GestureDetector(getContext(), new TapGestureListener());
+ }
+
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ tapGestureDetector.onTouchEvent(ev);
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
+ mOnItemClickListener = onItemClickListener;
+ }
+
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/EndlessRecyclerOnScrollListener.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/EndlessRecyclerOnScrollListener.java
new file mode 100644
index 0000000..073dff1
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/EndlessRecyclerOnScrollListener.java
@@ -0,0 +1,50 @@
+package com.moiseum.wolnelektury.components.recycler;
+
+/**
+ * Created by Piotr Ostrowski on 28.11.2017.
+ */
+
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+
+public abstract class EndlessRecyclerOnScrollListener extends RecyclerView.OnScrollListener {
+
+ public static String TAG = EndlessRecyclerOnScrollListener.class.getSimpleName();
+
+ /**
+ * The total number of items in the dataset after the last load
+ */
+ private int mPreviousTotal = 0;
+ /**
+ * True if we are still waiting for the last set of data to load.
+ */
+ private boolean mLoading = true;
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+
+ int visibleItemCount = recyclerView.getChildCount();
+ int totalItemCount = recyclerView.getLayoutManager().getItemCount();
+ int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
+
+ if (mLoading) {
+ if (totalItemCount > mPreviousTotal) {
+ mLoading = false;
+ mPreviousTotal = totalItemCount;
+ }
+ }
+ int visibleThreshold = 5;
+ if (!mLoading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)) {
+ // End has been reached
+ onLoadMore();
+ mLoading = true;
+ }
+ }
+
+ public void reset() {
+ mPreviousTotal = 0;
+ }
+
+ public abstract void onLoadMore();
+}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/RecyclerAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/RecyclerAdapter.java
new file mode 100644
index 0000000..c2b7137
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/RecyclerAdapter.java
@@ -0,0 +1,187 @@
+package com.moiseum.wolnelektury.components.recycler;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.moiseum.wolnelektury.connection.models.CategoryModel;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author golonkos
+ */
+
+public abstract class RecyclerAdapter extends RecyclerView.Adapter {
+
+ /**
+ * On click listener.
+ */
+ public interface OnItemClickListener {
+ /**
+ * @param item clicked item
+ * @param view clicked view
+ */
+ void onItemClicked(T item, View view, int position);
+ }
+
+ public enum Selection {
+ NONE, SINGLE
+ }
+
+ private static final int NO_POSITION = -1;
+
+ private LayoutInflater layoutInflater;
+ private OnItemClickListener onItemClickListener;
+
+ private List items = Collections.emptyList();
+
+ private T selectedItem;
+ private int selectedItemPosition = NO_POSITION;
+
+ private Selection selection = Selection.NONE;
+
+ private View.OnClickListener onClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = (int) v.getTag();
+ T item = getItem(position);
+ onItemClicked(v, item, position);
+ }
+ };
+
+ public RecyclerAdapter(Context context, Selection selection) {
+ layoutInflater = LayoutInflater.from(context);
+ this.selection = selection;
+ }
+
+ protected void onItemClicked(View view, T item, int position) {
+ selectItemAndNotify(item, position);
+ if (onItemClickListener != null) {
+ onItemClickListener.onItemClicked(item, view, position);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(VH viewHolder, final int position) {
+ viewHolder.itemView.setTag(position);
+ viewHolder.itemView.setOnClickListener(onClickListener);
+ T item = getItem(position);
+ viewHolder.itemView.setSelected(isSelected(getItem(position)));
+ viewHolder.bind(item, isSelected(item));
+ }
+
+ @Override
+ public int getItemCount() {
+ return items.size();
+ }
+
+
+ /**
+ * @param onItemClickListener item click listener
+ */
+ public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
+ this.onItemClickListener = onItemClickListener;
+ }
+
+
+ public OnItemClickListener getOnItemClickListener() {
+ return onItemClickListener;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ /**
+ * @param items new items
+ */
+ public void setItems(List items) {
+ this.items = items;
+ notifyDataSetChanged();
+ }
+
+ public void addItems(List items) {
+ this.items.addAll(items);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * @param position position of element to remove
+ * @return removed item or null if element not found
+ */
+ public T removeItem(int position) {
+ if (position >= 0 && position < items.size()) {
+ T item = items.remove(position);
+ notifyItemRemoved(position);
+ notifyItemRangeChanged(position, items.size());
+ return item;
+ }
+ return null;
+ }
+
+ public void clear() {
+ this.items.clear();
+ notifyDataSetChanged();
+ }
+
+ protected void addItem(int position, T item) {
+ items.add(position, item);
+ }
+
+ protected View inflate(int layoutResId, ViewGroup parent) {
+ return layoutInflater.inflate(layoutResId, parent, false);
+ }
+
+ /**
+ * @param position position of element
+ * @return item from specific position
+ */
+ public T getItem(int position) {
+ return this.items.get(position);
+ }
+
+ protected abstract String getItemId(T item);
+
+ private boolean isSelected(T item) {
+ return selectedItem != null && getItemId(selectedItem).contains(getItemId(item));
+ }
+
+ public void selectItem(T item) {
+ int position = NO_POSITION;
+ for (int i = 0; i < items.size(); i++) {
+ if (getItemId(item).equals(getItemId(items.get(i)))) {
+ position = i;
+ break;
+ }
+ }
+ selectItemAndNotify(item, position);
+ }
+
+ private void selectItemAndNotify(T item, int position) {
+ int selectedPositionToNotify = selectedItemPosition;
+ setSelectedItem(item, position);
+
+ if (selectedPositionToNotify != NO_POSITION) {
+ notifyItemChanged(selectedPositionToNotify);
+ }
+ notifyItemChanged(position);
+ }
+
+ private void setSelectedItem(T item, int position) {
+ switch (selection) {
+ case SINGLE:
+ selectedItem = item;
+ selectedItemPosition = position;
+ break;
+ case NONE:
+ break;
+ }
+ }
+
+
+}
+
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/ViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/ViewHolder.java
new file mode 100644
index 0000000..53f2f26
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/ViewHolder.java
@@ -0,0 +1,25 @@
+package com.moiseum.wolnelektury.components.recycler;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import butterknife.ButterKnife;
+
+public abstract class ViewHolder extends RecyclerView.ViewHolder {
+
+ public ViewHolder(View view) {
+ super(view);
+ ButterKnife.bind(this, view);
+ }
+
+ public View getView() {
+ return itemView;
+ }
+
+ protected Context getContext() {
+ return getView().getContext();
+ }
+
+ public abstract void bind(T item, boolean selected);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/ErrorHandler.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/ErrorHandler.java
new file mode 100644
index 0000000..a14e4fd
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/ErrorHandler.java
@@ -0,0 +1,53 @@
+package com.moiseum.wolnelektury.connection;
+
+import java.io.IOException;
+
+import retrofit2.Response;
+
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+
+/**
+ * @author golonkos.
+ */
+
+public class ErrorHandler {
+
+ private static final String TAG = ErrorHandler.class.getSimpleName();
+ private final Response response;
+
+ public ErrorHandler(Response response) {
+ this.response = response;
+ }
+
+ public void handle() throws IOException {
+ // There is no error model returned for this API
+ switch (response.code()) {
+ case HTTP_BAD_REQUEST:
+ case HTTP_NOT_FOUND:
+ case HTTP_BAD_METHOD:
+ case HTTP_INTERNAL_ERROR:
+ case HTTP_FORBIDDEN:
+ default:
+ throw new IOException("Unknown or unhandled exception for response " + response.code() + ", " + response.message());
+ }
+
+ }
+
+ // public ErrorModel parseError(Response response) {
+ // try {
+ // Gson gson = new GsonBuilder().create();
+ // return gson.fromJson(response.errorBody().string(), ErrorModel.class);
+ // } catch (IOException | JsonSyntaxException e) {
+ // Log.e(TAG, "Error while parsing error json", e);
+ // return null;
+ // }
+ // }
+
+ public int getResponseCode() {
+ return response.code();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClient.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClient.java
new file mode 100644
index 0000000..35eb345
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClient.java
@@ -0,0 +1,107 @@
+package com.moiseum.wolnelektury.connection;
+
+import android.content.Context;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.moiseum.wolnelektury.BuildConfig;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.connection.interceptors.NewApiInterceptor;
+import com.moiseum.wolnelektury.connection.interceptors.OAuthSigningInterceptor;
+import com.moiseum.wolnelektury.connection.interceptors.UnauthorizedInterceptor;
+import com.moiseum.wolnelektury.connection.models.OAuthTokenModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.io.File;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.Cache;
+import okhttp3.OkHttpClient;
+import okhttp3.logging.HttpLoggingInterceptor;
+import retrofit2.Call;
+import retrofit2.Retrofit;
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+/**
+ * @author golonkos.
+ */
+
+public class RestClient {
+ public static final int PAGINATION_LIMIT = 30;
+
+ public static final String MEDIA_URL = "http://wolnelektury.pl/media/";
+ public static final String MEDIA_URL_HTTPS = "https://wolnelektury.pl/media/";
+ public static final String BASE_URL = "https://wolnelektury.pl/api/";
+ public static final String WEB_OAUTH_AUTHORIZATION_URL = "";
+ public static final String WEB_PAYPAL_FORM_URL = "";
+
+ private static final String CACHE_DIR = "responses";
+ private static final int CACHE_SIZE_MB = 10;
+
+ private static final String CONSUMER_KEY = "";
+ private static final String CONSUMER_SECRET = "";
+
+ private final Retrofit retrofit;
+ private final OAuthSigningInterceptor oAuthInterceptor;
+
+ public RestClient(Context context) {
+ OAuthTokenModel currentToken = WLApplication.getInstance().getPreferences().getAccessToken();
+ oAuthInterceptor = new OAuthSigningInterceptor(CONSUMER_KEY, CONSUMER_SECRET, new Random());
+ if (currentToken != null) {
+ oAuthInterceptor.setToken(currentToken.getToken(), currentToken.getTokenSecret());
+ }
+ UnauthorizedInterceptor unauthorizedInterceptor = new UnauthorizedInterceptor();
+ NewApiInterceptor newApiInterceptor = new NewApiInterceptor();
+
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ //gsonBuilder.registerTypeAdapter(Date.class, new RestClientDateSerializer());
+
+ Gson gson = gsonBuilder.create();
+
+ int cacheSize = CACHE_SIZE_MB * 1024 * 1024;
+ File cacheDir = new File(context.getCacheDir(), CACHE_DIR);
+ Cache cache = new Cache(cacheDir, cacheSize);
+
+ OkHttpClient.Builder builder = new OkHttpClient.Builder();
+ builder.interceptors().add(newApiInterceptor);
+ builder.interceptors().add(oAuthInterceptor);
+ builder.interceptors().add(unauthorizedInterceptor);
+ if (BuildConfig.DEBUG) {
+ HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
+ loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
+ builder.interceptors().add(loggingInterceptor);
+ }
+ builder.writeTimeout(60, TimeUnit.SECONDS);
+ builder.readTimeout(60, TimeUnit.SECONDS);
+ builder.connectTimeout(60, TimeUnit.SECONDS);
+
+ OkHttpClient client = builder.cache(cache).build();
+ retrofit = new Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+ .client(client)
+ .build();
+ }
+
+ public Call call(RestClientCallback restClientCallback, Class clazz) {
+ S service = createService(clazz);
+ Call call = restClientCallback.execute(service);
+ call.enqueue(restClientCallback);
+ return call;
+ }
+
+ public S createService(Class clazz) {
+ return retrofit.create(clazz);
+ }
+
+ public void clearOAuthTokens() {
+ oAuthInterceptor.setToken(null, null);
+ }
+
+ public BooksService obtainBookService() {
+ return retrofit.create(BooksService.class);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClientCallback.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClientCallback.java
new file mode 100644
index 0000000..41a1ad5
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClientCallback.java
@@ -0,0 +1,44 @@
+package com.moiseum.wolnelektury.connection;
+
+import android.util.Log;
+
+import retrofit2.Call;
+import retrofit2.Response;
+
+public abstract class RestClientCallback implements retrofit2.Callback {
+
+ private static final String TAG = RestClientCallback.class.getSimpleName();
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ onSuccess(response.body());
+ } else {
+ try {
+ ErrorHandler errorHandler = new ErrorHandler<>(response);
+ errorHandler.handle();
+ } catch (Exception e) {
+ onFailure(e);
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e(TAG, t.getMessage(), t);
+ if (!call.isCanceled()) {
+ onFailure(new Exception(t));
+ } else {
+ onCancel();
+ }
+ }
+
+
+ public abstract void onSuccess(T data);
+
+ public abstract void onFailure(Exception e);
+
+ public abstract void onCancel();
+
+ public abstract Call execute(S service);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/WolneLekturyFirebaseMessagingService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/WolneLekturyFirebaseMessagingService.java
new file mode 100644
index 0000000..7a27db5
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/WolneLekturyFirebaseMessagingService.java
@@ -0,0 +1,105 @@
+package com.moiseum.wolnelektury.connection;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+
+import com.google.firebase.messaging.FirebaseMessaging;
+import com.google.firebase.messaging.FirebaseMessagingService;
+import com.google.firebase.messaging.RemoteMessage;
+import com.moiseum.wolnelektury.BuildConfig;
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+import com.moiseum.wolnelektury.view.main.MainActivity;
+
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import timber.log.Timber;
+
+/**
+ * Created by Piotr Ostrowski on 27.08.2018.
+ */
+public class WolneLekturyFirebaseMessagingService extends FirebaseMessagingService {
+
+ private static final String TAG = WolneLekturyFirebaseMessagingService.class.getSimpleName();
+ private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+
+ @Override
+ public void onNewToken(String s) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Refreshed token: " + s);
+ }
+ FirebaseMessaging.getInstance().subscribeToTopic(getString(R.string.default_notification_topic));
+ }
+
+ @Override
+ public void onMessageReceived(RemoteMessage remoteMessage) {
+ Log.d(TAG, "Received notification " + remoteMessage);
+ if (preferences.getNotifications()) {
+ String title = remoteMessage.getData().get("title");
+ String body = remoteMessage.getData().get("body");
+ String imageUrl = remoteMessage.getData().get("imageUrl");
+ Bitmap bitmap = getBitmapFromUrl(imageUrl);
+ sendNotification(title, body, bitmap);
+ } else {
+ Log.d(TAG, "Skipping notification cause of user preference");
+ }
+ }
+
+ private void sendNotification(String title, String body, Bitmap image) {
+ MainActivity.MainIntent intent = new MainActivity.MainIntent(R.string.app_name, this);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+ Uri defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+
+ NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, getString(R.string.default_notification_channel_id))
+ .setLargeIcon(image)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setContentTitle(title)
+ .setContentText(body)
+ .setColor(ContextCompat.getColor(this, R.color.colorAccent))
+ .setColorized(true)
+ .setStyle(new NotificationCompat.BigPictureStyle()
+ .bigPicture(image)
+ .setBigContentTitle(title)
+ .setSummaryText(body)
+ )
+ .setAutoCancel(true)
+ .setSound(defaultUri)
+ .setContentIntent(pendingIntent);
+
+ if (notificationManager != null) {
+ notificationManager.notify(0, notificationBuilder.build());
+ }
+ }
+
+ /*
+ *To get a Bitmap image from the URL received
+ * */
+ private Bitmap getBitmapFromUrl(String imageUrl) {
+ if (imageUrl == null) {
+ return null;
+ }
+ try {
+ URL url = new URL(imageUrl);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoInput(true);
+ connection.connect();
+ InputStream input = connection.getInputStream();
+ return BitmapFactory.decodeStream(input);
+ } catch (Exception e) {
+ Timber.tag(TAG).e(e, "Failed to fetch notification image " + e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileCacheUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileCacheUtils.java
new file mode 100644
index 0000000..dc14253
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileCacheUtils.java
@@ -0,0 +1,180 @@
+package com.moiseum.wolnelektury.connection.downloads;
+
+import android.os.Environment;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.connection.models.MediaModel;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import io.reactivex.Completable;
+import okhttp3.ResponseBody;
+
+import static android.os.Environment.isExternalStorageRemovable;
+
+/**
+ * Created by Piotr Ostrowski on 10.05.2017.
+ */
+
+public class FileCacheUtils {
+
+ private static final String FILES_CACHE = "FilesCache";
+ private static final int BUFFER_SIZE = 4096;
+ private static final long PROGRESS_UPDATE_RATE = 10;
+ private static final String TAG = FileCacheUtils.class.getSimpleName();
+
+ private FileCacheUtils() {
+ // nop.
+ }
+
+ public static String getCachedFileForUrl(String url) {
+ File cachedFile = new File(getCurrentCachePath() + File.separator + url.hashCode());
+ if (cachedFile.exists()) {
+ return cachedFile.getAbsolutePath();
+ } else {
+ return null;
+ }
+ }
+
+ public static boolean deleteFileForUrl(String url) {
+ File cachedFile = new File(getCurrentCachePath() + File.separator + url.hashCode());
+ if (cachedFile.exists()) {
+ return cachedFile.delete();
+ } else {
+ Log.e(TAG, "There is no file to be removed: " + url);
+ return false;
+ }
+ }
+
+ public static boolean writeResponseBodyToDiskCache(ResponseBody body, String fileUrl) {
+ try {
+ String cachePath = getCurrentCachePath();
+ File fileCacheDir = new File(cachePath);
+ File downloadFile = new File(cachePath + File.separator + fileUrl.hashCode() + ".download");
+ createCacheFolderAndFile(cachePath, fileCacheDir, downloadFile);
+
+ InputStream inputStream = null;
+ OutputStream outputStream = null;
+
+ try {
+ byte[] fileReader = new byte[BUFFER_SIZE];
+ long fileSize = body.contentLength();
+ long fileSizeDownloaded = 0;
+
+ inputStream = body.byteStream();
+ outputStream = new FileOutputStream(downloadFile);
+
+ int updateRate = 0;
+ while (true) {
+ int read = inputStream.read(fileReader);
+ if (read == -1) {
+ break;
+ }
+
+ outputStream.write(fileReader, 0, read);
+ fileSizeDownloaded += read;
+ if (updateRate++ % PROGRESS_UPDATE_RATE == 0) {
+ EventBus.getDefault().post(new DownloadProgressEvent(fileUrl, fileSizeDownloaded, fileSize));
+ }
+ }
+ outputStream.flush();
+
+ File audioFileDest = new File(cachePath + File.separator + fileUrl.hashCode());
+ boolean renamed = downloadFile.renameTo(audioFileDest);
+ if (!renamed) {
+ throw new IOException("Failed to rename downloaded file: " + audioFileDest.getAbsolutePath());
+ }
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to save file to cache: " + fileUrl, e);
+ downloadFile.delete();
+ return false;
+ } finally {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "File creation or streaming closure failed", e);
+ return false;
+ }
+ }
+
+ private static void createCacheFolderAndFile(String cachePath, File audioCacheDir, File audioFile) throws IOException {
+ if (!audioCacheDir.exists()) {
+ boolean result = audioCacheDir.mkdir();
+ if (!result) {
+ throw new IOException("Failed to create AudioCache Dir.");
+ }
+ }
+ if (!audioFile.exists()) {
+ boolean result = audioFile.createNewFile();
+ if (!result) {
+ throw new IOException("Failed to create file in path: " + audioFile.getAbsolutePath());
+ }
+ }
+ }
+
+ @NonNull
+ private static String getCurrentCachePath() {
+ File externalCacheDir = WLApplication.getInstance().getApplicationContext().getExternalCacheDir();
+ String cachePath = (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable()) &&
+ externalCacheDir != null ? externalCacheDir.getPath() : WLApplication.getInstance().getCacheDir().getPath();
+ return cachePath + File.separator + FILES_CACHE;
+ }
+
+ public static Completable deleteAudiobookFiles(List fileUrls) {
+ return Completable.fromAction(() -> {
+ for (String fileUrl : fileUrls) {
+ boolean deleted = deleteFileForUrl(fileUrl);
+ if (!deleted) {
+ Log.e(TAG, "Failed to delete file " + FileCacheUtils.getCachedFileForUrl(fileUrl));
+ }
+ }
+ });
+ }
+
+ public static Completable deleteEbookFile(final String epubUrl) {
+ return Completable.fromAction(() -> deleteFileForUrl(epubUrl));
+ }
+
+ /**
+ * Event indicating progress.
+ */
+ public static class DownloadProgressEvent {
+
+ private String fileUrl;
+ private long downloaded;
+ private long total;
+
+ public DownloadProgressEvent(String fileUrl, long downloaded, long total) {
+ this.fileUrl = fileUrl;
+ this.downloaded = downloaded;
+ this.total = total;
+ }
+
+ public String getFileUrl() {
+ return fileUrl;
+ }
+
+ public long getDownloaded() {
+ return downloaded;
+ }
+
+ public long getTotal() {
+ return total;
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileDownloadIntentService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileDownloadIntentService.java
new file mode 100644
index 0000000..742f0c7
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileDownloadIntentService.java
@@ -0,0 +1,126 @@
+package com.moiseum.wolnelektury.connection.downloads;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.connection.ErrorHandler;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import org.greenrobot.eventbus.EventBus;
+import org.parceler.Parcels;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import okhttp3.ResponseBody;
+import retrofit2.Call;
+import retrofit2.Response;
+
+/**
+ * Created by piotrostrowski on 07.05.2017.
+ */
+
+public class FileDownloadIntentService extends IntentService {
+
+ private static final String TAG = FileDownloadIntentService.class.getSimpleName();
+ public static final String FILE_URL_KEY = "FileUrlKey";
+ public static final String FILES_URLS_KEY = "FilesUrlsKey";
+
+ public static void downloadFile(Context context, String fileUrl) {
+ Intent downloadIntent = new Intent(context, FileDownloadIntentService.class);
+ downloadIntent.putExtra(FILE_URL_KEY, fileUrl);
+ context.startService(downloadIntent);
+ }
+
+ public static void downloadFiles(Context context, ArrayList filesUrls) {
+ Intent downloadIntent = new Intent(context, FileDownloadIntentService.class);
+ downloadIntent.putExtra(FILES_URLS_KEY, Parcels.wrap(filesUrls));
+ context.startService(downloadIntent);
+ }
+
+ public FileDownloadIntentService() {
+ super(TAG);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+
+ @Override
+ protected void onHandleIntent(@Nullable Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ if (intent.hasExtra(FILE_URL_KEY)) {
+ String fileUrl = intent.getStringExtra(FILE_URL_KEY);
+ checkCacheAndDownload(fileUrl);
+ } else if (intent.hasExtra(FILES_URLS_KEY)) {
+ ArrayList filesUrls = Parcels.unwrap(intent.getParcelableExtra(FILES_URLS_KEY));
+ for (String fileUrl : filesUrls) {
+ if (!checkCacheAndDownload(fileUrl)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private boolean checkCacheAndDownload(String fileUrl) {
+ if (FileCacheUtils.getCachedFileForUrl(fileUrl) != null) {
+ Log.v(TAG, fileUrl + " is already in cache.");
+ EventBus.getDefault().post(new DownloadFileEvent(fileUrl, true));
+ return true;
+ }
+ return downloadFile(fileUrl);
+ }
+
+ private boolean downloadFile(String fileUrl) {
+ RestClient client = WLApplication.getInstance().getRestClient();
+ BooksService booksService = client.createService(BooksService.class);
+ try {
+ Call call = booksService.downloadFileWithUrl(fileUrl);
+ Response response = call.execute();
+ if (response.isSuccessful()) {
+ boolean result = FileCacheUtils.writeResponseBodyToDiskCache(response.body(), fileUrl);
+ EventBus.getDefault().post(new DownloadFileEvent(fileUrl, result));
+ } else {
+ ErrorHandler errorHandler = new ErrorHandler<>(response);
+ errorHandler.handle();
+ //if nothing cause, throw exception
+ throw new UnsupportedOperationException("Unhandled exception");
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to download audio file: " + fileUrl, e);
+ EventBus.getDefault().post(new DownloadFileEvent(fileUrl, false));
+ return false;
+ }
+ return true;
+ }
+
+ public static class DownloadFileEvent {
+
+ private String fileUrl;
+ private boolean success;
+
+ DownloadFileEvent(String fileUrl, boolean success) {
+ this.fileUrl = fileUrl;
+ this.success = success;
+ }
+
+ public String getFileUrl() {
+ return fileUrl;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/NewApiInterceptor.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/NewApiInterceptor.java
new file mode 100644
index 0000000..2e874eb
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/NewApiInterceptor.java
@@ -0,0 +1,37 @@
+package com.moiseum.wolnelektury.connection.interceptors;
+
+import android.support.annotation.NonNull;
+
+import java.io.IOException;
+
+import okhttp3.HttpUrl;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+/**
+ * Created by Piotr Ostrowski on 24.09.2018.
+ */
+public class NewApiInterceptor implements Interceptor {
+
+ private static final String NEW_API_HEADER = "New-Api";
+ private static final String NEW_API_PARAM = "new_api";
+
+ @Override
+ public Response intercept(@NonNull Chain chain) throws IOException {
+ if (chain.request().header(NEW_API_HEADER) != null) {
+ HttpUrl httpUrl = chain.request()
+ .url()
+ .newBuilder()
+ .addQueryParameter(NEW_API_PARAM, Boolean.toString(true))
+ .build();
+ Request newRequest = chain.request()
+ .newBuilder()
+ .url(httpUrl)
+ .build();
+ return chain.proceed(newRequest);
+ }
+
+ return chain.proceed(chain.request());
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/OAuthSigningInterceptor.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/OAuthSigningInterceptor.java
new file mode 100644
index 0000000..0fc1092
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/OAuthSigningInterceptor.java
@@ -0,0 +1,245 @@
+package com.moiseum.wolnelektury.connection.interceptors;
+
+/**
+ * Created by Piotr Ostrowski on 06.06.2018.
+ */
+/*
+ * Copyright (C) 2015 Jake Wharton
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.moiseum.wolnelektury.connection.models.OAuthTokenModel;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.Map;
+import java.util.Random;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import okhttp3.HttpUrl;
+import okhttp3.Interceptor;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okio.Buffer;
+import okio.ByteString;
+
+public final class OAuthSigningInterceptor implements Interceptor {
+ private static final String TAG = OAuthSigningInterceptor.class.getSimpleName();
+ private static final String REQUEST_TOKEN_HEADER = "Token-Requested";
+ private static final String AUTH_REQUIRED_HEADER = "Authentication-Required";
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+
+ private static final String OAUTH_REALM = "realm=\"API\", ";
+ private static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
+ private static final String OAUTH_NONCE = "oauth_nonce";
+ private static final String OAUTH_SIGNATURE = "oauth_signature";
+ private static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
+ private static final String OAUTH_SIGNATURE_METHOD_VALUE = "HMAC-SHA1";
+ private static final String OAUTH_TIMESTAMP = "oauth_timestamp";
+ private static final String OAUTH_ACCESS_TOKEN = "oauth_token";
+ private static final String OAUTH_VERSION = "oauth_version";
+ private static final String OAUTH_VERSION_VALUE = "1.0";
+ private static final long ONE_SECOND = 1000;
+
+ private final String consumerKey;
+ private final String consumerSecret;
+ private final Random random;
+ private String accessToken;
+ private String accessSecret;
+
+ public OAuthSigningInterceptor(String consumerKey, String consumerSecret, Random random) {
+ this.consumerKey = consumerKey;
+ this.consumerSecret = consumerSecret;
+ this.random = random;
+ }
+
+ public void setToken(String accessToken, String accessSecret) {
+ this.accessToken = accessToken;
+ this.accessSecret = accessSecret;
+ }
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ if (chain.request().header(REQUEST_TOKEN_HEADER) != null) {
+ return handleRequestTokenRequest(chain);
+ } else if (chain.request().header(AUTH_REQUIRED_HEADER) != null || isSignedIn()) {
+ return chain.proceed(signRequest(chain.request()));
+ } else {
+ return chain.proceed(chain.request());
+ }
+ }
+
+ private boolean isSignedIn() {
+ return accessSecret != null && accessToken != null;
+ }
+
+ private Response handleRequestTokenRequest(Chain chain) throws IOException {
+ Response tokenResponse = chain.proceed(requestTokenRequest(chain.request()));
+ if (tokenResponse.isSuccessful() && tokenResponse.code() == 200 && tokenResponse.body() != null) {
+ String jsonResponse = paramJson(tokenResponse.body().string());
+ try {
+ Gson gson = new Gson();
+ OAuthTokenModel tokenModel = gson.fromJson(jsonResponse, OAuthTokenModel.class);
+ accessToken = tokenModel.getToken();
+ accessSecret = tokenModel.getTokenSecret();
+ return tokenResponse.newBuilder().body(ResponseBody.create(MediaType.parse("application/json"), jsonResponse)).build();
+ } catch (JsonSyntaxException e) {
+ Log.v(TAG, "Failed to parse Oauth Request Token response.", e);
+ }
+ }
+ return tokenResponse;
+ }
+
+ private Request signRequest(Request request) throws IOException {
+ if (accessToken == null || accessSecret == null) {
+ Log.e(TAG, "Missing authentication tokens, passing request unsigned.");
+ return request;
+ }
+
+ SortedMap parameters = getOAuthParams(request.url(), request.body());
+
+ String baseUrl = request.url().newBuilder().query(null).build().toString();
+ ByteString baseString = getBaseString(request.method(), baseUrl, parameters);
+ String signingKey = utf8(consumerSecret) + "&" + (accessSecret != null ? utf8(accessSecret) : "");
+ String signature = baseString.hmacSha1(ByteString.of(signingKey.getBytes())).base64();
+
+ String authorization = "OAuth " + OAUTH_REALM
+ + OAUTH_CONSUMER_KEY + "=\"" + parameters.get(OAUTH_CONSUMER_KEY) + "\", "
+ + OAUTH_NONCE + "=\"" + parameters.get(OAUTH_NONCE) + "\", "
+ + OAUTH_SIGNATURE + "=\"" + signature + "\", "
+ + OAUTH_SIGNATURE_METHOD + "=\"" + OAUTH_SIGNATURE_METHOD_VALUE + "\", "
+ + OAUTH_TIMESTAMP + "=\"" + parameters.get(OAUTH_TIMESTAMP) + "\", "
+ + OAUTH_ACCESS_TOKEN + "=\"" + accessToken + "\", "
+ + OAUTH_VERSION + "=\"" + OAUTH_VERSION_VALUE + "\"";
+
+ return request.newBuilder()
+ .addHeader(AUTHORIZATION_HEADER, authorization)
+ .build();
+ }
+
+ private Request requestTokenRequest(Request request) throws IOException {
+ SortedMap parameters = getOAuthParams(request.url(), request.body());
+
+ String baseUrl = request.url().newBuilder().query(null).build().toString();
+ ByteString baseString = getBaseString(request.method(), baseUrl, parameters);
+ String signingKey = utf8(consumerSecret) + "&" + (accessSecret != null ? utf8(accessSecret) : "");
+ String signature = baseString.hmacSha1(ByteString.of(signingKey.getBytes())).base64();
+
+ HttpUrl.Builder urlBuilder = request.url().newBuilder()
+ .addQueryParameter(OAUTH_CONSUMER_KEY, parameters.get(OAUTH_CONSUMER_KEY))
+ .addQueryParameter(OAUTH_NONCE, parameters.get(OAUTH_NONCE))
+ .addQueryParameter(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE)
+ .addQueryParameter(OAUTH_TIMESTAMP, parameters.get(OAUTH_TIMESTAMP))
+ .addQueryParameter(OAUTH_VERSION, OAUTH_VERSION_VALUE)
+ .addQueryParameter(OAUTH_SIGNATURE, signature);
+ if (accessToken != null) {
+ urlBuilder.addQueryParameter(OAUTH_ACCESS_TOKEN, accessToken);
+ }
+ HttpUrl requestUrl = urlBuilder.build();
+
+ return request.newBuilder().url(requestUrl).build();
+ }
+
+ private SortedMap getOAuthParams(HttpUrl url, RequestBody requestBody) throws IOException {
+ byte[] nonce = new byte[32];
+ random.nextBytes(nonce);
+
+ String oauthNonce = ByteString.of(nonce).base64().replaceAll("\\W", "");
+ String oauthTimestamp = String.valueOf(System.currentTimeMillis() / ONE_SECOND);
+
+ SortedMap parameters = new TreeMap<>();
+ parameters.put(OAUTH_CONSUMER_KEY, utf8(consumerKey));
+ parameters.put(OAUTH_NONCE, oauthNonce);
+ parameters.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE);
+ parameters.put(OAUTH_TIMESTAMP, oauthTimestamp);
+ parameters.put(OAUTH_VERSION, OAUTH_VERSION_VALUE);
+ if (accessToken != null) {
+ parameters.put(OAUTH_ACCESS_TOKEN, accessToken);
+ }
+
+ // Adding query params
+ for (int i = 0; i < url.querySize(); i++) {
+ parameters.put(utf8(url.queryParameterName(i)), utf8(url.queryParameterValue(i)));
+ }
+
+ // Adding body params
+ if (requestBody != null) {
+ Buffer body = new Buffer();
+ requestBody.writeTo(body);
+
+ while (!body.exhausted()) {
+ long keyEnd = body.indexOf((byte) '=');
+ if (keyEnd == -1) {
+ throw new IllegalStateException("Key with no value: " + body.readUtf8());
+ }
+ String key = body.readUtf8(keyEnd);
+ body.skip(1); // Equals.
+
+ long valueEnd = body.indexOf((byte) '&');
+ String value = valueEnd == -1 ? body.readUtf8() : body.readUtf8(valueEnd);
+ if (valueEnd != -1) {
+ body.skip(1); // Ampersand.
+ }
+
+ parameters.put(key, value);
+ }
+ }
+
+ return parameters;
+ }
+
+ @NonNull
+ private ByteString getBaseString(String method, String baseUrl, SortedMap parameters) throws IOException {
+ Buffer base = new Buffer();
+ base.writeUtf8(method);
+ base.writeByte('&');
+ base.writeUtf8(utf8(baseUrl));
+ base.writeByte('&');
+
+ boolean first = true;
+ for (Map.Entry entry : parameters.entrySet()) {
+ if (!first) {
+ base.writeUtf8(utf8("&"));
+ }
+ first = false;
+ base.writeUtf8(utf8(entry.getKey()));
+ base.writeUtf8(utf8("="));
+ base.writeUtf8(utf8(entry.getValue()));
+ }
+ return ByteString.of(base.readByteArray());
+ }
+
+ private String utf8(String escapedString) throws IOException {
+ return URLEncoder.encode(escapedString, "UTF-8")
+ .replace("+", "%20")
+ .replace("*", "%2A")
+ .replace("%7E", "~");
+ }
+
+ private String paramJson(String paramIn) {
+ paramIn = paramIn.replaceAll("=", "\":\"");
+ paramIn = paramIn.replaceAll("&", "\",\"");
+ return "{\"" + paramIn + "\"}";
+ }
+
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/UnauthorizedInterceptor.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/UnauthorizedInterceptor.java
new file mode 100644
index 0000000..91dc357
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/UnauthorizedInterceptor.java
@@ -0,0 +1,40 @@
+package com.moiseum.wolnelektury.connection.interceptors;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.view.main.MainActivity;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import okhttp3.Interceptor;
+import okhttp3.Response;
+
+/**
+ * Created by Piotr Ostrowski on 23.06.2018.
+ */
+public class UnauthorizedInterceptor implements Interceptor {
+
+ private static final String TAG = UnauthorizedInterceptor.class.getSimpleName();
+
+ @Override
+ public Response intercept(@NonNull Chain chain) throws IOException {
+ Response response = chain.proceed(chain.request());
+ if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ Log.e(TAG, "Provided credentials were invalid. Re-launching app");
+
+ WLApplication.getInstance().getPreferences().clearUserData();
+ WLApplication.getInstance().getRestClient().clearOAuthTokens();
+
+ Context context = WLApplication.getInstance().getApplicationContext();
+ MainActivity.MainIntent intent = new MainActivity.MainIntent(R.string.unauthorized, context);
+ context.startActivity(intent);
+ }
+ return response;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookDetailsModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookDetailsModel.java
new file mode 100644
index 0000000..0a2959e
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookDetailsModel.java
@@ -0,0 +1,237 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import com.google.gson.annotations.SerializedName;
+import com.moiseum.wolnelektury.utils.StringUtils;
+
+import org.parceler.Parcel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by piotrostrowski on 17.11.2017.
+ */
+
+@Parcel(Parcel.Serialization.BEAN)
+public class BookDetailsModel {
+
+ private static final String HREF_BASE = "http://wolnelektury.pl/api/books/";
+ private static final String MEDIA_TYPE_MP3 = "mp3";
+
+ private List genres;
+ private List kinds;
+ private BookModel parent;
+ private String title;
+ private String url;
+ private List media;
+ @SerializedName("simple_cover")
+ private String cover;
+ private List epochs;
+ private List authors;
+ private String pdf;
+ private String epub;
+ @SerializedName("simple_thumb")
+ private String coverThumb;
+ @SerializedName("fragment_data")
+ private FragmentModel fragment;
+ @SerializedName("audio_length")
+ private String audioLength;
+ private ReadingStateModel.ReadingState state;
+ private Boolean favouriteState;
+ @SerializedName("cover_color")
+ private String coverColor;
+ private String slug;
+
+ public BookDetailsModel() {
+ }
+
+ public List getGenres() {
+ return genres;
+ }
+
+ public void setGenres(List genres) {
+ this.genres = genres;
+ }
+
+ public List getKinds() {
+ return kinds;
+ }
+
+ public void setKinds(List kinds) {
+ this.kinds = kinds;
+ }
+
+ public BookModel getParent() {
+ return parent;
+ }
+
+ public void setParent(BookModel parent) {
+ this.parent = parent;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public List getMedia() {
+ return media;
+ }
+
+ public void setMedia(List media) {
+ this.media = media;
+ }
+
+ public String getCover() {
+ return cover;
+ }
+
+ public void setCover(String cover) {
+ this.cover = cover;
+ }
+
+ public List getEpochs() {
+ return epochs;
+ }
+
+ public void setEpochs(List epochs) {
+ this.epochs = epochs;
+ }
+
+ public List getAuthors() {
+ return authors;
+ }
+
+ public void setAuthors(List authors) {
+ this.authors = authors;
+ }
+
+ public String getPdf() {
+ return pdf;
+ }
+
+ public void setPdf(String pdf) {
+ this.pdf = pdf;
+ }
+
+ public String getEpub() {
+ return epub;
+ }
+
+ public void setEpub(String epub) {
+ this.epub = epub;
+ }
+
+ public String getCoverThumb() {
+ return coverThumb;
+ }
+
+ public void setCoverThumb(String coverThumb) {
+ this.coverThumb = coverThumb;
+ }
+
+ public String getCoverColor() {
+ return coverColor;
+ }
+
+ public void setCoverColor(String coverColor) {
+ this.coverColor = coverColor;
+ }
+
+ public FragmentModel getFragment() {
+ return fragment;
+ }
+
+ public void setFragment(FragmentModel fragment) {
+ this.fragment = fragment;
+ }
+
+ public String getAudioLength() {
+ return audioLength;
+ }
+
+ public void setAudioLength(String audioLength) {
+ this.audioLength = audioLength;
+ }
+
+ public boolean hasAudio() {
+ return media != null && media.size() > 0;
+ }
+
+ public ReadingStateModel.ReadingState getState() {
+ return state;
+ }
+
+ public void setState(ReadingStateModel.ReadingState state) {
+ this.state = state;
+ }
+
+ public String getSlug() {
+ return slug;
+ }
+
+ public void setSlug(String slug) {
+ this.slug = slug;
+ }
+
+ public BookModel getStorageModel(String slug) {
+ BookModel model = new BookModel();
+ model.setAuthor(StringUtils.joinCategory(getAuthors(), ", "));
+ model.setCover(getCover());
+ model.setCoverColor(getCoverColor());
+ model.setCoverThumb(getCoverThumb());
+ model.setEpoch(StringUtils.joinCategory(getEpochs(), ", "));
+ model.setGenre(StringUtils.joinCategory(getGenres(), ", "));
+ model.setKind(StringUtils.joinCategory(getKinds(), ", "));
+ model.setHref(HREF_BASE + slug + "/");
+ model.setTitle(getTitle());
+ model.setSlug(slug);
+ model.setUrl(getUrl());
+ model.setHasAudio(hasAudio());
+ return model;
+ }
+
+ public ArrayList getAudiobookMediaModels() {
+ ArrayList mediaModels = new ArrayList<>();
+ for (MediaModel mediaFile : getMedia()) {
+ if (MEDIA_TYPE_MP3.equals(mediaFile.getType())) {
+ mediaModels.add(mediaFile);
+ }
+ }
+ return mediaModels;
+ }
+
+ public ArrayList getAudiobookFilesUrls() {
+ ArrayList urls = new ArrayList<>();
+ for (MediaModel mediaFile : getMedia()) {
+ if (MEDIA_TYPE_MP3.equals(mediaFile.getType())) {
+ urls.add(mediaFile.getUrl());
+ }
+ }
+ return urls;
+ }
+
+ public boolean getFavouriteState() {
+ return favouriteState;
+ }
+
+ public void setFavouriteState(boolean favouriteState) {
+ this.favouriteState = favouriteState;
+ }
+
+ public String getFavouriteString(boolean favouriteState) {
+ return favouriteState ? "like" : "unlike";
+ }
+}
+
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookModel.java
new file mode 100644
index 0000000..d5b8519
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookModel.java
@@ -0,0 +1,246 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import com.google.gson.annotations.SerializedName;
+import com.moiseum.wolnelektury.storage.StringListConverter;
+
+import org.parceler.Parcel;
+
+import java.util.List;
+
+import io.objectbox.annotation.Convert;
+import io.objectbox.annotation.Entity;
+import io.objectbox.annotation.Id;
+
+/**
+ * Created by piotrostrowski on 16.11.2017.
+ */
+
+@Parcel(Parcel.Serialization.BEAN)
+@Entity
+public class BookModel {
+
+ @Id(assignable = true)
+ private long localId;
+
+ // API provided fields
+ private String kind;
+ private String author;
+ private String url;
+ @SerializedName("has_audio")
+ private boolean hasAudio;
+ private String title;
+ private String cover;
+ private String epoch;
+ private String href;
+ private String genre;
+ private String slug;
+ @SerializedName("cover_color")
+ private String coverColor;
+ private String key;
+ @SerializedName("full_sort_key")
+ private String sortedKey;
+ @SerializedName("simple_thumb")
+ private String coverThumb;
+ private boolean liked;
+
+ // Locally stored fields
+ private String ebookName;
+ private int currentChapter;
+ private int totalChapters;
+ private String ebookFileUrl;
+ private int currentAudioChapter;
+ private int totalAudioChapters;
+ @Convert(converter = StringListConverter.class, dbType = String.class)
+ private List audioFileUrls;
+
+ public BookModel() {
+ }
+
+ public long getLocalId() {
+ return localId;
+ }
+
+ public void setLocalId(long localId) {
+ this.localId = localId;
+ }
+
+ public String getKind() {
+ return kind;
+ }
+
+ public void setKind(String kind) {
+ this.kind = kind;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public boolean isHasAudio() {
+ return hasAudio;
+ }
+
+ public void setHasAudio(boolean hasAudio) {
+ this.hasAudio = hasAudio;
+ }
+
+ public String getCover() {
+ return cover;
+ }
+
+ public void setCover(String cover) {
+ this.cover = cover;
+ }
+
+ public String getEpoch() {
+ return epoch;
+ }
+
+ public void setEpoch(String epoch) {
+ this.epoch = epoch;
+ }
+
+ public String getHref() {
+ return href;
+ }
+
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public String getSlug() {
+ return slug;
+ }
+
+ public String getCoverColor(){return coverColor;}
+
+ public void setCoverColor(String coverColor){this.coverColor=coverColor;}
+
+ public void setSlug(String slug) {
+ this.slug = slug;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getSortedKey() {
+ return sortedKey;
+ }
+
+ public void setSortedKey(String sortedKey) {
+ this.sortedKey = sortedKey;
+ }
+
+ public String getCoverThumb() {
+ return coverThumb;
+ }
+
+ public void setCoverThumb(String coverThumb) {
+ this.coverThumb = coverThumb;
+ }
+
+ public String getEbookName() {
+ return ebookName;
+ }
+
+ public void setEbookName(String ebookName) {
+ this.ebookName = ebookName;
+ }
+
+ public int getCurrentChapter() {
+ return currentChapter;
+ }
+
+ public void setCurrentChapter(int currentChapter) {
+ this.currentChapter = currentChapter;
+ }
+
+ public int getTotalChapters() {
+ return totalChapters;
+ }
+
+ public void setTotalChapters(int totalChapters) {
+ this.totalChapters = totalChapters;
+ }
+
+ public String getEbookFileUrl() {
+ return ebookFileUrl;
+ }
+
+ public void setEbookFileUrl(String ebookFileUrl) {
+ this.ebookFileUrl = ebookFileUrl;
+ }
+
+ public int getCurrentAudioChapter() {
+ return currentAudioChapter;
+ }
+
+ public void setCurrentAudioChapter(int currentAudioChapter) {
+ this.currentAudioChapter = currentAudioChapter;
+ }
+
+ public int getTotalAudioChapters() {
+ return totalAudioChapters;
+ }
+
+ public void setTotalAudioChapters(int totalAudioChapters) {
+ this.totalAudioChapters = totalAudioChapters;
+ }
+
+ public List getAudioFileUrls() {
+ return audioFileUrls;
+ }
+
+ public void setAudioFileUrls(List audioFileUrls) {
+ this.audioFileUrls = audioFileUrls;
+ }
+
+ public boolean isEbookDownloaded() {
+ return ebookFileUrl != null;
+ }
+
+ public boolean isAudioDownloaded() {
+ return audioFileUrls != null && audioFileUrls.size() > 0;
+ }
+
+ public boolean isDeletable() {
+ return ebookFileUrl != null || (audioFileUrls != null && audioFileUrls.size() > 0);
+ }
+
+ public boolean isLiked() { return liked; }
+
+ public void setLiked(boolean liked) { this.liked = liked; }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/CategoryModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/CategoryModel.java
new file mode 100644
index 0000000..a4bf7da
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/CategoryModel.java
@@ -0,0 +1,61 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import org.parceler.Parcel;
+
+/**
+ * Created by piotrostrowski on 17.11.2017.
+ */
+
+@Parcel(Parcel.Serialization.BEAN)
+public class CategoryModel {
+
+ private String url;
+ private String href;
+ private String name;
+ private String slug;
+ private boolean checked;
+
+ public CategoryModel() {
+
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getHref() {
+ return href;
+ }
+
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getSlug() {
+ return slug;
+ }
+
+ public void setSlug(String slug) {
+ this.slug = slug;
+ }
+
+ public boolean isChecked() {
+ return checked;
+ }
+
+ public void setChecked(boolean checked) {
+ this.checked = checked;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FavouriteStateModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FavouriteStateModel.java
new file mode 100644
index 0000000..9058aab
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FavouriteStateModel.java
@@ -0,0 +1,17 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import com.google.gson.annotations.SerializedName;
+
+public class FavouriteStateModel {
+
+ @SerializedName("likes")
+ private boolean state;
+
+ public FavouriteStateModel() {
+ this.state = false;
+ }
+
+ public boolean getState() {
+ return state;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FragmentModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FragmentModel.java
new file mode 100644
index 0000000..6c4c866
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FragmentModel.java
@@ -0,0 +1,32 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import org.parceler.Parcel;
+
+/**
+ * Created by piotrostrowski on 30.11.2017.
+ */
+@Parcel(Parcel.Serialization.BEAN)
+public class FragmentModel {
+
+ private String html;
+ private String title;
+
+ public FragmentModel() {
+ }
+
+ public String getHtml() {
+ return html;
+ }
+
+ public void setHtml(String html) {
+ this.html = html;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/MediaModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/MediaModel.java
new file mode 100644
index 0000000..8c78e46
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/MediaModel.java
@@ -0,0 +1,60 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import org.parceler.Parcel;
+
+/**
+ * Created by piotrostrowski on 17.11.2017.
+ */
+
+@Parcel(Parcel.Serialization.BEAN)
+public class MediaModel {
+
+ private String url;
+ private String director;
+ private String type;
+ private String name;
+ private String artist;
+
+ public MediaModel() {
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDirector() {
+ return director;
+ }
+
+ public void setDirector(String director) {
+ this.director = director;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/NewsModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/NewsModel.java
new file mode 100644
index 0000000..f4d7479
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/NewsModel.java
@@ -0,0 +1,118 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.parceler.Parcel;
+
+import java.util.List;
+
+@Parcel(Parcel.Serialization.BEAN)
+public class NewsModel {
+
+ private String body;
+ private String lead;
+ private String title;
+ private String url;
+ @SerializedName("image_url")
+ private String imageUrl;
+ private String key;
+ private String time;
+ private String place;
+ @SerializedName("image_thumb")
+ private String thumbUrl;
+ @SerializedName("gallery_urls")
+ private List galleryUrl;
+ private String type;
+
+ public NewsModel() {
+
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public String getLead() {
+ return lead;
+ }
+
+ public void setLead(String lead) {
+ this.lead = lead;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getImageUrl() {
+ return imageUrl;
+ }
+
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = imageUrl;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getTime() {
+ return time;
+ }
+
+ public void setTime(String time) {
+ this.time = time;
+ }
+
+ public String getPlace() {
+ return place;
+ }
+
+ public void setPlace(String place) {
+ this.place = place;
+ }
+
+ public String getThumbUrl() {
+ return thumbUrl;
+ }
+
+ public void setThumbUrl(String thumbUrl) {
+ this.thumbUrl = thumbUrl;
+ }
+
+ public List getGalleryUrl() {
+ return galleryUrl;
+ }
+
+ public void setGalleryUrl(List galleryUrl) {
+ this.galleryUrl = galleryUrl;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/OAuthTokenModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/OAuthTokenModel.java
new file mode 100644
index 0000000..778ee51
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/OAuthTokenModel.java
@@ -0,0 +1,36 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.parceler.Parcel;
+
+/**
+ * Created by Piotr Ostrowski on 11.06.2018.
+ */
+@Parcel(Parcel.Serialization.BEAN)
+public class OAuthTokenModel {
+
+ @SerializedName("oauth_token_secret")
+ private String tokenSecret;
+ @SerializedName("oauth_token")
+ private String token;
+
+ public OAuthTokenModel() {
+ }
+
+ public String getTokenSecret() {
+ return tokenSecret;
+ }
+
+ public void setTokenSecret(String tokenSecret) {
+ this.tokenSecret = tokenSecret;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/ReadingStateModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/ReadingStateModel.java
new file mode 100644
index 0000000..698c6d8
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/ReadingStateModel.java
@@ -0,0 +1,61 @@
+package com.moiseum.wolnelektury.connection.models;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Created by Piotr Ostrowski on 23.06.2018.
+ */
+public class ReadingStateModel {
+
+ private static final String UNKNOWN = "unknown";
+ private static final String NOT_STARTED = "not_started";
+ private static final String READING = "reading";
+ private static final String COMPLETED = "complete";
+
+ public enum ReadingState {
+ @SerializedName(UNKNOWN)
+ STATE_UNKNOWN {
+ @Override
+ public String getStateName() {
+ return UNKNOWN;
+ }
+ },
+ @SerializedName(NOT_STARTED)
+ STATE_NOT_STARTED {
+ @Override
+ public String getStateName() {
+ return NOT_STARTED;
+ }
+ },
+ @SerializedName(READING)
+ STATE_READING {
+ @Override
+ public String getStateName() {
+ return READING;
+ }
+ },
+ @SerializedName(COMPLETED)
+ STATE_COMPLETED {
+ @Override
+ public String getStateName() {
+ return COMPLETED;
+ }
+ };
+
+ public abstract String getStateName();
+ }
+
+ private ReadingState state;
+
+ public ReadingStateModel() {
+ this.state = ReadingState.STATE_UNKNOWN;
+ }
+
+ public ReadingState getState() {
+ return state;
+ }
+
+ public void setState(ReadingState state) {
+ this.state = state;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/UserModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/UserModel.java
new file mode 100644
index 0000000..7f5c6d1
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/UserModel.java
@@ -0,0 +1,25 @@
+package com.moiseum.wolnelektury.connection.models;
+
+/**
+ * Created by Piotr Ostrowski on 21.06.2018.
+ */
+public class UserModel {
+ private String username;
+ private boolean premium;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public boolean isPremium() {
+ return premium;
+ }
+
+ public void setPremium(boolean premium) {
+ this.premium = premium;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/BooksService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/BooksService.java
new file mode 100644
index 0000000..1657930
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/BooksService.java
@@ -0,0 +1,81 @@
+package com.moiseum.wolnelektury.connection.services;
+
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.models.FavouriteStateModel;
+import com.moiseum.wolnelektury.connection.models.ReadingStateModel;
+
+import java.util.List;
+
+import io.reactivex.Single;
+import okhttp3.ResponseBody;
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+import retrofit2.http.Streaming;
+import retrofit2.http.Url;
+
+/**
+ * Created by Piotr Ostrowski on 16.11.2017.
+ */
+
+public interface BooksService {
+
+ @Headers("New-Api: true")
+ @GET("filter-books/")
+ Call> getSearchBooks(@Query("search") String search, @Query("epochs") String epochs, @Query("genres") String genres, @Query("kinds") String kinds,
+ @Query("audiobook") Boolean audiobook, @Query("lektura") Boolean lecture, @Query("after") String lastKey, @Query("count") int count);
+
+ @GET("books/{slug}")
+ Single getBookDetails(@Path("slug") String slug);
+
+ @Streaming
+ @GET
+ Call downloadFileWithUrl(@Url String fileUrl);
+
+ @Headers("New-Api: true")
+ @GET("newest/")
+ Call> getNewest();
+
+ @Headers("New-Api: true")
+ @GET("recommended/")
+ Call> getRecommended();
+
+ @Headers("New-Api: true")
+ @GET("audiobooks/")
+ Call> getAudiobooks(@Query("after") String lastKey, @Query("count") int count);
+
+ @Headers("Authentication-Required: true")
+ @POST("reading/{slug}/{state}/")
+ Single setReadingState(@Path("slug") String slug, @Path("state") String state);
+
+ @Headers("Authentication-Required: true")
+ @GET("reading/{slug}/")
+ Single getReadingState(@Path("slug") String slug);
+
+ @Headers({"Authentication-Required: true", "New-Api: true"})
+ @GET("shelf/{state}/")
+ Call> getReadenBooks(@Path("state") String state, @Query("after") String lastKey, @Query("count") int count);
+
+ @Headers("Authentication-Required: true")
+ @POST("like/{slug}/")
+ Single setFavouriteState(@Path("slug") String slug, @Query("action") String action);
+
+ @Headers("Authentication-Required: true")
+ @GET("like/{slug}/")
+ Single getFavouriteState(@Path("slug") String slug);
+
+ @GET("preview/")
+ Call> getPreview();
+
+ @GET("books/{slug}")
+ Call getPreviewMockup(@Path("slug") String slug);
+
+ @Headers({"Authentication-Required: true", "New-Api: true"})
+ @GET("shelf/likes/")
+ Call> getFavourites(@Query("after") String lastKey, @Query("count") int count);
+
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/CategoriesService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/CategoriesService.java
new file mode 100644
index 0000000..c51b088
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/CategoriesService.java
@@ -0,0 +1,25 @@
+package com.moiseum.wolnelektury.connection.services;
+
+import com.moiseum.wolnelektury.connection.models.CategoryModel;
+
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Query;
+
+/**
+ * Created by piotrostrowski on 25.11.2017.
+ */
+
+public interface CategoriesService {
+
+ @GET("epochs")
+ Call> getEpochs(@Query("book_only") boolean bookOnly);
+
+ @GET("genres")
+ Call> getGenres(@Query("book_only") boolean bookOnly);
+
+ @GET("kinds")
+ Call> getKinds(@Query("book_only") boolean bookOnly);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/NewsService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/NewsService.java
new file mode 100644
index 0000000..fa317eb
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/NewsService.java
@@ -0,0 +1,16 @@
+package com.moiseum.wolnelektury.connection.services;
+
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface NewsService {
+
+ @GET("blog")
+ Call> getNews(@Query("after") String lastKey, @Query("count") int count);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/UserService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/UserService.java
new file mode 100644
index 0000000..9ce35a9
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/UserService.java
@@ -0,0 +1,26 @@
+package com.moiseum.wolnelektury.connection.services;
+
+import com.moiseum.wolnelektury.connection.models.OAuthTokenModel;
+import com.moiseum.wolnelektury.connection.models.UserModel;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Headers;
+
+/**
+ * Created by Piotr Ostrowski on 06.06.2018.
+ */
+public interface UserService {
+
+ @Headers("Token-Requested: true")
+ @GET("oauth/request_token/")
+ Call requestToken();
+
+ @Headers("Token-Requested: true")
+ @GET("oauth/access_token/")
+ Call accessToken();
+
+ @Headers("Authentication-Required: true")
+ @GET("username/")
+ Call getUser();
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/events/BookFavouriteEvent.java b/Android/app/src/main/java/com/moiseum/wolnelektury/events/BookFavouriteEvent.java
new file mode 100644
index 0000000..0bd4497
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/events/BookFavouriteEvent.java
@@ -0,0 +1,13 @@
+package com.moiseum.wolnelektury.events;
+
+public class BookFavouriteEvent {
+ private boolean state;
+
+ public BookFavouriteEvent(boolean state) {
+ this.state = state;
+ }
+
+ public boolean getState() {
+ return state;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/events/LoggedInEvent.java b/Android/app/src/main/java/com/moiseum/wolnelektury/events/LoggedInEvent.java
new file mode 100644
index 0000000..df2cbd7
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/events/LoggedInEvent.java
@@ -0,0 +1,7 @@
+package com.moiseum.wolnelektury.events;
+
+/**
+ * Created by Piotr Ostrowski on 11.09.2018.
+ */
+public class LoggedInEvent {
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/storage/BookStorage.java b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/BookStorage.java
new file mode 100644
index 0000000..31d6628
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/BookStorage.java
@@ -0,0 +1,91 @@
+package com.moiseum.wolnelektury.storage;
+
+import android.app.Application;
+import android.support.annotation.Nullable;
+
+import com.moiseum.wolnelektury.BuildConfig;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.models.BookModel_;
+import com.moiseum.wolnelektury.connection.models.MyObjectBox;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.List;
+
+import io.objectbox.Box;
+import io.objectbox.BoxStore;
+import io.objectbox.android.AndroidObjectBrowser;
+import io.objectbox.query.Query;
+
+/**
+ * @author golonkos
+ */
+
+public class BookStorage {
+
+
+ public static class BookAddedEvent {
+ }
+
+ public static class BookDeletedEvent {
+ private final String slug;
+
+ public BookDeletedEvent(String slug) {
+ this.slug = slug;
+ }
+
+ public String getSlug() {
+ return slug;
+ }
+ }
+
+ private BoxStore boxStore;
+
+ public BookStorage(Application application) {
+ boxStore = MyObjectBox.builder().androidContext(application).build();
+ if (BuildConfig.DEBUG) {
+ new AndroidObjectBrowser(boxStore).start(application);
+ }
+ }
+
+ private Box getBox() {
+ return boxStore.boxFor(BookModel.class);
+ }
+
+ public void add(BookModel book) {
+ getBox().put(book);
+ EventBus.getDefault().post(new BookAddedEvent());
+ }
+
+ public void update(BookModel book) {
+ getBox().put(book);
+ }
+
+ @Nullable
+ public BookModel find(String slug) {
+ Query query = getBox().query().equal(BookModel_.slug, slug).build();
+ return query.findFirst();
+ }
+
+ public boolean exists(String slug) {
+ return find(slug) != null;
+ }
+
+ public void remove(String slug, boolean notify) {
+ BookModel book = find(slug);
+ if (book != null) {
+ getBox().remove(book);
+ if (notify) {
+ EventBus.getDefault().post(new BookDeletedEvent(slug));
+ }
+ }
+ }
+
+ public List all() {
+ return getBox().getAll();
+ }
+
+ public void removeAll() {
+ getBox().removeAll();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/storage/StringListConverter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/StringListConverter.java
new file mode 100644
index 0000000..24b45b7
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/StringListConverter.java
@@ -0,0 +1,37 @@
+package com.moiseum.wolnelektury.storage;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import io.objectbox.converter.PropertyConverter;
+
+/**
+ * Created by Piotr Ostrowski on 01.07.2018.
+ */
+public class StringListConverter implements PropertyConverter, String> {
+
+ @Override
+ public List convertToEntityProperty(String databaseValue) {
+ if (databaseValue == null) {
+ return new ArrayList<>();
+ }
+ return Arrays.asList(databaseValue.split(","));
+ }
+
+ @Override
+ public String convertToDatabaseValue(List entityProperty) {
+ if (entityProperty == null) {
+ return "";
+ }
+ if (entityProperty.isEmpty()) {
+ return "";
+ }
+ StringBuilder builder = new StringBuilder();
+ for (String property : entityProperty) {
+ builder.append(property).append(",");
+ }
+ builder.deleteCharAt(builder.length() - 1);
+ return builder.toString();
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/utils/SharedPreferencesUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/SharedPreferencesUtils.java
new file mode 100644
index 0000000..6120e64
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/SharedPreferencesUtils.java
@@ -0,0 +1,123 @@
+package com.moiseum.wolnelektury.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.moiseum.wolnelektury.connection.models.OAuthTokenModel;
+
+import de.adorsys.android.securestoragelibrary.SecurePreferences;
+
+/**
+ * Created by Piotr Ostrowski on 14.06.2018.
+ */
+public final class SharedPreferencesUtils {
+
+ private static final String PREFERENCES_FILENAME = "WolneLekturyPreferences";
+ private static final String ACCESS_TOKEN_KEY = "AccessToken";
+ private static final String ACCESS_TOKEN_SECRET_KEY = "AccessTokenSecret";
+ private static final String USERNAME_KEY = "Username";
+ private static final String PREMIUM_KEY = "Premium";
+ private static final String NOTIFICATIONS_KEY = "Notifications";
+ private static final String TEMPORARY_LOGIN_TOKEN_KEY = "TemporaryLoginTokenKey";
+
+ private OAuthTokenModel currentToken;
+ private String username;
+ private Boolean isPremium;
+ private Boolean notifications;
+ private String temporaryLoginToken;
+ private SharedPreferences preferences;
+
+ public SharedPreferencesUtils(Context context) {
+ this.preferences = context.getSharedPreferences(PREFERENCES_FILENAME, Context.MODE_PRIVATE);
+ }
+
+ public void storeAccessToken(OAuthTokenModel tokenModel) {
+ currentToken = tokenModel;
+ SecurePreferences.setValue(ACCESS_TOKEN_KEY, tokenModel.getToken());
+ SecurePreferences.setValue(ACCESS_TOKEN_SECRET_KEY, tokenModel.getTokenSecret());
+ }
+
+ public OAuthTokenModel getAccessToken() {
+ if (currentToken != null) {
+ return currentToken;
+ }
+
+ String token = SecurePreferences.getStringValue(ACCESS_TOKEN_KEY, null);
+ String tokenSecret = SecurePreferences.getStringValue(ACCESS_TOKEN_SECRET_KEY, null);
+
+ if (token == null || tokenSecret == null) {
+ return null;
+ }
+ currentToken = new OAuthTokenModel();
+ currentToken.setToken(token);
+ currentToken.setTokenSecret(tokenSecret);
+ return currentToken;
+ }
+
+ public String getUsername() {
+ if (username == null) {
+ username = preferences.getString(USERNAME_KEY, null);
+ }
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ preferences.edit().putString(USERNAME_KEY, username).apply();
+ }
+
+ public boolean isUserLoggedIn() {
+ return getAccessToken() != null;
+ }
+
+ public boolean isUserPremium() {
+ if (isPremium == null) {
+ isPremium = preferences.getBoolean(PREMIUM_KEY, false);
+ }
+ return isPremium && isUserLoggedIn();
+ }
+
+ public void setPremium(boolean isPremium) {
+ this.isPremium = isPremium;
+ preferences.edit().putBoolean(PREMIUM_KEY, isPremium).apply();
+ }
+
+ public boolean getNotifications() {
+ if (notifications == null) {
+ notifications = preferences.getBoolean(NOTIFICATIONS_KEY, true);
+ }
+ return notifications;
+ }
+
+ public void setNotifications(Boolean notifications) {
+ this.notifications = notifications;
+ preferences.edit().putBoolean(NOTIFICATIONS_KEY, notifications).apply();
+ }
+
+ public String getTemporaryLoginToken() {
+ if (temporaryLoginToken == null) {
+ temporaryLoginToken = preferences.getString(TEMPORARY_LOGIN_TOKEN_KEY, null);
+ }
+ return temporaryLoginToken;
+ }
+
+ public void setTemporaryLoginToken(String temporaryLoginToken) {
+ this.temporaryLoginToken = temporaryLoginToken;
+ preferences.edit().putString(TEMPORARY_LOGIN_TOKEN_KEY, temporaryLoginToken).apply();
+ }
+
+ public void clearUserData() {
+ currentToken = null;
+ username = null;
+ isPremium = null;
+ notifications = null;
+ temporaryLoginToken = null;
+ SecurePreferences.clearAllValues();
+ preferences.edit()
+ .remove(USERNAME_KEY)
+ .remove(PREMIUM_KEY)
+ .remove(NOTIFICATIONS_KEY)
+ .remove(TEMPORARY_LOGIN_TOKEN_KEY)
+ .apply();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/utils/StringUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/StringUtils.java
new file mode 100644
index 0000000..d890e9a
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/StringUtils.java
@@ -0,0 +1,42 @@
+package com.moiseum.wolnelektury.utils;
+
+import com.moiseum.wolnelektury.connection.models.CategoryModel;
+
+import java.util.List;
+
+/**
+ * Created by piotrostrowski on 19.11.2017.
+ */
+
+public class StringUtils {
+
+ public static String joinCategory(List list, String delim) {
+ StringBuilder sb = new StringBuilder();
+ String loopDelimiter = "";
+
+ for (CategoryModel s : list) {
+ sb.append(loopDelimiter);
+ sb.append(s.getName());
+ loopDelimiter = delim;
+ }
+
+ return sb.toString();
+ }
+
+ public static String joinSlugs(List list, String delim) {
+ if (list.size() == 0) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ String loopDelimiter = "";
+
+ for (CategoryModel s : list) {
+ sb.append(loopDelimiter);
+ sb.append(s.getSlug());
+ loopDelimiter = delim;
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/utils/TrackerUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/TrackerUtils.java
new file mode 100644
index 0000000..40d694c
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/TrackerUtils.java
@@ -0,0 +1,29 @@
+package com.moiseum.wolnelektury.utils;
+
+import android.content.Context;
+
+import com.moiseum.wolnelektury.base.WLApplication;
+
+import org.piwik.sdk.Piwik;
+import org.piwik.sdk.Tracker;
+import org.piwik.sdk.TrackerConfig;
+import org.piwik.sdk.extra.TrackHelper;
+
+/**
+ * @author golonkos
+ */
+
+public final class TrackerUtils {
+
+ private static final String PIWIK_URL = "https://piwik.nowoczesnapolska.org.pl/nocas/piwik.php";
+ private static final String TRACKER_NAME = "MainTracker";
+
+ public static Tracker create(Context context) {
+ return Piwik.getInstance(context).newTracker(new TrackerConfig(PIWIK_URL, 29, TRACKER_NAME));
+ }
+
+ public static void trackScreen(String path, String name) {
+ Tracker tracker = WLApplication.getInstance().getTracker();
+ TrackHelper.track().screen(path).title(name).with(tracker);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/AboutFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/AboutFragment.java
new file mode 100644
index 0000000..e6c1144
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/AboutFragment.java
@@ -0,0 +1,75 @@
+package com.moiseum.wolnelektury.view;
+
+import android.os.Bundle;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractFragment;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.events.LoggedInEvent;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+/**
+ * @author golonkos
+ */
+
+public class AboutFragment extends AbstractFragment {
+
+ public static AboutFragment newInstance() {
+ return new AboutFragment();
+ }
+
+ @BindView(R.id.btnBecomeAFriend)
+ Button btnBecomeAFriend;
+ @BindView(R.id.tvAbout)
+ TextView tvAbout;
+
+ private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_about;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ btnBecomeAFriend.setVisibility(preferences.isUserPremium() ? View.GONE : View.VISIBLE);
+ tvAbout.setText(Html.fromHtml(getString(R.string.about_text)));
+ tvAbout.setLinksClickable(true);
+ tvAbout.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ EventBus.getDefault().unregister(this);
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onLoggedIn(LoggedInEvent event) {
+ btnBecomeAFriend.setVisibility(preferences.isUserPremium() ? View.GONE : View.VISIBLE);
+ }
+
+ @OnClick(R.id.btnBecomeAFriend)
+ public void onBecomeAFriendClicked() {
+ showPayPalForm();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewActivity.java
new file mode 100644
index 0000000..1e30d1f
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewActivity.java
@@ -0,0 +1,42 @@
+package com.moiseum.wolnelektury.view;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractActivity;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+
+/**
+ * @author golonkos
+ */
+
+public class WebViewActivity extends AbstractActivity {
+
+ private static final String PARAM_URL = "PARAM_URL";
+
+ public static class WebViewIntent extends AbstractIntent {
+
+ public WebViewIntent(Context packageContext, String url) {
+ super(packageContext, WebViewActivity.class);
+ putExtra(PARAM_URL, url);
+ }
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_blank;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+ setBackButtonEnable(true);
+
+ if (savedInstanceState == null) {
+ String url = getIntent().getStringExtra(PARAM_URL);
+ Fragment fragment = WebViewFragment.newInstance(url);
+ getSupportFragmentManager().beginTransaction().add(R.id.flContainer, fragment).commit();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewFragment.java
new file mode 100644
index 0000000..38bc5dc
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewFragment.java
@@ -0,0 +1,115 @@
+package com.moiseum.wolnelektury.view;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.View;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractFragment;
+
+import butterknife.BindView;
+
+/**
+ * @author golonkos.
+ */
+
+public class WebViewFragment extends AbstractFragment {
+
+ private static final String PARAM_URL = "PARAM_URL";
+
+ @BindView(R.id.wvAbout)
+ WebView wvPage;
+ @BindView(R.id.btnBack)
+ Button btnBack;
+ @BindView(R.id.btnRefresh)
+ Button btnRefresh;
+ @BindView(R.id.btnNext)
+ Button btnNext;
+ @BindView(R.id.tvPageError)
+ TextView tvPageError;
+
+ private boolean loadFailed;
+
+ public static Fragment newInstance(String url) {
+ WebViewFragment fragment = new WebViewFragment();
+ Bundle args = new Bundle(1);
+ args.putString(PARAM_URL, url);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_web_view;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ initWebView();
+ }
+
+ private void initWebView() {
+ wvPage.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
+ wvPage.getSettings().setJavaScriptEnabled(true);
+ wvPage.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
+ wvPage.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ wvPage.loadUrl(url);
+ return true;
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ if (tvPageError == null) {
+ return;
+ }
+ loadFailed = true;
+ tvPageError.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ if (tvPageError == null) {
+ return;
+ }
+ if (!loadFailed) {
+ tvPageError.setVisibility(View.GONE);
+ }
+ btnBack.setEnabled(wvPage.canGoBack());
+ btnNext.setEnabled(wvPage.canGoForward());
+ }
+ });
+ String url = getArguments().getString(PARAM_URL);
+ wvPage.loadUrl(url);
+
+ btnRefresh.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ loadFailed = false;
+ wvPage.reload();
+ }
+ });
+
+ btnBack.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ wvPage.goBack();
+ }
+ });
+
+ btnNext.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ wvPage.goForward();
+ }
+ });
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookActivity.java
new file mode 100644
index 0000000..8f542af
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookActivity.java
@@ -0,0 +1,53 @@
+package com.moiseum.wolnelektury.view.book;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractActivity;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+
+/**
+ * Created by Piotr Ostrowski on 17.11.2017.
+ */
+
+public class BookActivity extends AbstractActivity {
+
+ private static final String BOOK_FRAGMENT_TAG = "BookFragmentTag";
+ static final String BOOK_SLUG_KEY = "BookSlugKey";
+ static final String BOOK_TYPE_KEY = "BookTypeKey";
+
+ public static class BookIntent extends AbstractIntent {
+
+ public BookIntent(String slug, BookType type, Context context) {
+ super(context, BookActivity.class);
+ putExtra(BOOK_SLUG_KEY, slug);
+ putExtra(BOOK_TYPE_KEY, type.name());
+ }
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_blank;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+ setTitle("");
+ if (!getIntent().hasExtra(BOOK_SLUG_KEY)) {
+ throw new IllegalStateException("Missing either slug or full ebook model.");
+ }
+ if (!getIntent().hasExtra(BOOK_TYPE_KEY)) {
+ throw new IllegalStateException("Missing book type.");
+ }
+
+ String bookSlug = getIntent().getStringExtra(BOOK_SLUG_KEY);
+ BookType type = BookType.valueOf(getIntent().getStringExtra(BOOK_TYPE_KEY));
+
+ BookFragment bookFragment = (BookFragment) getSupportFragmentManager().findFragmentByTag(BOOK_FRAGMENT_TAG);
+ if (bookFragment == null) {
+ bookFragment = BookFragment.newInstance(bookSlug, type);
+ getSupportFragmentManager().beginTransaction().add(R.id.flContainer, bookFragment, BOOK_FRAGMENT_TAG).commit();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookFragment.java
new file mode 100644
index 0000000..66282c3
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookFragment.java
@@ -0,0 +1,358 @@
+package com.moiseum.wolnelektury.view.book;
+
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.os.Bundle;
+import android.support.constraint.ConstraintLayout;
+import android.support.design.widget.AppBarLayout;
+import android.support.design.widget.CollapsingToolbarLayout;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.facebook.shimmer.ShimmerFrameLayout;
+import com.folioreader.Config;
+import com.folioreader.util.FolioReader;
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.utils.StringUtils;
+import com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton;
+import com.moiseum.wolnelektury.view.player.PlayerActivity;
+
+import org.sufficientlysecure.htmltextview.HtmlTextView;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+import static android.app.Activity.RESULT_OK;
+import static com.folioreader.ui.folio.activity.FolioActivity.PARAM_CHAPTERS_COUNT;
+import static com.folioreader.ui.folio.activity.FolioActivity.PARAM_CURRENT_CHAPTER;
+import static com.folioreader.ui.folio.activity.FolioActivity.PARAM_FILE_NAME;
+import static com.moiseum.wolnelektury.view.book.BookActivity.BOOK_SLUG_KEY;
+import static com.moiseum.wolnelektury.view.book.BookActivity.BOOK_TYPE_KEY;
+
+/**
+ * Created by Piotr Ostrowski on 17.11.2017.
+ */
+
+public class BookFragment extends PresenterFragment implements BookView {
+
+ private static final int BOOK_READER_CODE = 32434;
+ private static final String DEFAULT_OVERLAY_COLOR = "#80db4b16";
+
+ public static BookFragment newInstance(String slug, BookType type) {
+ BookFragment bookFragment = new BookFragment();
+ Bundle args = new Bundle();
+ args.putString(BOOK_SLUG_KEY, slug);
+ args.putString(BOOK_TYPE_KEY, type.name());
+ bookFragment.setArguments(args);
+ return bookFragment;
+ }
+
+ @BindView(R.id.clMainView)
+ View clMainView;
+ @BindView(R.id.ctlCollapse)
+ CollapsingToolbarLayout ctlCollapse;
+ @BindView(R.id.ivCoverBackground)
+ ImageView ivCoverBackground;
+ @BindView(R.id.ivCover)
+ ImageView ivCover;
+ @BindView(R.id.vCoverOverlay)
+ View vCoverOverlay;
+ @BindView(R.id.tvBookAuthor)
+ TextView tvBookAuthor;
+ @BindView(R.id.tvBookTitle)
+ TextView tvBookTitle;
+ @BindView(R.id.btnEbook)
+ ProgressDownloadButton btnEbook;
+ @BindView(R.id.btnAudiobook)
+ ProgressDownloadButton btnAudiobook;
+ @BindView(R.id.tvBookKind)
+ TextView tvBookKind;
+ @BindView(R.id.tvBookGenre)
+ TextView tvBookGenre;
+ @BindView(R.id.tvBookEpoch)
+ TextView tvBookEpoch;
+ @BindView(R.id.tvQuotationText)
+ HtmlTextView tvQuotationText;
+ @BindView(R.id.tvQuotationAuthor)
+ TextView tvQuotationAuthor;
+ @BindView(R.id.ibDeleteEbook)
+ ImageButton ibDeleteEbook;
+ @BindView(R.id.ibDeleteAudiobook)
+ ImageButton ibDeleteAudiobook;
+ @BindView(R.id.pbHeaderLoading)
+ ProgressBar pbHeaderLoading;
+ @BindView(R.id.rlHeaderLoadingContainer)
+ RelativeLayout rlHeaderLoadingContainer;
+ @BindView(R.id.fabShare)
+ FloatingActionButton fabShare;
+ @BindView(R.id.fabFavourite)
+ FloatingActionButton fabFavourite;
+ @BindView(R.id.shimmerContentContainer)
+ ShimmerFrameLayout shimmerContentContainer;
+ @BindView(R.id.rlEbookButtonsContainer)
+ RelativeLayout rlEbookButtonsContainer;
+ @BindView(R.id.rlAudioButtonsContainer)
+ RelativeLayout rlAudioButtonsContainer;
+ @BindView(R.id.vSecondDivider)
+ View vSecondDivider;
+ @BindView(R.id.ibRetry)
+ ImageButton ibRetry;
+ @BindView(R.id.clPremium)
+ View clPremium;
+
+ @Override
+ protected BookPresenter createPresenter() {
+ if (getArguments() == null || getArguments().getString(BOOK_SLUG_KEY) == null || getArguments().getString(BOOK_TYPE_KEY) == null) {
+ throw new IllegalStateException("Missing BookDetails data!");
+ }
+ BookType type = BookType.valueOf(getArguments().getString(BOOK_TYPE_KEY));
+ return new BookPresenter(getArguments().getString(BOOK_SLUG_KEY), type, this);
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_book;
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ shimmerContentContainer.startShimmerAnimation();
+ Toolbar toolbar = view.findViewById(R.id.bookToolbar);
+ setupToolbar(toolbar);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == BOOK_READER_CODE && resultCode == RESULT_OK) {
+ String bookName = data.getStringExtra(PARAM_FILE_NAME);
+ int currentChapter = data.getIntExtra(PARAM_CURRENT_CHAPTER, 0);
+ int count = data.getIntExtra(PARAM_CHAPTERS_COUNT, 0);
+ getPresenter().onBackFromReader(bookName, currentChapter, count);
+ }
+ }
+
+ @OnClick(R.id.btnEbook)
+ public void onEbookClick() {
+ getPresenter().launchEbookForState(btnEbook.getState());
+ }
+
+ @OnClick(R.id.btnAudiobook)
+ public void onAudiobookClick() {
+ getPresenter().launchAudiobookForState(btnAudiobook.getState());
+ }
+
+ @OnClick(R.id.ibDeleteEbook)
+ public void onDeleteEbookClick() {
+ getPresenter().deleteEbook();
+ Toast.makeText(getContext() ,getString(R.string.book_deleted_message), Toast.LENGTH_SHORT).show();
+ }
+
+ @OnClick(R.id.ibDeleteAudiobook)
+ public void onDeleteAudiobookClick() {
+ getPresenter().deleteAudiobook();
+ Toast.makeText(getContext() ,getString(R.string.book_deleted_message), Toast.LENGTH_SHORT).show();
+ }
+
+ @OnClick(R.id.ibRetry)
+ public void onRetryClick() {
+ ibRetry.setVisibility(View.GONE);
+ pbHeaderLoading.setVisibility(View.VISIBLE);
+ getPresenter().reloadBookDetails();
+ }
+
+ @OnClick(R.id.fabShare)
+ public void onShareClick() {
+ getPresenter().onShareEbookClicked();
+ }
+
+ @OnClick(R.id.fabFavourite)
+ public void onFavouriteClick() {
+ getPresenter().onFavouriteEbookClicked();
+ }
+
+ @OnClick(R.id.bSupportUs)
+ public void onSupportUsClick() {
+ showPayPalForm();
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------------------
+ // BookView
+ // ------------------------------------------------------------------------------------------------------------------------------------------
+
+ @Override
+ public void initializeBookView(BookDetailsModel book) {
+ shimmerContentContainer.stopShimmerAnimation();
+ fabShare.setVisibility(View.VISIBLE);
+ getPresenter().showFavouriteButton(book);
+ rlHeaderLoadingContainer.setVisibility(View.GONE);
+ rlEbookButtonsContainer.setVisibility(View.VISIBLE);
+
+ if (book.hasAudio()) {
+ rlAudioButtonsContainer.setVisibility(View.VISIBLE);
+ }
+ ctlCollapse.setTitle(book.getTitle());
+ ctlCollapse.setExpandedTitleColor(getResources().getColor(android.R.color.transparent));
+
+ Glide.with(getContext()).load(book.getCover()).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCoverBackground);
+ Glide.with(getContext()).load(book.getCover()).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCover);
+
+ vCoverOverlay.setAlpha(0.7f);
+ String colorHash = book.getCoverColor() != null ? book.getCoverColor() : DEFAULT_OVERLAY_COLOR;
+ vCoverOverlay.setBackgroundColor(Color.parseColor(colorHash));
+
+ if (book.getAuthors() != null && book.getAuthors().size() > 0) {
+ tvBookAuthor.setText(StringUtils.joinCategory(book.getAuthors(), ", "));
+ }
+ tvBookTitle.setText(book.getTitle());
+
+ if (book.getKinds() != null && book.getKinds().size() > 0) {
+ tvBookKind.setText(StringUtils.joinCategory(book.getKinds(), ", "));
+ }
+ tvBookKind.setBackgroundColor(Color.TRANSPARENT);
+ if (book.getGenres() != null && book.getGenres().size() > 0) {
+ tvBookGenre.setText(StringUtils.joinCategory(book.getGenres(), ", "));
+ }
+ tvBookGenre.setBackgroundColor(Color.TRANSPARENT);
+ if (book.getEpochs() != null && book.getEpochs().size() > 0) {
+ tvBookEpoch.setText(StringUtils.joinCategory(book.getEpochs(), ", "));
+ }
+ tvBookEpoch.setBackgroundColor(Color.TRANSPARENT);
+
+ if (book.getFragment() != null) {
+ tvQuotationText.setMinLines(0);
+ tvQuotationText.setHtml(book.getFragment().getHtml());
+ tvQuotationText.setBackgroundColor(Color.TRANSPARENT);
+ tvQuotationAuthor.setText(book.getFragment().getTitle());
+ tvQuotationAuthor.setBackgroundColor(Color.TRANSPARENT);
+ } else {
+ tvQuotationText.setVisibility(View.GONE);
+ tvQuotationAuthor.setVisibility(View.GONE);
+ vSecondDivider.setVisibility(View.GONE);
+ }
+ enableToolbarCollapse();
+ }
+
+ private void enableToolbarCollapse() {
+ ViewTreeObserver viewTreeObserver = shimmerContentContainer.getViewTreeObserver();
+ if (viewTreeObserver.isAlive()) {
+ viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ shimmerContentContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ if (ctlCollapse.getHeight() + shimmerContentContainer.getHeight() > clMainView.getHeight()) {
+ AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) ctlCollapse.getLayoutParams();
+ params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); // list other flags here by |
+ ctlCollapse.setLayoutParams(params);
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState state, boolean forAudiobook) {
+ if (forAudiobook) {
+ ibDeleteAudiobook.setVisibility(state.isDeletable() ? View.VISIBLE : View.INVISIBLE);
+ btnAudiobook.setState(state);
+ } else {
+ ibDeleteEbook.setVisibility(state.isDeletable() ? View.VISIBLE : View.INVISIBLE);
+ btnEbook.setState(state);
+ }
+ }
+
+ @Override
+ public void showCurrentStateProgress(int percentage, boolean forAudiobook) {
+ if (forAudiobook) {
+ btnAudiobook.setProgress(percentage);
+ } else {
+ btnEbook.setProgress(percentage);
+ }
+ }
+
+ @Override
+ public void showInitializationError() {
+ Toast.makeText(getContext(), R.string.book_loading_error, Toast.LENGTH_LONG).show();
+ pbHeaderLoading.setVisibility(View.GONE);
+ ibRetry.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void showDownloadFileError() {
+ Toast.makeText(getContext(), R.string.book_download_error, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void startShareActivity(String shareUrl) {
+ showShareActivity(shareUrl);
+ }
+
+ @Override
+ public void openBook(String downloadedBookUrl) {
+ FolioReader reader = new FolioReader(getContext());
+ Config config = new Config.ConfigBuilder().build();
+ Intent bookIntent = reader.createBookIntent(downloadedBookUrl, config);
+ startActivityForResult(bookIntent, BOOK_READER_CODE);
+ }
+
+ @Override
+ public void launchPlayer(BookDetailsModel book) {
+ if (getArguments() != null) {
+ PlayerActivity.PlayerIntent intent = new PlayerActivity.PlayerIntent(book, getArguments().getString(BOOK_SLUG_KEY), getContext());
+ startActivity(intent);
+ }
+ }
+
+ @Override
+ public void updateReadingProgress(int currentChapter, int count, boolean forAudiobook) {
+ if (forAudiobook) {
+ btnAudiobook.setCurrentReadCount(currentChapter + 1, count);
+ } else {
+ btnEbook.setCurrentReadCount(currentChapter, count);
+ }
+ }
+
+ @Override
+ public void startLikeClicked() {
+ fabFavourite.setImageResource(R.drawable.ic_fav_active);
+ }
+
+ @Override
+ public void stopLikeClicked() {
+ fabFavourite.setImageResource(R.drawable.ic_fav);
+ }
+
+ public void showFavouriteButton(BookDetailsModel book) {
+ fabFavourite.setVisibility(View.VISIBLE);
+ if(book.getFavouriteState()) {
+ fabFavourite.setImageResource(R.drawable.ic_fav_active);
+ }
+ }
+
+ @Override
+ public void showPremiumLock(boolean lock) {
+ if (!lock) {
+ clPremium.setVisibility(View.GONE);
+ } else {
+ clPremium.setVisibility(View.VISIBLE);
+ btnEbook.setClickable(false);
+ btnAudiobook.setClickable(false);
+ fabFavourite.setClickable(false);
+ fabShare.setClickable(false);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookPresenter.java
new file mode 100644
index 0000000..726e7eb
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookPresenter.java
@@ -0,0 +1,321 @@
+package com.moiseum.wolnelektury.view.book;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import com.folioreader.util.AppUtil;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils;
+import com.moiseum.wolnelektury.connection.downloads.FileDownloadIntentService;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.models.FavouriteStateModel;
+import com.moiseum.wolnelektury.connection.models.ReadingStateModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+import com.moiseum.wolnelektury.events.BookFavouriteEvent;
+import com.moiseum.wolnelektury.storage.BookStorage;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+import com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.reactivex.Completable;
+import io.reactivex.Single;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+
+/**
+ * Created by Piotr Ostrowski on 17.11.2017.
+ */
+
+class BookPresenter extends FragmentPresenter {
+
+ private static final String TAG = BookPresenter.class.getSimpleName();
+
+ private BookDetailsModel book;
+ private BookModel storedBook;
+ private String bookSlug;
+ private BookType bookType;
+
+ private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+ private BooksService booksService = WLApplication.getInstance().getRestClient().obtainBookService();
+ private BookStorage storage = WLApplication.getInstance().getBookStorage();
+
+ BookPresenter(String slug, BookType type, BookView view) {
+ super(view);
+ this.bookSlug = slug;
+ this.bookType = type;
+ }
+
+ @Override
+ public void onViewCreated(Bundle savedInstanceState) {
+ super.onViewCreated(savedInstanceState);
+ loadBookDetails();
+ getView().showPremiumLock(!preferences.isUserPremium() && bookType.shouldShowPremiumLock());
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ this.storedBook = storage.find(bookSlug);
+ if (storedBook != null && storedBook.isEbookDownloaded()) {
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, false);
+ getView().updateReadingProgress(storedBook.getCurrentChapter(), storedBook.getTotalChapters(), false);
+ }
+ if (storedBook != null && storedBook.isAudioDownloaded()) {
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, true);
+ getView().updateReadingProgress(storedBook.getCurrentAudioChapter(), storedBook.getTotalAudioChapters(), true);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ EventBus.getDefault().unregister(this);
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onMessageEvent(FileCacheUtils.DownloadProgressEvent event) {
+ if (event.getFileUrl().equals(book.getEpub())) {
+ int percentage = (int) ((double) event.getDownloaded() / (double) event.getTotal() * 100.0);
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, false);
+ getView().showCurrentStateProgress(percentage, false);
+ } else if (book.getAudiobookFilesUrls().contains(event.getFileUrl())) {
+ ArrayList filesUrls = book.getAudiobookFilesUrls();
+ int fileIndex = filesUrls.indexOf(event.getFileUrl());
+ double part = (double) event.getDownloaded() / (double) event.getTotal() / (double) filesUrls.size();
+ double completed = (double) fileIndex / (double) filesUrls.size();
+ int percentage = (int) ((part + completed) * 100.0);
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, true);
+ getView().showCurrentStateProgress(percentage, true);
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onMessageEvent(FileDownloadIntentService.DownloadFileEvent event) {
+ if (event.getFileUrl().equals(book.getEpub())) {
+ if (event.isSuccess()) {
+ storeDownloadedBook(false);
+ getView().showCurrentStateProgress(0, false);
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, false);
+ launchFolioReader();
+ } else {
+ getView().showCurrentStateProgress(0, false);
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, false);
+ getView().showDownloadFileError();
+ }
+ } else if (book.getAudiobookFilesUrls().contains(event.getFileUrl())) {
+ if (event.isSuccess()) {
+ ArrayList filesUrls = book.getAudiobookFilesUrls();
+ int fileIndex = filesUrls.indexOf(event.getFileUrl());
+ if (fileIndex == filesUrls.size() - 1) {
+ storeDownloadedBook(true);
+ getView().showCurrentStateProgress(0, true);
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, true);
+ launchAudioPlayer();
+ } else {
+ int percentage = (int) ((fileIndex + 1.0) / ((double) filesUrls.size()) * 100.0);
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, true);
+ getView().showCurrentStateProgress(percentage, true);
+ }
+ } else {
+ getView().showCurrentStateProgress(0, true);
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, true);
+ getView().showDownloadFileError();
+ }
+
+ }
+ }
+
+ void launchEbookForState(ProgressDownloadButton.ProgressDownloadButtonState state) {
+ if (state.isDownloaded()) {
+ launchFolioReader();
+ } else {
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, false);
+ FileDownloadIntentService.downloadFile(getView().getContext(), book.getEpub());
+ }
+ }
+
+ void launchAudiobookForState(ProgressDownloadButton.ProgressDownloadButtonState state) {
+ if (state.isDownloaded()) {
+ launchAudioPlayer();
+ } else {
+ getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, true);
+ FileDownloadIntentService.downloadFiles(getView().getContext(), book.getAudiobookFilesUrls());
+ }
+ }
+
+ void reloadBookDetails() {
+ loadBookDetails();
+ }
+
+ void deleteEbook() {
+ addDisposable(FileCacheUtils.deleteEbookFile(book.getEpub())
+ .andThen(Completable.fromAction(this::updateStoredBookAfterDeletion))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ () -> getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, false),
+ error -> { }
+ )
+ );
+ }
+
+ void deleteAudiobook() {
+ addDisposable(FileCacheUtils.deleteAudiobookFiles(book.getAudiobookFilesUrls())
+ .andThen(Completable.fromAction(this::updateStoredAudiobookAfterDeletion))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ () -> getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, true),
+ error -> { }
+ )
+ );
+ }
+
+ void onShareEbookClicked() {
+ getView().startShareActivity(book.getUrl());
+ }
+
+ void onFavouriteEbookClicked() {
+ if (!book.getFavouriteState()) {
+ getView().startLikeClicked();
+ updateFavouriteState(true);
+ } else {
+ getView().stopLikeClicked();
+ updateFavouriteState(false);
+ }
+ }
+
+ void onBackFromReader(String bookName, int currentChapter, int count) {
+ storedBook.setEbookName(bookName);
+ storedBook.setCurrentChapter(currentChapter);
+ storedBook.setTotalChapters(count);
+ storage.update(storedBook);
+ getView().updateReadingProgress(currentChapter, count, false);
+
+ if (currentChapter == count && book.getState() == ReadingStateModel.ReadingState.STATE_READING) {
+ updateReadingState(ReadingStateModel.ReadingState.STATE_COMPLETED);
+ }
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------------------
+ // Helper methods
+ // ------------------------------------------------------------------------------------------------------------------------------------------
+
+ private void updateStoredBookAfterDeletion() {
+ if (!storedBook.isAudioDownloaded()) {
+ storage.remove(storedBook.getSlug(), true);
+ storedBook = null;
+ return;
+ }
+
+ AppUtil.removeBookState(WLApplication.getInstance().getApplicationContext(), storedBook.getEbookName());
+ storedBook.setEbookName(null);
+ storedBook.setEbookFileUrl(null);
+ storedBook.setCurrentChapter(0);
+ storedBook.setTotalChapters(0);
+ storage.update(storedBook);
+ }
+
+ private void updateStoredAudiobookAfterDeletion() {
+ if (!storedBook.isEbookDownloaded()) {
+ storage.remove(storedBook.getSlug(), true);
+ storedBook = null;
+ return;
+ }
+
+ storedBook.setAudioFileUrls(null);
+ storedBook.setCurrentAudioChapter(0);
+ storedBook.setTotalAudioChapters(0);
+ storage.update(storedBook);
+ }
+
+ private Single getBookDetails() {
+ Single readingStateSingle = preferences.isUserLoggedIn() ? booksService.getReadingState(bookSlug) : Single.just(new ReadingStateModel());
+ Single favouriteStateModelSingle = preferences.isUserLoggedIn() ? booksService.getFavouriteState(bookSlug) : Single.just(new FavouriteStateModel());
+ Single bookDetailsSingle = Single.zip(
+ readingStateSingle,
+ favouriteStateModelSingle,
+ booksService.getBookDetails(bookSlug),
+ (readingStateModel, favouriteStateModel, bookDetailsModel) -> {
+ bookDetailsModel.setState(readingStateModel.getState());
+ bookDetailsModel.setFavouriteState(favouriteStateModel.getState());
+ return bookDetailsModel;
+ });
+ return bookDetailsSingle.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ }
+
+ private void loadBookDetails() {
+ addDisposable(getBookDetails().subscribe(bookDetailsModel -> {
+ book = bookDetailsModel;
+ getView().initializeBookView(bookDetailsModel);
+ }, error -> getView().showInitializationError()));
+ }
+
+ private void updateReadingState(ReadingStateModel.ReadingState state) {
+ if (preferences.isUserLoggedIn()) {
+ addDisposable(booksService.setReadingState(bookSlug, state.getStateName())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(updatedState -> book.setState(updatedState.getState()), error -> Log.e(TAG, "Failed to update reading state.", error)));
+ }
+ }
+
+ private void updateFavouriteState(boolean state) {
+ addDisposable(booksService.setFavouriteState(bookSlug, book.getFavouriteString(state))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(updatedState -> {
+ book.setFavouriteState(state);
+ EventBus.getDefault().post(new BookFavouriteEvent(state));
+ }, error -> Log.e(TAG, "Failed to update favourite state.", error)));
+ }
+
+ private void launchFolioReader() {
+ if (book.getState() == ReadingStateModel.ReadingState.STATE_NOT_STARTED) {
+ updateReadingState(ReadingStateModel.ReadingState.STATE_READING);
+ }
+ String downloadedBookUrl = FileCacheUtils.getCachedFileForUrl(book.getEpub());
+ if (downloadedBookUrl != null) {
+ getView().openBook(downloadedBookUrl);
+ }
+ }
+
+ private void launchAudioPlayer() {
+ if (book.getState() == ReadingStateModel.ReadingState.STATE_NOT_STARTED) {
+ updateReadingState(ReadingStateModel.ReadingState.STATE_READING);
+ }
+ getView().launchPlayer(book);
+ }
+
+ private void storeDownloadedBook(boolean forAudiobook) {
+ BookModel stored = storedBook == null ? book.getStorageModel(bookSlug) : storedBook;
+ if (forAudiobook) {
+ List mediaUrls = book.getAudiobookFilesUrls();
+ stored.setAudioFileUrls(mediaUrls);
+ stored.setTotalAudioChapters(mediaUrls.size());
+ } else {
+ stored.setEbookFileUrl(book.getEpub());
+ }
+ if (storedBook == null) {
+ storedBook = stored;
+ storage.add(storedBook);
+ } else {
+ storage.update(stored);
+ }
+ }
+
+ public void showFavouriteButton(BookDetailsModel book) {
+ if(preferences.isUserLoggedIn()) {
+ getView().showFavouriteButton(book);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookType.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookType.java
new file mode 100644
index 0000000..fd576be
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookType.java
@@ -0,0 +1,21 @@
+package com.moiseum.wolnelektury.view.book;
+
+/**
+ * Created by Piotr Ostrowski on 23.08.2018.
+ */
+public enum BookType {
+ TYPE_DEFAULT {
+ @Override
+ public boolean shouldShowPremiumLock() {
+ return false;
+ }
+ },
+ TYPE_PREMIUM {
+ @Override
+ public boolean shouldShowPremiumLock() {
+ return true;
+ }
+ };
+
+ public abstract boolean shouldShowPremiumLock();
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookView.java
new file mode 100644
index 0000000..217f748
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookView.java
@@ -0,0 +1,41 @@
+package com.moiseum.wolnelektury.view.book;
+
+import android.content.Context;
+
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton;
+
+/**
+ * Created by Piotr Ostrowski on 17.11.2017.
+ */
+
+public interface BookView {
+
+ void initializeBookView(BookDetailsModel book);
+
+ void changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState state, boolean forAudiobook);
+
+ void showCurrentStateProgress(int percentage, boolean forAudiobook);
+
+ void showInitializationError();
+
+ void showDownloadFileError();
+
+ void startShareActivity(String shareUrl);
+
+ Context getContext();
+
+ void openBook(String downloadedBookUrl);
+
+ void launchPlayer(BookDetailsModel book);
+
+ void updateReadingProgress(int currentChapter, int count, boolean forAudiobook);
+
+ void startLikeClicked();
+
+ void stopLikeClicked();
+
+ void showFavouriteButton(BookDetailsModel book);
+
+ void showPremiumLock(boolean lock);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/components/ProgressDownloadButton.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/components/ProgressDownloadButton.java
new file mode 100644
index 0000000..5d177d8
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/components/ProgressDownloadButton.java
@@ -0,0 +1,321 @@
+package com.moiseum.wolnelektury.view.book.components;
+
+/**
+ * Created by Piotr Ostrowski on 21.11.2017.
+ */
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.moiseum.wolnelektury.R;
+
+import java.util.Locale;
+
+public class ProgressDownloadButton extends View {
+
+ private static final int MAX_PROGRESS_VALUE = 100;
+
+ public enum ProgressDownloadButtonState {
+ STATE_INITIAL {
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isDownloaded() {
+ return false;
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height) {
+ int posY = getPosY(attributes, height);
+ canvas.drawText(attributes.initialText, attributes.paddingStart, posY, attributes.textPaint);
+ int top = (height - attributes.iconBitmap.getHeight()) / 2;
+ canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes
+ .bitmapPaint);
+ }
+ }, STATE_DOWNLOADING {
+ @Override
+ public boolean isEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDownloaded() {
+ throw new IllegalStateException("This method shall not be called within this state");
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height) {
+ int posY = getPosY(attributes, height);
+ String text = String.format(Locale.getDefault(), "%s: %d%%", attributes.loadingText, attributes.currentProgress);
+
+ // Draw text to overlap.
+ canvas.drawText(text, attributes.paddingStart, posY, attributes.textPaint);
+
+ // Draw icon to overlap.
+ int top = (height - attributes.iconBitmap.getHeight()) / 2;
+ canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes
+ .bitmapPaint);
+
+ // Draw progress
+ int currentProgress = attributes.currentProgress;
+ if (currentProgress >= 0 && currentProgress <= MAX_PROGRESS_VALUE) {
+ attributes.baseRect.right = attributes.baseRect.width() * currentProgress / MAX_PROGRESS_VALUE;
+ canvas.clipRect(attributes.baseRect);
+ }
+
+ // Draw current state.
+ canvas.drawRoundRect(attributes.outerRectF, attributes.cornerRadius, attributes.cornerRadius, attributes.textPaint);
+ canvas.drawText(text, attributes.paddingStart, posY, attributes.invertedPaint);
+ canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes
+ .bitmapInvertedPaint);
+ }
+ }, STATE_READING {
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isDownloaded() {
+ return true;
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return true;
+ }
+
+ @Override
+ public void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height) {
+ int posY = getPosY(attributes, height);
+ int top = (height - attributes.iconBitmap.getHeight()) / 2;
+ String text = String.format(Locale.getDefault(), "%1$s: %2$d/%3$d", attributes.downloadedText, attributes
+ .currentReadPosition, attributes.totalReadCount);
+
+ canvas.drawRoundRect(attributes.outerRectF, attributes.cornerRadius, attributes.cornerRadius, attributes.textPaint);
+ canvas.drawText(text, attributes.paddingStart, posY, attributes.invertedPaint);
+ canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes
+ .bitmapInvertedPaint);
+ }
+ };
+
+ private static int getPosY(ProgressDownloadButtonAttributes attributes, int height) {
+ return (int) ((height / 2) - ((attributes.textPaint.descent() + attributes.textPaint.ascent()) / 2));
+ }
+
+ public abstract boolean isEnabled();
+
+ public abstract boolean isDownloaded();
+
+ public abstract boolean isDeletable();
+
+ public abstract void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height);
+ }
+
+ private static class ProgressDownloadButtonAttributes {
+ private String initialText = "";
+ private String downloadedText = "";
+ private String loadingText;
+
+ private Rect baseRect = new Rect();
+ private RectF outerRectF = new RectF();
+ private RectF innerRectF = new RectF();
+
+ private int currentProgress = 0;
+ private int currentReadPosition;
+ private int totalReadCount;
+
+ private Paint textPaint;
+ private Paint invertedPaint;
+ private Paint bitmapPaint;
+ private Paint bitmapInvertedPaint;
+
+ private Bitmap iconBitmap;
+ private int cornerRadius;
+ private int innerCornerRadius;
+ private int borderSize;
+ private int paddingStart;
+ private int paddingEnd;
+ }
+
+
+ private ProgressDownloadButtonState currentState = ProgressDownloadButtonState.STATE_INITIAL;
+ private ProgressDownloadButtonAttributes attributes;
+
+ public ProgressDownloadButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initComponents(context, attrs, defStyle, 0);
+ }
+
+ public ProgressDownloadButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initComponents(context, attrs, 0, 0);
+ }
+
+ public ProgressDownloadButton(Context context) {
+ super(context);
+ }
+
+ // public InvertedTextProgressbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ // super(context, attrs, defStyleAttr, defStyleRes);
+ // initComponents(context, attrs, defStyleAttr, defStyleRes);
+ // }
+
+ /**
+ * Initializes the text paint. This has a fix size.
+ *
+ * @param attrs The XML attributes to use.
+ */
+ @SuppressLint("ResourceType")
+ private void initComponents(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ attributes = new ProgressDownloadButtonAttributes();
+
+ TypedArray baseAttributes = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.paddingStart, android.R.attr
+ .paddingEnd});
+ TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.ProgressDownloadButton, defStyleAttr, defStyleRes);
+
+ attributes.paddingStart = baseAttributes.getDimensionPixelOffset(0, 0);
+ attributes.paddingEnd = baseAttributes.getDimensionPixelOffset(1, 0);
+
+ Paint textPaint = new Paint();
+ textPaint.setColor(styledAttributes.getColor(R.styleable.ProgressDownloadButton_text_color, Color.BLACK));
+ textPaint.setStyle(Paint.Style.FILL);
+ textPaint.setTextSize(styledAttributes.getDimensionPixelSize(R.styleable.ProgressDownloadButton_text_size, context.getResources()
+ .getDimensionPixelSize(R.dimen.download_button_text_size_default)));
+ textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
+ textPaint.setTextAlign(Paint.Align.LEFT); // Text draw is started in the middle
+ textPaint.setLinearText(true);
+ textPaint.setAntiAlias(true);
+
+ // Define the inverted text paint.
+ Paint invertedPaint = new Paint(textPaint);
+ invertedPaint.setColor(styledAttributes.getColor(R.styleable.ProgressDownloadButton_text_inverted_color, Color.WHITE));
+ attributes.textPaint = textPaint;
+ attributes.invertedPaint = invertedPaint;
+
+ // Define paint for drawable
+ Paint bitmapPaint = new Paint();
+ bitmapPaint.setColorFilter(new PorterDuffColorFilter(styledAttributes.getColor(R.styleable.ProgressDownloadButton_text_color,
+ Color.BLACK), PorterDuff.Mode.SRC_ATOP));
+ attributes.bitmapPaint = bitmapPaint;
+
+ // Define paint for inverted drawable
+ Paint bitmapInvertedPaint = new Paint();
+ bitmapInvertedPaint.setColorFilter(new PorterDuffColorFilter(styledAttributes.getColor(R.styleable
+ .ProgressDownloadButton_text_inverted_color, Color.WHITE), PorterDuff.Mode.SRC_ATOP));
+ attributes.bitmapInvertedPaint = bitmapInvertedPaint;
+
+ // Define the text.
+ String initialText = styledAttributes.getString(R.styleable.ProgressDownloadButton_text_initial);
+ if (initialText != null) {
+ initialText = initialText.toUpperCase();
+ attributes.initialText = initialText;
+ }
+ String downloadedText = styledAttributes.getString(R.styleable.ProgressDownloadButton_text_downloaded);
+ if (downloadedText != null) {
+ downloadedText = downloadedText.toUpperCase();
+ attributes.downloadedText = downloadedText;
+ }
+ attributes.loadingText = context.getString(R.string.download_ebook_loading);
+
+ // Load drawable
+ attributes.iconBitmap = BitmapFactory.decodeResource(getResources(), styledAttributes.getResourceId(R.styleable
+ .ProgressDownloadButton_drawable, android.R.drawable.ic_delete));
+
+ attributes.borderSize = styledAttributes.getDimensionPixelSize(R.styleable.ProgressDownloadButton_border_size, context
+ .getResources().getDimensionPixelSize(R.dimen.download_button_border_size_default));
+ attributes.cornerRadius = styledAttributes.getDimensionPixelSize(R.styleable.ProgressDownloadButton_corner_radius, context
+ .getResources().getDimensionPixelSize(R.dimen.download_button_corner_radius_default));
+ attributes.innerCornerRadius = attributes.cornerRadius - attributes.borderSize;
+
+ // Recycle the TypedArray.
+ baseAttributes.recycle();
+ styledAttributes.recycle();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.getClipBounds(attributes.baseRect);
+ RectF outerRectF = attributes.outerRectF;
+ int borderSize = attributes.borderSize;
+ outerRectF.set(attributes.baseRect);
+ attributes.innerRectF.set(outerRectF.left + borderSize, outerRectF.top + borderSize, outerRectF.right - borderSize, outerRectF
+ .bottom - borderSize);
+
+ // Draw outline
+ canvas.drawRoundRect(outerRectF, attributes.cornerRadius, attributes.cornerRadius, attributes.textPaint);
+ canvas.drawRoundRect(attributes.innerRectF, attributes.innerCornerRadius, attributes.innerCornerRadius, attributes.invertedPaint);
+
+ // Draw current state
+ getState().draw(canvas, attributes, getWidth(), getHeight());
+ }
+
+ public void setState(ProgressDownloadButtonState state) {
+ this.currentState = state;
+ this.setEnabled(state.isEnabled());
+ invalidate();
+ }
+
+ public ProgressDownloadButtonState getState() {
+ return currentState;
+ }
+
+ /**
+ * Sets the text that will overlay.
+ *
+ * @param text The text to draw.
+ */
+ public void setText(String text) {
+ attributes.initialText = text;
+ }
+
+ /**
+ * Gets the current text to draw.
+ *
+ * @return The current text to draw.
+ */
+ public String getText() {
+ return attributes.initialText;
+ }
+
+ public int getCurrentProgress() {
+ return attributes.currentProgress;
+ }
+
+ public void setProgress(int progress) {
+ attributes.currentProgress = progress;
+ invalidate();
+ }
+
+ public void setCurrentReadCount(int position, int count) {
+ attributes.currentReadPosition = position;
+ attributes.totalReadCount = count;
+ invalidate();
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/AudiobooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/AudiobooksDataProvider.java
new file mode 100644
index 0000000..f4d0a42
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/AudiobooksDataProvider.java
@@ -0,0 +1,24 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+
+public class AudiobooksDataProvider extends DataProvider, BooksService> {
+
+ @Override
+ public Call> execute(BooksService service) {
+ return service.getAudiobooks(lastKeySlug , RestClient.PAGINATION_LIMIT);
+ }
+
+ @Override
+ protected Class getServiceClass() {
+ return BooksService.class;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListActivity.java
new file mode 100644
index 0000000..7b38560
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListActivity.java
@@ -0,0 +1,44 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractActivity;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+
+import static com.moiseum.wolnelektury.view.book.list.BookListActivity.BookListIntent.PARAM_LIST_TYPE;
+
+
+public class BookListActivity extends AbstractActivity {
+
+ public static final String BOOK_LIST_FRAGMENT_TAG = "BookListFragmentTag";
+
+ public static class BookListIntent extends AbstractIntent {
+
+ static final String PARAM_LIST_TYPE = "PARAM_LIST_TYPE";
+
+ public BookListIntent(BookListType type, Context context) {
+ super(context, BookListActivity.class);
+ putExtra(PARAM_LIST_TYPE, type);
+ }
+
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_blank;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+ BookListType type = (BookListType) getIntent().getSerializableExtra(PARAM_LIST_TYPE);
+ setTitle(type.getActivityTitle());
+
+ BooksListFragment bookListFragment = (BooksListFragment) getSupportFragmentManager().findFragmentByTag(BOOK_LIST_FRAGMENT_TAG);
+ if (bookListFragment == null) {
+ BooksListFragment fragment = BooksListFragment.newInstance(type);
+ getSupportFragmentManager().beginTransaction().add(R.id.flContainer, fragment, BOOK_LIST_FRAGMENT_TAG).commit();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListType.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListType.java
new file mode 100644
index 0000000..9e2b326
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListType.java
@@ -0,0 +1,292 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import android.support.annotation.StringRes;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.models.ReadingStateModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.util.List;
+
+/**
+ * @author golonkos
+ */
+
+public enum BookListType {
+
+ DOWNLOADED {
+ @Override
+ public DataProvider, BooksService> getDataProvider() {
+ return new DownloadedBooksDataProvider();
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return true;
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPageable() {
+ return false;
+ }
+
+ @Override
+ public String getNameForTracker() {
+ return "DownloadedList";
+ }
+
+ @Override
+ public int getActivityTitle() {
+ return R.string.nav_downloaded;
+ }
+
+ @Override
+ public int getEmptyListText() {
+ return R.string.downloaded_empty_list;
+ }
+ },
+
+ AUDIOBOOKS {
+ @Override
+ public DataProvider, BooksService> getDataProvider() {
+ return new AudiobooksDataProvider();
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return true;
+ }
+
+ @Override
+ public boolean isPageable() {
+ return true;
+ }
+
+ @Override
+ public String getNameForTracker() {
+ return "AudiobooksList";
+ }
+
+ @Override
+ public int getActivityTitle() {
+ return R.string.nav_audiobooks;
+ }
+
+ @Override
+ public int getEmptyListText() {
+ return R.string.audiobooks_empty_list;
+ }
+ },
+
+ NEWEST {
+ @Override
+ public DataProvider, BooksService> getDataProvider() {
+ return new NewestBooksDataProvider();
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPageable() {
+ return false;
+ }
+
+ @Override
+ public String getNameForTracker() {
+ return "NewestList";
+ }
+
+ @Override
+ public int getActivityTitle() {
+ return R.string.book_list_newest_title;
+ }
+
+ @Override
+ public int getEmptyListText() {
+ return R.string.newest_empty_list;
+ }
+ },
+
+ RECOMMENDED {
+ @Override
+ public DataProvider, BooksService> getDataProvider() {
+ return new RecommendedBooksDataProvider();
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPageable() {
+ return false;
+ }
+
+ @Override
+ public String getNameForTracker() {
+ return "RecommendedList";
+ }
+
+ @Override
+ public int getActivityTitle() {
+ return R.string.book_list_recommended_title;
+ }
+
+ @Override
+ public int getEmptyListText() {
+ return R.string.recommended_empty_list;
+ }
+ },
+
+ READING {
+ @Override
+ public DataProvider, BooksService> getDataProvider() {
+ return new ReadingStateDataProvider(ReadingStateModel.ReadingState.STATE_READING);
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPageable() {
+ return true;
+ }
+
+ @Override
+ public String getNameForTracker() {
+ return "NowReadingList";
+ }
+
+ @Override
+ public int getActivityTitle() {
+ return R.string.nav_reading;
+ }
+
+ @Override
+ public int getEmptyListText() {
+ return R.string.reading_empty_list;
+ }
+ },
+
+ FAVOURITES {
+ @Override
+ public DataProvider, BooksService> getDataProvider() {
+ return new FavouritesDataProvider();
+ }
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPageable() {
+ return true;
+ }
+
+ @Override
+ public String getNameForTracker() {
+ return "FavouritesList";
+ }
+
+ @Override
+ public int getActivityTitle() {
+ return R.string.nav_favourites;
+ }
+
+ @Override
+ public int getEmptyListText() {
+ return R.string.faviourites_empty_list;
+ }
+ },
+
+ COMPLETED {
+ @Override
+ public DataProvider, BooksService> getDataProvider() {
+ return new ReadingStateDataProvider(ReadingStateModel.ReadingState.STATE_COMPLETED);
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return false;
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPageable() {
+ return true;
+ }
+
+ @Override
+ public String getNameForTracker() {
+ return "CompletedList";
+ }
+
+ @Override
+ public int getActivityTitle() {
+ return R.string.nav_completed;
+ }
+
+ @Override
+ public int getEmptyListText() {
+ return R.string.completed_empty_list;
+ }
+ };
+
+ public abstract DataProvider, BooksService> getDataProvider();
+
+ public abstract boolean isDeletable();
+
+ public abstract boolean isSearchable();
+
+ public abstract boolean isPageable();
+
+ public abstract String getNameForTracker();
+
+ @StringRes
+ public abstract int getActivityTitle();
+
+ @StringRes
+ public abstract int getEmptyListText();
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListAdapter.java
new file mode 100644
index 0000000..07e3f7f
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListAdapter.java
@@ -0,0 +1,159 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.support.annotation.NonNull;
+import android.text.SpannableString;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter;
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+
+import butterknife.BindView;
+
+/**
+ * Created by piotrostrowski on 16.11.2017.
+ */
+
+public class BooksListAdapter extends RecyclerAdapter {
+
+ public interface BooksListDeletionListener {
+ void onDeleteBookClicked(BookModel book, int position);
+ }
+
+ private BooksListDeletionListener listener;
+ private View.OnClickListener deleteButtonClick = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (listener != null) {
+ int position = (int) v.getTag();
+ BookModel book = getItem(position);
+ listener.onDeleteBookClicked(book, position);
+ }
+ }
+ };
+
+ public BooksListAdapter(Context context) {
+ super(context, RecyclerAdapter.Selection.NONE);
+ }
+
+ @Override
+ protected String getItemId(BookModel item) {
+ return item.getSlug();
+ }
+
+ @NonNull
+ @Override
+ public BookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new BookViewHolder(inflate(R.layout.list_search, parent));
+ }
+
+ @Override
+ public void onBindViewHolder(BookViewHolder viewHolder, int position) {
+ super.onBindViewHolder(viewHolder, position);
+ viewHolder.ibDeleteEbook.setTag(position);
+ viewHolder.ibDeleteEbook.setOnClickListener(deleteButtonClick);
+ }
+
+ public void setOnDeleteListener(BooksListDeletionListener listener) {
+ this.listener = listener;
+ }
+
+ static class BookViewHolder extends ViewHolder {
+
+ @BindView(R.id.ivBookCover)
+ ImageView ivBookCover;
+ @BindView(R.id.tvBookAuthor)
+ TextView tvBookAuthor;
+ @BindView(R.id.tvBookTitle)
+ TextView tvBookTitle;
+ @BindView(R.id.tvBookEpoch)
+ TextView tvBookEpoch;
+ @BindView(R.id.tvBookGenre)
+ TextView tvBookGenre;
+ @BindView(R.id.tvBookKind)
+ TextView tvBookKind;
+ @BindView(R.id.ibDeleteEbook)
+ ImageButton ibDeleteEbook;
+ @BindView(R.id.ivEbook)
+ ImageView ivEbook;
+ @BindView(R.id.tvEbookReaden)
+ TextView tvEbookReaden;
+ @BindView(R.id.ivAudioBook)
+ ImageView ivAudioBook;
+ @BindView(R.id.tvAudioBookReaden)
+ TextView tvAudioBookReaden;
+ private final SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+
+ BookViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ public void bind(BookModel item, boolean selected) {
+ if (item.getCoverThumb() != null) {
+ String coverUrl = item.getCoverThumb();
+ if (!coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) {
+ coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl;
+ }
+ Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivBookCover);
+ } else {
+ ivBookCover.setImageResource(R.drawable.list_nocover);
+ }
+ tvBookAuthor.setText(item.getAuthor());
+ tvBookTitle.setText(item.getTitle());
+ tvBookEpoch.setText(item.getEpoch());
+ tvBookGenre.setText(item.getGenre());
+ tvBookKind.setText(item.getKind());
+ ibDeleteEbook.setVisibility(item.isDeletable() ? View.VISIBLE : View.GONE);
+ if (item.getCurrentChapter() != 0 && item.getTotalChapters() != 0) {
+ SpannableString progressSpannable = spanStringSize(item, false, R.string.reading_progress);
+ tvEbookReaden.setVisibility(View.VISIBLE);
+ tvEbookReaden.setText(progressSpannable);
+ } else {
+ tvEbookReaden.setVisibility(View.GONE);
+ }
+ if (item.isHasAudio()) {
+ ivAudioBook.setVisibility(View.VISIBLE);
+ if (item.getCurrentAudioChapter() != 0 && item.getTotalAudioChapters() != 0) {
+ SpannableString progressSpannable = spanStringSize(item, true, R.string.listening_progress);
+ tvAudioBookReaden.setVisibility(View.VISIBLE);
+ tvAudioBookReaden.setText(progressSpannable);
+ } else {
+ tvAudioBookReaden.setVisibility(View.GONE);
+ }
+ } else {
+ ivAudioBook.setVisibility(View.GONE);
+ tvAudioBookReaden.setVisibility(View.GONE);
+ }
+ }
+
+ private SpannableString spanStringSize(BookModel item, boolean isAudioPart, int resourceId) {
+ float currentChapter = isAudioPart ? (float) item.getCurrentAudioChapter() : (float) item.getCurrentChapter();
+ float totalChapter = isAudioPart ? (float) item.getTotalAudioChapters() : (float) item.getTotalChapters();
+ int progress = (int) ((currentChapter / totalChapter) * 100.f);
+
+ String progressText = getContext().getString(resourceId, progress);
+ SpannableString progressSpannable = new SpannableString(progressText.toUpperCase());
+ int size = ((int) getContext().getResources().getDimension(R.dimen.list_title_text_size));
+ progressSpannable.setSpan(new AbsoluteSizeSpan(size), progressText.indexOf(" "), progressText.length() - 1, 0); // set size
+ return progressSpannable;
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListFragment.java
new file mode 100644
index 0000000..0bb96fb
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListFragment.java
@@ -0,0 +1,184 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+import com.moiseum.wolnelektury.components.ProgressRecyclerView;
+import com.moiseum.wolnelektury.components.recycler.EndlessRecyclerOnScrollListener;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.view.book.BookActivity;
+import com.moiseum.wolnelektury.view.book.BookType;
+import com.moiseum.wolnelektury.view.main.MainActivity;
+import com.moiseum.wolnelektury.view.main.NavigationElement;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+/**
+ * @author golonkos
+ */
+
+public class BooksListFragment extends PresenterFragment implements BooksListView {
+
+ private static final String PARAM_LIST_TYPE = "PARAM_LIST_TYPE";
+
+ @BindView(R.id.rvBooksList)
+ ProgressRecyclerView rvBooksList;
+ @BindView(R.id.pbLoadMore)
+ ProgressBar pbLoadMore;
+ @BindView(R.id.btnReloadMore)
+ Button btnReloadMore;
+
+ public static BooksListFragment newInstance(BookListType type) {
+ BooksListFragment fragment = new BooksListFragment();
+ Bundle args = new Bundle(1);
+ args.putSerializable(PARAM_LIST_TYPE, type);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ private BookListType type;
+ private BooksListAdapter adapter;
+
+ private EndlessRecyclerOnScrollListener rvBooksScrollListener = new EndlessRecyclerOnScrollListener() {
+ @Override
+ public void onLoadMore() {
+ if (adapter.getItemCount() > 0) {
+ getPresenter().loadMoreBooks();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (getArguments() == null || getArguments().getSerializable(PARAM_LIST_TYPE) == null) {
+ throw new IllegalStateException("Missing list type parameter.");
+ }
+ type = (BookListType) getArguments().getSerializable(PARAM_LIST_TYPE);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected BooksListPresenter createPresenter() {
+ return new BooksListPresenter(this, type);
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_books_list;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
+ rvBooksList.setLayoutManager(layoutManager);
+ rvBooksList.addOnScrollListener(rvBooksScrollListener);
+
+ adapter = new BooksListAdapter(getContext());
+ adapter.setOnItemClickListener((item, view1, position) -> getPresenter().onBookClicked(item, position));
+ if (type.isDeletable()) {
+ adapter.setOnDeleteListener((book, position) -> {
+ getPresenter().onBookDeleteClicked(book);
+ adapter.removeItem(position);
+ Toast.makeText(getContext() ,getString(R.string.book_deleted_message), Toast.LENGTH_SHORT).show();
+ });
+ }
+ rvBooksList.setAdapter(adapter);
+ rvBooksList.setEmptyText(type.getEmptyListText());
+ if (type.isSearchable()) {
+ setHasOptionsMenu(true);
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (type.isSearchable()) {
+ inflater.inflate(R.menu.menu_searchable, menu);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_search) {
+ EventBus.getDefault().post(new MainActivity.ChangeNavigationEvent(NavigationElement.SEARCH));
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected String getNameForTracker() {
+ return type.getNameForTracker();
+ }
+
+ @Override
+ public void setData(List books) {
+ if (adapter.getItemCount() == 0) {
+ rvBooksList.setItems(books);
+ } else {
+ rvBooksList.addItems(books);
+ }
+ }
+
+ @Override
+ public void clearList() {
+ adapter.clear();
+ }
+
+ @Override
+ public void setProgressVisible(boolean visible) {
+ if (adapter.getItemCount() == 0) {
+ rvBooksList.setProgressVisible(visible);
+ } else {
+ pbLoadMore.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ @Override
+ public void showError(Exception e) {
+ Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show();
+ if (adapter.getItemCount() != 0) {
+ btnReloadMore.setVisibility(View.VISIBLE);
+ } else {
+ rvBooksList.showRetryButton(() -> getPresenter().reloadBooks());
+ }
+ }
+
+ @Override
+ public void openBookDetailsView(String bookSlug) {
+ startActivity(new BookActivity.BookIntent(bookSlug, BookType.TYPE_DEFAULT, getActivity()));
+ }
+
+ @Override
+ public void updateEmptyViewVisibility() {
+ rvBooksList.updateEmptyViewVisibility();
+ }
+
+ @Override
+ public void updateFavouriteState(boolean state, Integer clickedPosition) {
+ if (clickedPosition != null) {
+ adapter.getItem(clickedPosition).setLiked(state);
+ adapter.notifyDataSetChanged();
+ }
+ }
+
+ @OnClick(R.id.btnReloadMore)
+ public void onReloadMoreClicked() {
+ btnReloadMore.setVisibility(View.GONE);
+ pbLoadMore.setVisibility(View.VISIBLE);
+ getPresenter().loadMoreBooks();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListPresenter.java
new file mode 100644
index 0000000..030bbea
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListPresenter.java
@@ -0,0 +1,185 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.base.DataObserver;
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+import com.moiseum.wolnelektury.storage.BookStorage;
+import com.moiseum.wolnelektury.events.BookFavouriteEvent;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import io.reactivex.Completable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+
+/**
+ * @author golonkos
+ */
+
+class BooksListPresenter extends FragmentPresenter {
+
+ private static final String TAG = BooksListPresenter.class.getSimpleName();
+
+ private final DataProvider, BooksService> dataProvider;
+ private BookStorage storage;
+ private final BookListType bookListType;
+ private String lastKey;
+ private Integer clickedPosition;
+
+ BooksListPresenter(BooksListView view, BookListType type) {
+ super(view);
+ bookListType = type;
+ lastKey = null;
+ dataProvider = type.getDataProvider();
+ dataProvider.setDataObserver(new BooksListDataObserver());
+ storage = WLApplication.getInstance().getBookStorage();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onViewCreated(Bundle savedInstanceState) {
+ super.onViewCreated(savedInstanceState);
+ loadBooks();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ dataProvider.cancel();
+ EventBus.getDefault().unregister(this);
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onBookAdded(BookStorage.BookAddedEvent event) {
+ if (bookListType == BookListType.DOWNLOADED) {
+ reloadBooks();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onBookDeleted(BookStorage.BookDeletedEvent event) {
+ if (bookListType == BookListType.DOWNLOADED) {
+ reloadBooks();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onFavouriteStateChanged(BookFavouriteEvent event) {
+ // TODO: Change that full reload
+ if (bookListType == BookListType.FAVOURITES) {
+ reloadBooks();
+ } else {
+ getView().updateFavouriteState(event.getState(), clickedPosition);
+ }
+ }
+
+ public void reloadBooks() {
+ lastKey = null;
+ getView().clearList();
+ loadBooks();
+ }
+
+ public void loadMoreBooks() {
+ if (bookListType.isPageable()) {
+ loadBooks();
+ }
+ }
+
+ private void loadBooks() {
+ dataProvider.load(lastKey);
+ }
+
+ void onBookClicked(BookModel book, int position) {
+ clickedPosition = position;
+ getView().openBookDetailsView(book.getSlug());
+ }
+
+ void onBookDeleteClicked(BookModel book) {
+ BookModel downloadedBook = storage.find(book.getSlug());
+
+ List deletionOperations = new ArrayList<>();
+ if (downloadedBook != null && downloadedBook.isEbookDownloaded()) {
+ deletionOperations.add(FileCacheUtils.deleteEbookFile(downloadedBook.getEbookFileUrl()));
+ }
+ if (downloadedBook != null && downloadedBook.isAudioDownloaded()) {
+ deletionOperations.add(FileCacheUtils.deleteAudiobookFiles(downloadedBook.getAudioFileUrls()));
+ }
+
+ storage.remove(book.getSlug(), false);
+ addDisposable(Completable.concat(deletionOperations).andThen(Completable.fromAction(() -> {
+ })).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(() -> getView().updateEmptyViewVisibility(), error -> {
+ getView().showError(new Exception(error));
+ //restore books
+ loadBooks();
+ }));
+ }
+
+ private class BooksListDataObserver implements DataObserver> {
+
+ private List matchDownloadedBooks(List books) {
+ if (bookListType.isDeletable()) {
+ List merged = new ArrayList<>(books.size());
+ List downloadedBooks = storage.all();
+
+ for (BookModel book : books) {
+ boolean found = false;
+ for (BookModel downloaded : downloadedBooks) {
+ if (book.getSlug().equals(downloaded.getSlug())) {
+ merged.add(downloaded);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ merged.add(book);
+ }
+ }
+ return merged;
+ } else {
+ return books;
+ }
+ }
+
+ @Override
+ public void onLoadStarted() {
+ getView().setProgressVisible(true);
+ }
+
+ @Override
+ public void onLoadSuccess(List data) {
+ if (data.size() > 0) {
+ lastKey = data.get(data.size() - 1).getSortedKey();
+ data = matchDownloadedBooks(data);
+ }
+ getView().setProgressVisible(false);
+ getView().setData(data);
+ }
+
+ @Override
+ public void onLoadFailed(Exception e) {
+ getView().setProgressVisible(false);
+ getView().setData(Collections.emptyList());
+ getView().showError(e);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListView.java
new file mode 100644
index 0000000..c6249f4
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListView.java
@@ -0,0 +1,19 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import com.moiseum.wolnelektury.base.mvp.LoadingView;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+
+import java.util.List;
+
+/**
+ * @author golonkos
+ */
+interface BooksListView extends LoadingView> {
+ void openBookDetailsView(String bookSlug);
+
+ void updateEmptyViewVisibility();
+
+ void updateFavouriteState(boolean state, Integer clickedPosition);
+
+ void clearList();
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/DownloadedBooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/DownloadedBooksDataProvider.java
new file mode 100644
index 0000000..81b65cf
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/DownloadedBooksDataProvider.java
@@ -0,0 +1,35 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import com.moiseum.wolnelektury.storage.BookStorage;
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+/**
+ * @author golonkos
+ */
+public class DownloadedBooksDataProvider extends DataProvider, BooksService> {
+
+ @Override
+ public void load(String lastKey) {
+ if (dataObserver != null && lastKey == null) {
+ BookStorage bookStorage = WLApplication.getInstance().getBookStorage();
+ dataObserver.onLoadSuccess(bookStorage.all());
+ }
+ }
+
+ @Override
+ public Call> execute(BooksService service) {
+ return null;
+ }
+
+ @Override
+ protected Class getServiceClass() {
+ return null;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/FavouritesDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/FavouritesDataProvider.java
new file mode 100644
index 0000000..4634ba6
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/FavouritesDataProvider.java
@@ -0,0 +1,23 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+public class FavouritesDataProvider extends DataProvider,BooksService> {
+
+ @Override
+ public Call> execute(BooksService service) {
+ return service.getFavourites(lastKeySlug , RestClient.PAGINATION_LIMIT);
+ }
+
+ @Override
+ protected Class getServiceClass() {
+ return BooksService.class;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/NewestBooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/NewestBooksDataProvider.java
new file mode 100644
index 0000000..df703e9
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/NewestBooksDataProvider.java
@@ -0,0 +1,26 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+/**
+ * @author golonkos
+ */
+
+public class NewestBooksDataProvider extends DataProvider, BooksService> {
+
+ @Override
+ public Call> execute(BooksService service) {
+ return service.getNewest();
+ }
+
+ @Override
+ protected Class getServiceClass() {
+ return BooksService.class;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/ReadingStateDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/ReadingStateDataProvider.java
new file mode 100644
index 0000000..356c920
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/ReadingStateDataProvider.java
@@ -0,0 +1,33 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.models.ReadingStateModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+/**
+ * Created by Piotr Ostrowski on 24.06.2018.
+ */
+public class ReadingStateDataProvider extends DataProvider, BooksService> {
+
+ private ReadingStateModel.ReadingState state;
+
+ public ReadingStateDataProvider(ReadingStateModel.ReadingState state) {
+ this.state = state;
+ }
+
+ @Override
+ protected Class getServiceClass() {
+ return BooksService.class;
+ }
+
+ @Override
+ public Call> execute(BooksService service) {
+ return service.getReadenBooks(state.getStateName(), lastKeySlug, RestClient.PAGINATION_LIMIT);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/RecommendedBooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/RecommendedBooksDataProvider.java
new file mode 100644
index 0000000..a525498
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/RecommendedBooksDataProvider.java
@@ -0,0 +1,26 @@
+package com.moiseum.wolnelektury.view.book.list;
+
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+/**
+ * @author golonkos
+ */
+
+public class RecommendedBooksDataProvider extends DataProvider, BooksService> {
+
+ @Override
+ public Call> execute(BooksService service) {
+ return service.getRecommended();
+ }
+
+ @Override
+ protected Class getServiceClass() {
+ return BooksService.class;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/BookViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/BookViewHolder.java
new file mode 100644
index 0000000..73c7667
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/BookViewHolder.java
@@ -0,0 +1,56 @@
+package com.moiseum.wolnelektury.view.library;
+
+import android.graphics.Color;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+
+import butterknife.BindView;
+
+/**
+ * @author golonkos
+ */
+
+class BookViewHolder extends ViewHolder {
+
+ private static final String DEFAULT_OVERLAY_COLOR = "#80db4b16";
+
+ @BindView(R.id.ivBookCover)
+ ImageView ivBookCover;
+ @BindView(R.id.tvBookAuthor)
+ TextView tvBookAuthor;
+ @BindView(R.id.tvBookTitle)
+ TextView tvBookTitle;
+ @BindView(R.id.ivAudioBook)
+ ImageView ivAudioBook;
+ @BindView(R.id.llBookContent)
+ LinearLayout llBookContent;
+
+ BookViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ public void bind(BookModel item, boolean selected) {
+ if (item.getCoverThumb() != null) {
+ String coverUrl = item.getCoverThumb();
+ if (!coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) {
+ coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl;
+ }
+ Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivBookCover);
+ }
+ tvBookAuthor.setText(item.getAuthor());
+ tvBookTitle.setText(item.getTitle());
+ ivAudioBook.setVisibility(item.isHasAudio() ? View.VISIBLE : View.GONE);
+ String colorHash = item.getCoverColor() != null ? item.getCoverColor() : DEFAULT_OVERLAY_COLOR;
+ llBookContent.setBackgroundColor(Color.parseColor(colorHash));
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryAdapter.java
new file mode 100644
index 0000000..4dcc724
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryAdapter.java
@@ -0,0 +1,29 @@
+package com.moiseum.wolnelektury.view.library;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+
+/**
+ * @author golonkos
+ */
+
+public class LibraryAdapter extends RecyclerAdapter {
+
+ public LibraryAdapter(Context context) {
+ super(context, Selection.NONE);
+ }
+
+ @Override
+ protected String getItemId(BookModel item) {
+ return null;
+ }
+
+ @Override
+ public BookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new BookViewHolder(inflate(R.layout.book_item, parent));
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryFragment.java
new file mode 100644
index 0000000..e029f1b
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryFragment.java
@@ -0,0 +1,267 @@
+package com.moiseum.wolnelektury.view.library;
+
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+import com.moiseum.wolnelektury.components.ProgressRecyclerView;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.utils.StringUtils;
+import com.moiseum.wolnelektury.view.book.BookActivity;
+import com.moiseum.wolnelektury.view.book.BookType;
+import com.moiseum.wolnelektury.view.book.list.BookListActivity;
+import com.moiseum.wolnelektury.view.book.list.BookListType;
+import com.moiseum.wolnelektury.view.main.MainActivity;
+import com.moiseum.wolnelektury.view.main.NavigationElement;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+/**
+ * @author golonkos
+ */
+
+public class LibraryFragment extends PresenterFragment implements LibraryView {
+
+ @BindView(R.id.rlReadingNowContainer)
+ View rlReadingNowContainer;
+ @BindView(R.id.rvNowReading)
+ ProgressRecyclerView rvNowReading;
+ @BindView(R.id.rvNewest)
+ ProgressRecyclerView rvNewest;
+ @BindView(R.id.rvRecommended)
+ ProgressRecyclerView rvRecommended;
+ @BindView(R.id.ivBookCover)
+ ImageView ivBookCover;
+ @BindView(R.id.tvBookAuthor)
+ TextView tvBookAuthor;
+ @BindView(R.id.tvBookTitle)
+ TextView tvBookTitle;
+ @BindView(R.id.tvBookEpoch)
+ TextView tvBookEpoch;
+ @BindView(R.id.tvBookKind)
+ TextView tvBookKind;
+ @BindView(R.id.tvBookGenre)
+ TextView tvBookGenre;
+ @BindView(R.id.pbHeaderLoading)
+ ProgressBar pbHeaderLoading;
+ @BindView(R.id.tvEmpty)
+ TextView tvEmpty;
+ @BindView(R.id.ibRetry)
+ ImageButton ibRetry;
+ @BindView(R.id.rlHeaderLoadingContainer)
+ RelativeLayout rlHeaderLoadingContainer;
+ @BindView(R.id.rlBecomeAFriend)
+ View rlBecomeAFriend;
+ @BindView(R.id.vBecomeAFriendSeparator)
+ View vBecomeAFriendSeparator;
+ @BindView(R.id.ivAudiobook)
+ ImageView ivHeaderAudiobook;
+
+ public static LibraryFragment newInstance() {
+ return new LibraryFragment();
+ }
+
+ @Override
+ protected LibraryPresenter createPresenter() {
+ return new LibraryPresenter(this);
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_library;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ setHasOptionsMenu(true);
+ initList(rvNowReading);
+ initList(rvNewest);
+ initList(rvRecommended);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.menu_searchable, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_search) {
+ EventBus.getDefault().post(new MainActivity.ChangeNavigationEvent(NavigationElement.SEARCH));
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void showBecomeAFriendHeader(boolean premium) {
+ rlBecomeAFriend.setVisibility(premium ? View.GONE : View.VISIBLE);
+ vBecomeAFriendSeparator.setVisibility(premium ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void initHeader(BookDetailsModel item) {
+ if (item.getCoverThumb() != null) {
+ String coverUrl = item.getCoverThumb();
+ if (!coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) {
+ coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl;
+ }
+ Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivBookCover);
+ } else {
+ ivBookCover.setImageResource(R.drawable.list_nocover);
+ }
+ tvBookAuthor.setText(StringUtils.joinCategory(item.getAuthors(), ", "));
+ tvBookTitle.setText(item.getTitle());
+ tvBookEpoch.setText(StringUtils.joinCategory(item.getEpochs(), ", "));
+ tvBookKind.setText(StringUtils.joinCategory(item.getKinds(), ", "));
+ tvBookGenre.setText(StringUtils.joinCategory(item.getGenres(), ", "));
+ ivHeaderAudiobook.setVisibility(item.hasAudio() ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setProgressContainerVisible(boolean visible) {
+ if (visible) {
+ rlHeaderLoadingContainer.setVisibility(View.VISIBLE);
+ } else {
+ rlHeaderLoadingContainer.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void showHeaderError() {
+ ibRetry.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void setHeaderProgressVisible(boolean visible) {
+ pbHeaderLoading.setVisibility(visible ? View.VISIBLE : View.GONE);
+ if (visible) {
+ tvEmpty.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void showHeaderEmpty(boolean userLoggedIn) {
+ tvEmpty.setVisibility(View.VISIBLE);
+ tvEmpty.setText(userLoggedIn ? R.string.no_prapremiere_message_logged : R.string.no_prapremiere_message);
+ }
+
+ private void initList(ProgressRecyclerView rvList) {
+ rvList.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
+ rvList.setHasFixedSize(true);
+ rvList.setEmptyText(R.string.read_now_library_empty);
+ LibraryAdapter adapter = new LibraryAdapter(getContext());
+ adapter.setOnItemClickListener((item, view, position) -> getPresenter().onBookClicked(item));
+ rvList.setAdapter(adapter);
+ }
+
+ @Override
+ public void setNowReadingVisibility(boolean visible) {
+ rlReadingNowContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
+ rvNowReading.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setNewest(List books) {
+ rvNewest.setItems(books);
+ }
+
+ @Override
+ public void setNewestProgressVisible(boolean visible) {
+ rvNewest.setProgressVisible(visible);
+ }
+
+ @Override
+ public void showNewestError(Exception e) {
+ Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show();
+ rvNewest.showRetryButton(() -> getPresenter().reloadNewest());
+ }
+
+ @Override
+ public void setRecommended(List books) {
+ rvRecommended.setItems(books);
+ }
+
+ @Override
+ public void setRecommendedProgressVisible(boolean visible) {
+ rvRecommended.setProgressVisible(visible);
+ }
+
+ @Override
+ public void showRecommendedError(Exception e) {
+ Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show();
+ rvRecommended.showRetryButton(() -> getPresenter().reloadRecommended());
+ }
+
+ @Override
+ public void setNowReading(List books) {
+ rvNowReading.setItems(books);
+ }
+
+ @Override
+ public void setNowReadingProgressVisible(boolean visible) {
+ rvNowReading.setProgressVisible(visible);
+ }
+
+ @Override
+ public void showNowReadingError(Exception e) {
+ Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show();
+ rvNowReading.showRetryButton(() -> getPresenter().reloadNowReading());
+ }
+
+ @Override
+ public void openBookDetailsView(String slug, BookType bookType) {
+ startActivity(new BookActivity.BookIntent(slug, bookType, getContext()));
+ }
+
+ @OnClick(R.id.btnBecomeAFriend)
+ public void onBecomeAFriendClick() {
+ showPayPalForm();
+ }
+
+ @OnClick(R.id.btnNewestSeeAll)
+ public void onNewestSeeAllClicked() {
+ startActivity(new BookListActivity.BookListIntent(BookListType.NEWEST, getActivity()));
+ }
+
+ @OnClick(R.id.btnRecommendedSeeAll)
+ public void onRecommendedSeeAllClicked() {
+ startActivity(new BookListActivity.BookListIntent(BookListType.RECOMMENDED, getActivity()));
+ }
+
+ @OnClick(R.id.btnNowReadingSeeAll)
+ public void onNowReadingSeeAllClicked() {
+ getPresenter().onNowReadingSeeAllClicked();
+ }
+
+ @OnClick(R.id.ibRetry)
+ public void onHeaderRetryClicked() {
+ getPresenter().fetchHeader();
+ ibRetry.setVisibility(View.GONE);
+ }
+
+ @OnClick(R.id.libraryHeader)
+ public void onLibraryHeaderClicked() {
+ getPresenter().onPremiereHeaderClicked();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryPresenter.java
new file mode 100644
index 0000000..88f9b8b
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryPresenter.java
@@ -0,0 +1,245 @@
+package com.moiseum.wolnelektury.view.library;
+
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.base.DataObserver;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.RestClientCallback;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.connection.models.ReadingStateModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+import com.moiseum.wolnelektury.events.LoggedInEvent;
+import com.moiseum.wolnelektury.storage.BookStorage;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+import com.moiseum.wolnelektury.view.book.BookType;
+import com.moiseum.wolnelektury.view.book.list.NewestBooksDataProvider;
+import com.moiseum.wolnelektury.view.book.list.ReadingStateDataProvider;
+import com.moiseum.wolnelektury.view.book.list.RecommendedBooksDataProvider;
+import com.moiseum.wolnelektury.view.main.MainActivity;
+import com.moiseum.wolnelektury.view.main.NavigationElement;
+import com.moiseum.wolnelektury.view.main.events.PremiumStatusEvent;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+/**
+ * @author golonkos
+ */
+class LibraryPresenter extends FragmentPresenter {
+
+ private static final String TAG = LibraryPresenter.class.getSimpleName();
+ private final NewestBooksDataProvider newestBooksDataProvider;
+ private final RecommendedBooksDataProvider recommendedBooksDataProvider;
+ private final ReadingStateDataProvider nowReadingBooksDataProvider;
+ private BookDetailsModel premiereBook;
+ private Call currentCall;
+ private RestClient client = WLApplication.getInstance().getRestClient();
+
+ private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+
+ LibraryPresenter(LibraryView view) {
+ super(view);
+ newestBooksDataProvider = new NewestBooksDataProvider();
+ newestBooksDataProvider.setDataObserver(new NewestDataObserver());
+
+ recommendedBooksDataProvider = new RecommendedBooksDataProvider();
+ recommendedBooksDataProvider.setDataObserver(new RecommendedDataObserver());
+
+ nowReadingBooksDataProvider = new ReadingStateDataProvider(ReadingStateModel.ReadingState.STATE_READING);
+ nowReadingBooksDataProvider.setDataObserver(new NowReadingDataObserver());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onViewCreated(Bundle savedInstanceState) {
+ super.onViewCreated(savedInstanceState);
+ newestBooksDataProvider.load(null);
+ recommendedBooksDataProvider.load(null);
+ if (preferences.isUserLoggedIn()) {
+ getView().setNowReadingVisibility(true);
+ nowReadingBooksDataProvider.load(null);
+ }
+ fetchHeader();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getView().showBecomeAFriendHeader(preferences.isUserPremium());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ newestBooksDataProvider.cancel();
+ recommendedBooksDataProvider.cancel();
+ nowReadingBooksDataProvider.cancel();
+ if (currentCall != null) {
+ currentCall.cancel();
+ }
+ EventBus.getDefault().unregister(this);
+ }
+
+ void onNowReadingSeeAllClicked() {
+ EventBus.getDefault().post(new MainActivity.ChangeNavigationEvent(NavigationElement.NOW_READING));
+ }
+
+ void onBookClicked(BookModel book) {
+ getView().openBookDetailsView(book.getSlug(), BookType.TYPE_DEFAULT);
+ }
+
+ void reloadNewest() {
+ newestBooksDataProvider.load(null);
+ }
+
+ void reloadRecommended() {
+ recommendedBooksDataProvider.load(null);
+ }
+
+ void reloadNowReading() {
+ nowReadingBooksDataProvider.load(null);
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onBookAdded(BookStorage.BookAddedEvent event) {
+ if (preferences.isUserLoggedIn()) {
+ nowReadingBooksDataProvider.load(null);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onBookDeleted(BookStorage.BookDeletedEvent event) {
+ if (preferences.isUserLoggedIn()) {
+ nowReadingBooksDataProvider.load(null);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onPremiumChanged(PremiumStatusEvent event) {
+ getView().showBecomeAFriendHeader(event.isPremium());
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onLoggedIn(LoggedInEvent event) {
+ getView().showBecomeAFriendHeader(preferences.isUserPremium());
+ getView().setNowReadingVisibility(true);
+ nowReadingBooksDataProvider.load(null);
+ }
+
+ void onPremiereHeaderClicked() {
+ if (premiereBook != null) {
+ getView().openBookDetailsView(premiereBook.getSlug(), BookType.TYPE_PREMIUM);
+ }
+ }
+
+ void fetchHeader() {
+ getView().setProgressContainerVisible(true);
+ getView().setHeaderProgressVisible(true);
+ currentCall = client.call(new RestClientCallback, BooksService>() {
+ @Override
+ public void onSuccess(List data) {
+ getView().setHeaderProgressVisible(false);
+ if (data.size() > 0) {
+ premiereBook = data.get(0);
+ getView().initHeader(premiereBook);
+ getView().setProgressContainerVisible(false);
+ } else {
+ getView().showHeaderEmpty(preferences.isUserLoggedIn());
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ getView().showHeaderError();
+ }
+
+ @Override
+ public void onCancel() {
+ // nop.
+ }
+
+ @Override
+ public Call> execute(BooksService service) {
+ return service.getPreview();
+ }
+ }, BooksService.class);
+ }
+
+
+ private class NewestDataObserver implements DataObserver> {
+
+ @Override
+ public void onLoadStarted() {
+ getView().setNewestProgressVisible(true);
+ }
+
+ @Override
+ public void onLoadSuccess(List data) {
+ getView().setNewestProgressVisible(false);
+ getView().setNewest(data);
+ }
+
+ @Override
+ public void onLoadFailed(Exception e) {
+ getView().setNewestProgressVisible(false);
+ getView().showNewestError(e);
+ }
+ }
+
+ private class RecommendedDataObserver implements DataObserver> {
+
+ @Override
+ public void onLoadStarted() {
+ getView().setRecommendedProgressVisible(true);
+ }
+
+ @Override
+ public void onLoadSuccess(List data) {
+ getView().setRecommendedProgressVisible(false);
+ getView().setRecommended(data);
+ }
+
+ @Override
+ public void onLoadFailed(Exception e) {
+ getView().setRecommendedProgressVisible(false);
+ getView().showRecommendedError(e);
+ }
+ }
+
+ private class NowReadingDataObserver implements DataObserver> {
+
+ @Override
+ public void onLoadStarted() {
+ getView().setNowReadingProgressVisible(true);
+ }
+
+ @Override
+ public void onLoadSuccess(List data) {
+ getView().setNowReadingProgressVisible(false);
+ getView().setNowReading(data);
+ }
+
+ @Override
+ public void onLoadFailed(Exception e) {
+ getView().setNowReadingProgressVisible(false);
+ getView().showNowReadingError(e);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryView.java
new file mode 100644
index 0000000..a4955d5
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryView.java
@@ -0,0 +1,47 @@
+package com.moiseum.wolnelektury.view.library;
+
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.view.book.BookType;
+
+import java.util.List;
+
+/**
+ * @author golonkos
+ */
+
+interface LibraryView {
+ void setNewest(List books);
+
+ void setNewestProgressVisible(boolean visible);
+
+ void showNewestError(Exception e);
+
+ void setRecommended(List books);
+
+ void setRecommendedProgressVisible(boolean visible);
+
+ void showRecommendedError(Exception e);
+
+ void setNowReadingVisibility(boolean visible);
+
+ void setNowReading(List books);
+
+ void setNowReadingProgressVisible(boolean visible);
+
+ void showNowReadingError(Exception e);
+
+ void openBookDetailsView(String slug, BookType bookType);
+
+ void initHeader(BookDetailsModel item);
+
+ void setProgressContainerVisible(boolean visible);
+
+ void showHeaderError();
+
+ void showHeaderEmpty(boolean userLoggedIn);
+
+ void setHeaderProgressVisible(boolean visible);
+
+ void showBecomeAFriendHeader(boolean premium);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginActivity.java
new file mode 100644
index 0000000..0df9ce7
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginActivity.java
@@ -0,0 +1,76 @@
+package com.moiseum.wolnelektury.view.login;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractActivity;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+import com.moiseum.wolnelektury.base.mvp.PresenterActivity;
+
+import butterknife.OnClick;
+
+/**
+ * Created by Piotr Ostrowski on 11.09.2018.
+ */
+public class LoginActivity extends PresenterActivity implements LoginView {
+
+ public static class LoginIntent extends AbstractIntent {
+
+ public LoginIntent(Context context) {
+ super(context, LoginActivity.class);
+ }
+ }
+
+ private ProgressDialog progressDialog;
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_login;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+
+ }
+
+ @Override
+ protected LoginPresenter createPresenter() {
+ return new LoginPresenter(this);
+ }
+
+ @Override
+ public void setProgressDialogVisibility(boolean visible) {
+ if (visible && progressDialog == null) {
+ String dialogMessage = getString(R.string.main_view_progress);
+ progressDialog = ProgressDialog.show(this, null, dialogMessage, true, false);
+ } else if (!visible && progressDialog != null) {
+ progressDialog.hide();
+ progressDialog = null;
+ }
+ }
+
+ @Override
+ public void showToastMessage(int messageResId) {
+ Toast.makeText(this, messageResId, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void showCustomTabsAuthentication(Uri authorizationUrl) {
+ showBrowserView(authorizationUrl);
+ finish();
+ }
+
+ @OnClick(R.id.ibBack)
+ public void onBackClicked() {
+ finish();
+ }
+
+ @OnClick(R.id.btnLogin)
+ public void onLoginClicked() {
+ getPresenter().onLoginClicked();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginPresenter.java
new file mode 100644
index 0000000..c0ecd41
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginPresenter.java
@@ -0,0 +1,70 @@
+package com.moiseum.wolnelektury.view.login;
+
+import android.net.Uri;
+import android.util.Log;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.base.mvp.Presenter;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.RestClientCallback;
+import com.moiseum.wolnelektury.connection.models.OAuthTokenModel;
+import com.moiseum.wolnelektury.connection.services.UserService;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+
+import retrofit2.Call;
+
+/**
+ * Created by Piotr Ostrowski on 12.09.2018.
+ */
+public class LoginPresenter extends Presenter {
+
+ private static final String TAG = LoginPresenter.class.getSimpleName();
+
+ private RestClient client = WLApplication.getInstance().getRestClient();
+ private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+ private Call currentCall;
+
+ LoginPresenter(LoginView view) {
+ super(view);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (currentCall != null) {
+ currentCall.cancel();
+ }
+ }
+
+ protected void onLoginClicked() {
+ getView().setProgressDialogVisibility(true);
+ currentCall = client.call(new RestClientCallback() {
+
+ @Override
+ public void onSuccess(OAuthTokenModel data) {
+ preferences.setTemporaryLoginToken(data.getToken());
+ String authUrl = String.format(RestClient.WEB_OAUTH_AUTHORIZATION_URL, data.getToken());
+ getView().setProgressDialogVisibility(false);
+ getView().showCustomTabsAuthentication(Uri.parse(authUrl));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ Log.e(TAG, "Failed to obtain request token.", e);
+ getView().setProgressDialogVisibility(false);
+ getView().showToastMessage(R.string.login_request_token_failed);
+ }
+
+ @Override
+ public void onCancel() {
+ getView().setProgressDialogVisibility(false);
+ }
+
+ @Override
+ public Call execute(UserService service) {
+ return service.requestToken();
+ }
+ }, UserService.class);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginView.java
new file mode 100644
index 0000000..b734c4f
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginView.java
@@ -0,0 +1,16 @@
+package com.moiseum.wolnelektury.view.login;
+
+import android.net.Uri;
+import android.support.annotation.StringRes;
+
+/**
+ * Created by Piotr Ostrowski on 12.09.2018.
+ */
+public interface LoginView {
+
+ void setProgressDialogVisibility(boolean visible);
+
+ void showToastMessage(@StringRes int messageResId);
+
+ void showCustomTabsAuthentication(Uri authorizationUrl);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainActivity.java
new file mode 100644
index 0000000..4917561
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainActivity.java
@@ -0,0 +1,305 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.StringRes;
+import android.support.customtabs.CustomTabsClient;
+import android.support.customtabs.CustomTabsIntent;
+import android.support.customtabs.CustomTabsServiceConnection;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+import com.moiseum.wolnelektury.base.mvp.PresenterActivity;
+import com.moiseum.wolnelektury.view.book.BookActivity;
+import com.moiseum.wolnelektury.view.book.BookType;
+import com.moiseum.wolnelektury.view.supportus.SupportUsActivity;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+import static com.moiseum.wolnelektury.view.main.MainActivity.MainIntent.RELAUNCH_MESSAGE_KEY;
+
+public class MainActivity extends PresenterActivity implements MainView {
+
+ private ProgressDialog progressDialog;
+
+ public static class MainIntent extends AbstractIntent {
+
+ static final String RELAUNCH_MESSAGE_KEY = "RelaunchMessageKey";
+
+ public MainIntent(Context context) {
+ super(context, MainActivity.class);
+ }
+
+ public MainIntent(@StringRes int relaunchMessageResId, Context context) {
+ super(context, MainActivity.class);
+ this.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ this.putExtra(RELAUNCH_MESSAGE_KEY, relaunchMessageResId);
+ }
+ }
+
+ public static class ChangeNavigationEvent {
+ private final NavigationElement element;
+
+ public ChangeNavigationEvent(NavigationElement element) {
+ if (element == NavigationElement.SEPARATOR || element == NavigationElement.SUPPORT_US) {
+ throw new IllegalArgumentException("Unsupported navigation element");
+ }
+ this.element = element;
+ }
+
+ public NavigationElement getElement() {
+ return element;
+ }
+ }
+
+ @BindView(R.id.drawer_layout)
+ DrawerLayout drawerLayout;
+ @BindView(R.id.rvNavigation)
+ RecyclerView rvNavigation;
+ @BindView(R.id.btnLogin)
+ Button btnLogin;
+ @BindView(R.id.llLoggedInContainer)
+ View llLoggedInContainer;
+ @BindView(R.id.tvUsername)
+ TextView tvUsername;
+
+ private ActionBarDrawerToggle drawerToggle;
+ private NavigationAdapter navigationAdapter;
+ private NavigationElement currentNavigationElement;
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_main;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+ setBackButtonEnable(true);
+ initDrawer();
+ EventBus.getDefault().register(this);
+
+ if (getIntent().hasExtra(RELAUNCH_MESSAGE_KEY)) {
+ Toast.makeText(this, getIntent().getIntExtra(RELAUNCH_MESSAGE_KEY, 0), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ protected MainPresenter createPresenter() {
+ return new MainPresenter(this);
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ String action = intent.getAction();
+ String data = intent.getDataString();
+ if (Intent.ACTION_VIEW.equals(action) && data != null) {
+ getPresenter().onBrowserCallback(data);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ EventBus.getDefault().unregister(this);
+ }
+
+ private void initDrawer() {
+ navigationAdapter = new NavigationAdapter(this, () -> {
+ showPayPalForm();
+ drawerLayout.closeDrawers();
+ });
+ navigationAdapter.setOnItemClickListener((item, view, position) -> {
+ if (item != NavigationElement.SEPARATOR) {
+ selectItem(item);
+ }
+ });
+ navigationAdapter.selectItem(NavigationElement.LIBRARY);
+ rvNavigation.setLayoutManager(new LinearLayoutManager(this));
+ rvNavigation.setAdapter(navigationAdapter);
+
+ drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.open, R.string.close) {
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ }
+
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ super.onDrawerClosed(drawerView);
+ }
+ };
+ drawerLayout.addDrawerListener(drawerToggle);
+
+ selectItem(NavigationElement.LIBRARY);
+ }
+
+ public void selectItem(NavigationElement navigationElement) {
+ if (NavigationElement.PREMIUM == navigationElement) {
+ getPresenter().checkForPremiumBook();
+ } else {
+ currentNavigationElement = navigationElement;
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ fragmentManager.beginTransaction().replace(R.id.content, navigationElement.getFragment()).commit();
+ setTitle(navigationElement.getTitle());
+ }
+ drawerLayout.closeDrawers();
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ drawerToggle.syncState();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (NavigationElement.LIBRARY != currentNavigationElement) {
+ selectItem(NavigationElement.LIBRARY);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return drawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onHomeClicked() {
+ drawerLayout.openDrawer(GravityCompat.START);
+ }
+
+ @SuppressWarnings("unused")
+ @Subscribe
+ public void onChangeNavigation(ChangeNavigationEvent event) {
+ selectItem(event.getElement());
+ navigationAdapter.selectItem(event.getElement());
+ }
+
+ @OnClick(R.id.btnLogin)
+ public void onLoginClicked() {
+ getPresenter().onLoginClicked();
+ }
+
+ @OnClick(R.id.btnLogout)
+ public void onLogoutClicked() {
+ getPresenter().onLogoutClicked();
+ }
+
+ @Override
+ public void setLoggedIn(boolean loggedIn) {
+ if (loggedIn) {
+ btnLogin.setVisibility(View.GONE);
+ llLoggedInContainer.setVisibility(View.VISIBLE);
+ } else {
+ btnLogin.setVisibility(View.VISIBLE);
+ llLoggedInContainer.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void setLoggedUsername(String username) {
+ tvUsername.setText(username);
+ }
+
+ @Override
+ public void setProgressDialogVisibility(boolean visible) {
+ if (visible && progressDialog == null) {
+ String dialogMessage = getString(R.string.main_view_progress);
+ progressDialog = ProgressDialog.show(this, null, dialogMessage, true, false);
+ } else if (!visible && progressDialog != null) {
+ progressDialog.hide();
+ progressDialog = null;
+ }
+ }
+
+ @Override
+ public void showToastMessage(int messageResId) {
+ Toast.makeText(this, messageResId, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void showCustomTabsAuthentication(Uri authorizationUrl) {
+ showBrowserView(authorizationUrl);
+ }
+
+ @Override
+ public void showPremiumBook(String slug) {
+ startActivity(new BookActivity.BookIntent(slug, BookType.TYPE_PREMIUM, this));
+ }
+
+ @Override
+ public void showNoPremiumBookAvailable(boolean userLoggedIn) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_prapremiere_title);
+
+ if (!userLoggedIn) {
+ builder.setMessage(R.string.no_prapremiere_message)
+ .setPositiveButton(R.string.become_a_friend, (dialog, which) -> getPresenter().onBecomeAFriendClick())
+ .setNegativeButton(R.string.no_thanks, (dialog, which) -> {
+ // nop.
+ });
+ } else {
+ builder.setMessage(R.string.no_prapremiere_message_logged)
+ .setPositiveButton(R.string.OK, (dialog, which) -> {
+ // nop.
+ });
+ }
+ builder.create()
+ .show();
+ }
+
+ @Override
+ public void showPremiumForm() {
+ showPayPalForm();
+ }
+
+ @Override
+ public void showLoginFirst() {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.login)
+ .setMessage(R.string.login_first)
+ .setPositiveButton(R.string.login, (dialog, which) -> {
+ getPresenter().onLoginClicked();
+ })
+ .setNegativeButton(R.string.no_thanks, (dialog, which) -> {
+ // nop.
+ })
+ .create()
+ .show();
+ }
+
+ @Override
+ public void relaunch(int relaunchMessageResId) {
+ MainIntent intent = new MainIntent(relaunchMessageResId, this);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainPresenter.java
new file mode 100644
index 0000000..0e0c452
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainPresenter.java
@@ -0,0 +1,259 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.net.Uri;
+import android.util.Log;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.base.mvp.Presenter;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.RestClientCallback;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.connection.models.OAuthTokenModel;
+import com.moiseum.wolnelektury.connection.models.UserModel;
+import com.moiseum.wolnelektury.connection.services.BooksService;
+import com.moiseum.wolnelektury.connection.services.UserService;
+import com.moiseum.wolnelektury.events.LoggedInEvent;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+import com.moiseum.wolnelektury.view.main.events.PremiumStatusEvent;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.List;
+
+import retrofit2.Call;
+
+/**
+ * Created by Piotr Ostrowski on 12.06.2018.
+ */
+class MainPresenter extends Presenter {
+
+ private static final String TAG = MainPresenter.class.getSimpleName();
+ private static final String OAUTH_CALLBACK_VALUE = "wolnelekturyapp://oauth.callback/?oauth_token=";
+ private static final String PAYPAL_SUCCESS_CALLBACK_VALUE = "wolnelekturyapp://paypal_return";
+ private static final String PAYPAL_ERROR_CALLBACK_VALUE = "wolnelekturyapp://paypal_error";
+
+ private RestClient client = WLApplication.getInstance().getRestClient();
+ private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+ private Call currentCall;
+ private Call checkCall;
+
+ MainPresenter(MainView view) {
+ super(view);
+ getView().setLoggedIn(preferences.isUserLoggedIn());
+ getView().setLoggedUsername(preferences.getUsername());
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (preferences.isUserLoggedIn()) {
+ checkPremiumStatus();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (currentCall != null) {
+ currentCall.cancel();
+ }
+ if (checkCall != null) {
+ checkCall.cancel();
+ }
+ }
+
+ void onLoginClicked() {
+ getView().setProgressDialogVisibility(true);
+ currentCall = client.call(new RestClientCallback() {
+
+ @Override
+ public void onSuccess(OAuthTokenModel data) {
+ preferences.setTemporaryLoginToken(data.getToken());
+ String authUrl = String.format(RestClient.WEB_OAUTH_AUTHORIZATION_URL, data.getToken());
+ getView().setProgressDialogVisibility(false);
+ getView().showCustomTabsAuthentication(Uri.parse(authUrl));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ Log.e(TAG, "Failed to obtain request token.", e);
+ getView().setProgressDialogVisibility(false);
+ getView().showToastMessage(R.string.login_request_token_failed);
+ }
+
+ @Override
+ public void onCancel() {
+ getView().setProgressDialogVisibility(false);
+ }
+
+ @Override
+ public Call execute(UserService service) {
+ return service.requestToken();
+ }
+ }, UserService.class);
+ }
+
+ void onLogoutClicked() {
+ client.clearOAuthTokens();
+ preferences.clearUserData();
+ getView().relaunch(R.string.logout_successful);
+ }
+
+ void onBecomeAFriendClick() {
+ if (preferences.isUserLoggedIn()) {
+ getView().showPremiumForm();
+ } else {
+ getView().showLoginFirst();
+ }
+ }
+
+ void onBrowserCallback(String intentData) {
+ if (PAYPAL_SUCCESS_CALLBACK_VALUE.equals(intentData)) {
+ preferences.setPremium(true);
+ EventBus.getDefault().post(new PremiumStatusEvent(true));
+ getView().showToastMessage(R.string.premium_purchase_succeeded);
+ } else if (PAYPAL_ERROR_CALLBACK_VALUE.equals(intentData)) {
+ getView().showToastMessage(R.string.premium_purchase_failed);
+ } else {
+ onAuthorizationIntent(intentData);
+ }
+ }
+
+ void checkForPremiumBook() {
+ fetchHeader();
+ }
+
+ private void onAuthorizationIntent(String intentData) {
+ String correctDataString = OAUTH_CALLBACK_VALUE + preferences.getTemporaryLoginToken();
+ if (intentData.compareTo(correctDataString) != 0) {
+ getView().showToastMessage(R.string.login_auth_callback_malformed);
+ return;
+ }
+
+ getView().setProgressDialogVisibility(true);
+ currentCall = client.call(new RestClientCallback() {
+
+ @Override
+ public void onSuccess(OAuthTokenModel data) {
+ preferences.storeAccessToken(data);
+ fetchUser();
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ Log.e(TAG, "Failed to obtain access token.", e);
+ getView().setProgressDialogVisibility(false);
+ getView().showToastMessage(R.string.login_access_token_failed);
+ }
+
+ @Override
+ public void onCancel() {
+ getView().setProgressDialogVisibility(false);
+ }
+
+ @Override
+ public Call execute(UserService service) {
+ return service.accessToken();
+ }
+ }, UserService.class);
+ }
+
+ private void fetchUser() {
+ getView().setProgressDialogVisibility(true);
+
+ /*
+ * We are marking user as logged in cause we already have credentials at this point.
+ * If this request fails we can perform it later on and ignore those errors for now.
+ */
+ currentCall = client.call(new RestClientCallback() {
+ @Override
+ public void onSuccess(UserModel userModel) {
+ preferences.setUsername(userModel.getUsername());
+ preferences.setPremium(userModel.isPremium());
+ EventBus.getDefault().post(new LoggedInEvent());
+ getView().setProgressDialogVisibility(false);
+ getView().setLoggedIn(true);
+ getView().setLoggedUsername(userModel.getUsername());
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ EventBus.getDefault().post(new LoggedInEvent());
+ getView().setProgressDialogVisibility(false);
+ getView().setLoggedIn(true);
+ }
+
+ @Override
+ public void onCancel() {
+ EventBus.getDefault().post(new LoggedInEvent());
+ getView().setProgressDialogVisibility(false);
+ getView().setLoggedIn(true);
+ }
+
+ @Override
+ public Call execute(UserService service) {
+ return service.getUser();
+ }
+ }, UserService.class);
+ }
+
+ private void fetchHeader() {
+ getView().setProgressDialogVisibility(true);
+ currentCall = client.call(new RestClientCallback, BooksService>() {
+ @Override
+ public void onSuccess(List data) {
+ getView().setProgressDialogVisibility(false);
+ if (data.size() > 0) {
+ getView().showPremiumBook(data.get(0).getSlug());
+ } else {
+ getView().showNoPremiumBookAvailable(preferences.isUserLoggedIn());
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ getView().showToastMessage(R.string.fetching_premium_failed);
+ }
+
+ @Override
+ public void onCancel() {
+ // nop.
+ }
+
+ @Override
+ public Call> execute(BooksService service) {
+ return service.getPreview();
+ }
+ }, BooksService.class);
+ }
+
+ private void checkPremiumStatus() {
+ checkCall = client.call(new RestClientCallback() {
+ @Override
+ public void onSuccess(UserModel userModel) {
+ boolean currentPremiumStatus = preferences.isUserPremium();
+ preferences.setUsername(userModel.getUsername());
+ preferences.setPremium(userModel.isPremium());
+ if (currentPremiumStatus && !userModel.isPremium()) {
+ getView().relaunch(R.string.subscription_lost);
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ // nop
+ }
+
+ @Override
+ public void onCancel() {
+ // nop
+ }
+
+ @Override
+ public Call execute(UserService service) {
+ return service.getUser();
+ }
+ }, UserService.class);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainView.java
new file mode 100644
index 0000000..aa764de
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainView.java
@@ -0,0 +1,30 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.net.Uri;
+import android.support.annotation.StringRes;
+
+/**
+ * Created by Piotr Ostrowski on 12.06.2018.
+ */
+public interface MainView {
+
+ void setLoggedIn(boolean loggedIn);
+
+ void setLoggedUsername(String username);
+
+ void setProgressDialogVisibility(boolean visible);
+
+ void showToastMessage(@StringRes int messageResId);
+
+ void showCustomTabsAuthentication(Uri authorizationUrl);
+
+ void showPremiumBook(String slug);
+
+ void showNoPremiumBookAvailable(boolean userLoggedIn);
+
+ void showPremiumForm();
+
+ void showLoginFirst();
+
+ void relaunch(@StringRes int relaunchMessageResId);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationAdapter.java
new file mode 100644
index 0000000..58f0597
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationAdapter.java
@@ -0,0 +1,82 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter;
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+
+/**
+ * @author golonkos
+ */
+
+public class NavigationAdapter extends RecyclerAdapter> {
+
+ private static int TYPE_ITEM = 0;
+ private static int TYPE_SEPARATOR = 1;
+ private static int TYPE_SUPPORT = 2;
+ private static int TYPE_BLANK = 3;
+
+ private final SupportUsListener supportUsListener;
+
+ private final SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences();
+
+ NavigationAdapter(Context context, SupportUsListener supportUsListener) {
+ super(context, Selection.SINGLE);
+ this.supportUsListener = supportUsListener;
+ setItems(NavigationElement.valuesForNavigation());
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ NavigationElement element = getItem(position);
+ if (element.requiresLogin() && !preferences.isUserLoggedIn()) {
+ return TYPE_BLANK;
+ }
+
+ switch (element) {
+ case SEPARATOR:
+ return TYPE_SEPARATOR;
+ case SUPPORT_US:
+ return TYPE_SUPPORT;
+ default:
+ return TYPE_ITEM;
+ }
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ if (viewType == TYPE_SEPARATOR) {
+ return new SeparatorViewHolder(inflate(R.layout.navigation_separator_item, parent));
+ }
+ if (viewType == TYPE_SUPPORT) {
+ return new SupportViewHolder(inflate(R.layout.navigation_support_item, parent), supportUsListener);
+ }
+ if (viewType == TYPE_BLANK) {
+ return new NavigationBlankViewHolder(inflate(R.layout.navigation_blank, parent));
+ }
+ return new NavigationViewHolder(inflate(R.layout.navigation_item, parent));
+ }
+
+ @Override
+ protected String getItemId(NavigationElement item) {
+ return item.name();
+ }
+
+ @Override
+ protected void onItemClicked(View view, NavigationElement item, int position) {
+ if (item != NavigationElement.SEPARATOR && item != NavigationElement.SUPPORT_US) {
+ super.onItemClicked(view, item, position);
+ }
+ }
+
+ interface SupportUsListener {
+ void onSupportUsClicked();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationBlankViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationBlankViewHolder.java
new file mode 100644
index 0000000..fbc2399
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationBlankViewHolder.java
@@ -0,0 +1,20 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.view.View;
+
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+
+/**
+ * Created by Piotr Ostrowski on 02.07.2018.
+ */
+public class NavigationBlankViewHolder extends ViewHolder {
+
+ NavigationBlankViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ public void bind(NavigationElement item, boolean selected) {
+ // nop.
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationElement.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationElement.java
new file mode 100644
index 0000000..219a154
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationElement.java
@@ -0,0 +1,315 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.support.v4.app.Fragment;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.view.AboutFragment;
+import com.moiseum.wolnelektury.view.book.list.BookListType;
+import com.moiseum.wolnelektury.view.book.list.BooksListFragment;
+import com.moiseum.wolnelektury.view.library.LibraryFragment;
+import com.moiseum.wolnelektury.view.news.NewsListFragment;
+import com.moiseum.wolnelektury.view.search.BookSearchFragment;
+import com.moiseum.wolnelektury.view.settings.SettingsFragment;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author golonkos
+ */
+
+public enum NavigationElement {
+
+ LIBRARY {
+ @Override
+ public int getTitle() {
+ return R.string.nav_wolne_lektury;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return LibraryFragment.newInstance();
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_menu_library;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ PREMIUM {
+ @Override
+ public int getTitle() {
+ return R.string.nav_premium;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ // This in intentional. We have to handle this separately.
+ return null;
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_menu_star;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ NOW_READING {
+ @Override
+ public int getTitle() {
+ return R.string.nav_reading;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return BooksListFragment.newInstance(BookListType.READING);
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_book;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return true;
+ }
+ },
+
+ FAVOURITES {
+ @Override
+ public int getTitle() {
+ return R.string.nav_favourites;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return BooksListFragment.newInstance(BookListType.FAVOURITES);
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_menu_fav;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return true;
+ }
+ },
+
+ COMPLETED {
+ @Override
+ public int getTitle() {
+ return R.string.nav_completed;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return BooksListFragment.newInstance(BookListType.COMPLETED);
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_accept;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return true;
+ }
+ },
+
+ AUDIOBOOKS {
+ @Override
+ public int getTitle() {
+ return R.string.nav_audiobooks;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return BooksListFragment.newInstance(BookListType.AUDIOBOOKS);
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_menu_audiobook;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ DOWNLOADED {
+ @Override
+ public int getTitle() {
+ return R.string.nav_my_collection;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return BooksListFragment.newInstance(BookListType.DOWNLOADED);
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_menu_downloaded;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ SEARCH {
+ @Override
+ public int getTitle() {
+ return R.string.nav_catalog;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return BookSearchFragment.newInstance();
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_menu_search;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ ABOUT {
+ @Override
+ public int getTitle() {
+ return R.string.nav_about;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return AboutFragment.newInstance();
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_about;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ NEWS {
+ @Override
+ public int getTitle() {
+ return R.string.nav_news;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return NewsListFragment.newInstance();
+ }
+
+ @Override
+ public int getIcon() {
+ return R.drawable.ic_news;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ SETTINGS {
+ @Override
+ public int getTitle(){return R.string.settings;}
+
+ @Override
+ public Fragment getFragment() {return SettingsFragment.newInstance();}
+
+ @Override
+ public int getIcon(){return R.drawable.ic_settings;}
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ SUPPORT_US {
+ @Override
+ public int getTitle() {
+ return -1;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return null;
+ }
+
+ @Override
+ public int getIcon() {
+ return -1;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ },
+
+ SEPARATOR {
+ @Override
+ public int getTitle() {
+ return -1;
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return null;
+ }
+
+ @Override
+ public int getIcon() {
+ return -1;
+ }
+
+ @Override
+ public boolean requiresLogin() {
+ return false;
+ }
+ };
+
+ public abstract int getTitle();
+
+ public abstract Fragment getFragment();
+
+ public abstract int getIcon();
+
+ public abstract boolean requiresLogin();
+
+ public static List valuesForNavigation() {
+ return Arrays.asList(SUPPORT_US, LIBRARY, SEPARATOR, PREMIUM, SEARCH, AUDIOBOOKS, NOW_READING, FAVOURITES, COMPLETED, SEPARATOR, DOWNLOADED, SEPARATOR, NEWS, SETTINGS, ABOUT);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationViewHolder.java
new file mode 100644
index 0000000..28e23e4
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationViewHolder.java
@@ -0,0 +1,45 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.support.v4.content.ContextCompat;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+import com.moiseum.wolnelektury.utils.SharedPreferencesUtils;
+
+import butterknife.BindView;
+
+/**
+ * @author golonkos
+ */
+
+public class NavigationViewHolder extends ViewHolder {
+
+ @BindView(R.id.tvNavName)
+ TextView tvName;
+ @BindView(R.id.ivNavIcon)
+ ImageView ivNavIcon;
+
+ NavigationViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ public void bind(NavigationElement item, boolean selected) {
+ tvName.setText(item.getTitle());
+ ivNavIcon.setImageResource(item.getIcon());
+ int color = selected ? R.color.white : R.color.turquoise;
+ ivNavIcon.setColorFilter(ContextCompat.getColor(getContext(), color), PorterDuff.Mode.SRC_IN);
+ if (item.getTitle() == R.string.nav_premium) {
+ tvName.setTextColor(getContext().getResources().getColor(R.color.orange_light));
+ ivNavIcon.setColorFilter(ContextCompat.getColor(getContext(), R.color.orange_light), PorterDuff.Mode.SRC_IN);
+ }
+
+ }
+
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SeparatorViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SeparatorViewHolder.java
new file mode 100644
index 0000000..546ae15
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SeparatorViewHolder.java
@@ -0,0 +1,21 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.view.View;
+
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+
+/**
+ * @author golonkos.
+ */
+
+public class SeparatorViewHolder extends ViewHolder {
+
+ public SeparatorViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ public void bind(NavigationElement item, boolean selected) {
+
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SupportViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SupportViewHolder.java
new file mode 100644
index 0000000..39855e0
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SupportViewHolder.java
@@ -0,0 +1,35 @@
+package com.moiseum.wolnelektury.view.main;
+
+import android.view.View;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+
+import butterknife.OnClick;
+
+/**
+ * @author golonkos
+ */
+
+public class SupportViewHolder extends ViewHolder {
+
+ private final NavigationAdapter.SupportUsListener supportUsListener;
+
+ public SupportViewHolder(View view, NavigationAdapter.SupportUsListener supportUsListener) {
+ super(view);
+ this.supportUsListener = supportUsListener;
+ }
+
+ @Override
+ public void bind(NavigationElement item, boolean selected) {
+ //nop
+ }
+
+ @SuppressWarnings("unused")
+ @OnClick(R.id.btnSupportUs)
+ public void onSupportUsClicked() {
+ if (supportUsListener != null) {
+ supportUsListener.onSupportUsClicked();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/events/PremiumStatusEvent.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/events/PremiumStatusEvent.java
new file mode 100644
index 0000000..2d4a290
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/events/PremiumStatusEvent.java
@@ -0,0 +1,16 @@
+package com.moiseum.wolnelektury.view.main.events;
+
+/**
+ * Created by Piotr Ostrowski on 26.08.2018.
+ */
+public class PremiumStatusEvent {
+ private boolean premium;
+
+ public PremiumStatusEvent(boolean premium) {
+ this.premium = premium;
+ }
+
+ public boolean isPremium() {
+ return premium;
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListAdapter.java
new file mode 100644
index 0000000..8e603a2
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListAdapter.java
@@ -0,0 +1,57 @@
+package com.moiseum.wolnelektury.view.news;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter;
+import com.moiseum.wolnelektury.components.recycler.ViewHolder;
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+
+import butterknife.BindView;
+
+public class NewsListAdapter extends RecyclerAdapter {
+
+ static class NewsViewHolder extends ViewHolder {
+
+ @BindView(R.id.textViewDate)
+ TextView txtDate;
+ @BindView(R.id.textViewLead)
+ TextView txtLead;
+ @BindView(R.id.ivNewsThumb)
+ ImageView newsImage;
+
+ NewsViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ public void bind(NewsModel item, boolean selected) {
+ txtDate.setText(item.getTime());
+ txtLead.setText(item.getTitle());
+ Glide.with(getContext()).load(item.getThumbUrl()).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(newsImage);
+ }
+ }
+
+ NewsListAdapter(Context context) {
+ super(context, RecyclerAdapter.Selection.NONE);
+ }
+
+ @NonNull
+ @Override
+ public NewsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = inflate(R.layout.news_item, parent);
+ return new NewsViewHolder(view);
+ }
+
+ @Override
+ protected String getItemId(NewsModel item) {
+ return item.getKey();
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListFragment.java
new file mode 100644
index 0000000..f10688c
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListFragment.java
@@ -0,0 +1,105 @@
+package com.moiseum.wolnelektury.view.news;
+
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+import com.moiseum.wolnelektury.components.ProgressRecyclerView;
+import com.moiseum.wolnelektury.components.recycler.EndlessRecyclerOnScrollListener;
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+import com.moiseum.wolnelektury.view.news.single.NewsActivity;
+
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+public class NewsListFragment extends PresenterFragment implements NewsListView {
+
+ public static NewsListFragment newInstance() {
+ return new NewsListFragment();
+ }
+
+ @BindView(R.id.rvNews)
+ ProgressRecyclerView rvNews;
+ @BindView(R.id.pbLoadMore)
+ ProgressBar pbLoadMore;
+ @BindView(R.id.btnReloadMore)
+ Button btnReloadMore;
+
+ private NewsListAdapter adapter;
+ private EndlessRecyclerOnScrollListener rvBooksScrollListener = new EndlessRecyclerOnScrollListener() {
+ @Override
+ public void onLoadMore() {
+ if (adapter.getItemCount() > 0) {
+ getPresenter().loadMoreNews();
+ }
+ }
+ };
+
+ @Override
+ protected NewsListPresenter createPresenter() {
+ return new NewsListPresenter(this);
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_news;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
+ rvNews.setLayoutManager(layoutManager);
+ rvNews.addOnScrollListener(rvBooksScrollListener);
+
+ adapter = new NewsListAdapter(getContext());
+ adapter.setOnItemClickListener((item, view1, position) -> getPresenter().onNewsClicked(item));
+ rvNews.setAdapter(adapter);
+ }
+
+ @Override
+ public void setData(List data) {
+ if (adapter.getItemCount() == 0) {
+ rvNews.setItems(data);
+ } else {
+ rvNews.addItems(data);
+ }
+ }
+
+ @Override
+ public void setProgressVisible(boolean visible) {
+ if (adapter.getItemCount() == 0) {
+ rvNews.setProgressVisible(visible);
+ } else {
+ pbLoadMore.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ @Override
+ public void showError(Exception e) {
+ Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show();
+ if (adapter.getItemCount() != 0) {
+ btnReloadMore.setVisibility(View.VISIBLE);
+ } else {
+ rvNews.showRetryButton(() -> getPresenter().loadMoreNews());
+ }
+ }
+
+ @Override
+ public void launchNews(NewsModel news) {
+ startActivity(new NewsActivity.NewsIntent(news, getContext()));
+ }
+
+ @OnClick(R.id.btnReloadMore)
+ public void onReloadMoreClicked() {
+ btnReloadMore.setVisibility(View.GONE);
+ pbLoadMore.setVisibility(View.VISIBLE);
+ getPresenter().loadMoreNews();
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListPresenter.java
new file mode 100644
index 0000000..4b87b96
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListPresenter.java
@@ -0,0 +1,81 @@
+package com.moiseum.wolnelektury.view.news;
+
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.base.DataObserver;
+import com.moiseum.wolnelektury.base.DataProvider;
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+import com.moiseum.wolnelektury.connection.RestClient;
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+import com.moiseum.wolnelektury.connection.services.NewsService;
+
+import java.util.Collections;
+import java.util.List;
+
+import retrofit2.Call;
+
+/**
+ * Created by Piotr Ostrowski on 06.08.2018.
+ */
+class NewsListPresenter extends FragmentPresenter {
+
+ private class NewsListDataProvider extends DataProvider, NewsService> {
+
+ @Override
+ protected Class getServiceClass() {
+ return NewsService.class;
+ }
+
+ @Override
+ public Call> execute(NewsService service) {
+ return service.getNews(lastKeySlug, RestClient.PAGINATION_LIMIT);
+ }
+ }
+
+ private class NewsListDataObserver implements DataObserver> {
+
+ @Override
+ public void onLoadStarted() {
+ getView().setProgressVisible(true);
+ }
+
+ @Override
+ public void onLoadSuccess(List data) {
+ if (data.size() > 0) {
+ lastKey = data.get(data.size() - 1).getKey();
+ }
+ getView().setProgressVisible(false);
+ getView().setData(data);
+ }
+
+ @Override
+ public void onLoadFailed(Exception e) {
+ getView().setProgressVisible(false);
+ getView().setData(Collections.emptyList());
+ getView().showError(e);
+ }
+ }
+
+ private NewsListDataProvider dataProvider;
+ private String lastKey = null;
+
+ NewsListPresenter(NewsListView view) {
+ super(view);
+ dataProvider = new NewsListDataProvider();
+ dataProvider.setDataObserver(new NewsListDataObserver());
+ }
+
+ @Override
+ public void onViewCreated(Bundle savedInstanceState) {
+ super.onViewCreated(savedInstanceState);
+ dataProvider.load(null);
+ }
+
+ public void loadMoreNews() {
+ dataProvider.load(lastKey);
+ }
+
+ public void onNewsClicked(NewsModel newsModel) {
+ getView().launchNews(newsModel);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListView.java
new file mode 100644
index 0000000..35bfc0b
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListView.java
@@ -0,0 +1,14 @@
+package com.moiseum.wolnelektury.view.news;
+
+import com.moiseum.wolnelektury.base.mvp.LoadingView;
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+
+import java.util.List;
+
+/**
+ * Created by Piotr Ostrowski on 06.08.2018.
+ */
+public interface NewsListView extends LoadingView> {
+
+ void launchNews(NewsModel news);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsActivity.java
new file mode 100644
index 0000000..f5d4373
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsActivity.java
@@ -0,0 +1,50 @@
+package com.moiseum.wolnelektury.view.news.single;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractActivity;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+
+import org.parceler.Parcels;
+
+import static com.moiseum.wolnelektury.view.news.single.NewsActivity.NewsIntent.NEWS_KEY;
+
+/**
+ * Created by Piotr Ostrowski on 06.08.2018.
+ */
+public class NewsActivity extends AbstractActivity {
+
+ private static final String NEWS_FRAGMENT_TAG = "NewsFragmentTag";
+
+ public static class NewsIntent extends AbstractIntent {
+ static final String NEWS_KEY = "NewsKey";
+
+ public NewsIntent(NewsModel news, Context context) {
+ super(context, NewsActivity.class);
+ putExtra(NEWS_KEY, Parcels.wrap(news));
+ }
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_blank;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+ if (!getIntent().hasExtra(NEWS_KEY)) {
+ throw new IllegalStateException("Activity intent is missing news extras.");
+ }
+ setTitle("");
+
+ NewsModel news = Parcels.unwrap(getIntent().getParcelableExtra(NEWS_KEY));
+ NewsFragment newsFragment = (NewsFragment) getSupportFragmentManager().findFragmentByTag(NEWS_FRAGMENT_TAG);
+ if (newsFragment == null) {
+ newsFragment = NewsFragment.newInstance(news);
+ getSupportFragmentManager().beginTransaction().add(R.id.flContainer, newsFragment, NEWS_FRAGMENT_TAG).commit();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsFragment.java
new file mode 100644
index 0000000..cc2e108
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsFragment.java
@@ -0,0 +1,129 @@
+package com.moiseum.wolnelektury.view.news.single;
+
+import android.os.Bundle;
+import android.support.design.widget.AppBarLayout;
+import android.support.design.widget.CollapsingToolbarLayout;
+import android.support.v4.view.ViewPager;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+import com.moiseum.wolnelektury.view.news.zoom.ZoomActivity;
+
+import org.parceler.Parcels;
+import org.sufficientlysecure.htmltextview.HtmlTextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+import me.relex.circleindicator.CircleIndicator;
+
+/**
+ * Created by Piotr Ostrowski on 06.08.2018.
+ */
+public class NewsFragment extends PresenterFragment implements NewsView {
+
+ private static final String NEWS_ARGUMENT_KEY = "NewsArgumentKey";
+ private static final int FIVE_PAGES = 5;
+
+ public static NewsFragment newInstance(NewsModel news) {
+ NewsFragment fragment = new NewsFragment();
+ Bundle args = new Bundle(1);
+ args.putParcelable(NEWS_ARGUMENT_KEY, Parcels.wrap(news));
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @BindView(R.id.vpGallery)
+ ViewPager vpGallery;
+ @BindView(R.id.indicator)
+ CircleIndicator indicator;
+ @BindView(R.id.tvNewsTitle)
+ TextView tvNewsTitle;
+ @BindView(R.id.tvNewsTime)
+ TextView tvNewsTime;
+ @BindView(R.id.tvNewsPlace)
+ TextView tvNewsPlace;
+ @BindView(R.id.tvNewsBody)
+ HtmlTextView tvNewsBody;
+ @BindView(R.id.clMainView)
+ View clMainView;
+ @BindView(R.id.ctlCollapse)
+ CollapsingToolbarLayout ctlCollapse;
+ @BindView(R.id.llContentContainer)
+ LinearLayout llContentContainer;
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_single_news;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ Toolbar toolbar = view.findViewById(R.id.bookToolbar);
+ setupToolbar(toolbar);
+ }
+
+ @Override
+ protected NewsPresenter createPresenter() {
+ if (getArguments() == null || getArguments().getParcelable(NEWS_ARGUMENT_KEY) == null) {
+ throw new IllegalStateException("Fragment is missing arguments");
+ }
+ NewsModel news = Parcels.unwrap(getArguments().getParcelable(NEWS_ARGUMENT_KEY));
+ return new NewsPresenter(news, this);
+ }
+
+ @OnClick(R.id.fabShare)
+ public void onShareClick() {
+ getPresenter().onShareNewsClicked();
+ }
+
+ @Override
+ public void initializeNewsView(NewsModel news) {
+ ctlCollapse.setTitle(news.getTitle());
+ ctlCollapse.setExpandedTitleColor(getResources().getColor(android.R.color.transparent));
+ tvNewsTitle.setText(news.getTitle());
+ tvNewsTime.setText(news.getTime());
+ tvNewsPlace.setText(news.getPlace());
+ tvNewsBody.setHtml(news.getBody());
+
+ NewsGalleryAdapter galleryAdapter = new NewsGalleryAdapter(news.getGalleryUrl(), getContext());
+ vpGallery.setAdapter(galleryAdapter);
+ vpGallery.setOffscreenPageLimit(FIVE_PAGES);
+ indicator.setViewPager(vpGallery);
+ addDisposable(galleryAdapter.getPageClickObservable().subscribe(position -> {
+ ArrayList urls = new ArrayList<>(news.getGalleryUrl());
+ startActivity(new ZoomActivity.ZoomIntent(urls, position, getContext()));
+ }));
+ enableToolbarCollapse();
+ }
+
+ @Override
+ public void startShareActivity(String shareUrl) {
+ showShareActivity(shareUrl);
+ }
+
+ private void enableToolbarCollapse() {
+ ViewTreeObserver viewTreeObserver = llContentContainer.getViewTreeObserver();
+ if (viewTreeObserver.isAlive()) {
+ viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ llContentContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ if (ctlCollapse.getHeight() + llContentContainer.getHeight() > clMainView.getHeight()) {
+ AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) ctlCollapse.getLayoutParams();
+ params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
+ ctlCollapse.setLayoutParams(params);
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsGalleryAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsGalleryAdapter.java
new file mode 100644
index 0000000..a5dff6a
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsGalleryAdapter.java
@@ -0,0 +1,78 @@
+package com.moiseum.wolnelektury.view.news.single;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.moiseum.wolnelektury.R;
+
+import java.util.List;
+
+import io.reactivex.Observable;
+import io.reactivex.subjects.PublishSubject;
+
+/**
+ * Created by Piotr Ostrowski on 06.08.2018.
+ */
+public class NewsGalleryAdapter extends PagerAdapter {
+
+ private final LayoutInflater inflater;
+ private final List galleryUrls;
+
+ private PublishSubject pagerOnClickSubject = PublishSubject.create();
+ private View.OnClickListener pageClickListener = v -> {
+ int position = (int) v.getTag();
+ pagerOnClickSubject.onNext(position);
+ };
+
+ NewsGalleryAdapter(List galleryUrls, Context context) {
+ this.galleryUrls = galleryUrls;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @NonNull
+ @SuppressLint("InflateParams")
+ @Override
+ public Object instantiateItem(@NonNull ViewGroup container, int position) {
+ View view = inflater.inflate(R.layout.fragment_single_news_gallery_item, null);
+ view.setTag(position);
+ view.setOnClickListener(pageClickListener);
+ container.addView(view);
+
+ ImageView ivGallery = view.findViewById(R.id.tvGalleryImage);
+ Glide.with(view.getContext())
+ .load(galleryUrls.get(position))
+ .diskCacheStrategy(DiskCacheStrategy.ALL)
+ .dontTransform()
+ .into(ivGallery);
+
+ return view;
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
+ View view = (View) object;
+ container.removeView(view);
+ }
+
+ @Override
+ public int getCount() {
+ return galleryUrls.size();
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
+ return view == object;
+ }
+
+ Observable getPageClickObservable() {
+ return pagerOnClickSubject.hide();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsPresenter.java
new file mode 100644
index 0000000..0b0b476
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsPresenter.java
@@ -0,0 +1,39 @@
+package com.moiseum.wolnelektury.view.news.single;
+
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+import com.moiseum.wolnelektury.connection.models.NewsModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Piotr Ostrowski on 06.08.2018.
+ */
+class NewsPresenter extends FragmentPresenter {
+
+ private final NewsModel news;
+
+ NewsPresenter(NewsModel news, NewsView view) {
+ super(view);
+ this.news = news;
+
+ List galleryUrls = new ArrayList<>(news.getGalleryUrl().size() + 1);
+ if (news.getImageUrl() != null) {
+ galleryUrls.add(news.getImageUrl());
+ }
+ galleryUrls.addAll(news.getGalleryUrl());
+ this.news.setGalleryUrl(galleryUrls);
+ }
+
+ @Override
+ public void onViewCreated(Bundle savedInstanceState) {
+ super.onViewCreated(savedInstanceState);
+ getView().initializeNewsView(news);
+ }
+
+ void onShareNewsClicked() {
+ getView().startShareActivity(news.getUrl());
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsView.java
new file mode 100644
index 0000000..d6180d5
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsView.java
@@ -0,0 +1,11 @@
+package com.moiseum.wolnelektury.view.news.single;
+
+import com.moiseum.wolnelektury.connection.models.NewsModel; /**
+ * Created by Piotr Ostrowski on 06.08.2018.
+ */
+public interface NewsView {
+
+ void initializeNewsView(NewsModel news);
+
+ void startShareActivity(String shareUrl);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomActivity.java
new file mode 100644
index 0000000..63c4f81
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomActivity.java
@@ -0,0 +1,48 @@
+package com.moiseum.wolnelektury.view.news.zoom;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractActivity;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+
+import java.util.ArrayList;
+
+/**
+ * Created by Piotr Ostrowski on 07.08.2018.
+ */
+public class ZoomActivity extends AbstractActivity {
+
+ private static final String PHOTOS_URL_KEY = "PhotoUrls";
+ private static final String POSITION_KEY = "PositionKey";
+ private static final String ZOOM_FRAGMENT_TAG = "ZoomFragmentTag";
+
+ public static class ZoomIntent extends AbstractIntent {
+ public ZoomIntent(ArrayList photoUrls, int position, Context context) {
+ super(context, ZoomActivity.class);
+ putExtra(PHOTOS_URL_KEY, photoUrls);
+ putExtra(POSITION_KEY, position);
+ }
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_blank;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+ if (!getIntent().hasExtra(PHOTOS_URL_KEY) || !getIntent().hasExtra(POSITION_KEY)) {
+ throw new IllegalStateException("Activity intent is missing news extras.");
+ }
+
+ ArrayList photoUrls = getIntent().getStringArrayListExtra(PHOTOS_URL_KEY);
+ int position = getIntent().getIntExtra(POSITION_KEY, 0);
+ ZoomFragment zoomFragment = (ZoomFragment) getSupportFragmentManager().findFragmentByTag(ZOOM_FRAGMENT_TAG);
+ if (zoomFragment == null) {
+ zoomFragment = ZoomFragment.newInstance(photoUrls, position);
+ getSupportFragmentManager().beginTransaction().add(R.id.flContainer, zoomFragment, ZOOM_FRAGMENT_TAG).commit();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomFragment.java
new file mode 100644
index 0000000..5962e4f
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomFragment.java
@@ -0,0 +1,66 @@
+package com.moiseum.wolnelektury.view.news.zoom;
+
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+import android.view.View;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import butterknife.BindView;
+import me.relex.circleindicator.CircleIndicator;
+
+/**
+ * Created by Piotr Ostrowski on 07.08.2018.
+ */
+public class ZoomFragment extends PresenterFragment implements ZoomView {
+
+ private static final String PHOTOS_URL_KEY = "PhotoUrls";
+ private static final String POSITION_KEY = "PositionKey";
+ private static final int FIVE_PAGES = 5;
+
+ @BindView(R.id.vpGallery)
+ ViewPager vpGallery;
+ @BindView(R.id.indicator)
+ CircleIndicator indicator;
+
+ public static ZoomFragment newInstance(ArrayList photoUrls, int position) {
+ ZoomFragment fragment = new ZoomFragment();
+ Bundle args = new Bundle(1);
+ args.putStringArrayList(PHOTOS_URL_KEY, photoUrls);
+ args.putInt(POSITION_KEY, position);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ protected ZoomPresenter createPresenter() {
+ if (getArguments() == null || getArguments().getStringArrayList(PHOTOS_URL_KEY) == null || getArguments().getInt(POSITION_KEY, -1) == -1) {
+ throw new IllegalStateException("Fragment is missing arguments");
+ }
+ ArrayList urls = getArguments().getStringArrayList(PHOTOS_URL_KEY);
+ int position = getArguments().getInt(POSITION_KEY);
+ return new ZoomPresenter(urls, position, this);
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_zoom;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ // nop.
+ }
+
+ @Override
+ public void initializeZoomView(List photoUrls, int initialPosition) {
+ vpGallery.setAdapter(new ZoomPhotosAdapter(photoUrls, getContext()));
+ vpGallery.setOffscreenPageLimit(FIVE_PAGES);
+ vpGallery.setCurrentItem(initialPosition, false);
+ indicator.setViewPager(vpGallery);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPhotosAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPhotosAdapter.java
new file mode 100644
index 0000000..839a91e
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPhotosAdapter.java
@@ -0,0 +1,105 @@
+package com.moiseum.wolnelektury.view.news.zoom;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.resource.drawable.GlideDrawable;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.Target;
+import com.moiseum.wolnelektury.R;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+import it.sephiroth.android.library.imagezoom.ImageViewTouch;
+import it.sephiroth.android.library.imagezoom.ImageViewTouchBase;
+
+/**
+ * Created by Piotr Ostrowski on 07.08.2018.
+ */
+public class ZoomPhotosAdapter extends PagerAdapter {
+
+ private List photoUrls;
+ private LayoutInflater inflater;
+ private WeakReference mContext;
+
+ ZoomPhotosAdapter(List photoUrl, Context context) {
+ inflater = LayoutInflater.from(context);
+ mContext = new WeakReference<>(context);
+ this.photoUrls = photoUrl;
+ for (String photo : photoUrl) {
+ Glide.with(context).load(photo).dontTransform().preload();
+ }
+ }
+
+ @NonNull
+ @Override
+ public Object instantiateItem(@NonNull ViewGroup collection, int position) {
+ final String photoUrl = photoUrls.get(position);
+ View view = inflater.inflate(R.layout.zoom_item, collection, false);
+
+ final ProgressBar pbLoading = view.findViewById(R.id.pbLoading);
+ final TextView tvLoading = view.findViewById(R.id.tvLoading);
+ final Button btnRetry = view.findViewById(R.id.btnRetry);
+ final ImageViewTouch ivPhoto = view.findViewById(R.id.ivPointPhoto);
+
+ btnRetry.setOnClickListener(v -> {
+ pbLoading.setVisibility(View.VISIBLE);
+ tvLoading.setVisibility(View.VISIBLE);
+ btnRetry.setVisibility(View.GONE);
+ fetchImageWithGlide(photoUrl, pbLoading, tvLoading, btnRetry, ivPhoto);
+ });
+
+ ivPhoto.setDisplayType(ImageViewTouchBase.DisplayType.FIT_HEIGHT);
+ fetchImageWithGlide(photoUrl, pbLoading, tvLoading, btnRetry, ivPhoto);
+
+ collection.addView(view);
+ return view;
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
+ collection.removeView((View) view);
+ }
+
+ @Override
+ public int getCount() {
+ return photoUrls.size();
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
+ return view == object;
+ }
+
+ private void fetchImageWithGlide(String photoUrl, final ProgressBar pbLoading, final TextView tvLoading, final Button btnRetry, ImageViewTouch ivPhoto) {
+ if (mContext.get() != null) {
+ Glide.with(mContext.get()).load(photoUrl).dontTransform().listener(new RequestListener() {
+ @Override
+ public boolean onException(Exception e, String model, Target target, boolean isFirstResource) {
+ pbLoading.setVisibility(View.GONE);
+ tvLoading.setVisibility(View.GONE);
+ btnRetry.setVisibility(View.VISIBLE);
+ return true;
+ }
+
+ @Override
+ public boolean onResourceReady(GlideDrawable resource, String model, Target target, boolean isFromMemoryCache, boolean isFirstResource) {
+ pbLoading.setVisibility(View.GONE);
+ tvLoading.setVisibility(View.GONE);
+ return false;
+ }
+ }).into(ivPhoto);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPresenter.java
new file mode 100644
index 0000000..77ef263
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPresenter.java
@@ -0,0 +1,28 @@
+package com.moiseum.wolnelektury.view.news.zoom;
+
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+
+import java.util.List;
+
+/**
+ * Created by Piotr Ostrowski on 07.08.2018.
+ */
+public class ZoomPresenter extends FragmentPresenter {
+
+ private final List photoUrls;
+ private final int initialPosition;
+
+ ZoomPresenter(List photoUrls, int initialPosition, ZoomView view) {
+ super(view);
+ this.photoUrls = photoUrls;
+ this.initialPosition = initialPosition;
+ }
+
+ @Override
+ public void onViewCreated(Bundle savedInstanceState) {
+ super.onViewCreated(savedInstanceState);
+ getView().initializeZoomView(photoUrls, initialPosition);
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomView.java
new file mode 100644
index 0000000..bef12ae
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomView.java
@@ -0,0 +1,8 @@
+package com.moiseum.wolnelektury.view.news.zoom;
+
+import java.util.List; /**
+ * Created by Piotr Ostrowski on 07.08.2018.
+ */
+public interface ZoomView {
+ void initializeZoomView(List photoUrls, int initialPosition);
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerActivity.java
new file mode 100644
index 0000000..30aa6ce
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerActivity.java
@@ -0,0 +1,48 @@
+package com.moiseum.wolnelektury.view.player;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.AbstractActivity;
+import com.moiseum.wolnelektury.base.AbstractIntent;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+
+import org.parceler.Parcels;
+
+import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_KEY;
+import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_SLUG_KEY;
+
+/**
+ * Created by Piotr Ostrowski on 22.05.2018.
+ */
+public class PlayerActivity extends AbstractActivity {
+
+ private static final String PLAYER_FRAGMENT_TAG = "PlayerFragmentTag";
+
+ public static class PlayerIntent extends AbstractIntent {
+
+ static final String BOOK_KEY = "BookKey";
+ static final String BOOK_SLUG_KEY = "BookSlugKey";
+
+ public PlayerIntent(BookDetailsModel book, String slug, Context context) {
+ super(context, PlayerActivity.class);
+ putExtra(BOOK_KEY, Parcels.wrap(book));
+ putExtra(BOOK_SLUG_KEY, slug);
+ }
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.activity_blank;
+ }
+
+ @Override
+ public void prepareView(Bundle savedInstanceState) {
+ PlayerFragment playerFragment = (PlayerFragment) getSupportFragmentManager().findFragmentByTag(PLAYER_FRAGMENT_TAG);
+ if (playerFragment == null) {
+ playerFragment = PlayerFragment.newInstance(Parcels.unwrap(getIntent().getParcelableExtra(BOOK_KEY)), getIntent().getStringExtra(BOOK_SLUG_KEY));
+ getSupportFragmentManager().beginTransaction().add(R.id.flContainer, playerFragment, PLAYER_FRAGMENT_TAG).commit();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerFragment.java
new file mode 100644
index 0000000..ca92d2c
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerFragment.java
@@ -0,0 +1,243 @@
+package com.moiseum.wolnelektury.view.player;
+
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.utils.StringUtils;
+import com.moiseum.wolnelektury.view.player.header.PlayerHeaderFragment;
+import com.moiseum.wolnelektury.view.player.playlist.PlayerPlaylistFragment;
+
+import org.parceler.Parcels;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_KEY;
+import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_SLUG_KEY;
+
+
+/**
+ * Created by Piotr Ostrowski on 22.05.2018.
+ */
+public class PlayerFragment extends PresenterFragment implements PlayerView {
+
+ private static final String TAG = PlayerFragment.class.getSimpleName();
+ private AlertDialog errorDialog;
+
+ public static PlayerFragment newInstance(BookDetailsModel book, String slug) {
+ PlayerFragment playerFragment = new PlayerFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(BOOK_KEY, Parcels.wrap(book));
+ args.putString(BOOK_SLUG_KEY, slug);
+ playerFragment.setArguments(args);
+ return playerFragment;
+ }
+
+ private static final String HEADER_FRAGMENT_TAG = "HeaderFragmentTag";
+ private static final String LIST_FRAGMENT_TAG = "ListFragmentTag";
+
+ int userSelectedPosition = 0;
+ private boolean mUserIsSeeking = false;
+
+ private PlayerHeaderFragment headerFragment;
+ private PlayerPlaylistFragment playlistFragment;
+
+ private SeekBar.OnSeekBarChangeListener listener = new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ tvCurrentProgress.setText(getPresenter().getCurrentTimerText(progress));
+ if (fromUser) {
+ userSelectedPosition = progress;
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mUserIsSeeking = true;
+ getPresenter().playOrPause(true);
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mUserIsSeeking = false;
+ getPresenter().seekTo(userSelectedPosition);
+ getPresenter().playOrPause(false);
+ }
+ };
+
+ @BindView(R.id.sbPlayerProgress)
+ SeekBar sbPlayerProgress;
+ @BindView(R.id.tvCurrentProgress)
+ TextView tvCurrentProgress;
+ @BindView(R.id.tvTotalProgress)
+ TextView tvTotalProgress;
+ @BindView(R.id.tvChapterTitle)
+ TextView tvChapterTitle;
+ @BindView(R.id.tvArtist)
+ TextView tvArtist;
+ @BindView(R.id.ibToggleList)
+ ImageButton ibToggleList;
+ @BindView(R.id.ibPlayPause)
+ ImageButton ibPlayPause;
+ @BindView(R.id.ibPrevious)
+ ImageButton ibPrevious;
+ @BindView(R.id.ibNext)
+ ImageButton ibNext;
+
+ @Override
+ protected PlayerPresenter createPresenter() {
+ if (getArguments() == null || getArguments().getParcelable(BOOK_KEY) == null) {
+ throw new IllegalStateException("Book object is required at this point.");
+ }
+ return new PlayerPresenter(
+ Parcels.unwrap(getArguments().getParcelable(BOOK_KEY)),
+ getArguments().getString(BOOK_SLUG_KEY),
+ this,
+ getContext()
+ );
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_player;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ if (getArguments() == null || getArguments().getParcelable(BOOK_KEY) == null) {
+ throw new IllegalStateException("Book object is required at this point.");
+ }
+ BookDetailsModel book = Parcels.unwrap(getArguments().getParcelable(BOOK_KEY));
+ initializeHeaderAndPlaylistFragments(book);
+ sbPlayerProgress.setOnSeekBarChangeListener(listener);
+
+ int visibility = book.getAudiobookMediaModels().size() > 1 ? View.VISIBLE : View.GONE;
+ tvArtist.setVisibility(visibility);
+ ibToggleList.setVisibility(visibility);
+ ibPrevious.setVisibility(visibility);
+ ibNext.setVisibility(visibility);
+ }
+
+ @OnClick(R.id.ibToggleList)
+ public void onToggleListClicked() {
+ FragmentManager fragmentManager = getFragmentManager();
+ if (fragmentManager == null) {
+ return;
+ }
+
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ if (playlistFragment.isHidden()) {
+ fragmentTransaction.show(playlistFragment);
+ fragmentTransaction.hide(headerFragment);
+ } else {
+ fragmentTransaction.hide(playlistFragment);
+ fragmentTransaction.show(headerFragment);
+ }
+ fragmentTransaction.commit();
+ }
+
+ @OnClick(R.id.ibPrevious)
+ public void onPreviousClicked() {
+ getPresenter().changeChapter(false);
+ }
+
+ @OnClick(R.id.ibNext)
+ public void onNextClicked() {
+ getPresenter().changeChapter(true);
+ }
+
+ @OnClick(R.id.ibRewind)
+ public void onRewindClicked() {
+ getPresenter().seekToButton(false);
+ }
+
+ @OnClick(R.id.ibPlayPause)
+ public void onPauseClicked() {
+ getPresenter().playOrPause(false);
+ }
+
+ @OnClick(R.id.ibFastForward)
+ public void onFastForwardClicked() {
+ getPresenter().seekToButton(true);
+ }
+
+ @Override
+ public void setTrackDuration(int trackDuration, String totalProgress) {
+ sbPlayerProgress.setMax(trackDuration);
+ tvTotalProgress.setText(totalProgress);
+ }
+
+ @Override
+ public void setTrackPosition(int position, String currentProgress) {
+ if (!mUserIsSeeking) {
+ sbPlayerProgress.setProgress(position);
+ tvCurrentProgress.setText(currentProgress);
+ }
+ }
+
+ @Override
+ public void setTrackTexts(String title, int chapter) {
+ tvArtist.setText(getString(R.string.player_chapter_number, (chapter + 1)));
+ tvChapterTitle.setText(title);
+ }
+
+ @Override
+ public void setPlayButtonState(boolean playing) {
+ if (getContext() != null) {
+ Drawable drawable = ContextCompat.getDrawable(getContext(), playing ? R.drawable.pause_selector : R.drawable.play_selector);
+ ibPlayPause.setImageDrawable(drawable);
+ }
+ }
+
+ @Override
+ public void onPlayerError() {
+ if (getActivity() != null && errorDialog == null) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
+ dialogBuilder.setMessage(getString(R.string.load_player_failed));
+ dialogBuilder.setCancelable(false);
+ dialogBuilder.setPositiveButton(getString(R.string.close), (dialog, id) -> {
+ getActivity().finish();
+ dialog.dismiss();
+ });
+ dialogBuilder.setOnDismissListener(dialog -> errorDialog = null);
+ errorDialog = dialogBuilder.create();
+ errorDialog.show();
+ }
+ }
+
+ private void initializeHeaderAndPlaylistFragments(BookDetailsModel book) {
+ FragmentManager fragmentManager = getFragmentManager();
+ if (fragmentManager == null) {
+ return;
+ }
+
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ headerFragment = (PlayerHeaderFragment) fragmentManager.findFragmentByTag(HEADER_FRAGMENT_TAG);
+ if (headerFragment == null) {
+ headerFragment = PlayerHeaderFragment.newInstance(StringUtils.joinCategory(book.getAuthors(), ", "), book.getTitle(), book.getCoverThumb());
+ fragmentTransaction.add(R.id.flPlayerFragmentContainer, headerFragment, HEADER_FRAGMENT_TAG);
+ }
+ playlistFragment = (PlayerPlaylistFragment) fragmentManager.findFragmentByTag(LIST_FRAGMENT_TAG);
+ if (playlistFragment == null) {
+ playlistFragment = PlayerPlaylistFragment.newInstance(book.getAudiobookMediaModels());
+ fragmentTransaction.add(R.id.flPlayerFragmentContainer, playlistFragment, LIST_FRAGMENT_TAG);
+ }
+ fragmentTransaction.hide(playlistFragment);
+ fragmentTransaction.commit();
+ }
+}
+
+
+
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerPresenter.java
new file mode 100644
index 0000000..c409c75
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerPresenter.java
@@ -0,0 +1,198 @@
+package com.moiseum.wolnelektury.view.player;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import com.moiseum.wolnelektury.base.WLApplication;
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+import com.moiseum.wolnelektury.connection.models.BookDetailsModel;
+import com.moiseum.wolnelektury.connection.models.BookModel;
+import com.moiseum.wolnelektury.storage.BookStorage;
+import com.moiseum.wolnelektury.view.player.service.AudiobookLibrary;
+import com.moiseum.wolnelektury.view.player.service.AudiobookService;
+import com.moiseum.wolnelektury.view.player.service.MediaBrowserHelper;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Created by Piotr Ostrowski on 22.05.2018.
+ */
+public class PlayerPresenter extends FragmentPresenter {
+
+ private final BookDetailsModel book;
+ private final BookModel storedBook;
+
+ private final BookStorage storage = WLApplication.getInstance().getBookStorage();
+ private MediaBrowserHelper mMediaBrowserHelper;
+ private boolean mIsPlaying;
+
+ /**
+ * Customize the connection to our {@link android.support.v4.media.MediaBrowserServiceCompat}
+ * and implement our app specific desires.
+ */
+ private class MediaBrowserConnection extends MediaBrowserHelper {
+ private MediaBrowserConnection(Context context) {
+ super(context, AudiobookService.class);
+ }
+
+ @Override
+ protected void onChildrenLoaded(@NonNull String parentId,
+ @NonNull List children) {
+ super.onChildrenLoaded(parentId, children);
+
+ final MediaControllerCompat mediaController = getMediaController();
+ mediaController.getTransportControls().sendCustomAction(AudiobookService.ACTION_CLEAR_PLAYLIST, null);
+
+ AudiobookLibrary.createAudiobookMetadata(book);
+ for (final MediaBrowserCompat.MediaItem mediaItem : AudiobookLibrary.getMediaItems()) {
+ mediaController.addQueueItem(mediaItem.getDescription());
+ }
+
+ // Call prepare now so pressing play just works.
+ mediaController.getTransportControls().prepare();
+ if (storedBook != null) {
+ mediaController.getTransportControls().skipToQueueItem(storedBook.getCurrentAudioChapter());
+ mediaController.getTransportControls().pause();
+ }
+ }
+ }
+
+ /**
+ * Implementation of the {@link MediaControllerCompat.Callback} methods we're interested in.
+ *
+ * Here would also be where one could override
+ * {@code onQueueChanged(List queue)} to get informed when items
+ * are added or removed from the queue. We don't do this here in order to keep the UI
+ * simple.
+ */
+ private class PlayerMediaControllerCallback extends MediaControllerCompat.Callback {
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat playbackState) {
+ if (playbackState != null) {
+ if (playbackState.getState() == PlaybackStateCompat.STATE_ERROR) {
+ getView().onPlayerError();
+ }
+
+ mIsPlaying = playbackState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ getView().setPlayButtonState(mIsPlaying);
+
+ if (mIsPlaying && playbackState.getExtras() != null) {
+ int total = playbackState.getExtras().getInt(AudiobookService.EXTRA_PLAYBACK_TOTAL);
+ getView().setTrackDuration(total, getCurrentTimerText(total));
+ }
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat mediaMetadata) {
+ if (mediaMetadata == null) {
+ return;
+ }
+ if (!book.getAudiobookFilesUrls().contains(mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID))) {
+ return;
+ }
+
+ int currentChapter = (int) mediaMetadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER);
+ if (storedBook != null) {
+ storedBook.setCurrentAudioChapter(currentChapter);
+ storage.update(storedBook);
+ }
+
+ String chapterTitle = mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
+ getView().setTrackTexts(chapterTitle, currentChapter);
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) {
+ if (extras.containsKey(AudiobookService.EXTRA_PLAYBACK_CURRENT)) {
+ int position = extras.getInt(AudiobookService.EXTRA_PLAYBACK_CURRENT);
+ getView().setTrackPosition(position, getCurrentTimerText(position));
+ }
+ if (extras.containsKey(AudiobookService.EXTRA_PLAYBACK_TOTAL)) {
+ int total = extras.getInt(AudiobookService.EXTRA_PLAYBACK_TOTAL);
+ getView().setTrackDuration(total, getCurrentTimerText(total));
+ }
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ super.onSessionDestroyed();
+ }
+
+ @Override
+ public void onQueueChanged(List queue) {
+ super.onQueueChanged(queue);
+ }
+ }
+
+ PlayerPresenter(BookDetailsModel book, String slug, PlayerView view, Context context) {
+ super(view);
+ this.book = book;
+ this.storedBook = storage.find(slug);
+ mMediaBrowserHelper = new MediaBrowserConnection(context);
+ mMediaBrowserHelper.registerCallback(new PlayerMediaControllerCallback());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mMediaBrowserHelper.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mMediaBrowserHelper.onStop();
+ }
+
+ public void playOrPause(boolean pauseCall) {
+ if (mIsPlaying) {
+ mMediaBrowserHelper.getTransportControls().pause();
+ } else {
+ mMediaBrowserHelper.getTransportControls().play();
+ }
+ }
+
+ public void changeChapter(boolean next) {
+ if (next) {
+ mMediaBrowserHelper.getTransportControls().skipToNext();
+ } else {
+ mMediaBrowserHelper.getTransportControls().skipToPrevious();
+ }
+ }
+
+ public void seekToButton(boolean forward) {
+ if (forward) {
+ mMediaBrowserHelper.getTransportControls().fastForward();
+ } else {
+ mMediaBrowserHelper.getTransportControls().rewind();
+ }
+ }
+
+ public void seekTo(int userSelectedPosition) {
+ mMediaBrowserHelper.getTransportControls().seekTo(userSelectedPosition);
+ }
+
+ public String getCurrentTimerText(int currentPosition) {
+ StringBuilder sb = new StringBuilder();
+ int minutes = (currentPosition % (1000 * 60 * 60)) / (1000 * 60);
+ int seconds = ((currentPosition % (1000 * 60 * 60)) % (1000 * 60) / 1000);
+ if (currentPosition > (1000 * 60 * 60)) {
+ int hours = (currentPosition / (1000 * 60 * 60));
+ sb.append(String.format(Locale.getDefault(), "%01d", hours));
+ sb.append(":");
+ }
+ sb.append(String.format(Locale.getDefault(), "%02d", minutes));
+ sb.append(":");
+ sb.append(String.format(Locale.getDefault(), "%02d", seconds));
+ return sb.toString();
+ }
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerView.java
new file mode 100644
index 0000000..03f6a6d
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerView.java
@@ -0,0 +1,17 @@
+package com.moiseum.wolnelektury.view.player;
+
+/**
+ * Created by Piotr Ostrowski on 22.05.2018.
+ */
+interface PlayerView {
+
+ void setTrackDuration(int trackDuration, String totalProgress);
+
+ void setTrackPosition(int position, String currentProgress);
+
+ void setTrackTexts(String title, int chapter);
+
+ void setPlayButtonState(boolean playing);
+
+ void onPlayerError();
+}
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderFragment.java
new file mode 100644
index 0000000..12902fd
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderFragment.java
@@ -0,0 +1,103 @@
+package com.moiseum.wolnelektury.view.player.header;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.moiseum.wolnelektury.R;
+import com.moiseum.wolnelektury.base.mvp.PresenterFragment;
+import com.moiseum.wolnelektury.connection.RestClient;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+import retrofit2.http.HEAD;
+
+/**
+ * Created by Piotr Ostrowski on 22.05.2018.
+ */
+public class PlayerHeaderFragment extends PresenterFragment implements PlayerHeaderView {
+
+ private static final String AUTHOR_KEY = "AuthorKey";
+ private static final String TITLE_KEY = "TitleKey";
+ private static final String COVER_KEY = "CoverKey";
+
+ public static PlayerHeaderFragment newInstance(String author, String title, String coverUrl) {
+ PlayerHeaderFragment fragment = new PlayerHeaderFragment();
+ Bundle args = new Bundle();
+ args.putString(AUTHOR_KEY, author);
+ args.putString(TITLE_KEY, title);
+ args.putString(COVER_KEY, coverUrl);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+
+ @BindView(R.id.vCoverOverlay)
+ View vCoverOverlay;
+ @BindView(R.id.ivCoverBackground)
+ ImageView ivCoverBackground;
+ @BindView(R.id.ivCover)
+ ImageView ivCover;
+ @BindView(R.id.tvAuthor)
+ TextView tvAuthor;
+ @BindView(R.id.tvBookTitle)
+ TextView tvBookTitle;
+
+ @Override
+ protected PlayerHeaderPresenter createPresenter() {
+ if (getArguments() == null) {
+ throw new IllegalStateException("Missing fragment arguments.");
+ }
+ String author = getArguments().getString(AUTHOR_KEY);
+ String title = getArguments().getString(TITLE_KEY);
+ String coverUrl = getArguments().getString(COVER_KEY);
+ return new PlayerHeaderPresenter(author, title, coverUrl, this);
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.fragment_player_header;
+ }
+
+ @Override
+ public void prepareView(View view, Bundle savedInstanceState) {
+ initView();
+ }
+
+ public void initView() {
+ if (getArguments() != null) {
+ vCoverOverlay.setAlpha(0.7f);
+ vCoverOverlay.setBackgroundColor(Color.parseColor("#db4b16"));
+ tvAuthor.setText(getArguments().getString(AUTHOR_KEY));
+ tvBookTitle.setText(getArguments().getString(TITLE_KEY));
+ if (getArguments().getString(COVER_KEY) != null) {
+ String coverUrl = getArguments().getString(COVER_KEY);
+ if (coverUrl != null && !coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) {
+ coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl;
+ }
+ Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCover);
+ }
+ if (getArguments().getString(COVER_KEY) != null) {
+ String coverBackgroundUrl = getArguments().getString(COVER_KEY);
+ if (coverBackgroundUrl != null && !coverBackgroundUrl.contains(RestClient.MEDIA_URL) && !coverBackgroundUrl.contains(RestClient.MEDIA_URL_HTTPS)) {
+ coverBackgroundUrl = RestClient.MEDIA_URL_HTTPS + coverBackgroundUrl;
+ }
+ Glide.with(getContext()).load(coverBackgroundUrl).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCoverBackground);
+ }
+ }
+ }
+
+ @OnClick(R.id.ibBack)
+ public void onBackButtonClicked() {
+ if (getActivity() != null) {
+ getActivity().finish();
+ }
+ }
+}
+
diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderPresenter.java
new file mode 100644
index 0000000..4cf7a86
--- /dev/null
+++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderPresenter.java
@@ -0,0 +1,13 @@
+package com.moiseum.wolnelektury.view.player.header;
+
+import com.moiseum.wolnelektury.base.mvp.FragmentPresenter;
+
+/**
+ * Created by Piotr Ostrowski on 22.05.2018.
+ */
+public class PlayerHeaderPresenter extends FragmentPresenter