From 2f0d6da8508568e3f2aecc7c428e5972e37780b6 Mon Sep 17 00:00:00 2001 From: tehcneko <7764726+tehcneko@users.noreply.github.com> Date: Wed, 10 Feb 2021 14:49:54 +0800 Subject: [PATCH] [app] Add info --- .../manager/repo/model/Collaborator.java | 8 +- .../manager/repo/model/OnlineModule.java | 43 ++++- .../manager/ui/activity/RepoActivity.java | 2 + .../manager/ui/activity/RepoItemActivity.java | 159 ++++++++++++++---- .../manager/ui/widget/LinkifyTextView.java | 92 ++++++++++ .../util/chrome/CustomTabsURLSpan.java | 2 +- ...eleases.xml => item_repo_recyclerview.xml} | 0 ...se.xml => item_repo_title_description.xml} | 8 +- app/src/main/res/values/strings.xml | 4 + 9 files changed, 276 insertions(+), 42 deletions(-) create mode 100644 app/src/main/java/io/github/lsposed/manager/ui/widget/LinkifyTextView.java rename app/src/main/res/layout/{item_repo_releases.xml => item_repo_recyclerview.xml} (100%) rename app/src/main/res/layout/{item_repo_release.xml => item_repo_title_description.xml} (91%) diff --git a/app/src/main/java/io/github/lsposed/manager/repo/model/Collaborator.java b/app/src/main/java/io/github/lsposed/manager/repo/model/Collaborator.java index 734f922e..078501ba 100644 --- a/app/src/main/java/io/github/lsposed/manager/repo/model/Collaborator.java +++ b/app/src/main/java/io/github/lsposed/manager/repo/model/Collaborator.java @@ -35,7 +35,7 @@ public class Collaborator implements Serializable, Parcelable { private String login; @SerializedName("name") @Expose - private Object name; + private String name; public final static Creator CREATOR = new Creator() { public Collaborator createFromParcel(Parcel in) { @@ -51,7 +51,7 @@ public class Collaborator implements Serializable, Parcelable { protected Collaborator(Parcel in) { this.login = ((String) in.readValue((String.class.getClassLoader()))); - this.name = in.readValue((Object.class.getClassLoader())); + this.name = ((String) in.readValue((String.class.getClassLoader()))); } public Collaborator() { @@ -65,11 +65,11 @@ public class Collaborator implements Serializable, Parcelable { this.login = login; } - public Object getName() { + public String getName() { return name; } - public void setName(Object name) { + public void setName(String name) { this.name = name; } diff --git a/app/src/main/java/io/github/lsposed/manager/repo/model/OnlineModule.java b/app/src/main/java/io/github/lsposed/manager/repo/model/OnlineModule.java index 3f4553c8..c7f3908a 100644 --- a/app/src/main/java/io/github/lsposed/manager/repo/model/OnlineModule.java +++ b/app/src/main/java/io/github/lsposed/manager/repo/model/OnlineModule.java @@ -68,6 +68,15 @@ public class OnlineModule implements Serializable, Parcelable { @SerializedName("additionalAuthors") @Expose private List additionalAuthors = null; + @SerializedName("updatedAt") + @Expose + private String updatedAt; + @SerializedName("createdAt") + @Expose + private String createdAt; + @SerializedName("stargazerCount") + @Expose + private Integer stargazerCount; public final static Creator CREATOR = new Creator() { public OnlineModule createFromParcel(Parcel in) { @@ -79,7 +88,7 @@ public class OnlineModule implements Serializable, Parcelable { } }; - private final static long serialVersionUID = -2294634398588027071L; + private final static long serialVersionUID = 3372849627722130087L; protected OnlineModule(Parcel in) { this.name = ((String) in.readValue((String.class.getClassLoader()))); @@ -94,6 +103,9 @@ public class OnlineModule implements Serializable, Parcelable { this.sourceUrl = ((String) in.readValue((String.class.getClassLoader()))); this.hide = ((Boolean) in.readValue((Boolean.class.getClassLoader()))); in.readList(this.additionalAuthors, (Object.class.getClassLoader())); + this.updatedAt = ((String) in.readValue((String.class.getClassLoader()))); + this.createdAt = ((String) in.readValue((String.class.getClassLoader()))); + this.stargazerCount = ((Integer) in.readValue((Integer.class.getClassLoader()))); } public OnlineModule() { @@ -179,7 +191,7 @@ public class OnlineModule implements Serializable, Parcelable { this.sourceUrl = sourceUrl; } - public Boolean getHide() { + public Boolean isHide() { return hide; } @@ -195,6 +207,30 @@ public class OnlineModule implements Serializable, Parcelable { this.additionalAuthors = additionalAuthors; } + public String getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + public Integer getStargazerCount() { + return stargazerCount; + } + + public void setStargazerCount(Integer stargazerCount) { + this.stargazerCount = stargazerCount; + } + public void writeToParcel(Parcel dest, int flags) { dest.writeValue(name); dest.writeValue(description); @@ -208,6 +244,9 @@ public class OnlineModule implements Serializable, Parcelable { dest.writeValue(sourceUrl); dest.writeValue(hide); dest.writeList(additionalAuthors); + dest.writeValue(updatedAt); + dest.writeValue(createdAt); + dest.writeValue(stargazerCount); } public int describeContents() { diff --git a/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoActivity.java b/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoActivity.java index 262f16af..e7f4cf7b 100644 --- a/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoActivity.java +++ b/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoActivity.java @@ -43,6 +43,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import io.github.lsposed.manager.R; import io.github.lsposed.manager.databinding.ActivityAppListBinding; @@ -168,6 +169,7 @@ public class RepoActivity extends BaseActivity implements RepoLoader.Listener { public void setData(Collection modules) { fullList = new ArrayList<>(modules); + fullList = fullList.stream().filter((onlineModule -> !onlineModule.isHide())).collect(Collectors.toList()); fullList.sort((o1, o2) -> o1.getDescription().compareToIgnoreCase(o2.getDescription())); String queryStr = searchView != null ? searchView.getQuery().toString() : ""; runOnUiThread(() -> getFilter().filter(queryStr)); diff --git a/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoItemActivity.java b/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoItemActivity.java index 83810464..bbaee51b 100644 --- a/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoItemActivity.java +++ b/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoItemActivity.java @@ -22,6 +22,9 @@ package io.github.lsposed.manager.ui.activity; import android.os.Build; import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ClickableSpan; import android.text.util.Linkify; import android.view.View; import android.view.ViewGroup; @@ -38,24 +41,31 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayoutMediator; import java.util.ArrayList; import java.util.List; +import java.util.ListIterator; import io.github.lsposed.manager.R; import io.github.lsposed.manager.databinding.ActivityModuleDetailBinding; import io.github.lsposed.manager.databinding.ItemRepoReadmeBinding; -import io.github.lsposed.manager.databinding.ItemRepoReleaseBinding; -import io.github.lsposed.manager.databinding.ItemRepoReleasesBinding; +import io.github.lsposed.manager.databinding.ItemRepoRecyclerviewBinding; +import io.github.lsposed.manager.databinding.ItemRepoTitleDescriptionBinding; import io.github.lsposed.manager.repo.RepoLoader; +import io.github.lsposed.manager.repo.model.Collaborator; import io.github.lsposed.manager.repo.model.OnlineModule; import io.github.lsposed.manager.repo.model.Release; +import io.github.lsposed.manager.repo.model.ReleaseAsset; +import io.github.lsposed.manager.ui.widget.LinkifyTextView; import io.github.lsposed.manager.util.GlideApp; import io.github.lsposed.manager.util.LinearLayoutManagerFix; import io.github.lsposed.manager.util.NavUtil; +import io.github.lsposed.manager.util.chrome.CustomTabsURLSpan; import io.github.lsposed.manager.util.chrome.LinkTransformationMethod; import io.noties.markwon.Markwon; +import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.image.glide.GlideImagesPlugin; import io.noties.markwon.linkify.LinkifyPlugin; @@ -81,6 +91,7 @@ public class RepoItemActivity extends BaseActivity { markwon = Markwon.builder(this) .usePlugin(GlideImagesPlugin.create(GlideApp.with(this))) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) + .usePlugin(HtmlPlugin.create()) .build(); module = RepoLoader.getInstance().getOnlineModule(modulePackageName); binding.viewPager.setAdapter(new PagerAdapter()); @@ -94,7 +105,7 @@ public class RepoItemActivity extends BaseActivity { } } }); - int[] titles = new int[]{R.string.module_readme, R.string.module_releases}; + int[] titles = new int[]{R.string.module_readme, R.string.module_releases, R.string.module_information}; new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText(titles[position])).attach(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { WindowCompat.setDecorFitsSystemWindows(getWindow(), false); @@ -106,7 +117,81 @@ public class RepoItemActivity extends BaseActivity { } } - private class ReleaseAdapter extends RecyclerView.Adapter { + private class InformationAdapter extends RecyclerView.Adapter { + private final OnlineModule module; + + private int rowCount = 0; + private int homepageRow = -1; + private int collaboratorsRow = -1; + private int sourceUrlRow = -1; + + public InformationAdapter(OnlineModule module) { + this.module = module; + if (module.getHomepageUrl() != null) { + homepageRow = rowCount++; + } + if (module.getCollaborators() != null) { + collaboratorsRow = rowCount++; + } + if (module.getSourceUrl() != null) { + sourceUrlRow = rowCount++; + } + } + + @NonNull + @Override + public TitleDescriptionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new TitleDescriptionHolder(ItemRepoTitleDescriptionBinding.inflate(getLayoutInflater(), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull TitleDescriptionHolder holder, int position) { + if (position == homepageRow) { + holder.title.setText(R.string.module_information_homepage); + holder.description.setText(module.getHomepageUrl()); + } else if (position == collaboratorsRow) { + holder.title.setText(R.string.module_information_collaborators); + List collaborators = module.getCollaborators(); + SpannableStringBuilder sb = new SpannableStringBuilder(); + ListIterator iterator = collaborators.listIterator(); + while (iterator.hasNext()) { + Collaborator collaborator = iterator.next(); + String name = collaborator.getName() == null ? collaborator.getLogin() : collaborator.getName(); + sb.append(name); + CustomTabsURLSpan span = new CustomTabsURLSpan(RepoItemActivity.this, String.format("https://github.com/%s", collaborator.getLogin())); + sb.setSpan(span, sb.length() - name.length(), sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (iterator.hasNext()) { + sb.append(", "); + } + } + holder.description.setText(sb); + } else if (position == sourceUrlRow) { + holder.title.setText(R.string.module_information_source_url); + holder.description.setText(module.getSourceUrl()); + } + holder.itemView.setOnClickListener(v -> { + if (position == homepageRow) { + NavUtil.startURL(RepoItemActivity.this, module.getHomepageUrl()); + } else if (position == collaboratorsRow) { + ClickableSpan span = holder.description.getCurrentSpan(); + holder.description.clearCurrentSpan(); + + if (span instanceof CustomTabsURLSpan) { + span.onClick(v); + } + } else if (position == sourceUrlRow) { + NavUtil.startURL(RepoItemActivity.this, module.getSourceUrl()); + } + }); + } + + @Override + public int getItemCount() { + return rowCount; + } + } + + private class ReleaseAdapter extends RecyclerView.Adapter { private final List items; public ReleaseAdapter(List items) { @@ -115,21 +200,26 @@ public class RepoItemActivity extends BaseActivity { @NonNull @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false)); + public TitleDescriptionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new TitleDescriptionHolder(ItemRepoTitleDescriptionBinding.inflate(getLayoutInflater(), parent, false)); } @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + public void onBindViewHolder(@NonNull TitleDescriptionHolder holder, int position) { Release release = items.get(position); holder.title.setText(release.getName()); holder.description.setText(release.getDescription()); holder.itemView.setOnClickListener(v -> { - ArrayList names = new ArrayList<>(); - release.getReleaseAssets().forEach(releaseAsset -> names.add(releaseAsset.getName())); - new MaterialAlertDialogBuilder(RepoItemActivity.this) - .setItems(names.toArray(new String[0]), (dialog, which) -> NavUtil.startURL(RepoItemActivity.this, release.getReleaseAssets().get(which).getDownloadUrl())) - .show(); + List assets = release.getReleaseAssets(); + if (assets != null && !assets.isEmpty()) { + ArrayList names = new ArrayList<>(); + release.getReleaseAssets().forEach(releaseAsset -> names.add(releaseAsset.getName())); + new MaterialAlertDialogBuilder(RepoItemActivity.this) + .setItems(names.toArray(new String[0]), (dialog, which) -> NavUtil.startURL(RepoItemActivity.this, release.getReleaseAssets().get(which).getDownloadUrl())) + .show(); + } else { + Snackbar.make(binding.snackbar, "no assets", Snackbar.LENGTH_SHORT).show(); + } }); } @@ -137,20 +227,21 @@ public class RepoItemActivity extends BaseActivity { public int getItemCount() { return items.size(); } - - class ViewHolder extends RecyclerView.ViewHolder { - TextView title; - TextView description; + } - public ViewHolder(ItemRepoReleaseBinding binding) { - super(binding.getRoot()); - title = binding.appName; - description = binding.description; - } + static class TitleDescriptionHolder extends RecyclerView.ViewHolder { + TextView title; + LinkifyTextView description; + + public TitleDescriptionHolder(ItemRepoTitleDescriptionBinding binding) { + super(binding.getRoot()); + title = binding.title; + description = binding.description; } } + private class PagerAdapter extends RecyclerView.Adapter { @NonNull @@ -159,7 +250,7 @@ public class RepoItemActivity extends BaseActivity { if (viewType == 0) { return new ViewHolder(ItemRepoReadmeBinding.inflate(getLayoutInflater(), parent, false).getRoot(), viewType); } else { - return new ViewHolder(ItemRepoReleasesBinding.inflate(getLayoutInflater(), parent, false).getRoot(), viewType); + return new ViewHolder(ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), parent, false).getRoot(), viewType); } } @@ -192,24 +283,30 @@ public class RepoItemActivity extends BaseActivity { }); } } - if (position == 0) { - holder.textView.setTransformationMethod(new LinkTransformationMethod(RepoItemActivity.this)); - markwon.setMarkdown(holder.textView, module.getReadme()); - } else { - ReleaseAdapter adapter = new ReleaseAdapter(module.getReleases()); - holder.recyclerView.setAdapter(adapter); - holder.recyclerView.setLayoutManager(new LinearLayoutManagerFix(RepoItemActivity.this)); + switch (position) { + case 0: + holder.textView.setTransformationMethod(new LinkTransformationMethod(RepoItemActivity.this)); + markwon.setMarkdown(holder.textView, module.getReadme()); + break; + case 1: + holder.recyclerView.setAdapter(new ReleaseAdapter(module.getReleases())); + holder.recyclerView.setLayoutManager(new LinearLayoutManagerFix(RepoItemActivity.this)); + break; + case 2: + holder.recyclerView.setAdapter(new InformationAdapter(module)); + holder.recyclerView.setLayoutManager(new LinearLayoutManagerFix(RepoItemActivity.this)); + break; } } @Override public int getItemCount() { - return 2; + return 3; } @Override public int getItemViewType(int position) { - return position; + return position == 0 ? 0 : 1; } class ViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/io/github/lsposed/manager/ui/widget/LinkifyTextView.java b/app/src/main/java/io/github/lsposed/manager/ui/widget/LinkifyTextView.java new file mode 100644 index 00000000..1d52a2ed --- /dev/null +++ b/app/src/main/java/io/github/lsposed/manager/ui/widget/LinkifyTextView.java @@ -0,0 +1,92 @@ +/* + * Copyright 2015 Hippo Seven + * + * 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. + */ + +package io.github.lsposed.manager.ui.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; + +public class LinkifyTextView extends androidx.appcompat.widget.AppCompatTextView { + + private ClickableSpan mCurrentSpan; + + public LinkifyTextView(Context context) { + super(context); + } + + public LinkifyTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LinkifyTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ClickableSpan getCurrentSpan() { + return mCurrentSpan; + } + + public void clearCurrentSpan() { + mCurrentSpan = null; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + // Let the parent or grandparent of TextView to handles click aciton. + // Otherwise click effect like ripple will not work, and if touch area + // do not contain a url, the TextView will still get MotionEvent. + // onTouchEven must be called with MotionEvent.ACTION_DOWN for each touch + // action on it, so we analyze touched url here. + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mCurrentSpan = null; + + if (getText() instanceof Spanned) { + // Get this code from android.text.method.LinkMovementMethod. + // Work fine ! + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + if (null != layout) { + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] spans = ((Spanned) getText()).getSpans(off, off, ClickableSpan.class); + + if (spans.length > 0) { + mCurrentSpan = spans[0]; + } + } + } + } + + return super.onTouchEvent(event); + } +} diff --git a/app/src/main/java/io/github/lsposed/manager/util/chrome/CustomTabsURLSpan.java b/app/src/main/java/io/github/lsposed/manager/util/chrome/CustomTabsURLSpan.java index faf7b516..774b08e3 100644 --- a/app/src/main/java/io/github/lsposed/manager/util/chrome/CustomTabsURLSpan.java +++ b/app/src/main/java/io/github/lsposed/manager/util/chrome/CustomTabsURLSpan.java @@ -30,7 +30,7 @@ public class CustomTabsURLSpan extends URLSpan { private final BaseActivity activity; - CustomTabsURLSpan(BaseActivity activity, String url) { + public CustomTabsURLSpan(BaseActivity activity, String url) { super(url); this.activity = activity; } diff --git a/app/src/main/res/layout/item_repo_releases.xml b/app/src/main/res/layout/item_repo_recyclerview.xml similarity index 100% rename from app/src/main/res/layout/item_repo_releases.xml rename to app/src/main/res/layout/item_repo_recyclerview.xml diff --git a/app/src/main/res/layout/item_repo_release.xml b/app/src/main/res/layout/item_repo_title_description.xml similarity index 91% rename from app/src/main/res/layout/item_repo_release.xml rename to app/src/main/res/layout/item_repo_title_description.xml index 53b0e62a..f37299fc 100644 --- a/app/src/main/res/layout/item_repo_release.xml +++ b/app/src/main/res/layout/item_repo_title_description.xml @@ -39,7 +39,7 @@ tools:ignore="RtlSymmetry"> - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 669cf78a..152ffd09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,4 +150,8 @@ repository Readme Releases + Info + Homepage + Source code + Collaborators