Compare commits

...

2 Commits

14 changed files with 456 additions and 19 deletions

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
@ -40,8 +41,16 @@
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".CreateDishActivity" /> android:name=".CreateDishActivity"
android:exported="false" />
<activity
android:name=".ChangePasswordActivity"
android:exported="false" />
<activity
android:name=".EditProfileActivity"
android:exported="false" />
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,59 @@
package tech.mercantec.easyeat
import android.app.ProgressDialog
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.changePassword
import kotlin.concurrent.thread
class ChangePasswordActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_change_password)
findViewById<Button>(R.id.save).setOnClickListener {
val currentPassword = findViewById<EditText>(R.id.current_password).text.toString()
val newPassword = findViewById<EditText>(R.id.new_password).text.toString()
val confirmPassword = findViewById<EditText>(R.id.confirm_password).text.toString()
if (newPassword != confirmPassword) {
Toast.makeText(this, "Passwords must match", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val progressDialog = ProgressDialog(this)
progressDialog.setMessage("Loading...")
progressDialog.show()
thread {
try {
changePassword(this, currentPassword, newPassword)
} catch (e: ApiRequestException) {
runOnUiThread {
Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
}
return@thread
} finally {
runOnUiThread {
progressDialog.hide()
}
}
runOnUiThread {
Toast.makeText(this, "Password changed successfully", Toast.LENGTH_LONG).show()
}
finish()
}
}
}
}

View File

@ -0,0 +1,49 @@
package tech.mercantec.easyeat
import android.app.ProgressDialog
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.updateUser
import kotlin.concurrent.thread
class EditProfileActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit_profile)
findViewById<EditText>(R.id.username).setText(intent.extras?.getString("username"), TextView.BufferType.EDITABLE)
findViewById<EditText>(R.id.email).setText(intent.extras?.getString("email"), TextView.BufferType.EDITABLE)
findViewById<Button>(R.id.save).setOnClickListener {
val username = findViewById<EditText>(R.id.username).text.toString().trim()
val email = findViewById<EditText>(R.id.email).text.toString().trim()
val progressDialog = ProgressDialog(this)
progressDialog.setMessage("Loading...")
progressDialog.show()
thread {
try {
updateUser(this, username, email)
} catch (e: ApiRequestException) {
runOnUiThread {
Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
}
return@thread
} finally {
runOnUiThread {
progressDialog.hide()
}
}
finish()
}
}
}
}

View File

@ -39,8 +39,8 @@ fun request(ctx: Context, method: String, path: String, data: String?, autoRefre
outputStream.flush() outputStream.flush()
} }
if (responseCode == 401 && refreshToken != null) { if (responseCode == 401) {
if (!autoRefresh || !refreshAuthToken(ctx, refreshToken)) { if (!autoRefresh || refreshToken == null || !refreshAuthToken(ctx, refreshToken)) {
val intent = Intent(ctx, LoginActivity::class.java) val intent = Intent(ctx, LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ctx.startActivity(intent) ctx.startActivity(intent)

View File

@ -24,6 +24,16 @@ fun login(ctx: Context, email: String, password: String) {
} }
} }
fun logout(ctx: Context) {
with (ctx.getSharedPreferences("easyeat", Context.MODE_PRIVATE).edit()) {
remove("user-id")
remove("username")
remove("auth-token")
remove("refresh-token")
apply()
}
}
@Serializable @Serializable
data class CreateUserRequest(val email: String, val userName: String, val password: String) data class CreateUserRequest(val email: String, val userName: String, val password: String)
@ -79,7 +89,23 @@ fun isLoggedIn(ctx: Context): Boolean {
data class UserInfoResponse(val id: Int, val userName: String, val email: String) data class UserInfoResponse(val id: Int, val userName: String, val email: String)
fun getUserInfo(ctx: Context): UserInfoResponse { fun getUserInfo(ctx: Context): UserInfoResponse {
val response = requestJson<Unit, UserInfoResponse>(ctx, "GET", "/api/User/get", null) return requestJson<Unit, UserInfoResponse>(ctx, "GET", "/api/User/get", null)
}
return response
@Serializable
data class UpdateUserRequest(val userName: String, val email: String)
fun updateUser(ctx: Context, username: String, email: String) {
val request = UpdateUserRequest(username, email)
return requestJson<UpdateUserRequest, Unit>(ctx, "PUT", "/api/User/update", request)
}
@Serializable
data class ChangePasswordRequest(val oldPassword: String, val newPassword: String)
fun changePassword(ctx: Context, oldPassword: String, newPassword: String) {
val request = ChangePasswordRequest(oldPassword, newPassword)
return requestJson<ChangePasswordRequest, Unit>(ctx, "PUT", "/api/User/change-password", request)
} }

View File

@ -1,15 +1,21 @@
package tech.mercantec.easyeat.ui.profile package tech.mercantec.easyeat.ui.profile
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import tech.mercantec.easyeat.ChangePasswordActivity
import tech.mercantec.easyeat.EditProfileActivity
import tech.mercantec.easyeat.LoginActivity
import tech.mercantec.easyeat.WelcomeActivity
import tech.mercantec.easyeat.databinding.FragmentProfileBinding import tech.mercantec.easyeat.databinding.FragmentProfileBinding
import tech.mercantec.easyeat.helpers.ApiRequestException import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.UserInfoResponse import tech.mercantec.easyeat.helpers.UserInfoResponse
import tech.mercantec.easyeat.helpers.getUserInfo import tech.mercantec.easyeat.helpers.getUserInfo
import tech.mercantec.easyeat.helpers.logout
import kotlin.concurrent.thread import kotlin.concurrent.thread
class ProfileFragment : Fragment() { class ProfileFragment : Fragment() {
@ -31,9 +37,28 @@ class ProfileFragment : Fragment() {
activity?.runOnUiThread { activity?.runOnUiThread {
binding.username.text = userInfo.userName binding.username.text = userInfo.userName
binding.email.text = userInfo.email binding.email.text = userInfo.email
binding.editProfile.setOnClickListener {
val intent = Intent(activity, EditProfileActivity::class.java)
intent.putExtra("username", userInfo.userName)
intent.putExtra("email", userInfo.email)
startActivity(intent)
}
} }
} }
binding.changePassword.setOnClickListener {
startActivity(Intent(activity, ChangePasswordActivity::class.java))
}
binding.logout.setOnClickListener {
logout(requireContext())
val intent = Intent(activity, WelcomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
return binding.root return binding.root
} }
} }

View File

@ -1,10 +1,14 @@
package tech.mercantec.easyeat.ui.shopping_list package tech.mercantec.easyeat.ui.shopping_list
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import tech.mercantec.easyeat.R
import tech.mercantec.easyeat.databinding.FragmentShoppingListBinding import tech.mercantec.easyeat.databinding.FragmentShoppingListBinding
class ShoppingListFragment : Fragment() { class ShoppingListFragment : Fragment() {
@ -23,6 +27,27 @@ class ShoppingListFragment : Fragment() {
_binding = FragmentShoppingListBinding.inflate(inflater, container, false) _binding = FragmentShoppingListBinding.inflate(inflater, container, false)
val root: View = binding.root val root: View = binding.root
binding.addToShoppingList.setOnClickListener {
val view = requireActivity().layoutInflater.inflate(R.layout.dialog_add_to_shopping_list, null)
val dialog = AlertDialog.Builder(activity)
.setView(view)
.setPositiveButton(R.string.add_label, { dialog, id ->
})
.setNegativeButton(R.string.cancel_label, { dialog, id ->
dialog.cancel()
})
.create()
dialog.show()
val adapter = ArrayAdapter.createFromResource(requireContext(), R.array.units, android.R.layout.simple_spinner_item)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
dialog.findViewById<Spinner>(R.id.unit_selector).adapter = adapter
}
return root return root
} }

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="2dp"
android:text="@string/current_password_label"
/>
<EditText
android:id="@+id/current_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:autofillHints="password"
android:inputType="textPassword"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="2dp"
android:text="@string/new_password_label"
/>
<EditText
android:id="@+id/new_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:autofillHints="password"
android:inputType="textPassword"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="2dp"
android:text="@string/confirm_new_password_label"
/>
<EditText
android:id="@+id/confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:autofillHints="password"
android:inputType="textPassword"
/>
<Button
android:id="@+id/save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="@string/save_label"
/>
</LinearLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="2dp"
android:text="@string/username_label"
/>
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username_hint"
android:autofillHints="username"
android:inputType="text"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="2dp"
android:text="@string/email_label"
/>
<EditText
android:id="@+id/email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email_hint"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress"
/>
<Button
android:id="@+id/save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="@string/save_label"
/>
</LinearLayout>

View File

@ -17,7 +17,7 @@
<TextView <TextView
android:layout_marginTop="50sp" android:layout_marginTop="50sp"
android:layout_marginLeft="2sp" android:layout_marginStart="2sp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/email_label" android:text="@string/email_label"
@ -33,11 +33,11 @@
<TextView <TextView
android:layout_marginTop="20sp" android:layout_marginTop="20sp"
android:layout_marginLeft="2sp" android:layout_marginStart="2sp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/password_label" android:text="@string/password_label"
/> />
<EditText <EditText
android:id="@+id/password" android:id="@+id/password"

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="20dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_marginBottom="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_shopping_item_label"
android:textAlignment="center"
android:textStyle="bold"
android:textSize="18sp"
/>
<TextView
android:id="@+id/amount_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:text="@string/ingredient_amount_label"
/>
<EditText
android:id="@+id/amount"
android:layout_width="72dp"
android:layout_height="wrap_content"
android:layout_below="@id/amount_label"
android:layout_alignParentStart="true"
android:hint="@string/ingredient_amount_hint"
/>
<Spinner
android:id="@+id/unit_selector"
android:layout_width="72dp"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/amount"
android:layout_below="@+id/amount_label"
/>
<TextView
android:id="@+id/name_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_alignStart="@id/name"
android:text="@string/ingredient_name_label"
/>
<EditText
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/name_label"
android:layout_toEndOf="@id/unit_selector"
android:layout_alignParentEnd="true"
android:hint="@string/ingredient_name_hint"
/>
</RelativeLayout>

View File

@ -43,11 +43,36 @@
android:text="@string/loading" android:text="@string/loading"
/> />
<Button <LinearLayout
android:layout_marginTop="40dp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/edit_profile_label" android:orientation="vertical">
/>
</LinearLayout> <Button
android:id="@+id/edit_profile"
android:layout_marginTop="40dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/edit_profile_label"
/>
<Button
android:id="@+id/change_password"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/change_password_label"
/>
<Button
android:id="@+id/logout"
android:layout_marginTop="40dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/dark_gray"
android:text="@string/logout_label"
/>
</LinearLayout>
</LinearLayout>

View File

@ -1,9 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
tools:context=".ui.shopping_list.ShoppingListFragment">
</androidx.constraintlayout.widget.ConstraintLayout> <ListView
android:id="@+id/shopping_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@android:color/darker_gray"
android:dividerHeight="1dp"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_to_shopping_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/cyan"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white"
/>
</FrameLayout>

View File

@ -14,8 +14,12 @@
<string name="email_hint">john@doe.com</string> <string name="email_hint">john@doe.com</string>
<string name="password_label">Password</string> <string name="password_label">Password</string>
<string name="confirm_password_label">Confirm password</string> <string name="confirm_password_label">Confirm password</string>
<string name="current_password_label">Current password</string>
<string name="new_password_label">New password</string>
<string name="confirm_new_password_label">Confirm new password</string>
<string name="password_hint">&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;</string> <string name="password_hint">&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;</string>
<string name="back_label">Back</string> <string name="back_label">Back</string>
<string name="save_label">Save</string>
<string name="create_dish_heading">Create Dish</string> <string name="create_dish_heading">Create Dish</string>
<string name="ingredient_label">Ingredient</string> <string name="ingredient_label">Ingredient</string>
<string name="measurement_label">Measurement</string> <string name="measurement_label">Measurement</string>
@ -26,4 +30,19 @@
<string name="general">General</string> <string name="general">General</string>
<string name="ingredients">Ingredients</string> <string name="ingredients">Ingredients</string>
<string name="Instructions">Instructions</string> <string name="Instructions">Instructions</string>
<string name="cancel_label">Cancel</string>
<string name="add_shopping_item_label">Add shopping item</string>
<string name="ingredient_amount_label">Amount</string>
<string name="ingredient_amount_hint">500</string>
<string name="ingredient_name_label">Name</string>
<string name="ingredient_name_hint">Beef</string>
<string name="change_password_label">Change password</string>
<string name="logout_label">Log out</string>
<string-array name="units">
<item>g</item>
<item>kg</item>
<item>ml</item>
<item>dl</item>
<item>l</item>
</string-array>
</resources> </resources>