diff --git a/Documentation/Screenshots/Screenshot-2024-12-25-122152.png b/Documentation/Screenshots/Screenshot-2024-12-25-122152.png new file mode 100644 index 0000000..4569318 Binary files /dev/null and b/Documentation/Screenshots/Screenshot-2024-12-25-122152.png differ diff --git a/Documentation/Screenshots/Screenshot-2024-12-25-122254.png b/Documentation/Screenshots/Screenshot-2024-12-25-122254.png new file mode 100644 index 0000000..9e57515 Binary files /dev/null and b/Documentation/Screenshots/Screenshot-2024-12-25-122254.png differ diff --git a/Documentation/Screenshots/Screenshot-2024-12-25-122336.png b/Documentation/Screenshots/Screenshot-2024-12-25-122336.png new file mode 100644 index 0000000..f834773 Binary files /dev/null and b/Documentation/Screenshots/Screenshot-2024-12-25-122336.png differ diff --git a/Documentation/Screenshots/Screenshot-2024-12-25-122359.png b/Documentation/Screenshots/Screenshot-2024-12-25-122359.png new file mode 100644 index 0000000..ac648d4 Binary files /dev/null and b/Documentation/Screenshots/Screenshot-2024-12-25-122359.png differ diff --git a/PainTracker/app/src/main/AndroidManifest.xml b/PainTracker/app/src/main/AndroidManifest.xml index cf6e620..83287f2 100644 --- a/PainTracker/app/src/main/AndroidManifest.xml +++ b/PainTracker/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:theme="@style/Theme.PainTracker" tools:targetApi="31"> diff --git a/PainTracker/app/src/main/java/com/example/paintracker/RecordNotesFragment.kt b/PainTracker/app/src/main/java/com/example/paintracker/RecordNotesFragment.kt deleted file mode 100644 index 0a8f11e..0000000 --- a/PainTracker/app/src/main/java/com/example/paintracker/RecordNotesFragment.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.paintracker - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.example.paintracker.databinding.FragmentRecordNotesBinding - -class RecordNotesFragment : Fragment() { - - private var _binding: FragmentRecordNotesBinding? = null - - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - - _binding = FragmentRecordNotesBinding.inflate(inflater, container, false) - - return binding.root - } - - /*override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - }*/ - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/ServiceModule.kt b/PainTracker/app/src/main/java/com/example/paintracker/ServiceModule.kt index 66c28d4..50fcc1b 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/ServiceModule.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/ServiceModule.kt @@ -3,10 +3,14 @@ package com.example.paintracker import android.content.Context import com.example.paintracker.interfaces.IBitmapLoaderService import com.example.paintracker.interfaces.IConfigService +import com.example.paintracker.interfaces.IPainContext import com.example.paintracker.interfaces.IPathService import com.example.paintracker.interfaces.IVisualiserLayerIoService import com.example.paintracker.services.BitmapLoaderService import com.example.paintracker.services.ConfigService +import com.example.paintracker.data.PainContext +import com.example.paintracker.interfaces.INotesIoService +import com.example.paintracker.services.NotesIoService import com.example.paintracker.services.PathService import com.example.paintracker.services.VisualiserLayerIoService import dagger.Module @@ -47,4 +51,16 @@ object ServiceModule { fun provideBitmapLoaderService(): IBitmapLoaderService { return BitmapLoaderService() } + + @Provides + @Singleton + fun providePainContext(): IPainContext { + return PainContext() + } + + @Provides + @Singleton + fun provideNotesIoService(pathService: IPathService): INotesIoService { + return NotesIoService(pathService) + } } \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/base/Observable.kt b/PainTracker/app/src/main/java/com/example/paintracker/base/Observable.kt new file mode 100644 index 0000000..497037d --- /dev/null +++ b/PainTracker/app/src/main/java/com/example/paintracker/base/Observable.kt @@ -0,0 +1,17 @@ +package com.example.paintracker.base + +open class Observable { + private val listeners = mutableListOf<(String, Any?, Any?) -> Unit>() + + fun addChangeListener(listener: (propertyName: String, oldValue: Any?, newValue: Any?) -> Unit) { + listeners.add(listener) + } + + fun removeChangeListener(listener: (propertyName: String, oldValue: Any?, newValue: Any?) -> Unit) { + listeners.remove(listener) + } + + protected fun notifyListeners(propertyName: String, oldValue: Any?, newValue: Any?) { + listeners.forEach { it(propertyName, oldValue, newValue) } + } +} \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/data/PainContext.kt b/PainTracker/app/src/main/java/com/example/paintracker/data/PainContext.kt new file mode 100644 index 0000000..11a604c --- /dev/null +++ b/PainTracker/app/src/main/java/com/example/paintracker/data/PainContext.kt @@ -0,0 +1,16 @@ +package com.example.paintracker.data + +import com.example.paintracker.base.Observable +import com.example.paintracker.interfaces.IPainContext +import java.time.LocalDate +import kotlin.properties.Delegates + +class PainContext : Observable(), IPainContext { + override var selectedDate: LocalDate by Delegates.observable(LocalDate.now()) { _, oldValue, newValue -> + notifyListeners("selectedDate", oldValue, newValue) + } + + override var showAllLayers: Boolean by Delegates.observable(false) { _, oldValue, newValue -> + notifyListeners("showAllLayers", oldValue, newValue) + } +} \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/interfaces/INotesIoService.kt b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/INotesIoService.kt new file mode 100644 index 0000000..06c52a6 --- /dev/null +++ b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/INotesIoService.kt @@ -0,0 +1,8 @@ +package com.example.paintracker.interfaces + +import java.time.LocalDate + +interface INotesIoService { + fun loadNotes(date: LocalDate): String? + fun saveNotes(date: LocalDate, notes: String) +} \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IPainContext.kt b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IPainContext.kt new file mode 100644 index 0000000..7899831 --- /dev/null +++ b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IPainContext.kt @@ -0,0 +1,8 @@ +package com.example.paintracker.interfaces + +import java.time.LocalDate + +interface IPainContext { + var selectedDate: LocalDate + var showAllLayers: Boolean +} \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IPathService.kt b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IPathService.kt index c2d7e1c..fcfe277 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IPathService.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IPathService.kt @@ -1,12 +1,16 @@ package com.example.paintracker.interfaces import android.content.Context +import com.example.paintracker.data.VisualiserLayer +import java.nio.file.Path +import java.time.LocalDate -enum class Path { +enum class SpecialPath { APPDATAROOT } interface IPathService { fun initialize(context: Context) - fun getPath(path: Path): String + fun getPath(path: SpecialPath): String + fun getVisualLayerPath(date: LocalDate, layer: VisualiserLayer) : Path } \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IVisualiserLayerIoService.kt b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IVisualiserLayerIoService.kt index 7f49e0c..ed4895e 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IVisualiserLayerIoService.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/interfaces/IVisualiserLayerIoService.kt @@ -11,4 +11,5 @@ interface IVisualiserLayerIoService { fun loadAll(localDate: LocalDate, layers: MutableList, width: Int, height: Int) fun saveLayer(localDate: LocalDate, layer: VisualiserLayer, side: Side) fun deleteLayer(localDate: LocalDate, layer: VisualiserLayer, side: Side) + fun deleteLayers(localDate: LocalDate, layers: MutableList) } \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/services/NotesIoService.kt b/PainTracker/app/src/main/java/com/example/paintracker/services/NotesIoService.kt new file mode 100644 index 0000000..16db47e --- /dev/null +++ b/PainTracker/app/src/main/java/com/example/paintracker/services/NotesIoService.kt @@ -0,0 +1,45 @@ +package com.example.paintracker.services + +import com.example.paintracker.interfaces.INotesIoService +import com.example.paintracker.interfaces.IPathService +import com.example.paintracker.interfaces.SpecialPath +import java.io.File +import java.nio.file.Paths +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +class NotesIoService @Inject constructor( + private val pathService: IPathService +) : INotesIoService { + + override fun loadNotes(date: LocalDate): String? { + val dataRoot = pathService.getPath(SpecialPath.APPDATAROOT) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val datePart = date.format(formatter) + val datePath = Paths.get(dataRoot).resolve(datePart) + val notesPath = datePath.resolve("notes.txt") + val notes = File(notesPath.toString()) + if (notes.exists()) { + val fileContents = notes.readText() + return fileContents + } + else { + return null + } + } + + override fun saveNotes(date: LocalDate, notes: String) { + val dataRoot = pathService.getPath(SpecialPath.APPDATAROOT) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val datePart = date.format(formatter) + val datePath = Paths.get(dataRoot).resolve(datePart) + val notesPath = datePath.resolve("notes.txt") + val notesDirectory = datePath.toFile() + if (!notesDirectory.exists()) { + notesDirectory.mkdirs() + } + + File(notesPath.toString()).writeText(notes) + } +} \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/services/PathService.kt b/PainTracker/app/src/main/java/com/example/paintracker/services/PathService.kt index 6b6397c..21dce99 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/services/PathService.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/services/PathService.kt @@ -1,9 +1,13 @@ package com.example.paintracker.services import android.content.Context +import com.example.paintracker.data.VisualiserLayer import com.example.paintracker.interfaces.IPathService -import com.example.paintracker.interfaces.Path +import com.example.paintracker.interfaces.SpecialPath +import java.nio.file.Path import java.nio.file.Paths +import java.time.LocalDate +import java.time.format.DateTimeFormatter class PathService : IPathService { private lateinit var _context: Context @@ -12,9 +16,18 @@ class PathService : IPathService { _context = context } - override fun getPath(path: Path): String { + override fun getPath(path: SpecialPath): String { return when (path) { - Path.APPDATAROOT -> Paths.get(_context.filesDir.absolutePath, "data").toString() + SpecialPath.APPDATAROOT -> Paths.get(_context.filesDir.absolutePath, "data").toString() } } + + override fun getVisualLayerPath(date: LocalDate, layer: VisualiserLayer) : Path { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val datePart = date.format(formatter) + val dataRoot = getPath(SpecialPath.APPDATAROOT) + val datePath = Paths.get(dataRoot).resolve(datePart) + val layerPath = datePath.resolve(layer.painCategory?.id) + return layerPath + } } \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/services/VisualiserLayerIoService.kt b/PainTracker/app/src/main/java/com/example/paintracker/services/VisualiserLayerIoService.kt index 39c9774..150d3e5 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/services/VisualiserLayerIoService.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/services/VisualiserLayerIoService.kt @@ -6,13 +6,11 @@ import com.example.paintracker.data.VisualiserLayer import com.example.paintracker.interfaces.IBitmapLoaderService import com.example.paintracker.interfaces.IPathService import com.example.paintracker.interfaces.IVisualiserLayerIoService -import com.example.paintracker.interfaces.Path import com.example.paintracker.interfaces.Side import java.io.File import java.io.FileOutputStream import java.io.IOException import java.nio.file.Files -import java.nio.file.Paths import java.time.LocalDate import java.time.format.DateTimeFormatter import javax.inject.Inject @@ -24,16 +22,13 @@ class VisualiserLayerIoService @Inject constructor( private val bitmapLoaderService: IBitmapLoaderService ) : IVisualiserLayerIoService { - private val dataRoot: String by lazy { pathService.getPath(Path.APPDATAROOT) } - override fun loadAll(localDate: LocalDate, layers: MutableList, width: Int, height: Int) { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val datePart = localDate.format(formatter) Log.i("VisualiserLayerIoService", "Loading all images for '${datePart}', width = ${width}, height = ${height}.") - val datePath = Paths.get(dataRoot).resolve(datePart) for (visualLayer in layers) { - val layerPath = datePath.resolve(visualLayer.painCategory?.id) + val layerPath = pathService.getVisualLayerPath(localDate, visualLayer) val frontPath = layerPath.resolve("front.png") val backPath = layerPath.resolve("back.png") @@ -44,21 +39,18 @@ class VisualiserLayerIoService @Inject constructor( if(frontPath.exists()) { Log.i("VisualiserLayerIoService","Loading front image '${frontPath.parent}'") - visualLayer.frontDrawing = BitmapFactory.decodeFile(frontPath.toString()) + visualLayer.frontDrawing = BitmapFactory.decodeFile(frontPath.toString()) // bitmapLoaderService.loadBitmap(frontPath.toString(), width, height) } if(backPath.exists()) { Log.i("VisualiserLayerIoService","Loading back image '${backPath.parent}'") - visualLayer.backDrawing = BitmapFactory.decodeFile(backPath.toString()) + visualLayer.backDrawing = BitmapFactory.decodeFile(backPath.toString()) // bitmapLoaderService.loadBitmap(backPath.toString(), width, height) } } } override fun saveLayer(localDate: LocalDate, layer: VisualiserLayer, side: Side) { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val datePart = localDate.format(formatter) - val datePath = Paths.get(dataRoot).resolve(datePart) - val layerPath = datePath.resolve(layer.painCategory?.id) + val layerPath = pathService.getVisualLayerPath(localDate, layer) val frontPath = layerPath.resolve("front.png") val backPath = layerPath.resolve("back.png") val path = if (side == Side.FRONT) frontPath else backPath @@ -74,10 +66,7 @@ class VisualiserLayerIoService @Inject constructor( } override fun deleteLayer(localDate: LocalDate, layer: VisualiserLayer, side: Side) { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val datePart = localDate.format(formatter) - val datePath = Paths.get(dataRoot).resolve(datePart) - val layerPath = datePath.resolve(layer.painCategory?.id) + val layerPath = pathService.getVisualLayerPath(localDate, layer) val frontPath = layerPath.resolve("front.png") val backPath = layerPath.resolve("back.png") val path = if (side == Side.FRONT) frontPath else backPath @@ -94,6 +83,18 @@ class VisualiserLayerIoService @Inject constructor( } } + override fun deleteLayers(localDate: LocalDate, layers: MutableList) { + for (visualLayer in layers) { + val layerPath = pathService.getVisualLayerPath(localDate, visualLayer) + val frontPath = layerPath.resolve("front.png") + val backPath = layerPath.resolve("back.png") + Files.deleteIfExists(frontPath) + Files.deleteIfExists(backPath) + visualLayer.frontDrawing = null + visualLayer.backDrawing = null + } + } + private fun saveBitmapToFile(bitmap: Bitmap, path: String) { val file = File(path) try { diff --git a/PainTracker/app/src/main/java/com/example/paintracker/MainActivity.kt b/PainTracker/app/src/main/java/com/example/paintracker/ui/MainActivity.kt similarity index 68% rename from PainTracker/app/src/main/java/com/example/paintracker/MainActivity.kt rename to PainTracker/app/src/main/java/com/example/paintracker/ui/MainActivity.kt index c6cc28a..30ad84b 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/MainActivity.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/ui/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.paintracker +package com.example.paintracker.ui import android.app.DatePickerDialog import android.os.Bundle @@ -10,16 +10,23 @@ import androidx.navigation.ui.setupActionBarWithNavController import android.view.Menu import android.view.MenuItem import android.widget.Toast -import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController +import com.example.paintracker.R import com.example.paintracker.databinding.ActivityMainBinding +import com.example.paintracker.interfaces.IPainContext import com.google.android.material.bottomnavigation.BottomNavigationView +import dagger.hilt.android.AndroidEntryPoint import java.time.LocalDate +import javax.inject.Inject +@AndroidEntryPoint class MainActivity : AppCompatActivity() { + @Inject + lateinit var painContext: IPainContext private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var binding: ActivityMainBinding + private var selectedDate: LocalDate = LocalDate.now() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -63,30 +70,18 @@ class MainActivity : AppCompatActivity() { } private fun showDatePickerDialog() { - val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_content_main) as? NavHostFragment - val currentFragment = navHostFragment?.childFragmentManager?.primaryNavigationFragment - var painVisualiser: PainVisualiser? = null - if (currentFragment is RecordPainFragment) { - painVisualiser = currentFragment.painVisualiser - } - - if(painVisualiser == null) { - Toast.makeText(this, "Pain visualiser not found", Toast.LENGTH_SHORT).show() - return - } - - val year = painVisualiser.selectedDate.year //calendar.get(Calendar.YEAR) - val month = painVisualiser.selectedDate.monthValue - 1 //calendar.get(Calendar.MONTH) - val day = painVisualiser.selectedDate.dayOfMonth //calendar.get(Calendar.DAY_OF_MONTH) + val year = selectedDate.year + val month = selectedDate.monthValue - 1 + val day = selectedDate.dayOfMonth val datePickerDialog = DatePickerDialog( this, { _, selectedYear, selectedMonth, selectedDay -> - // Handle the selected date - val selectedDate = "$selectedDay/${selectedMonth + 1}/$selectedYear" - Toast.makeText(this, "Selected Date: $selectedDate", Toast.LENGTH_SHORT).show() + val curSelectedDate = "$selectedDay/${selectedMonth + 1}/$selectedYear" + Toast.makeText(this, "Selected Date: $curSelectedDate", Toast.LENGTH_SHORT).show() - painVisualiser.selectedDate = LocalDate.of(selectedYear, selectedMonth + 1, selectedDay) + selectedDate = LocalDate.of(selectedYear, selectedMonth + 1, selectedDay) + painContext.selectedDate = selectedDate }, year, month, diff --git a/PainTracker/app/src/main/java/com/example/paintracker/ui/RecordNotesFragment.kt b/PainTracker/app/src/main/java/com/example/paintracker/ui/RecordNotesFragment.kt new file mode 100644 index 0000000..119e966 --- /dev/null +++ b/PainTracker/app/src/main/java/com/example/paintracker/ui/RecordNotesFragment.kt @@ -0,0 +1,103 @@ +package com.example.paintracker.ui + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout.INVISIBLE +import android.widget.LinearLayout.VISIBLE +import com.example.paintracker.databinding.FragmentRecordNotesBinding +import com.example.paintracker.interfaces.IPainContext +import com.example.paintracker.interfaces.IPathService +import com.example.paintracker.data.PainContext +import com.example.paintracker.interfaces.INotesIoService +import com.google.android.material.floatingactionbutton.FloatingActionButton +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class RecordNotesFragment : Fragment () { + @Inject lateinit var painContext: IPainContext + @Inject lateinit var pathService: IPathService + @Inject lateinit var notesIoService: INotesIoService + + private var _binding: FragmentRecordNotesBinding? = null + + private val binding get() = _binding!! + private var saveButton: FloatingActionButton? = null + private var isDirty: Boolean = false + private var existingNotes: String? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentRecordNotesBinding.inflate(inflater, container, false) + + saveButton = _binding!!.saveButton + + saveButton!!.setOnClickListener { + saveNotes() + isDirty = false + reflectIsDirty() + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.notesEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + } + + override fun afterTextChanged(s: Editable?) { + isDirty = (existingNotes != s.toString()) + reflectIsDirty() + } + }) + + val painContext: PainContext = painContext as PainContext + painContext.addChangeListener { propertyName, oldValue, newValue -> + loadNotes() + } + + loadNotes() + } + + override fun onResume() { + super.onResume() + + loadNotes() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun reflectIsDirty() { + saveButton!!.visibility = if (isDirty) VISIBLE else INVISIBLE + } + + private fun loadNotes() { + if(_binding == null) { + return + } + + existingNotes = notesIoService.loadNotes(painContext.selectedDate) + binding.notesEditText.setText(existingNotes) + } + + private fun saveNotes() { + val notesContent = binding.notesEditText.text.toString() + notesIoService.saveNotes(painContext.selectedDate, notesContent) + } +} \ No newline at end of file diff --git a/PainTracker/app/src/main/java/com/example/paintracker/RecordPainFragment.kt b/PainTracker/app/src/main/java/com/example/paintracker/ui/RecordPainFragment.kt similarity index 50% rename from PainTracker/app/src/main/java/com/example/paintracker/RecordPainFragment.kt rename to PainTracker/app/src/main/java/com/example/paintracker/ui/RecordPainFragment.kt index 748b28d..458c045 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/RecordPainFragment.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/ui/RecordPainFragment.kt @@ -1,4 +1,4 @@ -package com.example.paintracker +package com.example.paintracker.ui import android.os.Bundle import androidx.fragment.app.Fragment @@ -6,18 +6,22 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.example.paintracker.databinding.FragmentRecordPainBinding - -/** - * A simple [Fragment] subclass as the default destination in the navigation. - */ +import com.example.paintracker.interfaces.IPainContext +import com.example.paintracker.data.PainContext +import com.example.paintracker.ui.widgets.PainVisualiser +import dagger.hilt.android.AndroidEntryPoint +import java.time.LocalDate +import javax.inject.Inject + +@AndroidEntryPoint class RecordPainFragment : Fragment() { + @Inject + lateinit var painContext: IPainContext var painVisualiser: PainVisualiser? = null private var _binding: FragmentRecordPainBinding? = null - // This property is only valid between onCreateView and - // onDestroyView. private val binding get() = _binding!! override fun onCreateView( @@ -27,14 +31,27 @@ class RecordPainFragment : Fragment() { _binding = FragmentRecordPainBinding.inflate(inflater, container, false) - painVisualiser = _binding!!.painVisualiser - return binding.root } - /*override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - }*/ + + painVisualiser = _binding!!.painVisualiser + + val painContext: PainContext = painContext as PainContext + painContext.addChangeListener { propertyName, oldValue, newValue -> + if(propertyName == "selectedDate") { + painVisualiser!!.selectedDate = newValue as LocalDate + } + } + } + + override fun onResume() { + super.onResume() + + painVisualiser!!.selectedDate = painContext.selectedDate + } override fun onDestroyView() { super.onDestroyView() diff --git a/PainTracker/app/src/main/java/com/example/paintracker/PainVisualiser.kt b/PainTracker/app/src/main/java/com/example/paintracker/ui/widgets/PainVisualiser.kt similarity index 91% rename from PainTracker/app/src/main/java/com/example/paintracker/PainVisualiser.kt rename to PainTracker/app/src/main/java/com/example/paintracker/ui/widgets/PainVisualiser.kt index 9ab0a5d..2ee85ec 100644 --- a/PainTracker/app/src/main/java/com/example/paintracker/PainVisualiser.kt +++ b/PainTracker/app/src/main/java/com/example/paintracker/ui/widgets/PainVisualiser.kt @@ -1,322 +1,328 @@ -package com.example.paintracker - -import android.app.AlertDialog -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Bitmap -import android.graphics.Canvas -import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.widget.Button -import android.widget.CheckBox -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.PopupMenu -import com.example.paintracker.data.PainCategory -import com.example.paintracker.data.VisualiserLayer -import com.example.paintracker.interfaces.IConfigService -import com.example.paintracker.interfaces.IVisualiserLayerIoService -import com.example.paintracker.interfaces.Side -import com.github.gcacace.signaturepad.views.SignaturePad -import com.google.android.material.floatingactionbutton.FloatingActionButton -import dagger.hilt.android.EntryPointAccessors -import java.time.LocalDate - -class PainVisualiser @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { - - private val configService: IConfigService - private val visualiserLayerIoService: IVisualiserLayerIoService - - var selectedDate: LocalDate = LocalDate.now() - set(value) { - field = value - - visualiserLayerIoService.loadAll(value, visualLayers, signaturePad.width, signaturePad.height) - frontDrawing = selectedVisualiserLayer?.frontDrawing - backDrawing = selectedVisualiserLayer?.backDrawing - isDirty = false - reflectIsDirty() - - switchDrawing() - } - private var painCategories: List = emptyList() - set(value) { - field = value - if (value.isNotEmpty()) { - Log.i("VisualiserLayerIoService","Pain categories have changed, updating visual layers.") - visualLayers.clear() - value.forEach { category -> - visualLayers.add(VisualiserLayer(category, null, null)) - } - - selectedCategory = value[0] - updateSelectedVisualLayer() - updateCategoryButtonColor() - updateDrawingColor() - - visualiserLayerIoService.loadAll(selectedDate, visualLayers, signaturePad.width, signaturePad.height) - frontDrawing = selectedVisualiserLayer?.frontDrawing - backDrawing = selectedVisualiserLayer?.backDrawing - isDirty = false - reflectIsDirty() - - switchDrawing() - } - } - - private var signaturePad: SignaturePad - private var imageView: ImageView - private var frontButton: Button - private var backButton: Button - private var categoryButton: FloatingActionButton - private var saveButton: FloatingActionButton - private var deleteButton: FloatingActionButton - private var showAllButton: CheckBox - private var isFront = true - private var showAllLayers = false - private var isDirty = false - private var selectedCategory: PainCategory? = null - private var selectedVisualiserLayer: VisualiserLayer? = null - - // Images for front and back - private val frontImageRes = R.drawable.body_front - private val backImageRes = R.drawable.body_back - - // Storage for drawing data - private var frontDrawing: Bitmap? = null - private var backDrawing: Bitmap? = null - - private var visualLayers: MutableList = mutableListOf() - - init { - Log.i("VisualiserLayerIoService","Initializing PainVisualizer...") - - val entryPoint = EntryPointAccessors.fromApplication( - context.applicationContext, - HiltServiceBridge::class.java - ) - configService = entryPoint.getConfigService() - visualiserLayerIoService = entryPoint.getVisualiserLayerIoService() - - // Inflate the layout - LayoutInflater.from(context).inflate(R.layout.pain_visualiser, this, true) - - // Find views - signaturePad = findViewById(R.id.signaturePad) - imageView = findViewById(R.id.imageView) - frontButton = findViewById(R.id.buttonFront) - backButton = findViewById(R.id.buttonBack) - categoryButton = findViewById(R.id.categoryButton) - showAllButton = findViewById(R.id.showAllButton) - saveButton = findViewById(R.id.saveButton) - deleteButton = findViewById(R.id.deleteButton) - - // Set initial image - imageView.setImageResource(frontImageRes) - - frontButton.setOnClickListener { - checkAndSaveIfDirty { - isFront = true - imageView.setImageResource(frontImageRes) - switchDrawing() - } - } - - backButton.setOnClickListener { - checkAndSaveIfDirty { - isFront = false - imageView.setImageResource(backImageRes) - switchDrawing() - } - } - - categoryButton.setOnClickListener { showCategoryDropdown() } - - showAllButton.setOnClickListener { - checkAndSaveIfDirty { - showAllLayers = showAllButton.isChecked - val signaturePad = findViewById(R.id.signaturePad) - signaturePad.isEnabled = !showAllLayers - switchDrawing() - } - } - - saveButton.setOnClickListener { - saveCurrentDrawing() - isDirty = false - reflectIsDirty() - } - - deleteButton.setOnClickListener { - if(!showAllLayers) { // Can't delete all layers yet - visualiserLayerIoService.deleteLayer(selectedDate, selectedVisualiserLayer!!, if(isFront) Side.FRONT else Side.BACK) - frontDrawing = selectedVisualiserLayer?.frontDrawing - backDrawing = selectedVisualiserLayer?.backDrawing - isDirty = false - reflectIsDirty() - switchDrawing() - } - } - - signaturePad.setOnSignedListener(object : SignaturePad.OnSignedListener { - override fun onSigned() { - //isDirty = true - //reflectIsDirty() - } - - override fun onClear() { - isDirty = false - reflectIsDirty() - } - - override fun onStartSigning() { - isDirty = true - reflectIsDirty() - } - }) - - painCategories = configService.getCurrent().painCategories - Log.i("VisualiserLayerIoService","PainVisualizer initialized.") - } - - private fun reflectIsDirty() { - saveButton.visibility = if (isDirty) VISIBLE else INVISIBLE - } - - private fun mergeAllLayers(): Bitmap { - Log.i("VisualiserLayerIoService","Merging all layers.") - val resultBitmap = Bitmap.createBitmap(signaturePad.width, signaturePad.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(resultBitmap) - - for (visualLayer in visualLayers) { - val drawing = if (isFront) visualLayer.frontDrawing else visualLayer.backDrawing - drawing?.let { canvas.drawBitmap(it, 0f, 0f, null) } - } - - return resultBitmap - } - - private fun checkAndSaveIfDirty(onCompleted: () -> Unit) { - if (isDirty) { - Log.i("VisualiserLayerIoService","Changes have not been saved, prompting to save.") - AlertDialog.Builder(context) - .setTitle("Unsaved changes") - .setMessage("Do you want to save your changes?") - .setPositiveButton("Yes") { _, _ -> - saveCurrentDrawing() - onCompleted() // Proceed after saving - } - .setNegativeButton("No") { _, _ -> - onCompleted() // Proceed without saving - } - .setOnCancelListener { - onCompleted() // Treat cancel as proceeding without saving - } - .show() - } else { - onCompleted() // If not dirty, proceed immediately - } - } - - private fun updateSelectedVisualLayer() { - Log.i("VisualiserLayerIoService","Updating selected visual layer.") - val curSelectedVisualiserLayer = visualLayers.find { it.painCategory == selectedCategory } - if(curSelectedVisualiserLayer != null) - { - frontDrawing = curSelectedVisualiserLayer.frontDrawing - backDrawing = curSelectedVisualiserLayer.backDrawing - selectedVisualiserLayer = curSelectedVisualiserLayer - } - } - - private fun saveCurrentDrawing() { - Log.i("VisualiserLayerIoService","Saving current visual layer.") - val bitmap = Bitmap.createBitmap( - signaturePad.width, - signaturePad.height, - Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap) - - signaturePad.draw(canvas) - - if (isFront) { - frontDrawing = bitmap - selectedVisualiserLayer?.frontDrawing = frontDrawing - } else { - backDrawing = bitmap - selectedVisualiserLayer?.backDrawing = backDrawing - } - - visualiserLayerIoService.saveLayer(selectedDate, selectedVisualiserLayer!!, if(isFront) Side.FRONT else Side.BACK) - deleteButton.visibility = VISIBLE - } - - private fun switchDrawing() { - Log.i("VisualiserLayerIoService","Switching to ${if (isFront) "front" else "back"}.") - var drawingToShow = if (isFront) frontDrawing else backDrawing - drawingToShow = if (showAllLayers) mergeAllLayers() else drawingToShow - - if (drawingToShow != null) { - signaturePad.signatureBitmap = drawingToShow - deleteButton.visibility = if(showAllLayers) INVISIBLE else VISIBLE - } else { - signaturePad.clear() - deleteButton.visibility = INVISIBLE - } - } - - private fun updateCategoryButtonColor() { - Log.i("VisualiserLayerIoService","Updating category button colour.") - selectedCategory?.let { - categoryButton.backgroundTintList = ColorStateList.valueOf(it.colour) - } - } - - private fun updateDrawingColor() { - Log.i("VisualiserLayerIoService","Updating drawing colour.") - selectedCategory?.let { - signaturePad.setPenColor(it.colour) - } - } - - private fun showCategoryDropdown() { - Log.i("VisualiserLayerIoService","Showing pain category dropdown.") - val popupMenu = PopupMenu(context, categoryButton) - painCategories.forEachIndexed { index, category -> - val menuItem = popupMenu.menu.add(0, index, index, category.displayName) - menuItem.isChecked = !showAllLayers && category == selectedCategory - menuItem.isCheckable = true - } - - popupMenu.setOnMenuItemClickListener { menuItem -> - Log.i("VisualiserLayerIoService","Selected pain category.") - - for (i in 0 until popupMenu.menu.size()) { - popupMenu.menu.getItem(i).isChecked = false - } - - checkAndSaveIfDirty { - showAllLayers = false - showAllButton.isChecked = showAllLayers - signaturePad.isEnabled = !showAllLayers - menuItem.isChecked = true - val selectedIndex = menuItem.itemId - selectedCategory = painCategories[selectedIndex] - updateSelectedVisualLayer() - updateCategoryButtonColor() - updateDrawingColor() - switchDrawing() - } - - true - } - - popupMenu.show() - } -} +package com.example.paintracker.ui.widgets + +import android.app.AlertDialog +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Bitmap +import android.graphics.Canvas +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.widget.Button +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.PopupMenu +import com.example.paintracker.HiltServiceBridge +import com.example.paintracker.R +import com.example.paintracker.data.PainCategory +import com.example.paintracker.data.VisualiserLayer +import com.example.paintracker.interfaces.IConfigService +import com.example.paintracker.interfaces.IVisualiserLayerIoService +import com.example.paintracker.interfaces.Side +import com.github.gcacace.signaturepad.views.SignaturePad +import com.google.android.material.floatingactionbutton.FloatingActionButton +import dagger.hilt.android.EntryPointAccessors +import java.time.LocalDate + +class PainVisualiser @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val configService: IConfigService + private val visualiserLayerIoService: IVisualiserLayerIoService + private val frontImageRes = R.drawable.body_front + private val backImageRes = R.drawable.body_back + + var selectedDate: LocalDate = LocalDate.now() + set(value) { + if(field == value) { + return + } + + field = value + + visualiserLayerIoService.loadAll(value, visualLayers, signaturePad.width, signaturePad.height) + frontDrawing = selectedVisualiserLayer?.frontDrawing + backDrawing = selectedVisualiserLayer?.backDrawing + isDirty = false + reflectIsDirty() + + switchDrawing() + } + private var painCategories: List = emptyList() + set(value) { + field = value + if (value.isNotEmpty()) { + Log.i("VisualiserLayerIoService","Pain categories have changed, updating visual layers.") + visualLayers.clear() + value.forEach { category -> + visualLayers.add(VisualiserLayer(category, null, null)) + } + + selectedCategory = value[0] + updateSelectedVisualLayer() + updateCategoryButtonColor() + updateDrawingColor() + + visualiserLayerIoService.loadAll(selectedDate, visualLayers, signaturePad.width, signaturePad.height) + frontDrawing = selectedVisualiserLayer?.frontDrawing + backDrawing = selectedVisualiserLayer?.backDrawing + isDirty = false + reflectIsDirty() + + switchDrawing() + } + } + + + private var signaturePad: SignaturePad + private var imageView: ImageView + private var frontButton: Button + private var backButton: Button + private var categoryButton: FloatingActionButton + private var saveButton: FloatingActionButton + private var deleteButton: FloatingActionButton + private var showAllButton: CheckBox + private var isFront = true + private var showAllLayers = false + private var isDirty = false + private var selectedCategory: PainCategory? = null + private var selectedVisualiserLayer: VisualiserLayer? = null + private var frontDrawing: Bitmap? = null + private var backDrawing: Bitmap? = null + private var visualLayers: MutableList = mutableListOf() + + init { + Log.i("VisualiserLayerIoService","Initializing PainVisualizer...") + + val entryPoint = EntryPointAccessors.fromApplication( + context.applicationContext, + HiltServiceBridge::class.java + ) + configService = entryPoint.getConfigService() + visualiserLayerIoService = entryPoint.getVisualiserLayerIoService() + + LayoutInflater.from(context).inflate(R.layout.pain_visualiser, this, true) + + signaturePad = findViewById(R.id.signaturePad) + imageView = findViewById(R.id.imageView) + frontButton = findViewById(R.id.buttonFront) + backButton = findViewById(R.id.buttonBack) + categoryButton = findViewById(R.id.categoryButton) + showAllButton = findViewById(R.id.showAllButton) + saveButton = findViewById(R.id.saveButton) + deleteButton = findViewById(R.id.deleteButton) + + imageView.setImageResource(frontImageRes) + + frontButton.setOnClickListener { + checkAndSaveIfDirty { + isFront = true + imageView.setImageResource(frontImageRes) + switchDrawing() + } + } + + backButton.setOnClickListener { + checkAndSaveIfDirty { + isFront = false + imageView.setImageResource(backImageRes) + switchDrawing() + } + } + + categoryButton.setOnClickListener { showCategoryDropdown() } + + showAllButton.setOnClickListener { + checkAndSaveIfDirty { + showAllLayers = showAllButton.isChecked + val signaturePad = findViewById(R.id.signaturePad) + signaturePad.isEnabled = !showAllLayers + switchDrawing() + } + } + + saveButton.setOnClickListener { + saveCurrentDrawing() + isDirty = false + reflectIsDirty() + } + + deleteButton.setOnClickListener { + if(showAllLayers) { + visualiserLayerIoService.deleteLayers(selectedDate, visualLayers) + } + else { + visualiserLayerIoService.deleteLayer(selectedDate, selectedVisualiserLayer!!, if(isFront) Side.FRONT else Side.BACK) + } + frontDrawing = selectedVisualiserLayer?.frontDrawing + backDrawing = selectedVisualiserLayer?.backDrawing + isDirty = false + reflectIsDirty() + switchDrawing() + } + + signaturePad.setOnSignedListener(object : SignaturePad.OnSignedListener { + override fun onSigned() { + //isDirty = true + //reflectIsDirty() + } + + override fun onClear() { + isDirty = false + reflectIsDirty() + } + + override fun onStartSigning() { + isDirty = true + reflectIsDirty() + } + }) + + painCategories = configService.getCurrent().painCategories + Log.i("VisualiserLayerIoService","PainVisualizer initialized.") + } + + private fun reflectIsDirty() { + saveButton.visibility = if (isDirty) VISIBLE else INVISIBLE + } + + private fun mergeAllLayers(): Bitmap? { + Log.i("VisualiserLayerIoService","Merging all layers.") + val resultBitmap = Bitmap.createBitmap(signaturePad.width, signaturePad.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(resultBitmap) + var containsData = false + + for (visualLayer in visualLayers) { + val drawing = if (isFront) visualLayer.frontDrawing else visualLayer.backDrawing + if(drawing != null) { + containsData = true + drawing.let { canvas.drawBitmap(it, 0f, 0f, null) } + } + } + + return if(containsData) resultBitmap else null + } + + private fun checkAndSaveIfDirty(onCompleted: () -> Unit) { + if (isDirty) { + Log.i("VisualiserLayerIoService","Changes have not been saved, prompting to save.") + AlertDialog.Builder(context) + .setTitle("Unsaved changes") + .setMessage("Do you want to save your changes?") + .setPositiveButton("Yes") { _, _ -> + saveCurrentDrawing() + onCompleted() // Proceed after saving + } + .setNegativeButton("No") { _, _ -> + onCompleted() // Proceed without saving + } + .setOnCancelListener { + onCompleted() // Treat cancel as proceeding without saving + } + .show() + } else { + onCompleted() // If not dirty, proceed immediately + } + } + + private fun updateSelectedVisualLayer() { + Log.i("VisualiserLayerIoService","Updating selected visual layer.") + val curSelectedVisualiserLayer = visualLayers.find { it.painCategory == selectedCategory } + if(curSelectedVisualiserLayer != null) + { + frontDrawing = curSelectedVisualiserLayer.frontDrawing + backDrawing = curSelectedVisualiserLayer.backDrawing + selectedVisualiserLayer = curSelectedVisualiserLayer + } + } + + private fun saveCurrentDrawing() { + Log.i("VisualiserLayerIoService","Saving current visual layer.") + val bitmap = Bitmap.createBitmap( + signaturePad.width, + signaturePad.height, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + + signaturePad.draw(canvas) + + if (isFront) { + frontDrawing = bitmap + selectedVisualiserLayer?.frontDrawing = frontDrawing + } else { + backDrawing = bitmap + selectedVisualiserLayer?.backDrawing = backDrawing + } + + visualiserLayerIoService.saveLayer(selectedDate, selectedVisualiserLayer!!, if(isFront) Side.FRONT else Side.BACK) + deleteButton.visibility = VISIBLE + } + + private fun switchDrawing() { + Log.i("VisualiserLayerIoService","Switching to ${if (isFront) "front" else "back"}.") + var drawingToShow = if (isFront) frontDrawing else backDrawing + drawingToShow = if (showAllLayers) mergeAllLayers() else drawingToShow + + if (drawingToShow != null) { + signaturePad.signatureBitmap = drawingToShow + deleteButton.visibility = VISIBLE + } else { + signaturePad.clear() + deleteButton.visibility = INVISIBLE + } + } + + private fun updateCategoryButtonColor() { + Log.i("VisualiserLayerIoService","Updating category button colour.") + selectedCategory?.let { + categoryButton.backgroundTintList = ColorStateList.valueOf(it.colour) + } + } + + private fun updateDrawingColor() { + Log.i("VisualiserLayerIoService","Updating drawing colour.") + selectedCategory?.let { + signaturePad.setPenColor(it.colour) + } + } + + private fun showCategoryDropdown() { + Log.i("VisualiserLayerIoService","Showing pain category dropdown.") + val popupMenu = PopupMenu(context, categoryButton) + painCategories.forEachIndexed { index, category -> + val menuItem = popupMenu.menu.add(0, index, index, category.displayName) + menuItem.isChecked = !showAllLayers && category == selectedCategory + menuItem.isCheckable = true + } + + popupMenu.setOnMenuItemClickListener { menuItem -> + Log.i("VisualiserLayerIoService","Selected pain category.") + + for (i in 0 until popupMenu.menu.size()) { + popupMenu.menu.getItem(i).isChecked = false + } + + checkAndSaveIfDirty { + showAllLayers = false + showAllButton.isChecked = showAllLayers + signaturePad.isEnabled = !showAllLayers + menuItem.isChecked = true + val selectedIndex = menuItem.itemId + selectedCategory = painCategories[selectedIndex] + updateSelectedVisualLayer() + updateCategoryButtonColor() + updateDrawingColor() + switchDrawing() + } + + true + } + + popupMenu.show() + } +} diff --git a/PainTracker/app/src/main/res/layout/activity_main.xml b/PainTracker/app/src/main/res/layout/activity_main.xml index 6d45d0f..1d6129c 100644 --- a/PainTracker/app/src/main/res/layout/activity_main.xml +++ b/PainTracker/app/src/main/res/layout/activity_main.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" - tools:context=".MainActivity"> + tools:context=".ui.MainActivity"> + android:fitsSystemWindows="true" + tools:context=".ui.RecordNotesFragment"> + + \ No newline at end of file diff --git a/PainTracker/app/src/main/res/layout/fragment_record_pain.xml b/PainTracker/app/src/main/res/layout/fragment_record_pain.xml index f7f924a..f51a4d0 100644 --- a/PainTracker/app/src/main/res/layout/fragment_record_pain.xml +++ b/PainTracker/app/src/main/res/layout/fragment_record_pain.xml @@ -5,9 +5,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".RecordPainFragment"> + tools:context=".ui.RecordPainFragment"> - + android:icon="@android:drawable/ic_menu_report_image" + android:title="@string/BottomNavMenuPain" /> + android:icon="@android:drawable/ic_menu_recent_history" + android:title="@string/BottomNavMenuNotes" /> \ No newline at end of file diff --git a/PainTracker/app/src/main/res/menu/menu_main.xml b/PainTracker/app/src/main/res/menu/menu_main.xml index cd0f864..a0f7cdd 100644 --- a/PainTracker/app/src/main/res/menu/menu_main.xml +++ b/PainTracker/app/src/main/res/menu/menu_main.xml @@ -1,7 +1,7 @@ + tools:context="com.example.paintracker.ui.MainActivity"> diff --git a/PainTracker/app/src/main/res/values/strings.xml b/PainTracker/app/src/main/res/values/strings.xml index 0267612..86bd8b5 100644 --- a/PainTracker/app/src/main/res/values/strings.xml +++ b/PainTracker/app/src/main/res/values/strings.xml @@ -14,4 +14,6 @@ Save Clear Enter your pain notes here + Pain + Notes \ No newline at end of file diff --git a/README.md b/README.md index 8bca815..bc65c98 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,58 @@ ![Build Status](https://github.com/devoctomy/PainTracker/actions/workflows/android.yml/badge.svg) -My attempt at a simple pain tracking app for physiotherapy. +Simple pain tracking app for Android written in Kotlin. + +I created this app to help track pain symptoms whilst receiving physiotherapy over an extended period of time. Usually when asked where I have felt discomfort over the previous week my answer would be quite vague, using an app like this I wanted to just dump out a PDF report for the previous week containing all the necessary information. + +## Requirements + +* Android Studio (Created using 2024.2.1 Patch 3) +* OpenJDK + +I am using the following, + +openjdk 21.0.4 2024-07-16 +OpenJDK Runtime Environment OpenLogic-OpenJDK (build 21.0.4+7-adhoc.Administrator.jdk21u) +OpenJDK 64-Bit Server VM OpenLogic-OpenJDK (build 21.0.4+7-adhoc.Administrator.jdk21u, mixed mode, sharing) + +* Android SDK 14 (API Level 34) +* Android SDK Build-Tools 36-rc3 +* Android SDK Platform-Tools 35.0.2 + +### Output from Android Studio + +Android Studio Ladybug | 2024.2.1 Patch 3 +Build #AI-242.23339.11.2421.12700392, built on November 22, 2024 +Runtime version: 21.0.3+-12282718-b509.11 amd64 +VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o. +Toolkit: sun.awt.windows.WToolkit +Windows 11.0 +GC: G1 Young Generation, G1 Concurrent GC, G1 Old Generation +Memory: 4096M +Cores: 32 +Registry: + ide.experimental.ui=true + i18n.locale= + +## Progress + +- [x] Date Selection, default to today +- [x] Pain input via a number of preset categories +- [x] Notes input +- [x] Ability to edit previously entered data +- [ ] Simple export to PDF functionality +- [ ] Ability to add custom pain categories +- [ ] Ability to see overview / listing of all data +- [ ] Google Drive backup / storage support + +## Screenshots + +![Front of body pain](https://github.com/devoctomy/PainTracker/raw/main/Documentation/Screenshots/Screenshot-2024-12-25-122152.png) + +![Pain notes](https://github.com/devoctomy/PainTracker/raw/main/Documentation/Screenshots/Screenshot-2024-12-25-122254.png) + +![Back of body pain](https://github.com/devoctomy/PainTracker/raw/main/Documentation/Screenshots/Screenshot-2024-12-25-122336.png) + +![Date Picker](https://github.com/devoctomy/PainTracker/raw/main/Documentation/Screenshots/Screenshot-2024-12-25-122359.png) +