{
+ return artistRepository.getArtists()
+ }
+
+ private fun getDaoCommunity(): DaoCommunity {
+ return IPv8Android.getInstance().getOverlay()
+ ?: throw IllegalStateException("DaoCommunity is not configured")
+ }
+
+ protected fun getTrustChainCommunity(): TrustChainCommunity {
+ return IPv8Android.getInstance().getOverlay()
+ ?: throw IllegalStateException("TrustChainCommunity is not configured")
+ }
+
+ protected val trustchain: TrustChainHelper by lazy {
+ TrustChainHelper(getTrustChainCommunity())
+ }
+
+ fun userInDao(dao: DAO): Boolean {
+ val publicKey = getTrustChainCommunity().myPeer.publicKey.keyToBin().toHex()
+ return dao.members.find { it.trustchainPublicKey == publicKey } != null
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/ProposalCreateScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/ProposalCreateScreen.kt
new file mode 100644
index 000000000..5aac0ee9d
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/ProposalCreateScreen.kt
@@ -0,0 +1,119 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.dao
+
+import android.app.Activity
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.ui.SnackbarHandler
+
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun ProposalCreateScreen(daoId: String, daoViewModel: DaoViewModel, navController: NavController) {
+ var satoshi by rememberSaveable { mutableStateOf("6000") }
+ var address by rememberSaveable { mutableStateOf("mkKcu9VCNTAerxbZLXvLSGBTBiLwqGqcDL") }
+ var chosenArtist by rememberSaveable { mutableStateOf("") }
+
+ val local = LocalContext.current
+
+ val dao = daoViewModel.getDao(daoId)
+
+ var expanded by remember { mutableStateOf(false) }
+
+ fun newProposal() {
+ if (dao != null) {
+ // validation
+ if (satoshi.toLong() <= 5000) {
+ SnackbarHandler.displaySnackbar("Amount should be larger than 5000.")
+ return
+ }
+
+ if ((daoViewModel.getDao(daoId)?.second?.balance ?: 0) < satoshi.toLong()) {
+ SnackbarHandler.displaySnackbar("Not enough balance.")
+ return
+ }
+
+ daoViewModel.transferFundsClickedByMe(
+ address,
+ satoshi.toLong(),
+ dao.first.calculateHash(),
+ local,
+ local as Activity
+ )
+
+ daoViewModel.refreshOneShot()
+ navController.popBackStack()
+ }
+ }
+
+ Card(
+ modifier = Modifier
+ .padding(20.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ OutlinedTextField(
+ value = satoshi,
+ onValueChange = { satoshi = it },
+ label = { Text("Amount (satoshi) (larger than 5000)") }
+ )
+ OutlinedTextField(
+ value = address,
+ onValueChange = { address = it },
+ label = { Text("Bitcoin Address") }
+ )
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = { expanded = true }) {
+ Icon(
+ Icons.Default.Person,
+ contentDescription = "Localized description",
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ }
+ OutlinedTextField(
+ value = chosenArtist,
+ onValueChange = { chosenArtist = it },
+ label = { Text("Artist") },
+ enabled = false
+ )
+ }
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ daoViewModel.getListsOfArtists().map { artist ->
+ DropdownMenuItem(
+ onClick = {
+ address = artist.bitcoinAddress; expanded = false; chosenArtist =
+ artist.name
+ }
+ ) {
+ Text(artist.name)
+ }
+ }
+ }
+ }
+ Spacer(modifier = Modifier.size(10.dp))
+ OutlinedButton(
+ onClick = {
+ newProposal()
+ }
+ ) {
+ Text("Create")
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/ProposalDetail.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/ProposalDetail.kt
new file mode 100644
index 000000000..965622a57
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/ProposalDetail.kt
@@ -0,0 +1,273 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.dao
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Card
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.OutlinedButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import nl.tudelft.trustchain.musicdao.core.dao.JoinProposal
+import nl.tudelft.trustchain.musicdao.core.dao.Proposal
+import nl.tudelft.trustchain.musicdao.core.dao.TransferProposal
+import nl.tudelft.trustchain.musicdao.ui.SnackbarHandler
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyState
+import com.google.accompanist.swiperefresh.SwipeRefresh
+import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
+
+@Composable
+fun ProposalDetailScreen(proposalId: String, daoViewModel: DaoViewModel) {
+ val proposal = daoViewModel.getProposal(proposalId)
+
+ val isRefreshing by daoViewModel.isRefreshing.collectAsState()
+ val refreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing)
+
+ if (proposal != null) {
+ SwipeRefresh(
+ state = refreshState,
+ onRefresh = {
+ daoViewModel.refreshOneShot()
+ }
+ ) {
+ ProposalDetailPure(proposal.first, daoViewModel)
+ }
+ } else {
+ EmptyState("Not found.", proposalId)
+ }
+}
+
+@Composable
+fun ProposalDetailPure(proposal: Proposal, daoViewModel: DaoViewModel) {
+ val context = LocalContext.current
+
+ when (proposal) {
+ is JoinProposal -> Column(
+ modifier = Modifier
+ .padding(20.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ ProposalCard(
+ proposal = proposal,
+ navigateToProposal = null
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+
+ Card {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ val dao = daoViewModel.getDao(proposal.daoId)
+
+ if (proposal.isClosed()) {
+ Text(
+ "You can not sign this proposal, it has already been closed."
+ )
+ } else {
+ if (dao != null) {
+ if (daoViewModel.userInDao(dao.second)) {
+ if (!daoViewModel.hasMadeProposalVote(proposal)) {
+ Text(
+ "You have not signed this proposal yet, you can do so below."
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+ OutlinedButton(
+ onClick = {
+ proposal.proposalId
+ val block =
+ daoViewModel.getProposal(proposal.proposalId)
+
+ if (block?.second != null) {
+ daoViewModel.upvoteJoin(
+ context,
+ proposal
+ )
+ } else {
+ SnackbarHandler.displaySnackbar("Could not find the proposal.")
+ }
+ }
+ ) {
+ Text("Sign this proposal")
+ }
+ } else {
+ Text(
+ "You have already signed this proposal earlier, please wait."
+ )
+ }
+ } else {
+ Text(
+ "You can not sign this proposal since you are not a member."
+ )
+ }
+ }
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.size(10.dp))
+
+ Card {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Text(
+ "Votes",
+ style = MaterialTheme.typography.subtitle2,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+ if (proposal.signatures.isEmpty()) {
+ Text(text = "No votes have been cast yet.")
+ }
+ proposal.signatures.map { vote ->
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 2.dp)
+ ) {
+ Row {
+ Box(
+ modifier = Modifier
+ .size(16.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colors.primary)
+ )
+ Spacer(modifier = Modifier.size(5.dp))
+ Text(
+ vote.bitcoinPublicKey,
+ style = MaterialTheme.typography.caption,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ is TransferProposal -> Column(
+ modifier = Modifier
+ .padding(20.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ ProposalCard(
+ proposal = proposal,
+ navigateToProposal = null
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+
+ Card {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ val dao = daoViewModel.getDao(proposal.daoId)
+
+ if (proposal.isClosed()) {
+ Text(
+ "You can not sign this proposal, it has already been closed."
+ )
+ } else {
+ if (dao != null) {
+ if (daoViewModel.userInDao(dao.second)) {
+ if (!daoViewModel.hasMadeProposalVote(proposal)) {
+ Text(
+ "You have not signed this proposal yet, you can do so below. "
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+ OutlinedButton(
+ onClick = {
+ proposal.proposalId
+ val block =
+ daoViewModel.getProposal(proposal.proposalId)
+
+ if (block?.second != null) {
+ daoViewModel.upvoteTransfer(
+ context,
+ proposal
+ )
+ } else {
+ SnackbarHandler.displaySnackbar("Could not find the proposal.")
+ }
+ }
+ ) {
+ Text("Sign this proposal")
+ }
+ } else {
+ Text(
+ "You have already signed this proposal earlier, please wait."
+ )
+ }
+ } else {
+ Text(
+ "You can not sign this proposal since you are not a member."
+ )
+ }
+ }
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.size(10.dp))
+
+ Card {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Text(
+ "Votes",
+ style = MaterialTheme.typography.subtitle2,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+ if (proposal.signatures.isEmpty()) {
+ Text(text = "No votes have been cast yet.")
+ }
+ proposal.signatures.map { vote ->
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 2.dp)
+ ) {
+ Row {
+ Box(
+ modifier = Modifier
+ .size(16.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colors.primary)
+ )
+ Spacer(modifier = Modifier.size(5.dp))
+ Text(
+ vote.bitcoinPublicKey,
+ style = MaterialTheme.typography.caption,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/persistence/RuntimeTypeAdaptorFactory.java b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/persistence/RuntimeTypeAdaptorFactory.java
new file mode 100644
index 000000000..d56838a28
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/dao/persistence/RuntimeTypeAdaptorFactory.java
@@ -0,0 +1,296 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.dao.persistence;
+
+/*
+ * Copyright (C) 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Adapts values whose runtime type may differ from their declaration type. This
+ * is necessary when a field's type is not the same type that GSON should create
+ * when deserializing that field. For example, consider these types:
+ * {@code
+ * abstract class Shape {
+ * int x;
+ * int y;
+ * }
+ * class Circle extends Shape {
+ * int radius;
+ * }
+ * class Rectangle extends Shape {
+ * int width;
+ * int height;
+ * }
+ * class Diamond extends Shape {
+ * int width;
+ * int height;
+ * }
+ * class Drawing {
+ * Shape bottomShape;
+ * Shape topShape;
+ * }
+ * }
+ * Without additional type information, the serialized JSON is ambiguous. Is
+ * the bottom shape in this drawing a rectangle or a diamond?
{@code
+ * {
+ * "bottomShape": {
+ * "width": 10,
+ * "height": 5,
+ * "x": 0,
+ * "y": 0
+ * },
+ * "topShape": {
+ * "radius": 2,
+ * "x": 4,
+ * "y": 1
+ * }
+ * }}
+ * This class addresses this problem by adding type information to the
+ * serialized JSON and honoring that type information when the JSON is
+ * deserialized: {@code
+ * {
+ * "bottomShape": {
+ * "type": "Diamond",
+ * "width": 10,
+ * "height": 5,
+ * "x": 0,
+ * "y": 0
+ * },
+ * "topShape": {
+ * "type": "Circle",
+ * "radius": 2,
+ * "x": 4,
+ * "y": 1
+ * }
+ * }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code
+ * "Rectangle"}) are configurable.
+ *
+ * Registering Types
+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
+ * name to the {@link #of} factory method. If you don't supply an explicit type
+ * field name, {@code "type"} will be used. {@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory
+ * = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly
+ * registered. This protects your application from injection attacks. If you
+ * don't supply an explicit type label, the type's simple name will be used.
+ * {@code
+ * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder:
+ * {@code
+ * Gson gson = new GsonBuilder()
+ * .registerTypeAdapterFactory(shapeAdapterFactory)
+ * .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining: {@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ * .registerSubtype(Rectangle.class)
+ * .registerSubtype(Circle.class)
+ * .registerSubtype(Diamond.class);
+ * }
+ *
+ * Serialization and deserialization
+ * In order to serialize and deserialize a polymorphic object,
+ * you must specify the base type explicitly.
+ * {@code
+ * Diamond diamond = new Diamond();
+ * String json = gson.toJson(diamond, Shape.class);
+ * }
+ * And then:
+ * {@code
+ * Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */
+final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
+ private final Class> baseType;
+ private final String typeFieldName;
+ private final Map> labelToSubtype = new LinkedHashMap<>();
+ private final Map, String> subtypeToLabel = new LinkedHashMap<>();
+ private final boolean maintainType;
+ private boolean recognizeSubtypes;
+
+ private RuntimeTypeAdapterFactory(
+ Class> baseType, String typeFieldName, boolean maintainType) {
+ if (typeFieldName == null || baseType == null) {
+ throw new NullPointerException();
+ }
+ this.baseType = baseType;
+ this.typeFieldName = typeFieldName;
+ this.maintainType = maintainType;
+ }
+
+ /**
+ * Creates a new runtime type adapter using for {@code baseType} using {@code
+ * typeFieldName} as the type field name. Type field names are case sensitive.
+ *
+ * @param maintainType true if the type field should be included in deserialized objects
+ */
+ public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) {
+ return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
+ }
+
+ /**
+ * Creates a new runtime type adapter using for {@code baseType} using {@code
+ * typeFieldName} as the type field name. Type field names are case sensitive.
+ */
+ public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) {
+ return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
+ }
+
+ /**
+ * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
+ * the type field name.
+ */
+ public static RuntimeTypeAdapterFactory of(Class baseType) {
+ return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
+ }
+
+ /**
+ * Ensures that this factory will handle not just the given {@code baseType}, but any subtype
+ * of that type.
+ */
+ public RuntimeTypeAdapterFactory recognizeSubtypes() {
+ this.recognizeSubtypes = true;
+ return this;
+ }
+
+ /**
+ * Registers {@code type} identified by {@code label}. Labels are case
+ * sensitive.
+ *
+ * @throws IllegalArgumentException if either {@code type} or {@code label}
+ * have already been registered on this type adapter.
+ */
+ public RuntimeTypeAdapterFactory registerSubtype(Class extends T> type, String label) {
+ if (type == null || label == null) {
+ throw new NullPointerException();
+ }
+ if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
+ throw new IllegalArgumentException("types and labels must be unique");
+ }
+ labelToSubtype.put(label, type);
+ subtypeToLabel.put(type, label);
+ return this;
+ }
+
+ /**
+ * Registers {@code type} identified by its {@link Class#getSimpleName simple
+ * name}. Labels are case sensitive.
+ *
+ * @throws IllegalArgumentException if either {@code type} or its simple name
+ * have already been registered on this type adapter.
+ */
+ public RuntimeTypeAdapterFactory registerSubtype(Class extends T> type) {
+ return registerSubtype(type, type.getSimpleName());
+ }
+
+ @Override
+ public TypeAdapter create(Gson gson, TypeToken type) {
+ if (type == null) {
+ return null;
+ }
+ Class> rawType = type.getRawType();
+ boolean handle =
+ recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType);
+ if (!handle) {
+ return null;
+ }
+
+ final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class);
+ final Map> labelToDelegate = new LinkedHashMap<>();
+ final Map, TypeAdapter>> subtypeToDelegate = new LinkedHashMap<>();
+ for (Map.Entry> entry : labelToSubtype.entrySet()) {
+ TypeAdapter> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
+ labelToDelegate.put(entry.getKey(), delegate);
+ subtypeToDelegate.put(entry.getValue(), delegate);
+ }
+
+ return new TypeAdapter() {
+ @Override
+ public R read(JsonReader in) throws IOException {
+ JsonElement jsonElement = jsonElementAdapter.read(in);
+ JsonElement labelJsonElement;
+ if (maintainType) {
+ labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
+ } else {
+ labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
+ }
+
+ if (labelJsonElement == null) {
+ throw new JsonParseException("cannot deserialize " + baseType
+ + " because it does not define a field named " + typeFieldName);
+ }
+ String label = labelJsonElement.getAsString();
+ @SuppressWarnings("unchecked") // registration requires that subtype extends T
+ TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label);
+ if (delegate == null) {
+ throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ + label + "; did you forget to register a subtype?");
+ }
+ return delegate.fromJsonTree(jsonElement);
+ }
+
+ @Override
+ public void write(JsonWriter out, R value) throws IOException {
+ Class> srcType = value.getClass();
+ String label = subtypeToLabel.get(srcType);
+ @SuppressWarnings("unchecked") // registration requires that subtype extends T
+ TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType);
+ if (delegate == null) {
+ throw new JsonParseException("cannot serialize " + srcType.getName()
+ + "; did you forget to register a subtype?");
+ }
+ JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
+
+ if (maintainType) {
+ jsonElementAdapter.write(out, jsonObject);
+ return;
+ }
+
+ JsonObject clone = new JsonObject();
+
+ if (jsonObject.has(typeFieldName)) {
+ throw new JsonParseException("cannot serialize " + srcType.getName()
+ + " because it already defines a field named " + typeFieldName);
+ }
+ clone.add(typeFieldName, new JsonPrimitive(label));
+
+ for (Map.Entry e : jsonObject.entrySet()) {
+ clone.add(e.getKey(), e.getValue());
+ }
+ jsonElementAdapter.write(out, clone);
+ }
+ }.nullSafe();
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/debug/DebugScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/debug/DebugScreen.kt
new file mode 100644
index 000000000..7345b5551
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/debug/DebugScreen.kt
@@ -0,0 +1,108 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.debug
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import nl.tudelft.trustchain.musicdao.core.torrent.status.TorrentStatus
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyState
+
+@ExperimentalMaterialApi
+@Composable
+@SuppressLint("NewApi")
+fun Debug(debugScreenViewModel: DebugScreenViewModel) {
+
+ val torrentHandleStatus by debugScreenViewModel.status.collectAsState(listOf())
+ val sessionStatus by debugScreenViewModel.sessionStatus.collectAsState()
+
+ Column {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text("Interface: ${sessionStatus?.interfaces}")
+ Text("DHT Peers: ${sessionStatus?.dhtNodes}")
+ Text("Upload-rate: ${sessionStatus?.uploadRate}")
+ Text("Download-rate: ${sessionStatus?.downloadRate}")
+ }
+ Divider()
+ LazyColumn {
+ items(torrentHandleStatus) {
+ TorrentStatusListItem(it)
+ Divider()
+ }
+ }
+ }
+ if (torrentHandleStatus.isEmpty()) {
+ EmptyState(
+ firstLine = "No torrents active",
+ secondLine = "Currently there are no torrents seeding or downloading",
+ )
+ }
+ Column(modifier = Modifier.height(height = 200.dp)) {}
+}
+
+@ExperimentalMaterialApi
+@Composable
+fun TorrentStatusListItem(torrentStatus: TorrentStatus) {
+ ListItem(
+ text = { Text(torrentStatus.infoHash) },
+ secondaryText = {
+ Column {
+ Text(torrentStatus.magnet)
+ Column(modifier = Modifier.padding(vertical = 10.dp)) {
+ if (torrentStatus.seeding == "true") {
+ LinearProgressIndicator(1.0f)
+ } else {
+ LinearProgressIndicator()
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(imageVector = Icons.Default.Person, contentDescription = null)
+ Text(torrentStatus.peers)
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(imageVector = Icons.Default.KeyboardArrowUp, contentDescription = null)
+ Text(torrentStatus.uploadedBytes)
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = null
+ )
+ Text(torrentStatus.downloadedBytes)
+ }
+ }
+ },
+ overlineText = {
+ if (torrentStatus.seeding == "true") {
+ Text("Seeding")
+ } else {
+ Text("Downloading")
+ }
+ },
+ icon = {
+ // TODO: turn this into boolean
+ if (torrentStatus.seeding == "true") {
+ Icon(imageVector = Icons.Default.KeyboardArrowUp, contentDescription = null)
+ } else {
+ Icon(imageVector = Icons.Default.KeyboardArrowDown, contentDescription = null)
+ }
+ },
+ modifier = Modifier
+ .clickable {}
+ .padding(bottom = 20.dp)
+ )
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/debug/DebugScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/debug/DebugScreenViewModel.kt
new file mode 100644
index 000000000..b933959fc
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/debug/DebugScreenViewModel.kt
@@ -0,0 +1,44 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.debug
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import nl.tudelft.trustchain.musicdao.core.torrent.TorrentEngine
+import nl.tudelft.trustchain.musicdao.core.torrent.status.SessionManagerStatus
+import nl.tudelft.trustchain.musicdao.core.torrent.status.TorrentStatus
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.O)
+@HiltViewModel
+class DebugScreenViewModel @Inject constructor(
+ private val torrentEngine: TorrentEngine,
+) : ViewModel() {
+
+ private val _status: MutableStateFlow> = MutableStateFlow(listOf())
+ val status: StateFlow> = _status
+
+ private val _sessionStatus: MutableStateFlow = MutableStateFlow(null)
+ val sessionStatus: StateFlow = _sessionStatus
+
+ init {
+ viewModelScope.launch {
+ while (isActive) {
+ _status.value = torrentEngine.getAllTorrentStatus()
+ delay(5000L)
+ }
+ }
+
+ viewModelScope.launch {
+ while (isActive) {
+ _sessionStatus.value = torrentEngine.getSessionManagerStatus()
+ delay(2000)
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/donate/DonateScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/donate/DonateScreen.kt
new file mode 100644
index 000000000..5aee39452
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/donate/DonateScreen.kt
@@ -0,0 +1,106 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.donate
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Button
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.ui.SnackbarHandler
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyState
+import nl.tudelft.trustchain.musicdao.ui.screens.profile_menu.CustomMenuItem
+import nl.tudelft.trustchain.musicdao.ui.screens.wallet.BitcoinWalletViewModel
+import kotlinx.coroutines.launch
+
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun DonateScreen(bitcoinWalletViewModel: BitcoinWalletViewModel, publicKey: String, navController: NavController) {
+ val donateScreenViewModel: DonateScreenViewModel = hiltViewModel()
+ val artist = donateScreenViewModel.artist.collectAsState()
+ val amount = rememberSaveable { mutableStateOf("0.1") }
+ val coroutine = rememberCoroutineScope()
+
+ LaunchedEffect(publicKey) {
+ coroutine.launch {
+ donateScreenViewModel.setArtist(publicKey)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun send() {
+ // Check if enough balance available
+ val confirmedBalance = bitcoinWalletViewModel.confirmedBalance.value
+ if (confirmedBalance == null || confirmedBalance.isZero || confirmedBalance.isNegative) {
+ SnackbarHandler.displaySnackbar("You don't have enough funds to make a donation")
+ return
+ }
+
+ coroutine.launch {
+ val result = bitcoinWalletViewModel.donate(publicKey, amount.value)
+ if (result) {
+ SnackbarHandler.displaySnackbar("Donation sent")
+ navController.popBackStack()
+ } else {
+ SnackbarHandler.displaySnackbar("Donation failed")
+ }
+ }
+ }
+
+ if (artist.value == null) {
+ EmptyState(firstLine = "404", secondLine = "This artist has not published a key you can donate to.")
+ return
+ }
+
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Your balance is ${bitcoinWalletViewModel.confirmedBalance.value?.toFriendlyString() ?: "0.00 BTC"}",
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(bottom = 10.dp)
+ )
+ Text(
+ text = "Amount",
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ OutlinedTextField(value = amount.value, onValueChange = { amount.value = it }, modifier = Modifier.padding(bottom = 10.dp))
+ Row {
+ Button(
+ onClick = {
+ amount.value = "0.001"
+ },
+ modifier = Modifier.padding(end = 10.dp)
+ ) {
+ Text("0.001")
+ }
+ Button(
+ onClick = {
+ amount.value = "0.01"
+ },
+ modifier = Modifier.padding(end = 10.dp)
+ ) {
+ Text("0.01")
+ }
+ Button(
+ onClick = {
+ amount.value = "0.1"
+ },
+ modifier = Modifier.padding(end = 10.dp)
+ ) {
+ Text("0.1")
+ }
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+ CustomMenuItem(text = "Confirm Send", onClick = { send() })
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/donate/DonateScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/donate/DonateScreenViewModel.kt
new file mode 100644
index 000000000..34bf6b667
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/donate/DonateScreenViewModel.kt
@@ -0,0 +1,23 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.donate
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.ViewModel
+import nl.tudelft.trustchain.musicdao.core.repositories.ArtistRepository
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Artist
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.O)
+@HiltViewModel
+class DonateScreenViewModel @Inject constructor(val artistRepository: ArtistRepository) : ViewModel() {
+
+ var artist: StateFlow = MutableStateFlow(null)
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ suspend fun setArtist(publicKey: String) {
+ artist = artistRepository.getArtistStateFlow(publicKey)
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/home/HomeScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/home/HomeScreen.kt
new file mode 100644
index 000000000..a1ab001ce
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/home/HomeScreen.kt
@@ -0,0 +1,74 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.home
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyState
+import nl.tudelft.trustchain.musicdao.ui.components.releases.ReleaseList
+import nl.tudelft.trustchain.musicdao.ui.screens.search.SearchScreenViewModel
+import com.google.accompanist.swiperefresh.SwipeRefresh
+import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
+
+@RequiresApi(Build.VERSION_CODES.O)
+@ExperimentalMaterialApi
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun HomeScreen(
+ navController: NavHostController,
+ screenViewModel: SearchScreenViewModel
+) {
+ val isRefreshing by screenViewModel.isRefreshing.observeAsState(false)
+ val releases by screenViewModel.searchResult.collectAsState(listOf())
+ val searchQuery by screenViewModel.searchQuery.collectAsState()
+ val refreshState = rememberSwipeRefreshState(isRefreshing)
+ val peerAmount by screenViewModel.peerAmount.observeAsState(0)
+ val totalReleaseAmount by screenViewModel.totalReleaseAmount.observeAsState(0)
+
+ SwipeRefresh(
+ state = refreshState,
+ onRefresh = { screenViewModel.refresh() }
+ ) {
+ Column {
+ TextField(
+ value = searchQuery,
+ onValueChange = {
+ screenViewModel.searchDebounced(it)
+ },
+ placeholder = { Text("Search") },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = null
+ )
+ },
+ maxLines = 1,
+ modifier = Modifier.fillMaxWidth()
+ )
+ Divider()
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(text = "Discovered $totalReleaseAmount releases")
+ Text(text = "Discovered $peerAmount peers")
+ }
+ ReleaseList(releasesState = releases, navController = navController)
+ if (releases.isEmpty()) {
+ EmptyState(
+ firstLine = "No releases found",
+ secondLine = "Make a release yourself or wait for releases to come in"
+ )
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/home/HomeScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/home/HomeScreenViewModel.kt
new file mode 100644
index 000000000..33f63309b
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/home/HomeScreenViewModel.kt
@@ -0,0 +1,34 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.home
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import nl.tudelft.trustchain.musicdao.core.ipv8.MusicCommunity
+import nl.tudelft.trustchain.musicdao.core.repositories.AlbumRepository
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Album
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.O)
+@HiltViewModel
+class HomeScreenViewModel @Inject constructor(
+ private val albumRepository: AlbumRepository,
+ private val musicCommunity: MusicCommunity
+) : ViewModel() {
+
+ private val _releases: MutableLiveData> = MutableLiveData()
+ var releases: LiveData> = _releases
+
+ private val _peerAmount: MutableLiveData = MutableLiveData()
+
+ init {
+ viewModelScope.launch {
+ releases = albumRepository.getAlbumsFlow()
+ _peerAmount.value = musicCommunity.getPeers().size
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/EditProfileScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/EditProfileScreen.kt
new file mode 100644
index 000000000..b59d043ee
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/EditProfileScreen.kt
@@ -0,0 +1,105 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.profile
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.OutlinedButton
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.ui.SnackbarHandler
+import kotlinx.coroutines.launch
+
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun EditProfileScreen(navController: NavController) {
+ val ownProfileViewScreenModel: MyProfileScreenViewModel = hiltViewModel()
+ val profile = ownProfileViewScreenModel.profile.collectAsState()
+
+ val name = remember { mutableStateOf(profile.value?.name) }
+ val bitcoinPublicKey = remember { mutableStateOf(profile.value?.bitcoinAddress) }
+ val biography = remember { mutableStateOf(profile.value?.biography) }
+ val socials = remember { mutableStateOf(profile.value?.socials) }
+ val coroutine = rememberCoroutineScope()
+
+ fun save() {
+ coroutine.launch {
+ val result = ownProfileViewScreenModel.publishEdit(
+ name = name.value ?: "",
+ bitcoinAddress = bitcoinPublicKey.value ?: "",
+ socials = socials.value ?: "",
+ biography = biography.value ?: ""
+ )
+ if (result) {
+ navController.popBackStack()
+ SnackbarHandler.displaySnackbar(text = "Successfully published your profile")
+ } else {
+ SnackbarHandler.displaySnackbar(text = "Could not publish, please fill in all fields")
+ }
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .padding(20.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Name", fontWeight = FontWeight.Bold)
+ TextField(
+ value = name.value ?: "",
+ onValueChange = { name.value = it }
+ )
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Public Key", fontWeight = FontWeight.Bold)
+ TextField(
+ value = ownProfileViewScreenModel.publicKey(),
+ enabled = false,
+ onValueChange = {}
+ )
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Bitcoin Public Key", fontWeight = FontWeight.Bold)
+ TextField(
+ value = bitcoinPublicKey.value ?: "",
+ onValueChange = { bitcoinPublicKey.value = it },
+ enabled = false
+ )
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Socials", fontWeight = FontWeight.Bold)
+ TextField(
+ value = socials.value ?: "",
+ onValueChange = { socials.value = it }
+ )
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Biography", fontWeight = FontWeight.Bold)
+ TextField(
+ value = biography.value ?: "",
+ onValueChange = { biography.value = it }
+ )
+ }
+
+ OutlinedButton(
+ onClick = {
+ save()
+ }
+ ) {
+ Text("Save")
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/MyProfileScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/MyProfileScreen.kt
new file mode 100644
index 000000000..5d5955afd
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/MyProfileScreen.kt
@@ -0,0 +1,32 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.profile
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyState
+
+@RequiresApi(Build.VERSION_CODES.O)
+@ExperimentalMaterialApi
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun MyProfileScreen(navController: NavController, profileScreenViewModel: MyProfileScreenViewModel) {
+
+ val profile = profileScreenViewModel.profile.collectAsState()
+
+ profile.value?.let {
+ Profile(it, navController = navController)
+ } ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ EmptyState(
+ firstLine = "You have not made a profile yet.",
+ secondLine = "Please make one first."
+ )
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/MyProfileScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/MyProfileScreenViewModel.kt
new file mode 100644
index 000000000..1d15c2e32
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/MyProfileScreenViewModel.kt
@@ -0,0 +1,44 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.profile
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import nl.tudelft.trustchain.musicdao.core.ipv8.MusicCommunity
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Artist
+import nl.tudelft.trustchain.musicdao.core.repositories.ArtistRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.O)
+@HiltViewModel
+class MyProfileScreenViewModel @Inject constructor(
+ private val artistRepository: ArtistRepository,
+ private val musicCommunity: MusicCommunity,
+) : ViewModel() {
+
+ private val _profile: MutableStateFlow = MutableStateFlow(null)
+ var profile: StateFlow = _profile
+
+ fun publicKey(): String {
+ return musicCommunity.publicKeyHex()
+ }
+
+ suspend fun publishEdit(
+ name: String,
+ bitcoinAddress: String,
+ socials: String,
+ biography: String
+ ): Boolean {
+ return artistRepository.edit(name, bitcoinAddress, socials, biography)
+ }
+
+ init {
+ viewModelScope.launch {
+ profile = artistRepository.getArtistStateFlow(publicKey())
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/Profile.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/Profile.kt
new file mode 100644
index 000000000..d89b0090d
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/Profile.kt
@@ -0,0 +1,89 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.profile
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.OutlinedButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Album
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Artist
+import nl.tudelft.trustchain.musicdao.ui.components.releases.NonLazyReleaseList
+import nl.tudelft.trustchain.musicdao.ui.navigation.Screen
+
+@ExperimentalFoundationApi
+@ExperimentalMaterialApi
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun Profile(artist: Artist, releases: List = listOf(), navController: NavController) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .background(Brush.verticalGradient(listOf(Color(0xFF77DF7C), Color(0xFF70C774))))
+ ) {
+ Text(
+ text = artist.name,
+ style = MaterialTheme.typography.h6,
+ modifier = Modifier
+ .padding(20.dp)
+ .align(
+ Alignment.BottomStart
+ )
+ )
+ }
+
+ Column(modifier = Modifier.padding(20.dp)) {
+ Row(modifier = Modifier.padding(bottom = 20.dp)) {
+ OutlinedButton(onClick = { }, modifier = Modifier.padding(end = 10.dp)) {
+ Text(text = "Follow")
+ }
+ OutlinedButton(onClick = { navController.navigate(Screen.Donate.createRoute(publicKey = artist.publicKey)) }) {
+ Text(text = "Donate")
+ }
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Releases", fontWeight = FontWeight.Bold)
+ if (releases.isEmpty()) {
+ Text("No releases by this artist")
+ } else {
+ NonLazyReleaseList(releasesState = releases, navController = navController)
+ }
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Public Key", fontWeight = FontWeight.Bold)
+ Text(text = artist.publicKey)
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Bitcoin Address", fontWeight = FontWeight.Bold)
+ Text(text = artist.bitcoinAddress)
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Biography", fontWeight = FontWeight.Bold)
+ Text(text = artist.biography)
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/ProfileScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/ProfileScreen.kt
new file mode 100644
index 000000000..2b832341a
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/ProfileScreen.kt
@@ -0,0 +1,45 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.profile
+
+import android.app.Activity
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.MusicActivity
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyState
+import dagger.hilt.android.EntryPointAccessors
+
+@ExperimentalMaterialApi
+@ExperimentalFoundationApi
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun ProfileScreen(publicKey: String, navController: NavController) {
+
+ val viewModelFactory = EntryPointAccessors.fromActivity(
+ LocalContext.current as Activity,
+ MusicActivity.ViewModelFactoryProvider::class.java
+ ).profileScreenViewModelFactory()
+
+ val viewModel: ProfileScreenViewModel = viewModel(
+ factory = ProfileScreenViewModel.provideFactory(viewModelFactory, publicKey = publicKey)
+ )
+
+ val profile = viewModel.profile.collectAsState()
+ val releases = viewModel.releases.collectAsState()
+
+ profile.value?.let {
+ Profile(artist = it, releases = releases.value, navController = navController)
+ } ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ EmptyState(firstLine = "404", secondLine = "This artist has not published any information yet.")
+ return
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/ProfileScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/ProfileScreenViewModel.kt
new file mode 100644
index 000000000..eb3201d7e
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile/ProfileScreenViewModel.kt
@@ -0,0 +1,52 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.profile
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Album
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Artist
+import nl.tudelft.trustchain.musicdao.core.repositories.ArtistRepository
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+@RequiresApi(Build.VERSION_CODES.O)
+class ProfileScreenViewModel @AssistedInject constructor(
+ @Assisted private val publicKey: String,
+ private val artistRepository: ArtistRepository
+) : ViewModel() {
+
+ private val _profile: MutableStateFlow = MutableStateFlow(null)
+ var profile: StateFlow = _profile
+
+ private val _releases: MutableStateFlow> = MutableStateFlow(listOf())
+ val releases: StateFlow> = _releases
+
+ init {
+ viewModelScope.launch {
+ profile = artistRepository.getArtistStateFlow(publicKey = publicKey)
+ _releases.value = artistRepository.getArtistReleases(publicKey = publicKey)
+ }
+ }
+
+ @AssistedFactory
+ interface ProfileScreenViewModelFactory {
+ fun create(publicKey: String): ProfileScreenViewModel
+ }
+
+ companion object {
+ fun provideFactory(
+ assistedFactory: ProfileScreenViewModelFactory,
+ publicKey: String
+ ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return assistedFactory.create(publicKey) as T
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile_menu/ProfileMenuScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile_menu/ProfileMenuScreen.kt
new file mode 100644
index 000000000..ffe91c1b7
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/profile_menu/ProfileMenuScreen.kt
@@ -0,0 +1,99 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.profile_menu
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.ui.navigation.Screen
+
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun ProfileMenuScreen(navController: NavController) {
+
+ Column {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .background(Brush.verticalGradient(listOf(Color(0xFF77DF7C), Color(0xFF70C774))))
+ ) {
+ Text(
+ text = "Your Profile",
+ style = MaterialTheme.typography.h6,
+ modifier = Modifier
+ .padding(20.dp)
+ .align(
+ Alignment.BottomStart
+ )
+ )
+ }
+
+ Column(modifier = Modifier.padding(20.dp)) {
+ CustomMenuItem(
+ text = "View Public Profile",
+ onClick = {
+ navController.navigate(Screen.MyProfile.route)
+ }
+ )
+ CustomMenuItem(
+ text = "Edit Profile",
+ onClick = {
+ navController.navigate(Screen.EditProfile.route)
+ }
+ )
+ CustomMenuItem(
+ text = "Create a new Release",
+ onClick = {
+ navController.navigate(Screen.CreateRelease.route)
+ }
+ )
+ CustomMenuItem(
+ text = "Wallet",
+ onClick = {
+ navController.navigate(Screen.BitcoinWallet.route)
+ }
+ )
+ }
+ }
+}
+
+@Composable
+fun CustomMenuItem(text: String, onClick: () -> Unit, enabled: Boolean = true, disabled: Boolean = false) {
+
+ val modifier = if (enabled && !disabled) {
+ Modifier.clickable(
+ onClick = { onClick() },
+ )
+ } else {
+ Modifier.graphicsLayer(alpha = 0.4f)
+ }
+
+ Row(
+ modifier = modifier
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(vertical = 15.dp)
+ ) {
+ Text(text = text, fontWeight = FontWeight.SemiBold, fontSize = 18.sp)
+ Spacer(Modifier.weight(1f))
+ Icon(imageVector = Icons.Default.KeyboardArrowRight, contentDescription = null)
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/ReleaseScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/ReleaseScreen.kt
new file mode 100644
index 000000000..fbb3c3bd4
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/ReleaseScreen.kt
@@ -0,0 +1,337 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.release
+
+import android.app.Activity
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.ShoppingCart
+import androidx.compose.material.icons.outlined.Favorite
+import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.material.icons.outlined.Person
+import androidx.compose.material.icons.outlined.PlayArrow
+import androidx.compose.runtime.*
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.MusicActivity
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Album
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Song
+import nl.tudelft.trustchain.musicdao.core.torrent.status.DownloadingTrack
+import nl.tudelft.trustchain.musicdao.ui.components.ReleaseCover
+import nl.tudelft.trustchain.musicdao.ui.components.player.PlayerViewModel
+import nl.tudelft.trustchain.musicdao.ui.util.dateToShortString
+import nl.tudelft.trustchain.musicdao.ui.navigation.Screen
+import nl.tudelft.trustchain.musicdao.ui.screens.torrent.TorrentStatusScreen
+import dagger.hilt.android.EntryPointAccessors
+import java.io.File
+
+@RequiresApi(Build.VERSION_CODES.O)
+@ExperimentalMaterialApi
+@Composable
+fun ReleaseScreen(
+ releaseId: String,
+ playerViewModel: PlayerViewModel,
+ navController: NavController
+) {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf("RELEASE", "TORRENT")
+
+ val viewModelFactory = EntryPointAccessors.fromActivity(
+ LocalContext.current as Activity,
+ MusicActivity.ViewModelFactoryProvider::class.java
+ ).noteDetailViewModelFactory()
+
+ val viewModel: ReleaseScreenViewModel = viewModel(
+ factory = ReleaseScreenViewModel.provideFactory(viewModelFactory, releaseId = releaseId)
+ )
+
+ val torrentStatus by viewModel.torrentState.collectAsState()
+ val albumState by viewModel.saturatedReleaseState.observeAsState()
+
+ val playingTrack = playerViewModel.playingTrack.collectAsState()
+
+ // Audio Player
+ val context = LocalContext.current
+
+ fun play(track: Song, cover: File?) {
+ playerViewModel.playDownloadedTrack(track, context, cover)
+ }
+
+ fun play(track: DownloadingTrack, cover: File?) {
+ playerViewModel.playDownloadingTrack(
+ Song(
+ file = track.file,
+ artist = track.artist,
+ title = track.title
+ ),
+ context,
+ cover
+ )
+ }
+
+ val scrollState = rememberScrollState()
+
+ albumState?.let { album ->
+ LaunchedEffect(
+ key1 = playerViewModel,
+ block = {
+ viewModel.torrentState.collect {
+ val current = playerViewModel.playingTrack.value ?: return@collect
+ val downloadingTracks =
+ viewModel.torrentState.value?.downloadingTracks ?: return@collect
+ val isPlaying = playerViewModel.exoPlayer.isPlaying
+ val targetTrack =
+ downloadingTracks.find { it.file.name == current.file?.name }
+ ?: return@collect
+
+ if (!isPlaying && targetTrack.progress > 20 && targetTrack.progress < 99) {
+ play(targetTrack, album.cover)
+ }
+ }
+ }
+ )
+
+ Column(
+ modifier = Modifier
+ .verticalScroll(scrollState)
+ .padding(bottom = 150.dp)
+ ) {
+ TabRow(selectedTabIndex = state) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ onClick = { state = index },
+ selected = (index == state),
+ text = { Text(title) }
+ )
+ }
+ }
+ if (state == 0) {
+ Column(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(top = 20.dp)
+ ) {
+ ReleaseCover(
+ file = album.cover,
+ modifier = Modifier
+ .height(200.dp)
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(10))
+ .background(Color.DarkGray)
+ .shadow(10.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+ }
+ Header(album, navController = navController)
+ if (album.songs != null && album.songs.isNotEmpty()) {
+ val files = album.songs
+ files.map {
+ val isPlayingModifier = playingTrack.value?.let { current ->
+ if (it.title == current.title) {
+ MaterialTheme.colors.primary
+ } else {
+ MaterialTheme.colors.onBackground
+ }
+ } ?: MaterialTheme.colors.onBackground
+
+ ListItem(
+ text = { Text(it.title, color = isPlayingModifier, maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ secondaryText = { Text(it.artist, color = isPlayingModifier) },
+ trailing = {
+ Icon(
+ imageVector = Icons.Default.MoreVert,
+ contentDescription = null
+ )
+ },
+ modifier = Modifier.clickable { play(it, album.cover) }
+ )
+ }
+ } else {
+ if (torrentStatus != null) {
+ val downloadingTracks = torrentStatus?.downloadingTracks
+ downloadingTracks?.map {
+ ListItem(
+ text = { Text(it.title) },
+ secondaryText = {
+ Column {
+ Text(album.artist, modifier = Modifier.padding(bottom = 5.dp))
+ LinearProgressIndicator(progress = it.progress.toFloat() / 100)
+ }
+ },
+ trailing = {
+ Icon(
+ imageVector = Icons.Default.MoreVert,
+ contentDescription = null
+ )
+ },
+ modifier = Modifier.clickable {
+ play(it, album.cover)
+ }
+ )
+ }
+ if (downloadingTracks == null || downloadingTracks.isEmpty()) {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+ }
+ }
+ }
+ if (state == 1) {
+ val current = torrentStatus
+ if (current != null) {
+ TorrentStatusScreen(current)
+ } else {
+ Text("Could not find torrent.")
+ }
+ }
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun Header(album: Album, navController: NavController) {
+ Column(modifier = Modifier.padding(top = 10.dp, start = 20.dp, end = 20.dp)) {
+ Text(
+ album.title,
+ style = MaterialTheme.typography.h6.merge(SpanStyle(fontWeight = FontWeight.ExtraBold)),
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ Text(
+ album.artist,
+ style = MaterialTheme.typography.body2.merge(SpanStyle(fontWeight = FontWeight.SemiBold)),
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ Text(
+ "UUID",
+ style = MaterialTheme.typography.body2.merge(SpanStyle(fontWeight = FontWeight.SemiBold)),
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ Text(
+ album.id,
+ style = MaterialTheme.typography.body2.merge(SpanStyle(fontWeight = FontWeight.SemiBold)),
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ Text(
+ "Artist Public Key",
+ style = MaterialTheme.typography.body2.merge(SpanStyle(fontWeight = FontWeight.SemiBold)),
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ Text(
+ album.publisher,
+ style = MaterialTheme.typography.body2.merge(SpanStyle(fontWeight = FontWeight.SemiBold)),
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+
+ Text(
+ "Album - ${dateToShortString(album.releaseDate.toString())}",
+ style = MaterialTheme.typography.body2.merge(
+ SpanStyle(fontWeight = FontWeight.SemiBold, color = Color.Gray)
+ ),
+ modifier = Modifier.padding(bottom = 10.dp)
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row {
+ IconButton(onClick = { /*TODO*/ }) {
+ Icon(
+ imageVector = Icons.Outlined.Favorite,
+ contentDescription = null
+ )
+ }
+ IconButton(
+ onClick = {
+ navController.navigate(
+ Screen.Profile.createRoute(publicKey = album.publisher)
+ )
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Person,
+ contentDescription = null
+ )
+ }
+ IconButton(
+ onClick = {
+ navController.navigate(
+ Screen.Donate.createRoute(publicKey = album.publisher)
+ )
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.ShoppingCart,
+ contentDescription = null
+ )
+ }
+
+ var expanded by remember { mutableStateOf(false) }
+ Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+ IconButton(onClick = { expanded = true }) {
+ Icon(
+ imageVector = Icons.Outlined.MoreVert,
+ contentDescription = null
+ )
+ }
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ DropdownMenuItem(
+ onClick = {
+ navController.navigate(
+ Screen.Profile.createRoute(publicKey = album.publisher)
+ )
+ }
+ ) {
+ Text("View Artist")
+ }
+ DropdownMenuItem(
+ onClick = {
+ navController.navigate(
+ Screen.Donate.createRoute(publicKey = album.publisher)
+ )
+ }
+ ) {
+ Text("Donate")
+ }
+ DropdownMenuItem(onClick = { }) {
+ Text("View Meta-data")
+ }
+ }
+ }
+ }
+ IconButton(onClick = { /*TODO*/ }) {
+ Icon(
+ imageVector = Icons.Outlined.PlayArrow,
+ contentDescription = null
+ )
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/ReleaseScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/ReleaseScreenViewModel.kt
new file mode 100644
index 000000000..dfa02c272
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/ReleaseScreenViewModel.kt
@@ -0,0 +1,72 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.release
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.*
+import nl.tudelft.trustchain.musicdao.core.cache.CacheDatabase
+import nl.tudelft.trustchain.musicdao.core.cache.entities.AlbumEntity
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Album
+import nl.tudelft.trustchain.musicdao.core.torrent.TorrentEngine
+import nl.tudelft.trustchain.musicdao.core.torrent.status.TorrentStatus
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+@OptIn(DelicateCoroutinesApi::class)
+@RequiresApi(Build.VERSION_CODES.O)
+class ReleaseScreenViewModel @AssistedInject constructor(
+ @Assisted private val releaseId: String,
+ private val database: CacheDatabase,
+ private val torrentEngine: TorrentEngine,
+) : ViewModel() {
+
+ @AssistedFactory
+ interface ReleaseScreenViewModelFactory {
+ fun create(releaseId: String): ReleaseScreenViewModel
+ }
+
+ companion object {
+ fun provideFactory(
+ assistedFactory: ReleaseScreenViewModelFactory,
+ releaseId: String
+ ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return assistedFactory.create(releaseId) as T
+ }
+ }
+ }
+
+ private var releaseLiveData: LiveData = MutableLiveData(null)
+ var saturatedReleaseState: LiveData = MutableLiveData()
+
+ private val _torrentState: MutableStateFlow = MutableStateFlow(null)
+ val torrentState: StateFlow = _torrentState
+
+ init {
+ viewModelScope.launch {
+ releaseLiveData = database.dao.getLiveData(releaseId)
+ saturatedReleaseState = releaseLiveData.map { it.toAlbum() }
+
+ val release = database.dao.get(releaseId)
+
+ release.let { _release ->
+ if (!_release.isDownloaded) {
+ torrentEngine.download(_release.magnet)
+ }
+
+ while (isActive) {
+ if (_release.infoHash != null) {
+ _torrentState.value = torrentEngine.getTorrentStatus(_release.infoHash)
+ }
+ delay(1000L)
+ }
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/create/CreateReleaseDialog.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/create/CreateReleaseDialog.kt
new file mode 100644
index 000000000..db76f42ac
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/create/CreateReleaseDialog.kt
@@ -0,0 +1,191 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.release.create
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.core.app.ActivityCompat.startActivityForResult
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.AppContainer
+import nl.tudelft.trustchain.musicdao.ui.SnackbarHandler
+import nl.tudelft.trustchain.musicdao.ui.util.dateToLongString
+import com.google.android.material.datepicker.MaterialDatePicker
+import kotlinx.coroutines.launch
+import java.time.Instant
+
+@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
+@RequiresApi(Build.VERSION_CODES.O)
+@ExperimentalComposeUiApi
+@Composable
+fun CreateReleaseDialog(navController: NavController) {
+ val viewModel: CreateReleaseDialogViewModel = hiltViewModel()
+
+ val fileList: MutableState> = remember { mutableStateOf(listOf()) }
+ val title = rememberSaveable { mutableStateOf("") }
+ val artist = rememberSaveable { mutableStateOf("") }
+ val date = rememberSaveable { mutableStateOf("") }
+
+ fun openFilePickerDialog() {
+ AppContainer.currentCallback = {
+ fileList.value = it
+ }
+ val selectFilesIntent = Intent(Intent.ACTION_GET_CONTENT)
+ selectFilesIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+ selectFilesIntent.type = "audio/*"
+ selectFilesIntent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "audio/*"))
+ val chooseFileActivity = Intent.createChooser(selectFilesIntent, "Choose a file")
+ startActivityForResult(AppContainer.activity, chooseFileActivity, 21, Bundle())
+ }
+
+ val context = LocalContext.current
+ fun showDatePicker() {
+ val picker = MaterialDatePicker.Builder.datePicker().build()
+ (context as AppCompatActivity).let {
+ picker.show(it.supportFragmentManager, picker.toString())
+ picker.addOnPositiveButtonClickListener {
+ date.value = Instant.ofEpochMilli(it).toString()
+ }
+ }
+ }
+
+ val scope = rememberCoroutineScope()
+ val localContext = LocalContext.current
+ fun publishRelease() {
+ scope.launch {
+ val result = viewModel.createRelease(
+ artist.value,
+ title.value,
+ releaseDate = Instant.now().toString(),
+ uris = fileList.value,
+ localContext
+ )
+ if (result) {
+ SnackbarHandler.displaySnackbar(text = "Successfully published your release.")
+ navController.popBackStack()
+ } else {
+ SnackbarHandler.displaySnackbar(text = "Could not publish your release.")
+ }
+ }
+ }
+
+ Column {
+ Surface(
+ modifier = Modifier
+ .requiredWidth(LocalConfiguration.current.screenWidthDp.dp * 1f)
+ .fillMaxSize()
+ .padding(4.dp)
+ ) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Create Release") },
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(Icons.Filled.Close, contentDescription = null)
+ }
+ }
+ )
+ },
+ content = {
+ Column(
+ modifier = Modifier
+ .padding(20.dp)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(
+ text = "Fill in the form below to create a new release",
+ style = MaterialTheme.typography.body1,
+ modifier = Modifier.padding(bottom = 10.dp)
+ )
+
+ TextField(
+ value = title.value,
+ onValueChange = { title.value = it },
+ placeholder = { Text("The title of your release") },
+ label = { Text("Title") },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ TextField(
+ value = artist.value,
+ onValueChange = { artist.value = it },
+ placeholder = { Text("Your artist name") },
+ label = { Text("Artist") },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Row {
+ IconButton(onClick = { openFilePickerDialog() }) {
+ Icon(
+ Icons.Default.Edit,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ }
+ TextField(
+ value = if (fileList.value.isEmpty()) "" else fileList.value.toString(),
+ onValueChange = {},
+ label = { Text("Files") },
+ enabled = false
+ )
+ }
+
+ Row {
+ IconButton(onClick = { showDatePicker() }) {
+ Icon(
+ Icons.Default.DateRange,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ }
+ TextField(
+ value = dateToLongString(date.value),
+ onValueChange = {},
+ enabled = false,
+ label = { Text("Release Date") }
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(25.dp),
+ modifier = Modifier.padding(vertical = 10.dp)
+ ) {
+ Checkbox(checked = true, enabled = false, onCheckedChange = {})
+ Text("Start seeding", color = Color.Gray)
+ }
+
+ OutlinedButton(
+ modifier = Modifier.align(Alignment.End),
+ onClick = { publishRelease() }
+ ) {
+ Text("Create Release")
+ }
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/create/CreateReleaseDialogViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/create/CreateReleaseDialogViewModel.kt
new file mode 100644
index 000000000..4fa7f961e
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/release/create/CreateReleaseDialogViewModel.kt
@@ -0,0 +1,26 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.release.create
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.ViewModel
+import nl.tudelft.trustchain.musicdao.core.repositories.album.CreateReleaseUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class CreateReleaseDialogViewModel @Inject constructor(private val createReleaseUseCase: CreateReleaseUseCase) :
+ ViewModel() {
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ suspend fun createRelease(
+ artist: String,
+ title: String,
+ releaseDate: String,
+ uris: List,
+ context: Context
+ ): Boolean {
+ return createReleaseUseCase.invoke(artist, title, releaseDate, uris, context)
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/search/SearchScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/search/SearchScreen.kt
new file mode 100644
index 000000000..a7987679a
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/search/SearchScreen.kt
@@ -0,0 +1,42 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.search
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavController
+import nl.tudelft.trustchain.musicdao.ui.components.releases.ReleaseList
+
+@ExperimentalFoundationApi
+@OptIn(ExperimentalMaterialApi::class)
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun SearchScreen(navController: NavController, screenViewModel: SearchScreenViewModel) {
+ val releases by screenViewModel.searchResult.collectAsState(listOf())
+ val searchQuery by screenViewModel.searchQuery.collectAsState()
+
+ Column {
+ TextField(
+ value = searchQuery,
+ onValueChange = {
+ screenViewModel.searchDebounced(it)
+ },
+ placeholder = { Text("Search") },
+ trailingIcon = { Icon(imageVector = Icons.Default.Search, contentDescription = null) },
+ maxLines = 1,
+ modifier = Modifier.fillMaxWidth()
+ )
+ ReleaseList(releasesState = releases, navController = navController)
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/search/SearchScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/search/SearchScreenViewModel.kt
new file mode 100644
index 000000000..31d9ea535
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/search/SearchScreenViewModel.kt
@@ -0,0 +1,97 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.search
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import nl.tudelft.trustchain.musicdao.core.ipv8.MusicCommunity
+import nl.tudelft.trustchain.musicdao.core.repositories.AlbumRepository
+import nl.tudelft.trustchain.musicdao.core.repositories.model.Album
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.O)
+@HiltViewModel
+class SearchScreenViewModel @Inject constructor(
+ private val albumRepository: AlbumRepository,
+ private val musicCommunity: MusicCommunity
+) : ViewModel() {
+
+ private val _isRefreshing: MutableLiveData = MutableLiveData()
+ val isRefreshing: LiveData = _isRefreshing
+
+ private val _searchQuery: MutableStateFlow = MutableStateFlow("")
+ val searchQuery: StateFlow = _searchQuery
+
+ private val _searchResult: MutableStateFlow> = MutableStateFlow(listOf())
+ val searchResult: StateFlow> = _searchResult
+
+ private val _peerAmount: MutableLiveData = MutableLiveData()
+ var peerAmount: LiveData = _peerAmount
+
+ private val _totalReleaseAmount: MutableLiveData = MutableLiveData()
+ var totalReleaseAmount: LiveData = _totalReleaseAmount
+
+ private var searchJob: Job? = null
+
+ init {
+ viewModelScope.launch {
+ _searchResult.value = downloadedFirstInListOfAlbums(albumRepository.getAlbums())
+ _peerAmount.value = musicCommunity.getPeers().size
+ _totalReleaseAmount.value = albumRepository.getAlbums().size
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun searchDebounced(searchText: String) {
+ _searchQuery.value = searchText
+
+ searchJob?.cancel()
+ searchJob = viewModelScope.launch {
+ delay(DEBOUNCE_DELAY)
+ search(searchText)
+ }
+ }
+
+ fun downloadedFirstInListOfAlbums(list: List): List {
+ // put downloaded albums first
+ val downloadedAlbums = list.sortedBy { album ->
+ album.songs != null && album.songs.isNotEmpty()
+ }.reversed()
+ return downloadedAlbums
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private suspend fun search(searchText: String) {
+ if (searchText.isEmpty()) {
+ _searchResult.value = downloadedFirstInListOfAlbums(albumRepository.getAlbums())
+ } else {
+ val result = albumRepository.searchAlbums(searchText)
+ _searchResult.value = downloadedFirstInListOfAlbums(result)
+ }
+ }
+
+ fun refresh() {
+ viewModelScope.launch {
+ _isRefreshing.value = true
+ delay(500)
+ if (_searchQuery.value.isEmpty()) {
+ _searchResult.value = downloadedFirstInListOfAlbums(albumRepository.getAlbums())
+ }
+ _peerAmount.value = musicCommunity.getPeers().size
+ _totalReleaseAmount.value = albumRepository.getAlbums().size
+ _isRefreshing.value = false
+ }
+ }
+
+ companion object {
+ private const val DEBOUNCE_DELAY = 200L
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/settings/SettingsScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/settings/SettingsScreen.kt
new file mode 100644
index 000000000..83c7dc87b
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/settings/SettingsScreen.kt
@@ -0,0 +1,54 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.settings
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ListItem
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.app.ActivityCompat.startActivityForResult
+import nl.tudelft.trustchain.musicdao.AppContainer
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+@RequiresApi(Build.VERSION_CODES.O)
+fun SettingsScreen(settingsScreenViewModel: SettingsScreenViewModel) {
+
+ val coroutine = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ suspend fun batchPublish(uri: Uri) {
+ settingsScreenViewModel.publishBatch(uri, context)
+ }
+
+ fun openFilePickerDialog() {
+ AppContainer.currentCallback = {
+ coroutine.launch {
+ batchPublish(it[0])
+ }
+ }
+ val selectFilesIntent = Intent(Intent.ACTION_GET_CONTENT)
+ selectFilesIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+ selectFilesIntent.type = "*/*"
+ val chooseFileActivity = Intent.createChooser(selectFilesIntent, "Choose a file")
+ startActivityForResult(AppContainer.activity, chooseFileActivity, 21, Bundle())
+ }
+
+ Column {
+ ListItem(
+ text = { Text(text = "Batch Publish") },
+ modifier = Modifier.clickable {
+ openFilePickerDialog()
+ }
+ )
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/settings/SettingsScreenViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/settings/SettingsScreenViewModel.kt
new file mode 100644
index 000000000..cc0e4a061
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/settings/SettingsScreenViewModel.kt
@@ -0,0 +1,30 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.settings
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.ViewModel
+import nl.tudelft.trustchain.musicdao.CachePath
+import nl.tudelft.trustchain.musicdao.core.repositories.album.BatchPublisher
+import nl.tudelft.trustchain.musicdao.ui.util.AndroidURIController
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.nio.file.Paths
+import javax.inject.Inject
+
+@HiltViewModel
+class SettingsScreenViewModel @Inject constructor(
+ private val batchPublisher: BatchPublisher,
+ private val cachePath: CachePath,
+ private val androidURIController: AndroidURIController
+) : ViewModel() {
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ suspend fun publishBatch(uri: Uri, context: Context) {
+ Log.d("MusicDao", "publishBatch: $uri")
+ val path = Paths.get("${cachePath.getPath()}/batch_publish/output.csv")
+ val output = androidURIController.copyIntoCache(uri, context, path) ?: return
+ batchPublisher.publish(output)
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/torrent/TorrentStatusScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/torrent/TorrentStatusScreen.kt
new file mode 100644
index 000000000..02decbc0a
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/torrent/TorrentStatusScreen.kt
@@ -0,0 +1,66 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.torrent
+
+import nl.tudelft.trustchain.musicdao.core.torrent.status.TorrentStatus
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ListItem
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun TorrentStatusScreen(torrent: TorrentStatus) {
+
+ Column {
+ ListItem(text = { Text("Info Hash") }, secondaryText = { Text(torrent.infoHash) })
+ ListItem(
+ text = { Text("Magnet Link") },
+ secondaryText = { SelectionContainer { Text(torrent.magnet) } },
+ modifier = Modifier.padding(bottom = 10.dp)
+ )
+ Divider()
+ ListItem(
+ text = { Text("Finished Downloading") },
+ secondaryText = { Text(torrent.finishedDownloading) }
+ )
+ ListItem(text = { Text("Pieces") }, secondaryText = { Text(torrent.pieces) })
+ ListItem(text = { Text("Files") }, secondaryText = { Text("${torrent.files}") })
+ Divider()
+ ListItem(
+ text = { Text("Seeding") },
+ secondaryText = { Text(torrent.seeding) }
+ )
+
+ Row {
+ ListItem(
+ text = { Text("Peers") },
+ secondaryText = { Text(torrent.peers) },
+ modifier = Modifier.weight(1f)
+ )
+ ListItem(
+ text = { Text("Seeders") },
+ secondaryText = { Text(torrent.seeders) },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row {
+ ListItem(
+ text = { Text("Uploaded Bytes") },
+ secondaryText = { Text(torrent.uploadedBytes) },
+ modifier = Modifier.weight(1f)
+ )
+ ListItem(
+ text = { Text("Downloaded Bytes") },
+ secondaryText = { Text(torrent.downloadedBytes) },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Divider()
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/wallet/BitcoinWalletScreen.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/wallet/BitcoinWalletScreen.kt
new file mode 100644
index 000000000..6dc1114e2
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/wallet/BitcoinWalletScreen.kt
@@ -0,0 +1,215 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.wallet
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.ArrowForward
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import nl.tudelft.trustchain.musicdao.core.wallet.UserWalletTransaction
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyState
+import nl.tudelft.trustchain.musicdao.ui.components.EmptyStateNotScrollable
+import nl.tudelft.trustchain.musicdao.ui.screens.profile_menu.CustomMenuItem
+import java.text.SimpleDateFormat
+import java.util.*
+
+@Composable
+fun BitcoinWalletScreen(bitcoinWalletViewModel: BitcoinWalletViewModel) {
+ val confirmedBalance = bitcoinWalletViewModel.confirmedBalance.collectAsState()
+ val estimatedBalance = bitcoinWalletViewModel.estimatedBalance.collectAsState()
+ val syncProgress = bitcoinWalletViewModel.syncProgress.collectAsState()
+ val status = bitcoinWalletViewModel.status.collectAsState()
+ val faucetInProgress = bitcoinWalletViewModel.faucetInProgress.collectAsState()
+ val walletTransactions = bitcoinWalletViewModel.walletTransactions.collectAsState()
+ val isStarted = bitcoinWalletViewModel.isStarted.collectAsState()
+
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf("ACTIONS", "TRANSACTIONS")
+
+ if (!isStarted.value) {
+ EmptyState(
+ firstLine = "Your wallet is not started yet.",
+ secondLine = "Please, wait for the wallet to be started.",
+ loadingIcon = true
+ )
+ return
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .background(MaterialTheme.colors.primary)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(20.dp)
+ .align(
+ Alignment.BottomStart
+ )
+ ) {
+ Text(
+ text = confirmedBalance.value?.toFriendlyString() ?: "0.00 BTC",
+ style = MaterialTheme.typography.h6
+ )
+ Text(
+ text = "${estimatedBalance.value ?: "0.00 BTC"} (Estimated)",
+ style = MaterialTheme.typography.subtitle1
+ )
+ }
+ Row(
+ modifier = Modifier
+ .padding(20.dp)
+ .align(
+ Alignment.TopEnd
+ ),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ "Sync Progress",
+ modifier = Modifier
+ .padding(end = 15.dp)
+ .align(Alignment.CenterVertically)
+ )
+ LinearProgressIndicator(
+ syncProgress.value?.let { (it.toFloat() / 100) }
+ ?: 0f,
+ color = MaterialTheme.colors.onPrimary,
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .fillMaxWidth()
+ )
+ }
+ }
+
+ TabRow(selectedTabIndex = state) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ onClick = { state = index },
+ selected = (index == state),
+ text = { Text(title) }
+ )
+ }
+ }
+
+ when (state) {
+ 0 -> {
+ Column(modifier = Modifier.padding(horizontal = 20.dp)) {
+ CustomMenuItem(
+ text = "Request from faucet",
+ onClick = {
+ bitcoinWalletViewModel.requestFaucet()
+ },
+ disabled = faucetInProgress.value
+ )
+ CustomMenuItem(
+ text = "Send",
+ onClick = { },
+ enabled = false
+ )
+ CustomMenuItem(
+ text = "Receive",
+ onClick = { },
+ enabled = false
+ )
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Public Key", fontWeight = FontWeight.Bold)
+ Text(text = bitcoinWalletViewModel.publicKey.value ?: "No Public Key")
+ }
+
+ Column(modifier = Modifier.padding(bottom = 20.dp)) {
+ Text(text = "Wallet Status", fontWeight = FontWeight.Bold)
+ Text(text = status.value ?: "No Status")
+ }
+ }
+ }
+ 1 -> {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (walletTransactions.value.isEmpty()) {
+ EmptyStateNotScrollable(
+ firstLine = "No Transactions",
+ secondLine = "No transactions have been made.",
+ modifier = Modifier
+ .align(Alignment.Center)
+ .padding(vertical = 50.dp)
+ )
+ } else {
+ Column(modifier = Modifier.fillMaxSize()) {
+ walletTransactions.value.map {
+ TransactionItem(
+ userWalletTransaction = it
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun TransactionItem(userWalletTransaction: UserWalletTransaction) {
+ ListItem(
+ icon = {
+ Icon(
+ imageVector = if (userWalletTransaction.value.isPositive) {
+ Icons.Outlined.ArrowForward
+ } else {
+ Icons.Outlined.ArrowBack
+ },
+ contentDescription = null
+ )
+ },
+ overlineText = {
+ Text(
+ text = dateToString(userWalletTransaction.date),
+ style = MaterialTheme.typography.caption
+ )
+ },
+ text = {
+ val text = if (userWalletTransaction.value.isPositive) {
+ "Received"
+ } else {
+ "Sent"
+ }
+ Text(text = text)
+ },
+ secondaryText = {
+ Text(text = userWalletTransaction.transaction.txId.toString())
+ },
+ trailing = {
+ Text(
+ text = userWalletTransaction.value.toFriendlyString(),
+ style = TextStyle(
+ color = if (userWalletTransaction.value.isPositive) {
+ Color.Green
+ } else {
+ Color.Red
+ }
+ )
+ )
+ }
+ )
+}
+
+fun dateToString(date: Date): String {
+ val formatter = SimpleDateFormat("dd MMMM, yyyy, HH:mm", Locale.US)
+ return formatter.format(date)
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/wallet/BitcoinWalletViewModel.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/wallet/BitcoinWalletViewModel.kt
new file mode 100644
index 000000000..a1a0602be
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/screens/wallet/BitcoinWalletViewModel.kt
@@ -0,0 +1,79 @@
+package nl.tudelft.trustchain.musicdao.ui.screens.wallet
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import nl.tudelft.trustchain.musicdao.core.repositories.ArtistRepository
+import nl.tudelft.trustchain.musicdao.core.wallet.UserWalletTransaction
+import nl.tudelft.trustchain.musicdao.core.wallet.WalletService
+import nl.tudelft.trustchain.musicdao.ui.SnackbarHandler
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import org.bitcoinj.core.Coin
+import org.bitcoinj.wallet.Wallet
+import javax.inject.Inject
+
+@HiltViewModel
+class BitcoinWalletViewModel @Inject constructor(val walletService: WalletService, val artistRepository: ArtistRepository) : ViewModel() {
+
+ val publicKey: MutableStateFlow = MutableStateFlow(null)
+ val confirmedBalance: MutableStateFlow = MutableStateFlow(null)
+ val estimatedBalance: MutableStateFlow = MutableStateFlow(null)
+ val status: MutableStateFlow = MutableStateFlow(null)
+ val syncProgress: MutableStateFlow = MutableStateFlow(null)
+ val walletTransactions: MutableStateFlow> = MutableStateFlow(listOf())
+
+ val faucetInProgress: MutableStateFlow = MutableStateFlow(false)
+ val isStarted: MutableStateFlow = MutableStateFlow(false)
+
+ init {
+
+ viewModelScope.launch {
+
+ while (isActive) {
+ syncProgress.value = walletService.percentageSynced()
+ status.value = walletService.walletStatus()
+
+ if (walletService.isStarted()) {
+ isStarted.value = true
+ publicKey.value = walletService.protocolAddress().toString()
+ estimatedBalance.value = walletService.estimatedBalance()
+ confirmedBalance.value = walletService.confirmedBalance()
+ walletTransactions.value = walletService.walletTransactions()
+ }
+ delay(REFRESH_DELAY)
+ }
+ }
+ }
+
+ fun requestFaucet() {
+ viewModelScope.launch {
+ faucetInProgress.value = true
+ val faucetRequestResult = walletService.defaultFaucetRequest()
+ if (faucetRequestResult) {
+ SnackbarHandler.displaySnackbar(text = "Successfully requested from faucet")
+ } else {
+ SnackbarHandler.displaySnackbar(text = "Something went wrong requesting from faucet")
+ }
+ faucetInProgress.value = false
+ }
+ }
+
+ fun wallet(): Wallet {
+ return walletService.wallet()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ suspend fun donate(publicKey: String, amount: String): Boolean {
+ val bitcoinPublicKey = artistRepository.getArtist(publicKey)?.bitcoinAddress ?: return false
+ return walletService.sendCoins(bitcoinPublicKey, amount)
+ }
+
+ companion object {
+ const val REFRESH_DELAY = 1000L
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/styling/MusicDAOTheme.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/styling/MusicDAOTheme.kt
new file mode 100644
index 000000000..36b5aa8ce
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/styling/MusicDAOTheme.kt
@@ -0,0 +1,28 @@
+package nl.tudelft.trustchain.musicdao.ui.styling
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.material.darkColors
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+object MusicDAOTheme {
+ val Red200 = Color(0xFF77DF7C)
+ val Red300 = Color(0xFF45C761)
+ val Red700 = Color(0xFF0CB829)
+
+ val DarkColors = darkColors(
+ primary = Red300,
+ primaryVariant = Red700,
+ onPrimary = Color.White,
+ secondary = Red300,
+ onSecondary = Color.White,
+ error = Red200
+ )
+
+ val Shapes = Shapes(
+ small = RoundedCornerShape(16.dp),
+ medium = RoundedCornerShape(16.dp),
+ large = RoundedCornerShape(0.dp)
+ )
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/AndroidURIController.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/AndroidURIController.kt
new file mode 100644
index 000000000..20523f3cf
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/AndroidURIController.kt
@@ -0,0 +1,30 @@
+package nl.tudelft.trustchain.musicdao.ui.util
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+import nl.tudelft.trustchain.musicdao.CachePath
+import org.apache.commons.io.FileUtils
+import java.io.File
+import java.io.InputStream
+import java.nio.file.Path
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.O)
+class AndroidURIController @Inject constructor(cacheDir: CachePath) {
+
+ val cachePath = cacheDir.getPath()!!
+
+ fun copyIntoCache(uri: Uri, context: Context, file: Path): File? {
+ val stream = uriToStream(uri, context) ?: return null
+ FileUtils.copyInputStreamToFile(stream, file.toFile())
+ return file.toFile()
+ }
+
+ companion object {
+ fun uriToStream(uri: Uri, context: Context): InputStream? {
+ return context.contentResolver.openInputStream(uri)
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/Chip.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/Chip.kt
new file mode 100644
index 000000000..41dae5132
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/Chip.kt
@@ -0,0 +1,79 @@
+package nl.tudelft.trustchain.musicdao.ui.util
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Chip(
+ startIcon: () -> ImageVector? = { null },
+ isStartIconEnabled: Boolean = false,
+ startIconTint: Color = Color.Unspecified,
+ onStartIconClicked: () -> Unit = { },
+ endIcon: () -> ImageVector? = { null },
+ isEndIconEnabled: Boolean = false,
+ endIconTint: Color = Color.Unspecified,
+ onEndIconClicked: () -> Unit = { },
+ color: Color = MaterialTheme.colors.primary,
+ contentDescription: String,
+ label: String,
+ isClickable: Boolean = false,
+ onClick: () -> Unit = { }
+) {
+ Surface(
+ modifier = Modifier.clickable(
+ enabled = isClickable,
+ onClick = { onClick() }
+ ),
+ elevation = 8.dp,
+ shape = MaterialTheme.shapes.small,
+ color = color
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ val leader = startIcon()
+ val trailer = endIcon()
+
+ if (leader != null) {
+ Icon(
+ leader,
+ contentDescription = contentDescription,
+ tint = startIconTint,
+ modifier = Modifier
+ .clickable(enabled = isStartIconEnabled, onClick = onStartIconClicked)
+ .padding(horizontal = 4.dp)
+ )
+ }
+
+ Text(
+ label,
+ modifier = Modifier.padding(6.dp),
+ style = MaterialTheme.typography.caption.copy(
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ )
+
+ if (trailer != null) {
+ Icon(
+ trailer,
+ contentDescription = contentDescription,
+ tint = endIconTint,
+ modifier = Modifier
+ .clickable(enabled = isEndIconEnabled, onClick = onEndIconClicked)
+ .padding(horizontal = 4.dp)
+ )
+ }
+ }
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/Parsing.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/Parsing.kt
new file mode 100644
index 000000000..f919fd752
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/Parsing.kt
@@ -0,0 +1,37 @@
+package nl.tudelft.trustchain.musicdao.ui.util
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import java.util.*
+
+@RequiresApi(Build.VERSION_CODES.O)
+fun dateToShortString(instant: String): String {
+ return try {
+ val time = Instant.parse(instant)
+ val result = DateTimeFormatter.ofPattern("MMM uuuu")
+ .withLocale(Locale.UK)
+ .withZone(ZoneId.systemDefault())
+ .format(time)
+ result
+ } catch (e: Exception) {
+ ""
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+fun dateToLongString(instant: String): String {
+ return try {
+ val time = Instant.parse(instant)
+ val result = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
+ .withLocale(Locale.UK)
+ .withZone(ZoneId.systemDefault())
+ .format(time)
+ result
+ } catch (e: Exception) {
+ ""
+ }
+}
diff --git a/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/PrettyPrint.kt b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/PrettyPrint.kt
new file mode 100644
index 000000000..7b77ae2db
--- /dev/null
+++ b/musicdao/src/main/java/nl/tudelft/trustchain/musicdao/ui/util/PrettyPrint.kt
@@ -0,0 +1,36 @@
+fun Any.prettyPrint(): String {
+ var indentLevel = 0
+ val indentWidth = 4
+
+ fun padding() = "".padStart(indentLevel * indentWidth)
+
+ val toString = toString()
+
+ val stringBuilder = StringBuilder(toString.length)
+
+ var i = 0
+ while (i < toString.length) {
+ when (val char = toString[i]) {
+ '(', '[', '{' -> {
+ indentLevel++
+ stringBuilder.appendLine(char).append(padding())
+ }
+ ')', ']', '}' -> {
+ indentLevel--
+ stringBuilder.appendLine().append(padding()).append(char)
+ }
+ ',' -> {
+ stringBuilder.appendLine(char).append(padding())
+ // ignore space after comma as we have added a newline
+ val nextChar = toString.getOrElse(i + 1) { char }
+ if (nextChar == ' ') i++
+ }
+ else -> {
+ stringBuilder.append(char)
+ }
+ }
+ i++
+ }
+
+ return stringBuilder.toString()
+}
diff --git a/musicdao/src/main/res/drawable-mdpi/ic_music.png b/musicdao/src/main/res/drawable-mdpi/ic_music.png
index 501dd7cf2..b8ce62559 100644
Binary files a/musicdao/src/main/res/drawable-mdpi/ic_music.png and b/musicdao/src/main/res/drawable-mdpi/ic_music.png differ
diff --git a/musicdao/src/main/res/drawable-xhdpi/ic_music.png b/musicdao/src/main/res/drawable-xhdpi/ic_music.png
index fa2d5a838..b8ce62559 100644
Binary files a/musicdao/src/main/res/drawable-xhdpi/ic_music.png and b/musicdao/src/main/res/drawable-xhdpi/ic_music.png differ
diff --git a/musicdao/src/main/res/drawable-xxhdpi/ic_music.png b/musicdao/src/main/res/drawable-xxhdpi/ic_music.png
index b05ebb582..b8ce62559 100644
Binary files a/musicdao/src/main/res/drawable-xxhdpi/ic_music.png and b/musicdao/src/main/res/drawable-xxhdpi/ic_music.png differ
diff --git a/musicdao/src/main/res/drawable/ic_baseline_account_balance_wallet_24.xml b/musicdao/src/main/res/drawable/ic_baseline_account_balance_wallet_24.xml
deleted file mode 100644
index 23f43e416..000000000
--- a/musicdao/src/main/res/drawable/ic_baseline_account_balance_wallet_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/musicdao/src/main/res/drawable/ic_baseline_add_24.xml b/musicdao/src/main/res/drawable/ic_baseline_add_24.xml
deleted file mode 100644
index 9eb39c359..000000000
--- a/musicdao/src/main/res/drawable/ic_baseline_add_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/musicdao/src/main/res/drawable/ic_baseline_bar_chart_24.xml b/musicdao/src/main/res/drawable/ic_baseline_bar_chart_24.xml
deleted file mode 100644
index 6c562aba9..000000000
--- a/musicdao/src/main/res/drawable/ic_baseline_bar_chart_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/musicdao/src/main/res/drawable/ic_baseline_search_24.xml b/musicdao/src/main/res/drawable/ic_baseline_search_24.xml
deleted file mode 100644
index 5023dc87e..000000000
--- a/musicdao/src/main/res/drawable/ic_baseline_search_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/musicdao/src/main/res/drawable/ic_loop_white_24dp.xml b/musicdao/src/main/res/drawable/ic_loop_white_24dp.xml
deleted file mode 100644
index e0d183ade..000000000
--- a/musicdao/src/main/res/drawable/ic_loop_white_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/musicdao/src/main/res/drawable/ic_shuffle_white_24dp.xml b/musicdao/src/main/res/drawable/ic_shuffle_white_24dp.xml
deleted file mode 100644
index 1192dec9f..000000000
--- a/musicdao/src/main/res/drawable/ic_shuffle_white_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/musicdao/src/main/res/layout/dialog_submit_release.xml b/musicdao/src/main/res/layout/dialog_submit_release.xml
deleted file mode 100644
index 79ff32c20..000000000
--- a/musicdao/src/main/res/layout/dialog_submit_release.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/dialog_tip_artist.xml b/musicdao/src/main/res/layout/dialog_tip_artist.xml
deleted file mode 100644
index f4608fb05..000000000
--- a/musicdao/src/main/res/layout/dialog_tip_artist.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_base.xml b/musicdao/src/main/res/layout/fragment_base.xml
deleted file mode 100644
index a5be48c82..000000000
--- a/musicdao/src/main/res/layout/fragment_base.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_playlist.xml b/musicdao/src/main/res/layout/fragment_playlist.xml
deleted file mode 100644
index a4612bf7f..000000000
--- a/musicdao/src/main/res/layout/fragment_playlist.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_recommendation.xml b/musicdao/src/main/res/layout/fragment_recommendation.xml
deleted file mode 100644
index 4da049be6..000000000
--- a/musicdao/src/main/res/layout/fragment_recommendation.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_release.xml b/musicdao/src/main/res/layout/fragment_release.xml
deleted file mode 100644
index 82f9a1df6..000000000
--- a/musicdao/src/main/res/layout/fragment_release.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_release_cover.xml b/musicdao/src/main/res/layout/fragment_release_cover.xml
deleted file mode 100644
index c316a61d5..000000000
--- a/musicdao/src/main/res/layout/fragment_release_cover.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_release_overview.xml b/musicdao/src/main/res/layout/fragment_release_overview.xml
deleted file mode 100644
index 4064f7cab..000000000
--- a/musicdao/src/main/res/layout/fragment_release_overview.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_trackplaying.xml b/musicdao/src/main/res/layout/fragment_trackplaying.xml
deleted file mode 100644
index 1faac7ee3..000000000
--- a/musicdao/src/main/res/layout/fragment_trackplaying.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/fragment_wallet.xml b/musicdao/src/main/res/layout/fragment_wallet.xml
deleted file mode 100644
index 0f42f9684..000000000
--- a/musicdao/src/main/res/layout/fragment_wallet.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/track_row.xml b/musicdao/src/main/res/layout/track_row.xml
deleted file mode 100644
index 2e2aa3a20..000000000
--- a/musicdao/src/main/res/layout/track_row.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/track_table_divider.xml b/musicdao/src/main/res/layout/track_table_divider.xml
deleted file mode 100644
index 097292cc8..000000000
--- a/musicdao/src/main/res/layout/track_table_divider.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
diff --git a/musicdao/src/main/res/layout/track_table_row.xml b/musicdao/src/main/res/layout/track_table_row.xml
deleted file mode 100644
index a0c6669fb..000000000
--- a/musicdao/src/main/res/layout/track_table_row.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/menu/menu_add_playlist.xml b/musicdao/src/main/res/menu/menu_add_playlist.xml
deleted file mode 100644
index 7b789b82d..000000000
--- a/musicdao/src/main/res/menu/menu_add_playlist.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
diff --git a/musicdao/src/main/res/menu/menu_searchable.xml b/musicdao/src/main/res/menu/menu_searchable.xml
deleted file mode 100644
index a8bfaf94b..000000000
--- a/musicdao/src/main/res/menu/menu_searchable.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
diff --git a/musicdao/src/main/res/navigation/musicdao_navgraph.xml b/musicdao/src/main/res/navigation/musicdao_navgraph.xml
deleted file mode 100644
index 2e3a20c6c..000000000
--- a/musicdao/src/main/res/navigation/musicdao_navgraph.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/navigation/nav_graph.xml b/musicdao/src/main/res/navigation/nav_graph.xml
deleted file mode 100644
index ef0b028a3..000000000
--- a/musicdao/src/main/res/navigation/nav_graph.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/values/colors.xml b/musicdao/src/main/res/values/colors.xml
deleted file mode 100644
index bfa7361e6..000000000
--- a/musicdao/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
- #232323
- #1a1a1a
- #6aefae
- #6aefae
- #303030
- #4d4d4d
-
- #6aefae
- #4d4d4d
- #333333
- #232323
-
-
- - @color/first
- - @color/second
- - @color/third
- - @color/fourth
-
-
diff --git a/musicdao/src/main/res/values/dimens.xml b/musicdao/src/main/res/values/dimens.xml
deleted file mode 100644
index 072444f88..000000000
--- a/musicdao/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- 8dp
-
diff --git a/musicdao/src/main/res/values/strings.xml b/musicdao/src/main/res/values/strings.xml
index 5a6505fbf..b2cddf808 100644
--- a/musicdao/src/main/res/values/strings.xml
+++ b/musicdao/src/main/res/values/strings.xml
@@ -1,20 +1,6 @@
+
-
-
- Hello blank fragment
- MainActivity
-
- First Fragment
- Second Fragment
- Next
- Previous
-
- Hello first fragment
- Hello second fragment. Arg: %1$s
- Add playlist
MusicDAO
Search through music
- Connectivity stats
- View balance
-
+
diff --git a/musicdao/src/main/res/values/styles.xml b/musicdao/src/main/res/values/styles.xml
index de62580a8..258ab8c4f 100644
--- a/musicdao/src/main/res/values/styles.xml
+++ b/musicdao/src/main/res/values/styles.xml
@@ -1,11 +1,8 @@
+
-
-
-
-
-
-
diff --git a/musicdao/src/main/res/xml/searchable.xml b/musicdao/src/main/res/xml/searchable.xml
index f5d58e9e3..09be8ece5 100644
--- a/musicdao/src/main/res/xml/searchable.xml
+++ b/musicdao/src/main/res/xml/searchable.xml
@@ -1,4 +1,4 @@
+ android:label="@string/app_label" />
diff --git a/musicdao/src/test/java/com/example/musicdao/FaucetEndpointTest.kt b/musicdao/src/test/java/com/example/musicdao/FaucetEndpointTest.kt
deleted file mode 100644
index 4bce70401..000000000
--- a/musicdao/src/test/java/com/example/musicdao/FaucetEndpointTest.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.example.musicdao
-
-import org.junit.Ignore
-import org.junit.Test
-import java.io.InputStream
-import java.net.URL
-
-class FaucetEndpointTest {
- val id = "abc123xyz"
- val endpointAddress = "http://134.122.59.107:3000"
-
- @Test
- @Ignore("Unreliable tests") // unit test should not depend on external server
- fun getCoins() {
- val obj = URL("$endpointAddress?id=$id")
- val con: InputStream? = obj.openStream()
- con?.close()
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/MagnetLinkTests.kt b/musicdao/src/test/java/com/example/musicdao/MagnetLinkTests.kt
deleted file mode 100644
index 050e8b819..000000000
--- a/musicdao/src/test/java/com/example/musicdao/MagnetLinkTests.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.example.musicdao
-
-import com.example.musicdao.util.Util
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class MagnetLinkTests {
- @Test
- fun extractDisplayName() {
- val magnetLink =
- "magnet:?xt=urn:btih:45E4170514EE0CE20ABACF1FE256F9C73F95EF47&dn=Royalty%20Free%20Background%20Music%20Pack&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2F9.rarbg.to%3A2920%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.pirateparty.gr%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce"
- assertEquals(
- "Royalty%20Free%20Background%20Music%20Pack",
- Util.extractNameFromMagnet(magnetLink)
- )
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/catalog/PlaylistCoverFragmentTest.kt b/musicdao/src/test/java/com/example/musicdao/catalog/PlaylistCoverFragmentTest.kt
deleted file mode 100644
index 16d8d39a0..000000000
--- a/musicdao/src/test/java/com/example/musicdao/catalog/PlaylistCoverFragmentTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.example.musicdao.catalog
-
-import com.goterl.lazysodium.LazySodiumJava
-import com.goterl.lazysodium.SodiumJava
-import nl.tudelft.ipv8.attestation.trustchain.*
-import nl.tudelft.ipv8.keyvault.JavaCryptoProvider
-import nl.tudelft.ipv8.keyvault.LibNaClSK
-import nl.tudelft.ipv8.keyvault.PrivateKey
-import nl.tudelft.ipv8.util.hexToBytes
-import org.junit.Assert
-import org.junit.Test
-import java.util.*
-
-private val lazySodium = LazySodiumJava(SodiumJava())
-
-/**
- * Part of this code was ported from TrustChainBlockTest, from the
- * nl.tudelft.ipv8.attestation.trustchain package
- */
-class PlaylistCoverFragmentTest {
- private val privateKey =
- JavaCryptoProvider.keyFromPrivateBin("4c69624e61434c534b3a069c289bd6031de93d49a8c35c7b2f0758c77c7b24b97842d08097abb894d8e98ba8a91ebc063f0687909f390b7ed9ec1d78fcc462298b81a51b2e3b5b9f77f2".hexToBytes())
- private val block = TrustChainBlock(
- "publish_release",
- TransactionEncoding.encode(
- mapOf(
- "title" to "title",
- "artists" to "artists",
- "date" to "date"
- )
- ),
- privateKey.pub().keyToBin(),
- GENESIS_SEQ,
- ANY_COUNTERPARTY_PK,
- UNKNOWN_SEQ,
- GENESIS_HASH,
- EMPTY_SIG,
- Date(0)
- )
-
- private fun getPrivateKey(): PrivateKey {
- val privateKey = "81df0af4c88f274d5228abb894a68906f9e04c902a09c68b9278bf2c7597eaf6"
- val signSeed = "c5c416509d7d262bddfcef421fc5135e0d2bdeb3cb36ae5d0b50321d766f19f2"
- return LibNaClSK(privateKey.hexToBytes(), signSeed.hexToBytes(), lazySodium)
- }
-
- @Test
- fun filter() {
-//
-// block.sign(privateKey)
-// val payload = HalfBlockPayload.fromHalfBlock(block, sign = false)
-// val message = payload.serialize()
-//
-// Assert.assertTrue(privateKey.pub().verify(block.signature, message))
-// Assert.assertEquals(
-// "4c69624e61434c504b3a80d4a88a7d010d2fb488913b5f1b5644a5eb2edbae589f81ac28e866ffe3c90b3a41a0304dc512874131fc96fa324ca3e6afeaa473dbc1e8da895c7a7c746af80000000130303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030300000000030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303000000004746573740000000b61316432736964326934320000000000000000",
-// message.toHex()
-// )
-// Assert.assertEquals(
-// "3a02014eb9e8ba9753208481db2be600da5ff23904a565a9fe0e9e663ec6774d08ecf77b1214e0cbb58f537ed595dd5ff2bfd0208e621e83674deb8b337e5101",
-// block.signature.toHex()
-// )
- val fragment = PlaylistCoverFragment(block)
- Assert.assertTrue(fragment.filter("title"))
- Assert.assertTrue(fragment.filter("Title"))
- Assert.assertTrue(fragment.filter("arti"))
- Assert.assertTrue(fragment.filter("dat"))
- Assert.assertFalse(fragment.filter("something else"))
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/catalog/PlaylistsOverviewFragmentTest.kt b/musicdao/src/test/java/com/example/musicdao/catalog/PlaylistsOverviewFragmentTest.kt
deleted file mode 100644
index e1462d551..000000000
--- a/musicdao/src/test/java/com/example/musicdao/catalog/PlaylistsOverviewFragmentTest.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.example.musicdao.catalog
-
-import nl.tudelft.ipv8.attestation.trustchain.*
-import nl.tudelft.ipv8.keyvault.JavaCryptoProvider
-import nl.tudelft.ipv8.util.hexToBytes
-import org.junit.Assert
-import org.junit.Test
-import java.util.*
-
-class PlaylistsOverviewFragmentTest {
- private val privateKey =
- JavaCryptoProvider.keyFromPrivateBin("4c69624e61434c534b3a069c289bd6031de93d49a8c35c7b2f0758c77c7b24b97842d08097abb894d8e98ba8a91ebc063f0687909f390b7ed9ec1d78fcc462298b81a51b2e3b5b9f77f2".hexToBytes())
- private val bitcoinPublicKey = "some-key"
- private val wellStructuredBlock = TrustChainBlock(
- "publish_release",
- TransactionEncoding.encode(
- mapOf(
- "magnet" to "magnet:?xt=urn:btih:a83cc13bf4a07e85b938dcf06aa707955687ca7c",
- "title" to "title",
- "artists" to "artists",
- "date" to "date",
- "torrentInfoName" to "torrentInfoName",
- "publisher" to bitcoinPublicKey
- )
- ),
- privateKey.pub().keyToBin(),
- GENESIS_SEQ,
- ANY_COUNTERPARTY_PK,
- UNKNOWN_SEQ,
- GENESIS_HASH,
- EMPTY_SIG,
- Date(0)
- )
-
- private val wrongStructuredBlock = TrustChainBlock(
- "publish_release",
- TransactionEncoding.encode(
- mapOf(
- "magnet" to "",
- "title" to "",
- "artists" to "",
- "date" to "",
- "torrentInfoName" to ""
- )
- ),
- privateKey.pub().keyToBin(),
- GENESIS_SEQ,
- ANY_COUNTERPARTY_PK,
- UNKNOWN_SEQ,
- GENESIS_HASH,
- EMPTY_SIG,
- Date(0)
- )
-
- @Test
- fun showAllReleases() {
- val releaseOverviewFragment = PlaylistsOverviewFragment()
- val releaseBlocks = mapOf(
- wellStructuredBlock to 0,
- wrongStructuredBlock to 0
- )
- Assert.assertEquals(
- 1,
- releaseOverviewFragment.refreshReleaseBlocks(releaseBlocks)
- )
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/dialog/SubmitReleaseDialogTest.kt b/musicdao/src/test/java/com/example/musicdao/dialog/SubmitReleaseDialogTest.kt
deleted file mode 100644
index 518b23ab1..000000000
--- a/musicdao/src/test/java/com/example/musicdao/dialog/SubmitReleaseDialogTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.example.musicdao.dialog
-
-import com.example.musicdao.catalog.PlaylistsOverviewFragment
-import io.mockk.mockk
-import org.junit.Assert
-import org.junit.Test
-
-class SubmitReleaseDialogTest {
- @Test
- fun validateReleaseBlock() {
- val musicService = mockk()
- val dialog = SubmitReleaseDialog(musicService)
- val magnetLink =
- "magnet:?xt=urn:btih:45E4170514EE0CE20ABACF1FE256F9C73F95EF47&dn=Royalty%20Free%20Background%20Music%20Pack&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2F9.rarbg.to%3A2920%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.pirateparty.gr%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce"
- val valid = dialog.validateReleaseBlock("a", "b", "c", magnetLink)
- Assert.assertNotNull(valid)
- // Release blocks require a magnet link to be published
- val invalid = dialog.validateReleaseBlock("a", "b", "c", "")
- Assert.assertNull(invalid)
- // Release blocks require metadata to be published
- val invalid2 = dialog.validateReleaseBlock("", "", "", magnetLink)
- Assert.assertNull(invalid2)
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/ipv8/KeywordSearchMessageTest.kt b/musicdao/src/test/java/com/example/musicdao/ipv8/KeywordSearchMessageTest.kt
deleted file mode 100644
index b03e2aa84..000000000
--- a/musicdao/src/test/java/com/example/musicdao/ipv8/KeywordSearchMessageTest.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.example.musicdao.ipv8
-
-import com.goterl.lazysodium.LazySodiumJava
-import com.goterl.lazysodium.SodiumJava
-import nl.tudelft.ipv8.keyvault.LibNaClSK
-import nl.tudelft.ipv8.util.hexToBytes
-import org.junit.Assert
-import org.junit.Test
-
-private val lazySodium = LazySodiumJava(SodiumJava())
-
-class KeywordSearchMessageTest {
- private val key = LibNaClSK.fromBin(
- "4c69624e61434c534b3a054b2367b4854a8bf2d12fcd12158a6731fcad9cfbff7dd71f9985eb9f064c8118b1a89c931819d3482c73ebd9be9ee1750dc143981f7a481b10496c4e0ef982".hexToBytes(),
- lazySodium
- )
- private val originPublicKey = key.pub().keyToBin()
- private val ttl = 2u
- private val keyword = "keyword"
- private val payload =
- KeywordSearchMessage(originPublicKey, ttl, keyword)
-
- @Test
- fun checkTTL() {
- Assert.assertTrue(payload.checkTTL())
- Assert.assertFalse(payload.checkTTL())
- }
-
- @Test
- fun serializeAndDeserialize() {
- val serialized = payload.serialize()
- val (deserialized, size) = com.example.musicdao.ipv8.KeywordSearchMessage.deserialize(
- serialized
- )
- Assert.assertEquals(serialized.size, size)
- Assert.assertEquals(payload.keyword, deserialized.keyword)
- Assert.assertEquals(payload.ttl, deserialized.ttl)
- Assert.assertArrayEquals(payload.originPublicKey, deserialized.originPublicKey)
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/ipv8/MusicCommunityTest.kt b/musicdao/src/test/java/com/example/musicdao/ipv8/MusicCommunityTest.kt
deleted file mode 100644
index 8f0dd3991..000000000
--- a/musicdao/src/test/java/com/example/musicdao/ipv8/MusicCommunityTest.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-package com.example.musicdao.ipv8
-
-import com.frostwire.jlibtorrent.Sha1Hash
-import com.squareup.sqldelight.db.SqlDriver
-import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.spyk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
-import nl.tudelft.ipv8.Peer
-import nl.tudelft.ipv8.attestation.trustchain.*
-import nl.tudelft.ipv8.attestation.trustchain.store.TrustChainSQLiteStore
-import nl.tudelft.ipv8.keyvault.JavaCryptoProvider
-import nl.tudelft.ipv8.messaging.EndpointAggregator
-import nl.tudelft.ipv8.peerdiscovery.Network
-import nl.tudelft.ipv8.sqldelight.Database
-
-import org.junit.Assert
-import org.junit.Test
-import java.util.*
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class MusicCommunityTest {
- private fun createTrustChainStore(): TrustChainSQLiteStore {
- val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
- Database.Schema.create(driver)
- val database = Database(driver)
- return TrustChainSQLiteStore(database)
- }
-
- private fun getCommunity(): MusicCommunity {
- val settings = TrustChainSettings()
- val store = createTrustChainStore()
- val community = MusicCommunity.Factory(settings = settings, database = store).create()
- val newKey = JavaCryptoProvider.generateKey()
- community.myPeer = Peer(newKey)
- community.endpoint = spyk(EndpointAggregator(mockk(relaxed = true), null))
- community.network = Network()
- community.maxPeers = 20
- return community
- }
-
- @Test
- fun localKeywordSearch() = runBlockingTest {
- val crawler = TrustChainCrawler()
- val trustChainCommunity = spyk(getCommunity())
- crawler.trustChainCommunity = trustChainCommunity
-
- val newKey = JavaCryptoProvider.generateKey()
-
- val block = TrustChainBlock(
- "publish_release",
- TransactionEncoding.encode(
- mapOf(
- "magnet" to "magnet:?xt=urn:btih:a83cc13bf4a07e85b938dcf06aa707955687ca7c",
- "title" to "title",
- "artists" to "artists",
- "date" to "date",
- "torrentInfoName" to "torrentInfoName"
- )
- ),
- newKey.pub().keyToBin(),
- 1u,
- ANY_COUNTERPARTY_PK,
- 0u,
- GENESIS_HASH,
- EMPTY_SIG,
- Date()
- )
-
- trustChainCommunity.database.addBlock(block)
-
- val peer = Peer(newKey)
- crawler.crawlChain(peer, 1u)
-
- Assert.assertEquals(1, trustChainCommunity.database.getAllBlocks().size)
-
- Assert.assertNotNull(trustChainCommunity.localKeywordSearch("title"))
- Assert.assertNotNull(trustChainCommunity.localKeywordSearch("artists"))
- Assert.assertNull(trustChainCommunity.localKeywordSearch("somethingelse"))
- }
-
- @Test
- fun performRemoteKeywordSearch() {
- val trustChainCommunity = spyk(getCommunity())
-
- val newKey2 = JavaCryptoProvider.generateKey()
- val neighborPeer = Peer(newKey2)
- every {
- trustChainCommunity.getPeers()
- } returns listOf(neighborPeer)
-
- val count =
- trustChainCommunity.performRemoteKeywordSearch("keyword", 1u, ANY_COUNTERPARTY_PK)
- Assert.assertEquals(count, 1)
- }
-
- @Test
- fun sendSwarmHealthMessage() {
- val musicCommunity = spyk(getCommunity())
- val swarmHealth = SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 0.toUInt())
- Assert.assertFalse(musicCommunity.sendSwarmHealthMessage(swarmHealth))
- val newKey = JavaCryptoProvider.generateKey()
- val neighborPeer = Peer(newKey)
- every {
- musicCommunity.getPeers()
- } returns listOf(neighborPeer)
- Assert.assertTrue(musicCommunity.sendSwarmHealthMessage(swarmHealth))
- every {
- musicCommunity.getPeers()
- } returns listOf(neighborPeer, neighborPeer)
- Assert.assertTrue(musicCommunity.sendSwarmHealthMessage(swarmHealth))
- }
-
- @Test
- fun communicateReleaseBlocks() {
- val community = spyk(getCommunity())
- val newKey = JavaCryptoProvider.generateKey()
-
- val block = TrustChainBlock(
- "publish_release",
- TransactionEncoding.encode(
- mapOf(
- "magnet" to "magnet:?xt=urn:btih:a83cc13bf4a07e85b938dcf06aa707955687ca7c",
- "title" to "title",
- "artists" to "artists",
- "date" to "date",
- "torrentInfoName" to "torrentInfoName"
- )
- ),
- newKey.pub().keyToBin(),
- 1u,
- ANY_COUNTERPARTY_PK,
- 0u,
- GENESIS_HASH,
- EMPTY_SIG,
- Date()
- )
-
- community.database.addBlock(block)
-
- // 0 peers: we have 1 block, but no one to send it to
- Assert.assertEquals(community.communicateReleaseBlocks(), 0)
- val newKey2 = JavaCryptoProvider.generateKey()
- val neighborPeer = Peer(newKey2)
- every {
- community.getPeers()
- } returns listOf(neighborPeer)
- // 1 peer: send to 1 person, because we have 1 block
- Assert.assertEquals(community.communicateReleaseBlocks(), 1)
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/ipv8/SwarmHealthTest.kt b/musicdao/src/test/java/com/example/musicdao/ipv8/SwarmHealthTest.kt
deleted file mode 100644
index a0cede4c9..000000000
--- a/musicdao/src/test/java/com/example/musicdao/ipv8/SwarmHealthTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.example.musicdao.ipv8
-
-import com.example.musicdao.ipv8.SwarmHealth.Deserializer.KEEP_TIME_HOURS
-import com.frostwire.jlibtorrent.Sha1Hash
-import org.junit.Assert
-import org.junit.Test
-import java.util.*
-
-class SwarmHealthTest {
- @Test
- fun mergeMaps() {
- val map1 = mutableMapOf()
- val map2 = mutableMapOf()
-
- map1[Sha1Hash.max()] = SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 1.toUInt())
- map2[Sha1Hash.max()] = SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 1.toUInt())
- map2[Sha1Hash.min()] = SwarmHealth(Sha1Hash.min().toString(), 1.toUInt(), 1.toUInt())
-
- val map3 = map1 + map2
- Assert.assertEquals(map3.size, 2)
- }
-
- @Test
- fun compare() {
- val a = SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 0.toUInt())
- val b = SwarmHealth(Sha1Hash.max().toString(), 0.toUInt(), 1.toUInt())
- val c = SwarmHealth(Sha1Hash.max().toString(), 0.toUInt(), 0.toUInt())
-
- Assert.assertFalse(a > b)
- Assert.assertFalse(b > a)
- Assert.assertTrue(a > c)
- Assert.assertTrue(b > c)
-
- Assert.assertEquals(SwarmHealth.pickBest(a, c), a)
- Assert.assertEquals(SwarmHealth.pickBest(a, b), b)
- Assert.assertEquals(SwarmHealth.pickBest(c, b), b)
- }
-
- @Test
- fun serializeDeserialize() {
- val a = SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 0.toUInt())
- val serialized = a.serialize()
- val (b, _) = SwarmHealth.deserialize(serialized)
- Assert.assertEquals(a, b)
- }
-
- @Test
- fun notEquals() {
- val a = SwarmHealth(
- Sha1Hash.max().toString(),
- 1.toUInt(),
- 0.toUInt(),
- Date().time.toULong() - 1000.toULong()
- )
- val b =
- SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 0.toUInt(), Date().time.toULong())
- Assert.assertNotEquals(a, b)
- }
-
- @Test
- fun isUpToDate() {
- val a = SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 0.toUInt())
- Assert.assertTrue(a.isUpToDate())
- val oldDate = Date().time - 2 * 3600 * KEEP_TIME_HOURS * 1000
- val b = SwarmHealth(Sha1Hash.max().toString(), 1.toUInt(), 0.toUInt(), oldDate.toULong())
- Assert.assertFalse(b.isUpToDate())
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/net/ContentSeederTest.kt b/musicdao/src/test/java/com/example/musicdao/net/ContentSeederTest.kt
deleted file mode 100644
index 620bfe101..000000000
--- a/musicdao/src/test/java/com/example/musicdao/net/ContentSeederTest.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.example.musicdao.net
-
-import com.frostwire.jlibtorrent.SessionManager
-import com.frostwire.jlibtorrent.Sha1Hash
-import com.frostwire.jlibtorrent.TorrentHandle
-import com.frostwire.jlibtorrent.TorrentInfo
-import io.mockk.every
-import io.mockk.mockk
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Test
-import java.io.File
-
-class ContentSeederTest {
- private lateinit var contentSeeder: ContentSeeder
-
- @Before
- fun init() {
- val sessionManager = SessionManager()
- val saveDir = File("./src/test/resources")
- Assert.assertTrue(saveDir.isDirectory)
- // Test whether it reads all local torrent files correctly
- contentSeeder = ContentSeeder.getInstance(saveDir, sessionManager)
- Assert.assertNotNull(contentSeeder)
- }
-
- @Test
- fun start() {
- val count = contentSeeder.start()
- Assert.assertEquals(0, count)
- }
-
- @Test
- fun addValidTorrent() {
- val torrentInfoName = "RFBMP"
- // Adding an existing and valid torrent file
- val torrentFile = File("./src/test/resources/RFBMP.torrent")
- val validTorrentInfo =
- contentSeeder.saveTorrentInfoToFile(TorrentInfo(torrentFile), torrentInfoName)
- Assert.assertTrue(validTorrentInfo)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun addInvalidTorrent() {
- val torrentInfoName = "RFBMP"
- // Adding a nonexisting torrent file
- val torrentFile = File("./somenonexistingfile.torrent")
- contentSeeder.saveTorrentInfoToFile(TorrentInfo(torrentFile), torrentInfoName)
- }
-
- @Test
- fun updateSwarmHealth() {
- val torrentHandle = mockk()
- every { torrentHandle.status().numPeers() } returns 1
- every { torrentHandle.status().numSeeds() } returns 1
- every { torrentHandle.infoHash() } returns Sha1Hash.max()
- contentSeeder.updateSwarmHealth(torrentHandle)
- Assert.assertEquals(contentSeeder.swarmHealthMap.size, 1)
- contentSeeder.updateSwarmHealth(torrentHandle)
- Assert.assertEquals(contentSeeder.swarmHealthMap.size, 1)
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/playlist/ReleaseFragmentTest.kt b/musicdao/src/test/java/com/example/musicdao/playlist/ReleaseFragmentTest.kt
deleted file mode 100644
index 0909abdba..000000000
--- a/musicdao/src/test/java/com/example/musicdao/playlist/ReleaseFragmentTest.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.example.musicdao.playlist
-
-import com.frostwire.jlibtorrent.SessionManager
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.spyk
-import org.junit.Assert
-import org.junit.Test
-import java.io.File
-
-class ReleaseFragmentTest {
- @Test
- fun resolveTorrentUrl() {
- val sessionManager = mockk()
- val releaseFragment = spyk(
- ReleaseFragment(
- "magnet",
- "artists",
- "title",
- "10-10-2010",
- "publisherKey",
- "RFBMP",
- sessionManager
- )
- )
- val resources = File("./src/test/resources")
- Assert.assertTrue(resources.isDirectory)
- every {
- releaseFragment.context?.cacheDir
- } returns resources
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/playlist/TrackFragmentTest.kt b/musicdao/src/test/java/com/example/musicdao/playlist/TrackFragmentTest.kt
deleted file mode 100644
index 00253e915..000000000
--- a/musicdao/src/test/java/com/example/musicdao/playlist/TrackFragmentTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.example.musicdao.playlist
-
-import io.mockk.mockk
-import org.junit.Assert
-import org.junit.Test
-
-class TrackFragmentTest {
- @Test
- fun getDownloadProgress() {
- val expectedPercentage = 10
- val releaseFragment = mockk()
-
- val track = TrackFragment("name", 0, releaseFragment, "1Mb", 10)
- val percentage = track.getProgress()
- Assert.assertEquals(expectedPercentage, percentage)
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/util/ReleaseFactoryTest.kt b/musicdao/src/test/java/com/example/musicdao/util/ReleaseFactoryTest.kt
deleted file mode 100644
index 3a5e231ec..000000000
--- a/musicdao/src/test/java/com/example/musicdao/util/ReleaseFactoryTest.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.example.musicdao.util
-
-import android.content.ClipData
-import android.content.Intent
-import android.net.Uri
-import io.mockk.every
-import io.mockk.mockk
-import org.junit.Assert
-import org.junit.Test
-
-class ReleaseFactoryTest {
- @Test
- fun uriListFromLocalFiles() {
- val intent = mockk()
- val uri = mockk()
- every { intent.data } returns uri
- every { intent.clipData } returns null
- val list = ReleaseFactory.uriListFromLocalFiles(intent)
- Assert.assertEquals(1, list.size)
-
- every { intent.clipData } returns ClipData.newRawUri("a", uri)
- val list2 = ReleaseFactory.uriListFromLocalFiles(intent)
- Assert.assertEquals(1, list2.size)
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/util/UtilTest.kt b/musicdao/src/test/java/com/example/musicdao/util/UtilTest.kt
deleted file mode 100644
index b2b688729..000000000
--- a/musicdao/src/test/java/com/example/musicdao/util/UtilTest.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.example.musicdao.util
-
-import com.frostwire.jlibtorrent.Sha1Hash
-import com.frostwire.jlibtorrent.TorrentInfo
-import org.junit.Assert
-import org.junit.Test
-import java.io.File
-
-class UtilTest {
- @Test
- fun calculatePieceIndex() {
- val torrentFile = this.javaClass.getResource("/RFBMP.torrent")?.path
- Assert.assertNotNull(torrentFile)
- if (torrentFile == null) return
- Assert.assertNotNull(File(torrentFile))
- val fileIndex = 1
- val torrentInfo = TorrentInfo(File(torrentFile))
- val x = Util.calculatePieceIndex(fileIndex, torrentInfo)
- Assert.assertEquals(82, x)
- }
-
- @Test
- fun extractInfoHashFromMagnet() {
- val magnet = "magnet:?xt=urn:btih:a83cc13bf4a07e85b938dcf06aa707955687ca7c&dn=displayname"
- val name = Util.extractInfoHash(magnet)
- Assert.assertEquals(name, Sha1Hash("a83cc13bf4a07e85b938dcf06aa707955687ca7c"))
- Assert.assertEquals(
- name.toString(),
- Sha1Hash("a83cc13bf4a07e85b938dcf06aa707955687ca7c").toString()
- )
- Assert.assertNotEquals(name, "somethingelse")
- }
-
- @Test
- fun extractNameFromMagnet() {
- val magnet = "magnet:?xt=urn:btih:a83cc13bf4a07e85b938dcf06aa707955687ca7c&dn=displayname"
- val name = Util.extractNameFromMagnet(magnet)
- Assert.assertEquals(name, "displayname")
- Assert.assertNotEquals(name, "somethingelse")
- }
-
- @Test
- fun readableBytes() {
- val eightMbs: Long = 1024 * 1024 * 8
- val eightKbs: Long = 1024 * 8
- val eightBytes: Long = 8
- Assert.assertEquals(Util.readableBytes(eightMbs), "8Mb")
- Assert.assertEquals(Util.readableBytes(eightKbs), "8Kb")
- Assert.assertEquals(Util.readableBytes(eightBytes), "8B")
- }
-
- @Test
- fun checkAndSanitizeTrackNames() {
-// val fileName = "Valid_File-Name.mp3"
-// val fileName2 = "invalid-File.txt"
-// Assert.assertEquals(
-// "Valid File-Name",
-// Util.getTitle(fileName)
-// )
-// Assert.assertNull(Util.getTitle(fileName2)) TODO
- }
-
- @Test
- fun setSequentialPriorities() {
-// val priorities: Array = arrayOf( TODO
-// Priority.SEVEN,
-// Priority.SEVEN,
-// Priority.SEVEN,
-// Priority.NORMAL,
-// Priority.IGNORE
-// )
-// val torrent = mockk()
-// every { torrent.torrentHandle.piecePriorities() } returns priorities
-// every { torrent.interestedPieceIndex } returns 1
-// every { torrent.piecesToPrepare } returns 2
-// val expectedPriorites: Array = arrayOf(
-// Priority.SIX,
-// Priority.SEVEN,
-// Priority.SEVEN,
-// Priority.FIVE,
-// Priority.NORMAL
-// )
-// val answer = Util.setSequentialPriorities(torrent, onlyCalculating = true)
-// Assert.assertArrayEquals(expectedPriorites, answer)
- }
-}
diff --git a/musicdao/src/test/java/com/example/musicdao/wallet/WalletServiceTest.kt b/musicdao/src/test/java/com/example/musicdao/wallet/WalletServiceTest.kt
deleted file mode 100644
index 8873fce1e..000000000
--- a/musicdao/src/test/java/com/example/musicdao/wallet/WalletServiceTest.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.example.musicdao.wallet
-
-import com.example.musicdao.MusicService
-import io.mockk.every
-import io.mockk.mockk
-import org.junit.Assert
-import org.junit.Test
-import java.io.File
-
-class WalletServiceTest {
- @Test
- fun startup() {
- val musicService = mockk()
- val saveDir = File("./src/test/resources")
- Assert.assertTrue(saveDir.isDirectory)
- every {
- musicService.applicationContext.cacheDir
- } returns saveDir
- // TODO this test is failing on the RegTestNet.get() call, because of an
- // ExceptionInInitializerError
-// val service = WalletService(musicService)
-// service.startup()
-// Assert.assertEquals(
-// 1,
-// service.app.peerGroup().connectedPeers.size
-// )
- }
-}
diff --git a/peerchat/src/test/java/nl/tudelft/trustchain/debug/ExampleUnitTest.kt b/peerchat/src/test/java/nl/tudelft/trustchain/debug/ExampleUnitTest.kt
index 6c3b3f2a4..487585659 100644
--- a/peerchat/src/test/java/nl/tudelft/trustchain/debug/ExampleUnitTest.kt
+++ b/peerchat/src/test/java/nl/tudelft/trustchain/debug/ExampleUnitTest.kt
@@ -11,6 +11,6 @@ import org.junit.Test
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
+ assertEquals(4, (2 + 2))
}
}
diff --git a/trustchain-explorer/src/test/java/nl/tudelft/trustchain/explorer/ExampleUnitTest.kt b/trustchain-explorer/src/test/java/nl/tudelft/trustchain/explorer/ExampleUnitTest.kt
index e74243e68..0ab61edfe 100644
--- a/trustchain-explorer/src/test/java/nl/tudelft/trustchain/explorer/ExampleUnitTest.kt
+++ b/trustchain-explorer/src/test/java/nl/tudelft/trustchain/explorer/ExampleUnitTest.kt
@@ -11,6 +11,6 @@ import org.junit.Test
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
+ assertEquals(4, (2 + 2))
}
}
diff --git a/trustchain-payloadgenerator/src/test/java/nl/tudelft/trustchain/payloadgenerator/ExampleUnitTest.kt b/trustchain-payloadgenerator/src/test/java/nl/tudelft/trustchain/payloadgenerator/ExampleUnitTest.kt
index e99feca9c..790d853fc 100644
--- a/trustchain-payloadgenerator/src/test/java/nl/tudelft/trustchain/payloadgenerator/ExampleUnitTest.kt
+++ b/trustchain-payloadgenerator/src/test/java/nl/tudelft/trustchain/payloadgenerator/ExampleUnitTest.kt
@@ -11,6 +11,6 @@ import org.junit.Test
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
+ assertEquals(4, (2 + 2))
}
}
diff --git a/trustchain-trader/src/test/java/nl/tudelft/trustchain/trader/ExampleUnitTest.kt b/trustchain-trader/src/test/java/nl/tudelft/trustchain/trader/ExampleUnitTest.kt
index 402998819..b5f67e3c4 100644
--- a/trustchain-trader/src/test/java/nl/tudelft/trustchain/trader/ExampleUnitTest.kt
+++ b/trustchain-trader/src/test/java/nl/tudelft/trustchain/trader/ExampleUnitTest.kt
@@ -11,6 +11,6 @@ import org.junit.Test
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
+ assertEquals(4, (2 + 2))
}
}
diff --git a/trustchain-voter/src/test/java/nl/tudelft/trustchain/voting/ExampleUnitTest.kt b/trustchain-voter/src/test/java/nl/tudelft/trustchain/voting/ExampleUnitTest.kt
index 6c63e6eac..e5c0a340b 100644
--- a/trustchain-voter/src/test/java/nl/tudelft/trustchain/voting/ExampleUnitTest.kt
+++ b/trustchain-voter/src/test/java/nl/tudelft/trustchain/voting/ExampleUnitTest.kt
@@ -11,6 +11,6 @@ import org.junit.Test
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
+ assertEquals(4, (2 + 2))
}
}
diff --git a/valuetransfer/build.gradle b/valuetransfer/build.gradle
index 67c9928d8..05533da2c 100644
--- a/valuetransfer/build.gradle
+++ b/valuetransfer/build.gradle
@@ -96,7 +96,7 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
- implementation 'androidx.room:room-runtime:2.3.0'
+ implementation "androidx.room:room-runtime:$room_version"
implementation project(path: ':peerchat')
implementation project(path: ':eurotoken')
implementation project(path: ':ig-ssi')
@@ -108,7 +108,7 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
- annotationProcessor 'androidx.room:room-compiler:2.3.0'
+ annotationProcessor "androidx.room:room-compiler:$room_version"
// Blocking dialog and top snacbkbar
implementation 'com.jaredrummler:blocking-dialog:1.0.0'
diff --git a/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/dialogs/ExchangeTransferMoneyLinkDialog.kt b/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/dialogs/ExchangeTransferMoneyLinkDialog.kt
index 5fe6c899d..2779be629 100644
--- a/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/dialogs/ExchangeTransferMoneyLinkDialog.kt
+++ b/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/dialogs/ExchangeTransferMoneyLinkDialog.kt
@@ -286,6 +286,7 @@ class ExchangeTransferMoneyLinkDialog(
}
val swapped = symbols.substring(4) + symbols.substring(0, 4)
+ @Suppress("DEPRECATION")
return swapped.toCharArray()
.map { it.toInt() }
.fold(0) { previousMod: Int, _char: Int ->
diff --git a/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/util/UtilFunctions.kt b/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/util/UtilFunctions.kt
index d3ec062c3..a4a5e018b 100644
--- a/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/util/UtilFunctions.kt
+++ b/valuetransfer/src/main/java/nl/tudelft/trustchain/valuetransfer/util/UtilFunctions.kt
@@ -251,6 +251,7 @@ fun ByteArray.md5(): String {
fun String.getInitials(): String {
val initials = StringBuilder()
this.split(" ").forEach {
+ @Suppress("DEPRECATION")
if (it.isNotEmpty()) initials.append("${it[0].toUpperCase()}.")
}
return initials.toString()