diff --git a/app/build.gradle b/app/build.gradle index 4527751c..8b195a07 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,9 +67,16 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.1.0" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'com.github.bumptech.glide:glide:4.11.0' - implementation 'com.google.android.material:material:1.2.1' + implementation 'com.github.bumptech.glide:okhttp3-integration:4.11.0' + implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.takisoft.preferencex:preferencex:1.1.0' implementation 'com.takisoft.preferencex:preferencex-colorpicker:1.1.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + final def markwon_version = '4.6.2' + implementation "io.noties.markwon:core:$markwon_version" + implementation "io.noties.markwon:image:$markwon_version" + implementation "io.noties.markwon:html:$markwon_version" implementation 'rikka.insets:insets:1.0.1' implementation 'rikka.recyclerview:recyclerview-utils:1.2.0' implementation "rikka.widget:switchbar:1.0.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d2261b8f..3b9cbd72 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" package="io.github.lsposed.manager"> + + + . + * + * Copyright (C) 2020 EdXposed Contributors + * Copyright (C) 2021 LSPosed Contributors + */ + +package io.github.lsposed.manager.repo; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.github.lsposed.manager.App; +import io.github.lsposed.manager.repo.model.OnlineModule; +import io.github.lsposed.manager.util.ModuleUtil; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class RepoLoader { + private static RepoLoader instance = null; + private OnlineModule[] onlineModules = new OnlineModule[0]; + private final Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json"); + private final List listeners = new CopyOnWriteArrayList<>(); + private boolean isLoading = false; + + public static synchronized RepoLoader getInstance() { + if (instance == null) { + instance = new RepoLoader(); + instance.loadRemoteData(); + } + return instance; + } + + public void loadRemoteData() { + synchronized (this) { + if (isLoading) { + return; + } + isLoading = true; + } + App.getOkHttpClient().newCall(new Request.Builder() + .url("https://modules.lsposed.org/modules.json") + .build()).enqueue(new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + synchronized (this) { + isLoading = false; + } + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + ResponseBody body = response.body(); + if (body != null) { + String bodyString = body.string(); + Gson gson = new Gson(); + onlineModules = gson.fromJson(bodyString, OnlineModule[].class); + Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); + } + for (Listener listener : listeners) { + listener.repoLoaded(); + } + synchronized (this) { + isLoading = false; + } + } + }); + } + + public void addListener(Listener listener) { + if (!listeners.contains(listener)) + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public OnlineModule[] getOnlineModules() { + return onlineModules; + } + + public interface Listener { + void repoLoaded(); + } +} 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 new file mode 100644 index 00000000..734f922e --- /dev/null +++ b/app/src/main/java/io/github/lsposed/manager/repo/model/Collaborator.java @@ -0,0 +1,85 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2020 EdXposed Contributors + * Copyright (C) 2021 LSPosed Contributors + */ + +package io.github.lsposed.manager.repo.model; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class Collaborator implements Serializable, Parcelable { + + @SerializedName("login") + @Expose + private String login; + @SerializedName("name") + @Expose + private Object name; + public final static Creator CREATOR = new Creator() { + + public Collaborator createFromParcel(Parcel in) { + return new Collaborator(in); + } + + public Collaborator[] newArray(int size) { + return (new Collaborator[size]); + } + + }; + private final static long serialVersionUID = -7125602393430154154L; + + protected Collaborator(Parcel in) { + this.login = ((String) in.readValue((String.class.getClassLoader()))); + this.name = in.readValue((Object.class.getClassLoader())); + } + + public Collaborator() { + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public Object getName() { + return name; + } + + public void setName(Object name) { + this.name = name; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(login); + dest.writeValue(name); + } + + public int describeContents() { + return 0; + } + +} 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 new file mode 100644 index 00000000..3f4553c8 --- /dev/null +++ b/app/src/main/java/io/github/lsposed/manager/repo/model/OnlineModule.java @@ -0,0 +1,217 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2020 EdXposed Contributors + * Copyright (C) 2021 LSPosed Contributors + */ + +package io.github.lsposed.manager.repo.model; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class OnlineModule implements Serializable, Parcelable { + + @SerializedName("name") + @Expose + private String name; + @SerializedName("description") + @Expose + private String description; + @SerializedName("url") + @Expose + private String url; + @SerializedName("homepageUrl") + @Expose + private String homepageUrl; + @SerializedName("collaborators") + @Expose + private List collaborators = new ArrayList<>(); + @SerializedName("releases") + @Expose + private List releases = new ArrayList<>(); + @SerializedName("readme") + @Expose + private String readme; + @SerializedName("summary") + @Expose + private String summary; + @SerializedName("scope") + @Expose + private List scope = new ArrayList<>(); + @SerializedName("sourceUrl") + @Expose + private String sourceUrl; + @SerializedName("hide") + @Expose + private Boolean hide; + @SerializedName("additionalAuthors") + @Expose + private List additionalAuthors = null; + public final static Creator CREATOR = new Creator() { + + public OnlineModule createFromParcel(Parcel in) { + return new OnlineModule(in); + } + + public OnlineModule[] newArray(int size) { + return (new OnlineModule[size]); + } + + }; + private final static long serialVersionUID = -2294634398588027071L; + + protected OnlineModule(Parcel in) { + this.name = ((String) in.readValue((String.class.getClassLoader()))); + this.description = ((String) in.readValue((String.class.getClassLoader()))); + this.url = ((String) in.readValue((String.class.getClassLoader()))); + this.homepageUrl = ((String) in.readValue((String.class.getClassLoader()))); + in.readList(this.collaborators, (Collaborator.class.getClassLoader())); + in.readList(this.releases, (Release.class.getClassLoader())); + this.readme = ((String) in.readValue((String.class.getClassLoader()))); + this.summary = ((String) in.readValue((String.class.getClassLoader()))); + in.readList(this.scope, (String.class.getClassLoader())); + this.sourceUrl = ((String) in.readValue((String.class.getClassLoader()))); + this.hide = ((Boolean) in.readValue((Boolean.class.getClassLoader()))); + in.readList(this.additionalAuthors, (Object.class.getClassLoader())); + } + + public OnlineModule() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHomepageUrl() { + return homepageUrl; + } + + public void setHomepageUrl(String homepageUrl) { + this.homepageUrl = homepageUrl; + } + + public List getCollaborators() { + return collaborators; + } + + public void setCollaborators(List collaborators) { + this.collaborators = collaborators; + } + + public List getReleases() { + return releases; + } + + public void setReleases(List releases) { + this.releases = releases; + } + + public String getReadme() { + return readme; + } + + public void setReadme(String readme) { + this.readme = readme; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public List getScope() { + return scope; + } + + public void setScope(List scope) { + this.scope = scope; + } + + public String getSourceUrl() { + return sourceUrl; + } + + public void setSourceUrl(String sourceUrl) { + this.sourceUrl = sourceUrl; + } + + public Boolean getHide() { + return hide; + } + + public void setHide(Boolean hide) { + this.hide = hide; + } + + public List getAdditionalAuthors() { + return additionalAuthors; + } + + public void setAdditionalAuthors(List additionalAuthors) { + this.additionalAuthors = additionalAuthors; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(name); + dest.writeValue(description); + dest.writeValue(url); + dest.writeValue(homepageUrl); + dest.writeList(collaborators); + dest.writeList(releases); + dest.writeValue(readme); + dest.writeValue(summary); + dest.writeList(scope); + dest.writeValue(sourceUrl); + dest.writeValue(hide); + dest.writeList(additionalAuthors); + } + + public int describeContents() { + return 0; + } + +} diff --git a/app/src/main/java/io/github/lsposed/manager/repo/model/Release.java b/app/src/main/java/io/github/lsposed/manager/repo/model/Release.java new file mode 100644 index 00000000..445a9f6f --- /dev/null +++ b/app/src/main/java/io/github/lsposed/manager/repo/model/Release.java @@ -0,0 +1,191 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2020 EdXposed Contributors + * Copyright (C) 2021 LSPosed Contributors + */ + +package io.github.lsposed.manager.repo.model; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class Release implements Serializable, Parcelable { + + @SerializedName("name") + @Expose + private String name; + @SerializedName("url") + @Expose + private String url; + @SerializedName("description") + @Expose + private String description; + @SerializedName("descriptionHTML") + @Expose + private String descriptionHTML; + @SerializedName("createdAt") + @Expose + private String createdAt; + @SerializedName("publishedAt") + @Expose + private String publishedAt; + @SerializedName("updatedAt") + @Expose + private String updatedAt; + @SerializedName("tagName") + @Expose + private String tagName; + @SerializedName("isPrerelease") + @Expose + private Boolean isPrerelease; + @SerializedName("releaseAssets") + @Expose + private List releaseAssets = new ArrayList<>(); + public final static Creator CREATOR = new Creator() { + + public Release createFromParcel(Parcel in) { + return new Release(in); + } + + public Release[] newArray(int size) { + return (new Release[size]); + } + + }; + private final static long serialVersionUID = 1047772731795034659L; + + protected Release(Parcel in) { + this.name = ((String) in.readValue((String.class.getClassLoader()))); + this.url = ((String) in.readValue((String.class.getClassLoader()))); + this.description = ((String) in.readValue((String.class.getClassLoader()))); + this.descriptionHTML = ((String) in.readValue((String.class.getClassLoader()))); + this.createdAt = ((String) in.readValue((String.class.getClassLoader()))); + this.publishedAt = ((String) in.readValue((String.class.getClassLoader()))); + this.updatedAt = ((String) in.readValue((String.class.getClassLoader()))); + this.tagName = ((String) in.readValue((String.class.getClassLoader()))); + this.isPrerelease = ((Boolean) in.readValue((Boolean.class.getClassLoader()))); + in.readList(this.releaseAssets, (ReleaseAsset.class.getClassLoader())); + } + + public Release() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDescriptionHTML() { + return descriptionHTML; + } + + public void setDescriptionHTML(String descriptionHTML) { + this.descriptionHTML = descriptionHTML; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + public String getPublishedAt() { + return publishedAt; + } + + public void setPublishedAt(String publishedAt) { + this.publishedAt = publishedAt; + } + + public String getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } + + public String getTagName() { + return tagName; + } + + public void setTagName(String tagName) { + this.tagName = tagName; + } + + public Boolean getIsPrerelease() { + return isPrerelease; + } + + public void setIsPrerelease(Boolean isPrerelease) { + this.isPrerelease = isPrerelease; + } + + public List getReleaseAssets() { + return releaseAssets; + } + + public void setReleaseAssets(List releaseAssets) { + this.releaseAssets = releaseAssets; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(name); + dest.writeValue(url); + dest.writeValue(description); + dest.writeValue(descriptionHTML); + dest.writeValue(createdAt); + dest.writeValue(publishedAt); + dest.writeValue(updatedAt); + dest.writeValue(tagName); + dest.writeValue(isPrerelease); + dest.writeList(releaseAssets); + } + + public int describeContents() { + return 0; + } + +} diff --git a/app/src/main/java/io/github/lsposed/manager/repo/model/ReleaseAsset.java b/app/src/main/java/io/github/lsposed/manager/repo/model/ReleaseAsset.java new file mode 100644 index 00000000..be447871 --- /dev/null +++ b/app/src/main/java/io/github/lsposed/manager/repo/model/ReleaseAsset.java @@ -0,0 +1,98 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2020 EdXposed Contributors + * Copyright (C) 2021 LSPosed Contributors + */ + +package io.github.lsposed.manager.repo.model; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class ReleaseAsset implements Serializable, Parcelable { + + @SerializedName("name") + @Expose + private String name; + @SerializedName("contentType") + @Expose + private String contentType; + @SerializedName("downloadUrl") + @Expose + private String downloadUrl; + public final static Creator CREATOR = new Creator() { + + public ReleaseAsset createFromParcel(Parcel in) { + return new ReleaseAsset(in); + } + + public ReleaseAsset[] newArray(int size) { + return (new ReleaseAsset[size]); + } + + }; + private final static long serialVersionUID = -4273789818349239422L; + + protected ReleaseAsset(Parcel in) { + this.name = ((String) in.readValue((String.class.getClassLoader()))); + this.contentType = ((String) in.readValue((String.class.getClassLoader()))); + this.downloadUrl = ((String) in.readValue((String.class.getClassLoader()))); + } + + public ReleaseAsset() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public void setDownloadUrl(String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(name); + dest.writeValue(contentType); + dest.writeValue(downloadUrl); + } + + public int describeContents() { + return 0; + } + +} diff --git a/app/src/main/java/io/github/lsposed/manager/ui/activity/MainActivity.java b/app/src/main/java/io/github/lsposed/manager/ui/activity/MainActivity.java index bccf4add..31fa6f29 100644 --- a/app/src/main/java/io/github/lsposed/manager/ui/activity/MainActivity.java +++ b/app/src/main/java/io/github/lsposed/manager/ui/activity/MainActivity.java @@ -44,6 +44,7 @@ public class MainActivity extends BaseActivity { } }); binding.modules.setOnClickListener(new StartActivityListener(ModulesActivity.class, true)); + binding.download.setOnClickListener(new StartActivityListener(RepoActivity.class, false)); binding.logs.setOnClickListener(new StartActivityListener(LogsActivity.class, true)); binding.settings.setOnClickListener(new StartActivityListener(SettingsActivity.class, false)); binding.about.setOnClickListener(new StartActivityListener(AboutActivity.class, false)); 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 new file mode 100644 index 00000000..2b6adff3 --- /dev/null +++ b/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoActivity.java @@ -0,0 +1,152 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2020 EdXposed Contributors + * Copyright (C) 2021 LSPosed Contributors + */ + +package io.github.lsposed.manager.ui.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.gson.Gson; + +import java.io.IOException; + +import io.github.lsposed.manager.App; +import io.github.lsposed.manager.R; +import io.github.lsposed.manager.databinding.ActivityAppListBinding; +import io.github.lsposed.manager.repo.RepoLoader; +import io.github.lsposed.manager.repo.model.OnlineModule; +import io.github.lsposed.manager.util.LinearLayoutManagerFix; +import me.zhanghai.android.fastscroll.FastScrollerBuilder; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Request; +import okhttp3.Response; + +public class RepoActivity extends BaseActivity implements RepoLoader.Listener { + private ActivityAppListBinding binding; + private RepoAdapter adapter; + private RepoLoader repoLoader = RepoLoader.getInstance(); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityAppListBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + binding.toolbar.setNavigationOnClickListener(view -> onBackPressed()); + binding.masterSwitch.setVisibility(View.GONE); + ActionBar bar = getSupportActionBar(); + assert bar != null; + bar.setDisplayHomeAsUpEnabled(true); + adapter = new RepoAdapter(); + binding.recyclerView.setAdapter(adapter); + binding.recyclerView.setLayoutManager(new LinearLayoutManagerFix(this)); + setupRecyclerViewInsets(binding.recyclerView, binding.getRoot()); + FastScrollerBuilder fastScrollerBuilder = new FastScrollerBuilder(binding.recyclerView); + if (!preferences.getBoolean("md2", true)) { + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, + DividerItemDecoration.VERTICAL); + binding.recyclerView.addItemDecoration(dividerItemDecoration); + } else { + fastScrollerBuilder.useMd2Style(); + } + repoLoader.addListener(this); + fastScrollerBuilder.build(); + binding.swipeRefreshLayout.setOnRefreshListener(() -> { + repoLoader.loadRemoteData(); + }); + } + + @Override + protected void onResume() { + super.onResume(); + adapter.setData(repoLoader.getOnlineModules()); + } + + @Override + public void repoLoaded() { + runOnUiThread(() -> { + binding.swipeRefreshLayout.setRefreshing(false); + adapter.setData(repoLoader.getOnlineModules()); + }); + } + + private class RepoAdapter extends RecyclerView.Adapter { + private OnlineModule[] modules = new OnlineModule[0]; + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_onlinemodule, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.appName.setText(modules[position].getDescription()); + String summary = modules[position].getSummary(); + if (summary != null) { + holder.appDescription.setText(modules[position].getSummary()); + } else { + holder.appDescription.setVisibility(View.GONE); + } + holder.itemView.setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(RepoActivity.this, RepoItemActivity.class); + intent.putExtra("module", (Parcelable) modules[position]); + startActivity(intent); + }); + } + + @Override + public int getItemCount() { + return modules.length; + } + + public void setData(OnlineModule[] modules) { + this.modules = modules; + notifyDataSetChanged(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + View root; + TextView appName; + TextView appDescription; + + ViewHolder(View itemView) { + super(itemView); + root = itemView.findViewById(R.id.item_root); + appName = itemView.findViewById(R.id.app_name); + appDescription = itemView.findViewById(R.id.description); + } + } + } +} 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 new file mode 100644 index 00000000..238fd775 --- /dev/null +++ b/app/src/main/java/io/github/lsposed/manager/ui/activity/RepoItemActivity.java @@ -0,0 +1,219 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2020 EdXposed Contributors + * Copyright (C) 2021 LSPosed Contributors + */ + +package io.github.lsposed.manager.ui.activity; + +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.ArrayList; +import java.util.List; + +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.repo.model.OnlineModule; +import io.github.lsposed.manager.repo.model.Release; +import io.github.lsposed.manager.util.LinearLayoutManagerFix; +import io.github.lsposed.manager.util.NavUtil; +import io.noties.markwon.Markwon; + +public class RepoItemActivity extends BaseActivity { + ActivityModuleDetailBinding binding; + private Markwon markwon; + private OnlineModule module; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityModuleDetailBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + module = getIntent().getParcelableExtra("module"); + binding.toolbar.setNavigationOnClickListener(view -> onBackPressed()); + ActionBar bar = getSupportActionBar(); + assert bar != null; + bar.setTitle(module.getDescription()); + bar.setSubtitle(module.getName()); + bar.setDisplayHomeAsUpEnabled(true); + markwon = Markwon.create(this); + binding.viewPager.setAdapter(new PagerAdapter()); + binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + if (position == 0) { + binding.appBar.setLiftOnScrollTargetViewId(R.id.scrollView); + } else { + binding.appBar.setLiftOnScrollTargetViewId(R.id.recyclerView); + } + } + }); + int[] titles = new int[]{R.string.module_readme, R.string.module_readme}; + 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); + ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, insets) -> { + Insets insets1 = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()); + v.setPadding(insets1.left, insets1.top, insets1.right, 0); + return insets; + }); + } + } + + private class ReleaseAdapter extends RecyclerView.Adapter { + private final List items; + + public ReleaseAdapter(List items) { + this.items = items; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder 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(); + }); + } + + @Override + 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; + } + } + } + + private class PagerAdapter extends RecyclerView.Adapter { + + @NonNull + @Override + public PagerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == 0) { + return new ViewHolder(ItemRepoReadmeBinding.inflate(getLayoutInflater(), parent, false).getRoot(), viewType); + } else { + return new ViewHolder(ItemRepoReleasesBinding.inflate(getLayoutInflater(), parent, false).getRoot(), viewType); + } + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ViewCompat.setOnApplyWindowInsetsListener(holder.itemView, (v, insets) -> { + Insets insets1 = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()); + if (position == 0) { + v.setPadding(0, 0, 0, insets1.bottom); + } else { + holder.recyclerView.setPadding(0, 0, 0, insets1.bottom); + } + return WindowInsetsCompat.CONSUMED; + }); + if (holder.itemView.isAttachedToWindow()) { + holder.itemView.requestApplyInsets(); + } else { + holder.itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + v.removeOnAttachStateChangeListener(this); + v.requestApplyInsets(); + } + + @Override + public void onViewDetachedFromWindow(View v) { + + } + }); + } + } + if (position == 0) { + binding.appBar.setLiftOnScrollTargetViewId(R.id.scrollView); + markwon.setMarkdown(holder.textView, module.getReadme()); + } else { + binding.appBar.setLiftOnScrollTargetViewId(R.id.recyclerView); + ReleaseAdapter adapter = new ReleaseAdapter(module.getReleases()); + holder.recyclerView.setAdapter(adapter); + holder.recyclerView.setLayoutManager(new LinearLayoutManagerFix(RepoItemActivity.this)); + } + } + + @Override + public int getItemCount() { + return 2; + } + + @Override + public int getItemViewType(int position) { + return position; + } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView textView; + RecyclerView recyclerView; + + public ViewHolder(@NonNull View itemView, int viewType) { + super(itemView); + if (viewType == 0) { + textView = itemView.findViewById(R.id.readme); + } else { + recyclerView = itemView.findViewById(R.id.recyclerView); + } + } + } + } +} diff --git a/app/src/main/res/drawable/ic_get_app.xml b/app/src/main/res/drawable/ic_get_app.xml new file mode 100644 index 00000000..85098596 --- /dev/null +++ b/app/src/main/res/drawable/ic_get_app.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 80280d1e..5dbd012e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -174,6 +174,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_onlinemodule.xml b/app/src/main/res/layout/item_onlinemodule.xml new file mode 100644 index 00000000..53b0e62a --- /dev/null +++ b/app/src/main/res/layout/item_onlinemodule.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_repo_readme.xml b/app/src/main/res/layout/item_repo_readme.xml new file mode 100644 index 00000000..a9bb394c --- /dev/null +++ b/app/src/main/res/layout/item_repo_readme.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_repo_release.xml b/app/src/main/res/layout/item_repo_release.xml new file mode 100644 index 00000000..53b0e62a --- /dev/null +++ b/app/src/main/res/layout/item_repo_release.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_repo_releases.xml b/app/src/main/res/layout/item_repo_releases.xml new file mode 100644 index 00000000..de008a7f --- /dev/null +++ b/app/src/main/res/layout/item_repo_releases.xml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36c72b58..669cf78a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,4 +146,8 @@ Backup Restore %s has been updated + Module repository + repository + Readme + Releases