diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 272b993ce..46fb5d23d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/Status.java b/app/src/main/java/com/keylesspalace/tusky/Status.java index d69855a3f..1b5100cd2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/Status.java @@ -1,9 +1,13 @@ package com.keylesspalace.tusky; +import android.graphics.drawable.Drawable; import android.os.Build; +import android.provider.MediaStore; import android.text.Html; import android.text.Spanned; +import com.android.volley.toolbox.NetworkImageView; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -38,6 +42,9 @@ public class Status { /** whether the authenticated user has favourited this status */ private boolean favourited; private Visibility visibility; + private MediaAttachment[] attachments = null; + + public static final int MAX_MEDIA_ATTACHMENTS = 4; public Status(String id, String accountId, String displayName, String username, Spanned content, String avatar, Date createdAt, boolean reblogged, boolean favourited, @@ -52,6 +59,7 @@ public class Status { this.reblogged = reblogged; this.favourited = favourited; this.visibility = Visibility.valueOf(visibility.toUpperCase()); + this.attachments = new MediaAttachment[0]; } public String getId() { @@ -98,6 +106,10 @@ public class Status { return visibility; } + public MediaAttachment[] getAttachments() { + return attachments; + } + public void setRebloggedByUsername(String name) { rebloggedByUsername = name; } @@ -110,6 +122,10 @@ public class Status { this.favourited = favourited; } + public void setAttachments(MediaAttachment[] attachments) { + this.attachments = attachments; + } + @Override public int hashCode() { return id.hashCode(); @@ -173,6 +189,21 @@ public class Status { String username = account.getString("acct"); String avatar = account.getString("avatar"); + JSONArray mediaAttachments = object.getJSONArray("media_attachments"); + MediaAttachment[] attachments = null; + if (mediaAttachments != null) { + int n = mediaAttachments.length(); + attachments = new MediaAttachment[n]; + for (int i = 0; i < n; i++) { + JSONObject attachment = mediaAttachments.getJSONObject(i); + String url = attachment.getString("url"); + String previewUrl = attachment.getString("preview_url"); + String type = attachment.getString("type"); + attachments[i] = new MediaAttachment(url, previewUrl, + MediaAttachment.Type.valueOf(type.toUpperCase())); + } + } + Status reblog = null; /* This case shouldn't be hit after the first recursion at all. But if this method is * passed unusual data this check will prevent extra recursion */ @@ -193,6 +224,9 @@ public class Status { id, accountId, displayName, username, contentPlus, avatar, createdAt, reblogged, favourited, visibility); } + if (attachments != null) { + status.setAttachments(attachments); + } return status; } @@ -204,4 +238,33 @@ public class Status { } return statuses; } + + public static class MediaAttachment { + enum Type { + IMAGE, + VIDEO, + } + + private String url; + private String previewUrl; + private Type type; + + public MediaAttachment(String url, String previewUrl, Type type) { + this.url = url; + this.previewUrl = previewUrl; + this.type = type; + } + + public String getUrl() { + return url; + } + + public String getPreviewUrl() { + return previewUrl; + } + + public Type getType() { + return type; + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java index fb261bb13..b43883e02 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java @@ -6,4 +6,5 @@ public interface StatusActionListener { void onReblog(final boolean reblog, final int position); void onFavourite(final boolean favourite, final int position); void onMore(View view, final int position); + void onViewMedia(String url, Status.MediaAttachment.Type type); } diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java index 41a831699..388021bbb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java @@ -2,8 +2,10 @@ package com.keylesspalace.tusky; import android.content.Context; import android.support.annotation.Nullable; +import android.support.v7.widget.PagerSnapHelper; import android.support.v7.widget.RecyclerView; import android.text.Spanned; +import android.text.style.ImageSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -53,6 +55,7 @@ public class TimelineAdapter extends RecyclerView.Adapter { } else { holder.setRebloggedByUsername(rebloggedByUsername); } + holder.setMediaPreviews(status.getAttachments(), listener); holder.setupButtons(listener, position); if (status.getVisibility() == Status.Visibility.PRIVATE) { holder.disableReblogging(); @@ -112,6 +115,11 @@ public class TimelineAdapter extends RecyclerView.Adapter { private ImageButton moreButton; private boolean favourited; private boolean reblogged; + private NetworkImageView mediaPreview0; + private NetworkImageView mediaPreview1; + private NetworkImageView mediaPreview2; + private NetworkImageView mediaPreview3; + private String[] mediaAttachmentUrls; public ViewHolder(View itemView) { super(itemView); @@ -128,6 +136,10 @@ public class TimelineAdapter extends RecyclerView.Adapter { moreButton = (ImageButton) itemView.findViewById(R.id.status_more); reblogged = false; favourited = false; + mediaPreview0 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_0); + mediaPreview1 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_1); + mediaPreview2 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_2); + mediaPreview3 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_3); } public void setDisplayName(String name) { @@ -234,6 +246,37 @@ public class TimelineAdapter extends RecyclerView.Adapter { } } + public void setMediaPreviews(final Status.MediaAttachment[] attachments, + final StatusActionListener listener) { + final NetworkImageView[] previews = { + mediaPreview0, + mediaPreview1, + mediaPreview2, + mediaPreview3 + }; + Context context = mediaPreview0.getContext(); + ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); + int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); + for (int i = 0; i < n; i++) { + String previewUrl = attachments[i].getPreviewUrl(); + previews[i].setImageUrl(previewUrl, imageLoader); + previews[i].setVisibility(View.VISIBLE); + final String url = attachments[i].getUrl(); + final Status.MediaAttachment.Type type = attachments[i].getType(); + previews[i].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewMedia(url, type); + } + }); + } + // Hide any of the placeholder previews beyond the ones set. + for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { + previews[i].setImageUrl(null, imageLoader); + previews[i].setVisibility(View.GONE); + } + } + public void setupButtons(final StatusActionListener listener, final int position) { reblogButton.setOnClickListener(new View.OnClickListener() { @Override diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java index f687e99a0..609531518 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java @@ -1,11 +1,13 @@ package com.keylesspalace.tusky; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.DividerItemDecoration; @@ -184,7 +186,8 @@ public class TimelineFragment extends Fragment implements } public void onFetchTimelineFailure(Exception exception) { - Toast.makeText(getContext(), R.string.error_fetching_timeline, Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), R.string.error_fetching_timeline, Toast.LENGTH_SHORT) + .show(); swipeRefreshLayout.setRefreshing(false); } @@ -312,4 +315,24 @@ public class TimelineFragment extends Fragment implements }); popup.show(); } + + public void onViewMedia(String url, Status.MediaAttachment.Type type) { + switch (type) { + case IMAGE: { + Fragment newFragment = ViewMediaFragment.newInstance(url); + FragmentManager manager = getFragmentManager(); + manager.beginTransaction() + .add(R.id.overlay_fragment_container, newFragment) + .addToBackStack(null) + .commit(); + break; + } + case VIDEO: { + Intent intent = new Intent(getContext(), ViewVideoActivity.class); + intent.putExtra("url", url); + startActivity(intent); + break; + } + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaFragment.java b/app/src/main/java/com/keylesspalace/tusky/ViewMediaFragment.java new file mode 100644 index 000000000..f799ca800 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaFragment.java @@ -0,0 +1,45 @@ +package com.keylesspalace.tusky; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; + +public class ViewMediaFragment extends Fragment { + public static ViewMediaFragment newInstance(String url) { + Bundle arguments = new Bundle(); + ViewMediaFragment fragment = new ViewMediaFragment(); + arguments.putString("url", url); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, final ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_view_media, container, false); + + Bundle arguments = getArguments(); + String url = arguments.getString("url"); + NetworkImageView image = (NetworkImageView) rootView.findViewById(R.id.view_media_image); + ImageLoader imageLoader = VolleySingleton.getInstance(getContext()).getImageLoader(); + image.setImageUrl(url, imageLoader); + + rootView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + return rootView; + } + + private void dismiss() { + getFragmentManager().popBackStack(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java new file mode 100644 index 000000000..4b64bcc14 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java @@ -0,0 +1,21 @@ +package com.keylesspalace.tusky; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.widget.MediaController; +import android.widget.VideoView; + +public class ViewVideoActivity extends AppCompatActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_video); + String url = getIntent().getStringExtra("url"); + VideoView videoView = (VideoView) findViewById(R.id.video_player); + videoView.setVideoPath(url); + MediaController controller = new MediaController(this); + videoView.setMediaController(controller); + controller.show(); + videoView.start(); + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3ff98df8e..a65e5f3b1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - + tools:context="com.keylesspalace.tusky.MainActivity"> - + android:layout_height="match_parent" + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + - + - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_view_video.xml b/app/src/main/res/layout/activity_view_video.xml new file mode 100644 index 000000000..d904f05ec --- /dev/null +++ b/app/src/main/res/layout/activity_view_video.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_view_media.xml b/app/src/main/res/layout/fragment_view_media.xml new file mode 100644 index 000000000..50de8b593 --- /dev/null +++ b/app/src/main/res/layout/fragment_view_media.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index a20f4a4a3..b9f42f217 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -76,11 +76,62 @@ android:layout_toEndOf="@+id/status_avatar" android:layout_below="@+id/status_name_bar" /> + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5f1f79943..eeda0aca9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,4 +4,5 @@ #303F9F #FF4081 #4F4F4F + #000000 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 4f478227e..b1cfad053 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -5,4 +5,5 @@ 4dp 8dp 5dp + 4dp