[app] Add module repo

This commit is contained in:
tehcneko 2021-02-09 19:53:03 +08:00
parent f006c403c2
commit 5aaa6ca017
19 changed files with 1436 additions and 1 deletions

View File

@ -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"

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="io.github.lsposed.manager">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
@ -26,6 +27,9 @@
android:name=".ui.activity.LogsActivity"
android:label="@string/Logs" />
<activity android:name=".ui.activity.AppListActivity" />
<activity android:name=".ui.activity.RepoActivity"
android:label="@string/module_repo"/>
<activity android:name=".ui.activity.RepoItemActivity" />
<activity
android:name=".ui.activity.MainActivity"
android:label="@string/app_name"

View File

@ -5,17 +5,24 @@ import android.app.Application;
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import io.github.lsposed.manager.repo.RepoLoader;
import io.github.lsposed.manager.ui.activity.CrashReportActivity;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
public class App extends Application {
public static final String TAG = "LSPosedManager";
@SuppressLint("StaticFieldLeak")
private static App instance = null;
private static OkHttpClient okHttpClient;
private static Cache okHttpCache;
private SharedPreferences pref;
public static App getInstance() {
@ -60,5 +67,24 @@ public class App extends Application {
instance = this;
pref = PreferenceManager.getDefaultSharedPreferences(this);
RepoLoader.getInstance().loadRemoteData();
}
@NonNull
public static OkHttpClient getOkHttpClient() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder()
.cache(getOkHttpCache())
.build();
}
return okHttpClient;
}
@NonNull
private static Cache getOkHttpCache() {
if (okHttpCache == null) {
okHttpCache = new Cache(new File(App.getInstance().getCacheDir(), "http_cache"), 50L * 1024L * 1024L);
}
return okHttpCache;
}
}

View File

@ -0,0 +1,111 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
* 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<Listener> 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();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<Collaborator> CREATOR = new Creator<Collaborator>() {
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<Collaborator> collaborators = new ArrayList<>();
@SerializedName("releases")
@Expose
private List<Release> releases = new ArrayList<>();
@SerializedName("readme")
@Expose
private String readme;
@SerializedName("summary")
@Expose
private String summary;
@SerializedName("scope")
@Expose
private List<String> scope = new ArrayList<>();
@SerializedName("sourceUrl")
@Expose
private String sourceUrl;
@SerializedName("hide")
@Expose
private Boolean hide;
@SerializedName("additionalAuthors")
@Expose
private List<Object> additionalAuthors = null;
public final static Creator<OnlineModule> CREATOR = new Creator<OnlineModule>() {
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<Collaborator> getCollaborators() {
return collaborators;
}
public void setCollaborators(List<Collaborator> collaborators) {
this.collaborators = collaborators;
}
public List<Release> getReleases() {
return releases;
}
public void setReleases(List<Release> 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<String> getScope() {
return scope;
}
public void setScope(List<String> 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<Object> getAdditionalAuthors() {
return additionalAuthors;
}
public void setAdditionalAuthors(List<Object> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<ReleaseAsset> releaseAssets = new ArrayList<>();
public final static Creator<Release> CREATOR = new Creator<Release>() {
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<ReleaseAsset> getReleaseAssets() {
return releaseAssets;
}
public void setReleaseAssets(List<ReleaseAsset> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<ReleaseAsset> CREATOR = new Creator<ReleaseAsset>() {
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;
}
}

View File

@ -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));

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<RepoAdapter.ViewHolder> {
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);
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<ReleaseAdapter.ViewHolder> {
private final List<Release> items;
public ReleaseAdapter(List<Release> 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<String> 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<PagerAdapter.ViewHolder> {
@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);
}
}
}
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M13,5v6h1.17L12,13.17 9.83,11L11,11L11,5h2m2,-2L9,3v6L5,9l7,7 7,-7h-4L15,3zM19,18L5,18v2h14v-2z" />
</vector>

View File

@ -174,6 +174,58 @@
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardPreventCornerOverlap="false">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="18dp"
android:paddingTop="16dp"
android:paddingEnd="18dp"
android:paddingBottom="16dp">
<ImageView
android:id="@+id/download_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_centerVertical="true"
android:contentDescription="@string/module_repo"
app:srcCompat="@drawable/ic_get_app" />
<TextView
android:id="@+id/download_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_toEndOf="@id/download_icon"
android:text="@string/module_repo"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/download_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/download_title"
android:layout_alignStart="@id/download_title"
android:layout_marginTop="2dp"
android:text="@string/module_repo_summary"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@android:color/darker_gray" />
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:id="@+id/logs"
android:layout_width="match_parent"

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?actionBarTheme"
android:elevation="4dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?colorActionBar"
app:popupTheme="?actionBarPopupTheme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.viewpager2.widget.ViewPager2>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,66 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?listItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="?attr/listPreferredItemHeight"
android:paddingVertical="16dp"
android:paddingStart="16dp"
android:paddingEnd="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/item_root"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="RtlSymmetry">
<TextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/description"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceSmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/app_name"
app:layout_constraintTop_toBottomOf="@id/app_name"
tools:text="@tools:sample/lorem" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,39 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false">
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/readme"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,66 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?listItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="?attr/listPreferredItemHeight"
android:paddingVertical="16dp"
android:paddingStart="16dp"
android:paddingEnd="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/item_root"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="RtlSymmetry">
<TextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/description"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceSmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/app_name"
app:layout_constraintTop_toBottomOf="@id/app_name"
tools:text="@tools:sample/lorem" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,25 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<io.github.lsposed.manager.ui.widget.RecyclerViewBugFixed android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@ -146,4 +146,8 @@
<string name="menu_backup">Backup</string>
<string name="menu_restore">Restore</string>
<string name="xposed_module_updated_notification_content">%s has been updated</string>
<string name="module_repo">Module repository</string>
<string name="module_repo_summary">repository</string>
<string name="module_readme">Readme</string>
<string name="module_releases">Releases</string>
</resources>