diff --git a/app/src/main/java/org/fdroid/fdroid/compose/ComposeUtils.kt b/app/src/main/java/org/fdroid/fdroid/compose/ComposeUtils.kt
index e94d47456..cbafe6728 100644
--- a/app/src/main/java/org/fdroid/fdroid/compose/ComposeUtils.kt
+++ b/app/src/main/java/org/fdroid/fdroid/compose/ComposeUtils.kt
@@ -60,11 +60,13 @@ object ComposeUtils {
onClick: () -> Unit,
modifier: Modifier = Modifier,
imageVector: ImageVector? = null,
+ color: Color = MaterialTheme.colors.primary,
) {
OutlinedButton(
onClick = onClick,
shape = RoundedCornerShape(32.dp),
- modifier = modifier.heightIn(min = ButtonDefaults.MinHeight)
+ modifier = modifier.heightIn(min = ButtonDefaults.MinHeight),
+ colors = ButtonDefaults.outlinedButtonColors(contentColor = color),
) {
if (imageVector != null) {
Icon(
diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java
index 7889d647c..4f3ce9393 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java
@@ -187,7 +187,7 @@ public class ManageReposActivity extends AppCompatActivity implements RepoAdapte
@Override
public void onClicked(Repository repo) {
- RepoDetailsActivity.launch(this, repo.getRepoId());
+ RepoDetailsActivity.Companion.launch(this, repo.getRepoId());
}
/**
diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java
deleted file mode 100644
index 9ec01862e..000000000
--- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java
+++ /dev/null
@@ -1,536 +0,0 @@
-package org.fdroid.fdroid.views.repos;
-
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.net.Uri;
-import android.nfc.NdefMessage;
-import android.nfc.NfcAdapter;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.CompoundButton;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.NavUtils;
-import androidx.core.content.ContextCompat;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.lifecycle.ViewModelStoreOwner;
-import androidx.lifecycle.viewmodel.MutableCreationExtras;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.appbar.MaterialToolbar;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.materialswitch.MaterialSwitch;
-import com.google.android.material.textfield.TextInputLayout;
-
-import org.fdroid.database.Repository;
-import org.fdroid.download.Mirror;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.compat.LocaleCompat;
-import org.fdroid.fdroid.data.App;
-import org.fdroid.fdroid.views.apps.AppListActivity;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-
-public class RepoDetailsActivity extends AppCompatActivity {
- private static final String TAG = "RepoDetailsActivity";
-
- static final String ARG_REPO_ID = "repo_id";
-
- static void launch(Context context, long repoId) {
- Intent intent = new Intent(context, RepoDetailsActivity.class);
- intent.putExtra(ARG_REPO_ID, repoId);
- context.startActivity(intent);
- }
-
- /**
- * If the repo has been updated at least once, then we will show
- * all of this info, otherwise they will be hidden.
- */
- private static final int[] SHOW_IF_EXISTS = {
- R.id.label_repo_name,
- R.id.text_repo_name,
- R.id.label_description,
- R.id.text_description,
- R.id.label_num_apps,
- R.id.text_num_apps,
- R.id.button_view_apps,
- R.id.label_last_update,
- R.id.text_last_update,
- R.id.label_last_update_downloaded,
- R.id.text_last_update_downloaded,
- R.id.label_username,
- R.id.text_username,
- R.id.button_edit_credentials,
- R.id.label_repo_fingerprint,
- R.id.text_repo_fingerprint,
- R.id.text_repo_fingerprint_description,
- };
- /**
- * If the repo has not been updated yet, then we only show
- * these, otherwise they are hidden.
- */
- private static final int[] HIDE_IF_EXISTS = {
- R.id.text_not_yet_updated,
- };
- private Repository repo;
- private long repoId;
- private View repoView;
-
- private MirrorAdapter adapterToNotify;
-
- private RepoDetailsViewModel model;
-
- /**
- * Help function to make switching between two view states easier.
- * Perhaps there is a better way to do this. I recall that using Adobe
- * Flex, there was a thing called "ViewStates" for exactly this. Wonder if
- * that exists in Android?
- */
- private static void setMultipleViewVisibility(View parent, int[] viewIds, int visibility) {
- for (int viewId : viewIds) {
- parent.findViewById(viewId).setVisibility(visibility);
- }
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- FDroidApp fdroidApp = (FDroidApp) getApplication();
- fdroidApp.setSecureWindow(this);
- fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
-
- repoId = getIntent().getLongExtra(ARG_REPO_ID, 0);
- repo = FDroidApp.getRepoManager(this).getRepository(repoId);
- if (repo == null) {
- // repo must have been deleted just now (maybe slow UI?)
- finish();
- return;
- }
-
- ViewModelStoreOwner owner = this;
- ViewModelProvider.Factory factory = RepoDetailsViewModel.Companion.getFactory();
- MutableCreationExtras extras = new MutableCreationExtras();
- extras.set(RepoDetailsViewModel.Companion.getAPP_KEY(), getApplication());
- extras.set(RepoDetailsViewModel.Companion.getREPO_KEY(), repo);
- model = ViewModelProvider.create(owner, factory, extras).get(RepoDetailsViewModel.class);
-
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_repo_details);
-
- MaterialToolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
- repoView = findViewById(R.id.repo_view);
-
- TextView inputUrl = findViewById(R.id.input_repo_url);
- inputUrl.setText(repo.getAddress());
-
- RecyclerView officialMirrorListView = findViewById(R.id.official_mirror_list);
- officialMirrorListView.setLayoutManager(new LinearLayoutManager(this));
- adapterToNotify = new MirrorAdapter(repo, repo.getAllMirrors(false));
- officialMirrorListView.setAdapter(adapterToNotify);
-
- RecyclerView userMirrorListView = findViewById(R.id.user_mirror_list);
- userMirrorListView.setLayoutManager(new LinearLayoutManager(this));
- MirrorAdapter userMirrorAdapter = new MirrorAdapter(repo, repo.getUserMirrors().size());
- userMirrorAdapter.setUserMirrors(repo.getUserMirrors());
- userMirrorListView.setAdapter(userMirrorAdapter);
-
- // update UI when repo in DB changes
- model.getRepoLiveData().observe(this, repo -> {
- if (repo == null) {
- // repo was deleted, close repo details
- finish();
- return;
- }
- this.repo = repo;
- updateRepoView();
- });
-
- TextView numApps = repoView.findViewById(R.id.text_num_apps);
- model.getNumberOfAppsLiveData().observe(this, number -> {
- String countStr = String.format(LocaleCompat.getDefault(), "%d", number);
- numApps.setText(countStr);
- });
-
- MaterialSwitch archiveRepoSwitch = findViewById(R.id.archiveRepo);
- model.getLiveData().observe(this, s -> {
- switch (s.getArchiveState()) {
- case ENABLED:
- archiveRepoSwitch.setEnabled(true);
- archiveRepoSwitch.setChecked(true);
- break;
- case DISABLED:
- archiveRepoSwitch.setEnabled(true);
- archiveRepoSwitch.setChecked(false);
- break;
- case UNKNOWN:
- archiveRepoSwitch.setEnabled(false);
- break;
- }
- });
- archiveRepoSwitch.setOnClickListener(v -> model.setArchiveRepoEnabled(archiveRepoSwitch.isChecked()));
-
- ImageView qrCode = findViewById(R.id.qr_code);
- model.getQrCodeLiveData().observe(this, bitmap -> {
- if (qrCode != null) {
- qrCode.setImageBitmap(bitmap);
- }
- });
- model.generateQrCode(this);
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- /*
- * After, for example, a repo update, the details will have changed in the
- * database. However, or local reference to the Repo object will not
- * have been updated. The safest way to deal with this is to reload the
- * repo object directly from the database.
- */
- repo = FDroidApp.getRepoManager(this).getRepository(repoId);
- updateRepoView();
-
- processIntent(getIntent());
- }
-
- @Override
- public void onNewIntent(Intent i) {
- super.onNewIntent(i);
- // onResume gets called after this to handle the intent
- setIntent(i);
- }
-
- private void processIntent(Intent i) {
- if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(i.getAction())) {
- Parcelable[] rawMsgs = i.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
- NdefMessage msg = (NdefMessage) rawMsgs[0];
- String url = new String(msg.getRecords()[0].getPayload());
- Utils.debugLog(TAG, "Got this URL: " + url);
- Toast.makeText(this, "Got this URL: " + url, Toast.LENGTH_LONG).show();
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- intent.setClass(this, ManageReposActivity.class);
- startActivity(intent);
- finish();
- }
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.repo_details_activity, menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- Intent intent;
- int itemId = item.getItemId();
- if (itemId == android.R.id.home) {
- NavUtils.navigateUpFromSameTask(this);
- return true;
- } else if (itemId == R.id.menu_delete) {
- promptForDelete();
- return true;
- } else if (itemId == R.id.action_share) {
- intent = new Intent(Intent.ACTION_SEND);
- intent.setType("text/plain");
- intent.putExtra(Intent.EXTRA_TEXT, repo.getShareUri());
- startActivity(Intent.createChooser(intent,
- getResources().getString(R.string.share_repository)));
- }
-
- return super.onOptionsItemSelected(item);
- }
-
- private void setupDescription(View parent, Repository repo) {
-
- TextView descriptionLabel = parent.findViewById(R.id.label_description);
- TextView description = parent.findViewById(R.id.text_description);
-
- String desc = repo.getDescription(App.getLocales());
- if (desc == null || TextUtils.isEmpty(desc)) {
- descriptionLabel.setVisibility(View.GONE);
- description.setVisibility(View.GONE);
- description.setText("");
- } else {
- descriptionLabel.setVisibility(View.VISIBLE);
- description.setVisibility(View.VISIBLE);
- description.setText(desc.replaceAll("\n", " "));
- }
- }
-
- private void setupRepoFingerprint(View parent, Repository repo) {
- TextView repoFingerprintView = parent.findViewById(R.id.text_repo_fingerprint);
- TextView repoFingerprintDescView = parent.findViewById(R.id.text_repo_fingerprint_description);
-
- String repoFingerprint;
-
- // TODO show the current state of the signature check, not just whether there is a key or not
- if (TextUtils.isEmpty(repo.getCertificate())) {
- repoFingerprint = getResources().getString(R.string.unsigned);
- repoFingerprintView.setTextColor(ContextCompat.getColor(this, R.color.unsigned));
- repoFingerprintDescView.setVisibility(View.VISIBLE);
- repoFingerprintDescView.setText(getResources().getString(R.string.unsigned_description));
- } else {
- // this is based on repo.fingerprint always existing, which it should
- repoFingerprint = Utils.formatFingerprint(this, repo.getFingerprint());
- repoFingerprintDescView.setVisibility(View.GONE);
- }
-
- repoFingerprintView.setText(repoFingerprint);
- }
-
- private void setupCredentials(View parent, Repository repo) {
-
- TextView usernameLabel = parent.findViewById(R.id.label_username);
- TextView username = parent.findViewById(R.id.text_username);
- Button changePassword = parent.findViewById(R.id.button_edit_credentials);
- changePassword.setOnClickListener(this::showChangePasswordDialog);
-
- if (TextUtils.isEmpty(repo.getUsername())) {
- usernameLabel.setVisibility(View.GONE);
- username.setVisibility(View.GONE);
- username.setText("");
- changePassword.setVisibility(View.GONE);
- } else {
- usernameLabel.setVisibility(View.VISIBLE);
- username.setVisibility(View.VISIBLE);
- username.setText(repo.getUsername());
- changePassword.setVisibility(View.VISIBLE);
- }
- }
-
- private void updateRepoView() {
- TextView officialMirrorsLabel = repoView.findViewById(R.id.label_official_mirrors);
- RecyclerView officialMirrorList = repoView.findViewById(R.id.official_mirror_list);
- if (repo.getAllMirrors().size() > 1) {
- // don't show this if there is only the canonical URL available, and no other mirrors
- officialMirrorsLabel.setVisibility(View.VISIBLE);
- officialMirrorList.setVisibility(View.VISIBLE);
- } else {
- officialMirrorsLabel.setVisibility(View.GONE);
- officialMirrorList.setVisibility(View.GONE);
- }
-
- TextView userMirrorsLabel = repoView.findViewById(R.id.label_user_mirrors);
- RecyclerView userMirrorList = repoView.findViewById(R.id.user_mirror_list);
- if (repo.getUserMirrors().size() > 0) {
- userMirrorsLabel.setVisibility(View.VISIBLE);
- userMirrorList.setVisibility(View.VISIBLE);
- } else {
- userMirrorsLabel.setVisibility(View.GONE);
- userMirrorList.setVisibility(View.GONE);
- }
-
- if (repo.getLastUpdated() != null) {
- updateViewForExistingRepo(repoView);
- } else {
- setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.VISIBLE);
- setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.GONE);
- }
- }
-
- private void updateViewForExistingRepo(View repoView) {
- setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.VISIBLE);
- setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.GONE);
-
- TextView name = repoView.findViewById(R.id.text_repo_name);
- TextView numAppsButton = repoView.findViewById(R.id.button_view_apps);
- TextView lastUpdated = repoView.findViewById(R.id.text_last_update);
- TextView lastDownloaded = repoView.findViewById(R.id.text_last_update_downloaded);
-
- name.setText(repo.getName(App.getLocales()));
- if (repo.getEnabled()) {
- numAppsButton.setOnClickListener(view -> {
- Intent i = new Intent(this, AppListActivity.class);
- i.putExtra(AppListActivity.EXTRA_REPO_ID, repo.getRepoId());
- startActivity(i);
- });
- numAppsButton.setVisibility(View.VISIBLE);
- } else {
- numAppsButton.setVisibility(View.GONE);
- }
-
- setupDescription(repoView, repo);
- setupRepoFingerprint(repoView, repo);
- setupCredentials(repoView, repo);
-
- if (repo.getTimestamp() == -1) {
- lastUpdated.setText(R.string.unknown);
- } else {
- int format = DateUtils.isToday(repo.getTimestamp()) ?
- DateUtils.FORMAT_SHOW_TIME :
- DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
- lastUpdated.setText(DateUtils.formatDateTime(this, repo.getTimestamp(), format));
- }
- if (repo.getLastUpdated() == null) {
- lastDownloaded.setText(R.string.unknown);
- } else {
- int format = DateUtils.isToday(repo.getLastUpdated()) ?
- DateUtils.FORMAT_SHOW_TIME :
- DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
- lastDownloaded.setText(DateUtils.formatDateTime(this, repo.getLastUpdated(), format));
- }
- }
-
- private void promptForDelete() {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.repo_confirm_delete_title)
- .setMessage(R.string.repo_confirm_delete_body)
- .setPositiveButton(R.string.delete, (dialog, which) -> {
- model.deleteRepository();
- finish();
- }).setNegativeButton(android.R.string.cancel, (dialog, which) -> {
- // Do nothing...
- }
- ).show();
- }
-
- private void showChangePasswordDialog(final View parentView) {
- final View view = getLayoutInflater().inflate(R.layout.login, (ViewGroup) parentView, false);
- final AlertDialog credentialsDialog = new MaterialAlertDialogBuilder(this).setView(view).create();
- final TextInputLayout nameInputLayout = view.findViewById(R.id.edit_name);
- final TextInputLayout passwordInputLayout = view.findViewById(R.id.edit_password);
- final EditText nameInput = nameInputLayout.getEditText();
- final EditText passwordInput = passwordInputLayout.getEditText();
-
- nameInput.setText(repo.getUsername());
- passwordInput.requestFocus();
-
- credentialsDialog.setTitle(R.string.repo_edit_credentials);
- credentialsDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
- getString(R.string.cancel), (dialog, which) -> dialog.dismiss());
-
- credentialsDialog.setButton(DialogInterface.BUTTON_POSITIVE,
- getString(R.string.ok), (dialog, which) -> {
-
- final String name = nameInput.getText().toString();
- final String password = passwordInput.getText().toString();
-
- if (!TextUtils.isEmpty(name)) {
- model.updateUsernameAndPassword(name, password);
- updateRepoView();
- dialog.dismiss();
- } else {
- Toast.makeText(RepoDetailsActivity.this, R.string.repo_error_empty_username,
- Toast.LENGTH_LONG).show();
- }
- });
-
- credentialsDialog.show();
- }
-
- private class MirrorAdapter extends RecyclerView.Adapter {
- private final Repository repo;
- private final List mirrors;
- private final HashSet disabledMirrors;
-
- class MirrorViewHolder extends RecyclerView.ViewHolder {
- View view;
-
- MirrorViewHolder(View view) {
- super(view);
- this.view = view;
- }
- }
-
- MirrorAdapter(Repository repo, List mirrors) {
- this.repo = repo;
- this.mirrors = mirrors;
- disabledMirrors = new HashSet<>(repo.getDisabledMirrors());
- }
-
- MirrorAdapter(Repository repo, int userMirrorSize) {
- this.repo = repo;
- this.mirrors = new ArrayList<>(userMirrorSize);
- disabledMirrors = new HashSet<>(repo.getDisabledMirrors());
- }
-
- void setUserMirrors(List userMirrors) {
- for (String url : userMirrors) {
- this.mirrors.add(new Mirror(url));
- }
- }
-
- @NonNull
- @Override
- public MirrorAdapter.MirrorViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.repo_item, parent, false);
- return new MirrorViewHolder(itemView);
- }
-
- @Override
- public void onBindViewHolder(@NonNull MirrorViewHolder holder, final int position) {
- TextView repoNameTextView = holder.view.findViewById(R.id.repo_name);
- Mirror mirror = mirrors.get(position);
- repoNameTextView.setText(mirror.getBaseUrl());
-
- final String itemMirror = mirror.getBaseUrl();
- boolean enabled = true;
- for (String disabled : disabledMirrors) {
- if (TextUtils.equals(itemMirror, disabled)) {
- enabled = false;
- break;
- }
- }
- CompoundButton switchView = holder.view.findViewById(R.id.repo_switch);
- // reset recycled CheckedChangeListener before checking to avoid bugs
- switchView.setOnCheckedChangeListener(null);
- switchView.setChecked(enabled);
- switchView.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (isChecked) {
- disabledMirrors.remove(itemMirror);
- } else {
- disabledMirrors.add(itemMirror);
- }
-
- List mirrors = repo.getAllMirrors(true);
- int totalMirrors = mirrors.size();
- if (disabledMirrors.size() == totalMirrors) {
- // if all mirrors are disabled, re-enable canonical repo as mirror
- disabledMirrors.remove(repo.getAddress());
- adapterToNotify.notifyDataSetChanged();
- }
- ArrayList toDisableMirrors = new ArrayList<>(disabledMirrors);
- model.updateDisabledMirrors(toDisableMirrors);
- });
-
- View repoUnverified = holder.view.findViewById(R.id.repo_unverified);
- repoUnverified.setVisibility(View.GONE);
-
- View repoUnsigned = holder.view.findViewById(R.id.repo_unsigned);
- repoUnsigned.setVisibility(View.GONE);
- }
-
- @Override
- public int getItemCount() {
- if (mirrors == null) {
- return 0;
- }
- return mirrors.size();
- }
- }
-}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.kt
new file mode 100644
index 000000000..155a29d11
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.kt
@@ -0,0 +1,203 @@
+package org.fdroid.fdroid.views.repos
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.widget.ImageView
+import android.widget.Toast
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.MutableCreationExtras
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.textfield.TextInputLayout
+import org.fdroid.download.Mirror
+import org.fdroid.fdroid.FDroidApp
+import org.fdroid.fdroid.R
+import org.fdroid.fdroid.compose.ComposeUtils.FDroidContent
+import org.fdroid.fdroid.views.apps.AppListActivity
+
+class RepoDetailsActivity : AppCompatActivity() {
+
+ companion object {
+ private const val TAG = "RepoDetailsActivity"
+ const val ARG_REPO_ID = "repo_id"
+
+ fun launch(context: Context, repoId: Long) {
+ val intent = Intent(context, RepoDetailsActivity::class.java).apply {
+ putExtra(ARG_REPO_ID, repoId)
+ }
+ context.startActivity(intent)
+ }
+ }
+
+ private lateinit var viewModel: RepoDetailsViewModel
+
+ // Only call this once in onCreate()
+ private fun initViewModel() {
+ val repoId = intent.getLongExtra(ARG_REPO_ID, 0)
+ val repo = FDroidApp.getRepoManager(this).getRepository(repoId)
+ if (repo == null) {
+ // repo must have been deleted just now (maybe slow UI?)
+ finish()
+ return
+ }
+
+ val factory = RepoDetailsViewModel.Factory
+ val extras = MutableCreationExtras().apply {
+ set(RepoDetailsViewModel.APP_KEY, application)
+ set(RepoDetailsViewModel.Companion.REPO_KEY, repo)
+ }
+ viewModel =
+ ViewModelProvider.create(this, factory, extras)[RepoDetailsViewModel::class.java]
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ (application as FDroidApp).setSecureWindow(this)
+ (application as FDroidApp).applyPureBlackBackgroundInDarkTheme(this)
+
+ initViewModel()
+
+ setContent {
+ val state by viewModel.state.collectAsState()
+ val numberOfApps by viewModel.numberAppsFlow.collectAsState(0)
+
+ FDroidContent {
+ RepoDetailsScreen(
+ state = state,
+ numberOfApps = numberOfApps,
+ // app bar
+ onBackClicked = { onBackPressedDispatcher.onBackPressed() },
+ onShareClicked = this::onShareClicked,
+ onShowQrCodeClicked = this::onShowQrCodeClicked,
+ onDeleteClicked = this::onDeleteClicked,
+ onInfoClicked = this::onInfoClicked,
+ // other buttons
+ onShowAppsClicked = this::onShowAppsClicked,
+ onToggleArchiveClicked = { enabled ->
+ viewModel.setArchiveRepoEnabled(enabled)
+ },
+ onEditCredentialsClicked = this::onEditCredentialsClicked,
+ // mirrors
+ setMirrorEnabled = { m, enabled -> viewModel.setMirrorEnabled(m, enabled) },
+ onShareMirror = this::onShareMirror,
+ onDeleteMirror = this::onDeleteMirror,
+ )
+ }
+ }
+ }
+
+ private fun onShareClicked() {
+ val uri = viewModel.state.value.repo.getShareUri()
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, uri)
+ }
+ startActivity(
+ Intent.createChooser(intent, getResources().getString(R.string.share_repository))
+ )
+ }
+
+ private fun onShowQrCodeClicked() {
+ viewModel.generateQrCode(this)
+
+ val imageView = ImageView(this)
+ viewModel.qrCodeLiveData.observe(this) {
+ imageView.setImageBitmap(it)
+ }
+
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.share_repository)
+ .setView(imageView)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ private fun onDeleteClicked() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.repo_confirm_delete_title)
+ .setMessage(R.string.repo_confirm_delete_body)
+ .setPositiveButton(R.string.delete) { _, _ ->
+ viewModel.deleteRepository()
+ finish()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ private fun onInfoClicked() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.repo_details)
+ .setMessage(R.string.repo_details_info_text)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ private fun onShowAppsClicked() {
+ val repo = viewModel.state.value.repo
+ if (!repo.enabled) {
+ return
+ }
+ val intent = Intent(this, AppListActivity::class.java).apply {
+ putExtra(AppListActivity.EXTRA_REPO_ID, repo.repoId)
+ }
+ startActivity(intent)
+ }
+
+ private fun onEditCredentialsClicked() {
+ val repo = viewModel.state.value.repo
+
+ val view = layoutInflater.inflate(R.layout.login, null, false)
+ val usernameInput = view.findViewById(R.id.edit_name)
+ val passwordInput = view.findViewById(R.id.edit_password)
+
+ usernameInput.editText?.setText(repo.username ?: "")
+ passwordInput.requestFocus()
+
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.repo_basic_auth_title)
+ .setView(view)
+ .setPositiveButton(R.string.ok) { dialog, _ ->
+ val username = usernameInput.editText?.text.toString()
+ val password = passwordInput.editText?.text.toString()
+
+ if (username.isNotBlank()) {
+ viewModel.updateUsernameAndPassword(username, password)
+ } else {
+ Toast.makeText(this, R.string.repo_error_empty_username, Toast.LENGTH_LONG)
+ .show()
+ }
+ dialog.dismiss()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ private fun onShareMirror(mirror: Mirror) {
+ val fingerprint = viewModel.state.value.repo.fingerprint
+ val uri = mirror.getFDroidLinkUrl(fingerprint)
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, uri)
+ }
+ startActivity(
+ Intent.createChooser(intent, getResources().getString(R.string.share_mirror))
+ )
+ }
+
+ private fun onDeleteMirror(mirror: Mirror) {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.repo_confirm_delete_mirror_title)
+ .setMessage(R.string.repo_confirm_delete_mirror_body)
+ .setPositiveButton(R.string.delete) { dialog, _ ->
+ viewModel.deleteUserMirror(mirror)
+ dialog.dismiss()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsScreen.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsScreen.kt
new file mode 100644
index 000000000..b12bf19d8
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsScreen.kt
@@ -0,0 +1,477 @@
+package org.fdroid.fdroid.views.repos
+
+import android.text.format.DateUtils
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Card
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Divider
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Dns
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Fingerprint
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Public
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material.icons.filled.Share
+import androidx.compose.material.primarySurface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.Bottom
+import androidx.compose.ui.Alignment.Companion.End
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
+import androidx.compose.ui.unit.dp
+import androidx.core.os.LocaleListCompat
+import org.fdroid.database.Repository
+import org.fdroid.download.Mirror
+import org.fdroid.fdroid.FDroidApp
+import org.fdroid.fdroid.R
+import org.fdroid.fdroid.Utils
+import org.fdroid.fdroid.compose.ComposeUtils.FDroidButton
+import org.fdroid.fdroid.compose.ComposeUtils.FDroidContent
+import org.fdroid.fdroid.compose.ComposeUtils.FDroidOutlineButton
+import org.fdroid.fdroid.compose.FDroidExpandableRow
+import org.fdroid.fdroid.compose.FDroidSwitchRow
+
+@Composable
+fun RepoDetailsScreen(
+ state: RepoDetailsState,
+ numberOfApps: Int,
+ // app bar functions
+ onBackClicked: () -> Unit,
+ onShareClicked: () -> Unit,
+ onShowQrCodeClicked: () -> Unit,
+ onDeleteClicked: () -> Unit,
+ onInfoClicked: () -> Unit,
+ // other buttons
+ onShowAppsClicked: () -> Unit,
+ onToggleArchiveClicked: (Boolean) -> Unit,
+ onEditCredentialsClicked: () -> Unit,
+ // mirror actions
+ setMirrorEnabled: (Mirror, Boolean) -> Unit,
+ onShareMirror: (Mirror) -> Unit,
+ onDeleteMirror: (Mirror) -> Unit,
+) {
+ val officialMirrors = state.repo.getAllOfficialMirrors()
+ val userMirrors = state.repo.getAllUserMirrors()
+ val disabledMirrors = state.repo.disabledMirrors.toHashSet()
+
+ Scaffold(topBar = {
+ TopAppBar(elevation = 4.dp,
+ backgroundColor = MaterialTheme.colors.primarySurface,
+ navigationIcon = {
+ IconButton(onClick = onBackClicked) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
+ }
+ },
+ title = {
+ Text(
+ text = stringResource(R.string.repo_details),
+ modifier = Modifier.alpha(ContentAlpha.high),
+ )
+ },
+ actions = {
+ IconButton(onClick = onShareClicked) {
+ Icon(Icons.Default.Share, stringResource(R.string.share_repository))
+ }
+ IconButton(onClick = onShowQrCodeClicked) {
+ Icon(Icons.Default.QrCode, stringResource(R.string.show_repository_qr))
+ }
+ IconButton(onClick = onDeleteClicked) {
+ Icon(Icons.Default.Delete, stringResource(R.string.delete))
+ }
+ IconButton(onClick = onInfoClicked) {
+ Icon(Icons.Default.Info, stringResource(R.string.repo_details))
+ }
+ })
+ }) { paddingContent ->
+ Box(
+ modifier = Modifier.padding(paddingContent)
+ ) {
+ Column(
+ verticalArrangement = spacedBy(16.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState()),
+ ) {
+ Spacer(modifier = Modifier) // spacedBy will provide the padding
+ GeneralInfoCard(
+ state.repo,
+ state.archiveState,
+ numberOfApps,
+ onShowAppsClicked,
+ onToggleArchiveClicked,
+ )
+ BasicAuthCard(state.repo, onEditCredentialsClicked)
+ if (state.repo.certificate.isEmpty()) {
+ UnsignedCard()
+ } else {
+ FingerprintCard(state.repo)
+ }
+ // The repo's address is currently also an official mirror.
+ // So if there is only one mirror, this is the address => don't show this section.
+ // If there are 2 or more official mirrors, it makes sense to allow users
+ // to disable the canonical address.
+ if (officialMirrors.size > 2) {
+ OfficialMirrors(
+ mirrors = officialMirrors,
+ disabledMirrors = disabledMirrors,
+ setMirrorEnabled = setMirrorEnabled,
+ )
+ }
+ if (userMirrors.isNotEmpty()) {
+ UserMirrors(
+ mirrors = userMirrors,
+ disabledMirrors = disabledMirrors,
+ setMirrorEnabled = setMirrorEnabled,
+ onShareMirror = onShareMirror,
+ onDeleteMirror = onDeleteMirror,
+ )
+ }
+ // TODO: Add button to add user mirror?
+ Spacer(modifier = Modifier) // spacedBy will provide the padding
+ }
+ }
+ }
+}
+
+@Composable
+private fun GeneralInfoCard(
+ repo: Repository,
+ archiveState: ArchiveState,
+ numberOfApps: Int,
+ onShowAppsClicked: () -> Unit,
+ onToggleArchiveClicked: (Boolean) -> Unit,
+) {
+ val localeList = LocaleListCompat.getDefault()
+ val isDevPreview = LocalInspectionMode.current
+ val description = if (isDevPreview) {
+ LoremIpsum(42).values.joinToString(" ")
+ } else {
+ repo.getDescription(localeList)
+ }?.replace("\n", " ")
+
+ val lastIndexUpdateTime = DateUtils.getRelativeTimeSpanString(
+ repo.timestamp,
+ System.currentTimeMillis(),
+ DateUtils.MINUTE_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_ALL
+ )
+ val lastIndexUpdate = stringResource(R.string.repo_last_update_index, lastIndexUpdateTime)
+
+ val lastUpdatedTime = repo.lastUpdated?.let {
+ DateUtils.getRelativeTimeSpanString(
+ it, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_ALL
+ )
+ } ?: stringResource(R.string.repositories_last_update_never)
+ val lastUpdated = stringResource(R.string.repo_last_update_check, lastUpdatedTime)
+
+ Card {
+ Column(
+ verticalArrangement = spacedBy(8.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ ) {
+ Row(
+ horizontalArrangement = spacedBy(16.dp),
+ ) {
+ RepoIcon(repo, Modifier.size(48.dp))
+ Column(horizontalAlignment = Alignment.Start) {
+ Text(
+ text = repo.getName(localeList) ?: "Unknown Repository",
+ maxLines = 1,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.h6,
+ )
+ Text(
+ text = repo.address.replaceFirst("https://", ""),
+ style = MaterialTheme.typography.body2,
+ )
+ Text(
+ text = pluralStringResource(
+ R.plurals.repo_num_apps_text,
+ numberOfApps,
+ numberOfApps,
+ ),
+ style = MaterialTheme.typography.caption,
+ )
+ Text(
+ text = lastIndexUpdate,
+ style = MaterialTheme.typography.caption,
+ )
+ Text(
+ text = lastUpdated,
+ style = MaterialTheme.typography.caption,
+ )
+ }
+ }
+
+ if (repo.enabled) FDroidButton(
+ stringResource(R.string.repo_num_apps_button),
+ modifier = Modifier.align(End),
+ onClick = onShowAppsClicked,
+ )
+
+ if (description != null) {
+ Text(
+ text = description,
+ style = MaterialTheme.typography.body2,
+ )
+ }
+ if (description != null && archiveState != ArchiveState.UNKNOWN) {
+ Divider()
+ }
+ if (archiveState != ArchiveState.UNKNOWN) {
+ FDroidSwitchRow(
+ text = stringResource(R.string.repo_archive_toggle_description),
+ checked = archiveState == ArchiveState.ENABLED,
+ enabled = true,
+ onCheckedChange = onToggleArchiveClicked,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun BasicAuthCard(
+ repo: Repository,
+ onEditCredentialsClicked: () -> Unit,
+) {
+ val username: String = repo.username ?: return
+ if (username.isBlank()) {
+ return
+ }
+
+ Card {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.repo_basic_auth_title),
+ style = MaterialTheme.typography.caption,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Row {
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(
+ text = stringResource(R.string.repo_basic_auth_username, username),
+ style = MaterialTheme.typography.body2,
+ )
+ Text(
+ text = stringResource(R.string.repo_basic_auth_password),
+ style = MaterialTheme.typography.body2,
+ )
+ }
+ FDroidOutlineButton(
+ text = stringResource(R.string.repo_basic_auth_edit),
+ onClick = onEditCredentialsClicked,
+ imageVector = Icons.Default.Edit,
+ modifier = Modifier.align(Bottom),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun FingerprintExpandable(repo: Repository) {
+ val isDevPreview = LocalInspectionMode.current
+ val fingerprint: String = if (isDevPreview) {
+ Utils.formatFingerprint(LocalContext.current, "A".repeat(64))
+ } else {
+ repo.fingerprint?.let {
+ Utils.formatFingerprint(LocalContext.current, it)
+ } ?: stringResource(R.string.unsigned)
+ }
+
+ FDroidExpandableRow(
+ text = stringResource(R.string.repo_fingerprint),
+ imageVectorStart = Icons.Default.Fingerprint,
+ ) {
+ Text(
+ text = fingerprint,
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier.padding(horizontal = 24.dp),
+ )
+ }
+}
+
+@Composable
+private fun UnsignedCard() {
+ Card {
+ Text(
+ text = stringResource(R.string.repo_unsigned_description),
+ color = colorResource(R.color.unsigned),
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ )
+ }
+}
+
+@Composable
+private fun OfficialMirrors(
+ mirrors: List,
+ disabledMirrors: HashSet,
+ setMirrorEnabled: (Mirror, Boolean) -> Unit,
+) {
+ FDroidExpandableRow(
+ text = stringResource(R.string.repo_official_mirrors),
+ imageVectorStart = Icons.Default.Public,
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 24.dp)
+ ) {
+ mirrors.forEachIndexed { idx, m ->
+ FDroidSwitchRow(
+ text = m.baseUrl,
+ checked = !disabledMirrors.contains(m.baseUrl),
+ onCheckedChange = { checked -> setMirrorEnabled(m, checked) },
+ modifier = Modifier.padding(vertical = 8.dp),
+ )
+ if (idx < mirrors.size - 1) Divider()
+ }
+ }
+ }
+}
+
+@Composable
+private fun UserMirrors(
+ mirrors: List,
+ disabledMirrors: HashSet,
+ setMirrorEnabled: (Mirror, Boolean) -> Unit,
+ onShareMirror: (Mirror) -> Unit,
+ onDeleteMirror: (Mirror) -> Unit,
+) {
+ FDroidExpandableRow(
+ text = stringResource(R.string.repo_user_mirrors),
+ imageVectorStart = Icons.Default.Dns,
+ ) {
+ mirrors.forEachIndexed { idx, m ->
+ Column(
+ modifier = Modifier.padding(horizontal = 24.dp)
+ ) {
+ FDroidSwitchRow(
+ text = m.baseUrl,
+ checked = !disabledMirrors.contains(m.baseUrl),
+ onCheckedChange = { checked -> setMirrorEnabled(m, checked) },
+ modifier = Modifier.padding(vertical = 8.dp),
+ )
+ Row(
+ horizontalArrangement = spacedBy(16.dp),
+ ) {
+ FDroidOutlineButton(
+ text = stringResource(R.string.menu_share),
+ imageVector = Icons.Default.Share,
+ onClick = { onShareMirror(m) },
+ )
+ FDroidOutlineButton(
+ text = stringResource(R.string.delete),
+ imageVector = Icons.Default.Delete,
+ onClick = { onDeleteMirror(m) },
+ color = Color.Red,
+ )
+ }
+ if (idx < mirrors.size - 1) Divider()
+ }
+ }
+ }
+}
+
+/* Previews */
+
+@Composable
+@Preview
+fun RepoDetailsScreenPreview() {
+ val repo = FDroidApp.createSwapRepo("https://example.org/fdroid/repo", "foo bar")
+ FDroidContent {
+ RepoDetailsScreen(
+ RepoDetailsState(repo, ArchiveState.ENABLED),
+ numberOfApps = 42,
+ {}, {}, {}, {}, {}, // app bar
+ {}, {}, {}, // other buttons
+ { _, _ -> }, { _ -> }, { _ -> } // mirror
+ )
+ }
+}
+
+@Composable
+@Preview
+fun BasicAuthCardPreview() {
+ val repo = FDroidApp.createSwapRepo("https://example.org/fdroid/repo", "foo bar")
+ // TODO set RepositoryPreferences
+ FDroidContent {
+ BasicAuthCard(repo, { })
+ }
+}
+
+@Composable
+@Preview
+fun UnsignedCardPreview() {
+ FDroidContent {
+ UnsignedCard()
+ }
+}
+
+@Composable
+@Preview
+fun OfficialMirrorsPreview() {
+ FDroidContent {
+ OfficialMirrors(
+ mirrors = listOf(Mirror("https://mirror.example.com/fdroid/repo")),
+ disabledMirrors = HashSet(),
+ setMirrorEnabled = { _, _ -> },
+ )
+ }
+}
+
+@Composable
+@Preview
+fun UserMirrorsPreview() {
+ FDroidContent {
+ UserMirrors(
+ mirrors = listOf(Mirror("https://mirror.example.com/fdroid/repo")),
+ disabledMirrors = HashSet(),
+ setMirrorEnabled = { _, _ -> },
+ onShareMirror = { _ -> },
+ onDeleteMirror = { _ -> },
+ )
+ }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt
index 3daae527a..29a2ffaf4 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt
+++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.fdroid.database.Repository
+import org.fdroid.download.Mirror
import org.fdroid.fdroid.FDroidApp
import org.fdroid.fdroid.R
import org.fdroid.fdroid.data.DBHelper
@@ -109,7 +110,7 @@ class RepoDetailsViewModel(
fun deleteRepository() {
val repoId = _state.value.repo.repoId
viewModelScope.launch(Dispatchers.IO) {
- repositoryDao.deleteRepository(repoId)
+ repoManager.deleteRepository(repoId)
}
}
@@ -127,6 +128,20 @@ class RepoDetailsViewModel(
}
}
+ fun setMirrorEnabled(mirror: Mirror, enabled: Boolean) {
+ val repoId = _state.value.repo.repoId
+ viewModelScope.launch(Dispatchers.IO) {
+ repoManager.setMirrorEnabled(repoId, mirror, enabled)
+ }
+ }
+
+ fun deleteUserMirror(mirror: Mirror) {
+ val repoId = _state.value.repo.repoId
+ viewModelScope.launch(Dispatchers.IO) {
+ repoManager.deleteUserMirror(repoId, mirror)
+ }
+ }
+
private fun Repository.archiveState(): ArchiveState {
val isEnabled = repoManager.getRepositories().find { r ->
r.isArchiveRepo && r.certificate == certificate
diff --git a/app/src/main/res/layout/activity_repo_details.xml b/app/src/main/res/layout/activity_repo_details.xml
deleted file mode 100644
index 3cdea4991..000000000
--- a/app/src/main/res/layout/activity_repo_details.xml
+++ /dev/null
@@ -1,204 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/repo_details_activity.xml b/app/src/main/res/menu/repo_details_activity.xml
deleted file mode 100644
index 52d663a53..000000000
--- a/app/src/main/res/menu/repo_details_activity.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 08e6962a1..2e4dd2f3e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -465,6 +465,7 @@ This often occurs with apps installed via Google Play or other sources, if they
Repository List
A repository is a source of apps. This list shows all currently added repositories. Disabled repositories are not used.\n\nIf an app is in more than one repository, the repository higher in the list is automatically preferred. You can reorder repositories by long pressing and dragging them.
Repository
+ A repository is a source of apps.\n\nMirrors are used to distribute the load of downloading apps across multiple servers. Mirrors closer to you may be faster.\n\nOfficial mirrors are defined by the repository owner. They cannot be deleted, only disabled. You can define additional custom mirrors.
Address
Number of apps
Show apps
@@ -474,9 +475,14 @@ This often occurs with apps installed via Google Play or other sources, if they
Description
Last update
Last update downloaded
- Official mirrors
- User mirrors
+ Official Mirrors
+ User Mirrors
Name
+ 1 app
+ %d apps
+ Last repo update: %s
+ Last check for repo update: %s
+ This repository is not signed (or it is not yet fully loaded). When a repository is unsigned, F-Droid cannot verify the list of apps included in the repository. Be careful with the apps that you download from this repository.
This means that the list of
apps could not be verified. You should be careful
with apps downloaded from unsigned indexes.
@@ -491,6 +497,8 @@ This often occurs with apps installed via Google Play or other sources, if they
apps from it will no longer be available.\n\nNote: All
previously installed apps will remain on your device.
+ Delete Mirror?
+ You can continue to install apps from this repository using the other mirrors.
Disabled "%1$s".\n\nYou will
need to re-enable this repository to install apps from it.
@@ -500,6 +508,12 @@ This often occurs with apps installed via Google Play or other sources, if they
Looking for package repository at\n%1$s
Share Repository
+ Share Mirror
+ Show Repository QR Code
+ Basic Auth
+ Username: %s
+ Password: ***
+ Edit
Your device is not on the same Wi-Fi as the local repo you just added! Try joining
this network: %s
@@ -793,4 +807,6 @@ This often occurs with apps installed via Google Play or other sources, if they
Official IPFS Gateways
Custom IPFS Gateways
+ Expand
+ Collapse
diff --git a/libs/database/src/main/java/org/fdroid/database/Repository.kt b/libs/database/src/main/java/org/fdroid/database/Repository.kt
index aaed4ef1a..c025da66e 100644
--- a/libs/database/src/main/java/org/fdroid/database/Repository.kt
+++ b/libs/database/src/main/java/org/fdroid/database/Repository.kt
@@ -202,6 +202,14 @@ public data class Repository internal constructor(
}.ifEmpty { listOf(org.fdroid.download.Mirror(address)) }
}
+ public fun getAllUserMirrors(): List {
+ return userMirrors.map { org.fdroid.download.Mirror(it) }
+ }
+
+ public fun getAllOfficialMirrors(): List {
+ return getAllMirrors(false)
+ }
+
/**
* Returns all mirrors, including [disabledMirrors].
*/
diff --git a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt
index 3a1785495..5fd5fd6fa 100644
--- a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt
+++ b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt
@@ -22,6 +22,7 @@ import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.DownloaderFactory
import org.fdroid.download.HttpManager
+import org.fdroid.download.Mirror
import org.fdroid.repo.AddRepoState
import org.fdroid.repo.RepoAdder
import org.fdroid.repo.RepoUriGetter
@@ -238,4 +239,36 @@ public class RepoManager @JvmOverloads constructor(
return uri != null && RepoUriGetter.isSwapUri(uri)
}
+ public fun setMirrorEnabled(repoId: Long, mirror: Mirror, enabled: Boolean) {
+ val repo = repositoryDao.getRepository(repoId) ?: return
+ val disabled = repo.disabledMirrors.toMutableList()
+
+ if (enabled) {
+ if (disabled.contains(mirror.baseUrl)) {
+ disabled.remove(mirror.baseUrl)
+ repositoryDao.updateDisabledMirrors(repoId, disabled)
+ }
+ } else {
+ if (!disabled.contains(mirror.baseUrl)) {
+ disabled.add(mirror.baseUrl)
+
+ if (disabled.size == repo.getAllMirrors().size) {
+ // if all mirrors are disabled, re-enable canonical repo as mirror
+ disabled.remove(repo.address)
+ }
+
+ repositoryDao.updateDisabledMirrors(repoId, disabled)
+ }
+ }
+ }
+
+ public fun deleteUserMirror(repoId: Long, mirror: Mirror) {
+ val repo = repositoryDao.getRepository(repoId) ?: return
+ val user = repo.userMirrors.toMutableList()
+
+ if (user.contains(mirror.baseUrl)) {
+ user.remove(mirror.baseUrl)
+ repositoryDao.updateUserMirrors(repoId, user)
+ }
+ }
}
diff --git a/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt b/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt
index 18a1efa1f..20ccd09f3 100644
--- a/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt
+++ b/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt
@@ -36,6 +36,11 @@ public data class Mirror @JvmOverloads constructor(
return URLBuilder(url).appendPathSegments(path.trimStart('/')).build()
}
+ public fun getFDroidLinkUrl(repoFingerprint: String?): String {
+ val fpr = repoFingerprint?.let { "?fingerprint=$repoFingerprint" } ?: ""
+ return "https://fdroid.link/#$url$fpr"
+ }
+
public fun isOnion(): Boolean = url.isOnion()
public fun isLocal(): Boolean = url.isLocal()