Skip to content

Commit

Permalink
Merge pull request #4 from ptrLx/dev
Browse files Browse the repository at this point in the history
feature for image import, new screenshots
  • Loading branch information
ptrLx authored Mar 15, 2023
2 parents 8ae9a75 + cf8d15c commit bda091f
Show file tree
Hide file tree
Showing 28 changed files with 182 additions and 71 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ local.properties
*.code-workspace
/app/release/*
sample-database.zip
todo.md
thomas-sample-img
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Changelog

## [1.1.0] - 2023-03-15

### New features

* Added feature for image import (only via copy)
* New Screenshots of updated UI with new sample pictures

## [1.0.0] - 2023-02-20

Initial release including:

* Implementation of base application in Kotlin and UI in Jetpack Compose:
* Request for folder permission
* CRUD for capture image, happiness, diary
* Flashbacks, diary view, calendar view and streak-count
* Room database storage and data export to JSON
* Dark- / light theme aligned with system design
* App Logo v1
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
<h1> <img src="logo/v1/OneShot_709x709.svg"
width="128"
height="128"
style="float:left;">

# OneShot

<img alt="Logo" src="logo/v1/OneShot.svg" width="170" />
</h1>

**Remember the happy days!**

<br>

OneShot is made to remind you of the special moments. Because every day has at least one. And that's what counts in life! So make it your habit and remember the happy days!

## Features
Expand All @@ -14,18 +21,20 @@ It stores your photos in a local folder and your diary within the app. Import an

<br>
<div style="display:flex;">
<img alt="preview 1" src="assets/preview_1.png" width="30%">
<img style="padding-left: 8px;" alt="preview 2" src="assets/preview_2.png" width="30%">
<img style="padding-left: 8px;" alt="preview 3" src="assets/preview_3.png" width="30%">
<img alt="preview 1" src="fastlane/metadata/android/en-US/images/phoneScreenshots/preview_1.jpg" width="30%">
<img style="padding-left: 8px;" alt="preview 2" src="fastlane/metadata/android/en-US/images/phoneScreenshots/preview_2.jpg" width="30%">
<img style="padding-left: 8px;" alt="preview 3" src="fastlane/metadata/android/en-US/images/phoneScreenshots/preview_3.jpg" width="30%">
</div>

<div style="display:flex; padding-top: 8px;">
<img alt="preview 1" src="assets/preview_4.png" width="30%">
<img style="padding-left: 8px;" alt="preview 2" src="assets/preview_5.png" width="30%">
<img style="padding-left: 8px;" alt="preview 3" src="assets/preview_6.png" width="30%">
<img alt="preview 1" src="fastlane/metadata/android/en-US/images/phoneScreenshots/preview_4.jpg" width="30%">
<img style="padding-left: 8px;" alt="preview 2" src="fastlane/metadata/android/en-US/images/phoneScreenshots/preview_5.jpg" width="30%">
<img style="padding-left: 8px;" alt="preview 3" src="fastlane/metadata/android/en-US/images/phoneScreenshots/preview_6.jpg" width="30%">
</div>
<br>

Thank's to [@Cynog](https://github.com/Cynog) for providing the beautiful images in the screenshots!

## Privacy

Your data is yours!
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ android {
applicationId "de.ptrlx.oneshot"
minSdk 29
targetSdk 32
versionCode 1
versionName "1.0"
versionCode 110
versionName '1.1.0'

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@ package de.ptrlx.oneshot.feature_diary.domain.util

import android.content.Context
import android.net.Uri
import android.util.Log
import android.os.FileUtils
import androidx.documentfile.provider.DocumentFile
import de.ptrlx.oneshot.feature_diary.domain.model.DiaryEntry
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.internal.readJson
import okio.use

class DiaryFileManager(baseLocation: Uri, context: Context) {
val baseLocation = baseLocation
val context = context

class DiaryFileManager(val baseLocation: Uri, val context: Context) {
private val baseFolder: DocumentFile? = DocumentFile.fromTreeUri(context, baseLocation)
private var diaryEntryFileRepository = HashMap<String, Uri?>()
private val baseLocationDocumentPath = baseLocation.encodedPath.toString().substring(6)
Expand Down Expand Up @@ -56,14 +52,47 @@ class DiaryFileManager(baseLocation: Uri, context: Context) {
/**
* Create a new dummy file before launching camara activity.
*
* @param filename
* @param filename filename of new dummy
* @return the URI of the dummy file or null if file could not be created
*/
fun createNewImageDummy(filename: String): Uri? {
val file: DocumentFile? = baseFolder?.createFile("image/jpg", filename)
return file?.uri
}

/**
* Copy an image to baseFolder.
*
* @param srcUri Source Uri of image
* @param filename filename of new image
*/
fun copyImage(srcUri: Uri, filename: String): Uri? {
val file: DocumentFile? = baseFolder?.createFile("image/jpg", filename)
return file?.let { df ->
val inStream = context.contentResolver.openInputStream(srcUri)
inStream?.let {
val uri = context.contentResolver.openOutputStream(df.uri)?.let { outStream ->
FileUtils.copy(
inStream,
outStream
)

outStream.flush()
outStream.close()

df.uri
} ?: run {
deleteImage(filename)
null
}

inStream.close()
uri
}
}
}


/**
* Delete an image from baseFolder.
*
Expand All @@ -77,7 +106,7 @@ class DiaryFileManager(baseLocation: Uri, context: Context) {
* Write a JSON export to base location
*
* @param filename
* @param export JSON String of export
* @param entries list of diary entries to export
* @return success of write
*/
fun writeJSONExport(filename: String, entries: List<DiaryEntry>): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.ptrlx.oneshot.feature_diary.presentation.diary

import android.app.Activity
import android.content.Context
import android.net.Uri
import de.ptrlx.oneshot.feature_diary.domain.model.DiaryEntry
import de.ptrlx.oneshot.feature_diary.domain.util.HappinessType
Expand All @@ -16,9 +15,10 @@ sealed class DiaryEvent {
val text: String? = null,
val happiness: HappinessType? = null
) : DiaryEvent()
data class ImageImport(val uri: Uri): DiaryEvent()

object WriteExport : DiaryEvent()
object ReadImport : DiaryEvent()
object WriteDBExport : DiaryEvent()
object ReadDBImport : DiaryEvent()
object CaptureImageAborted : DiaryEvent()
object CaptureUpdateEntry : DiaryEvent()
object SnackbarDismissed : DiaryEvent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class DiaryViewModel @Inject constructor(
// private var searchQuery: String by mutableStateOf("")
// private var searchEntries: List<DiaryEntry> by mutableStateOf(emptyList())

var fielManager: DiaryFileManager? by mutableStateOf(null)
var fileManager: DiaryFileManager? by mutableStateOf(null)
private set

var currentImageAddEditEntry: DiaryEntry? by mutableStateOf(null)
Expand Down Expand Up @@ -77,7 +77,7 @@ class DiaryViewModel @Inject constructor(
var snackbarCause: SnackbarCause = SnackbarCause.DELETE_ENTRY
private set

var currentIsCapture: Boolean = true
var currentIsNewEntry: Boolean = true
private set

init {
Expand Down Expand Up @@ -119,10 +119,14 @@ class DiaryViewModel @Inject constructor(
showModalBottomSheet()
Log.d(LOG_TAG, "Image saved")
}
is DiaryEvent.ImageImport -> {
createNewImageDummyOrImport(copyImageUri = event.uri)
showModalBottomSheet()
}
is DiaryEvent.CaptureImageAborted -> {
// cleanup created (but empty) file
currentImageAddEditEntry?.let { entry ->
fielManager?.deleteImage(entry.relativePath)
fileManager?.deleteImage(entry.relativePath)
currentImageAddEditEntry = null
}
}
Expand All @@ -149,7 +153,7 @@ class DiaryViewModel @Inject constructor(
}
is DiaryEvent.DiaryEditEntry -> {
currentImageAddEditEntry = event.entry
currentIsCapture = false
currentIsNewEntry = false
showModalBottomSheet()
}
is DiaryEvent.FilterHappinessType -> {
Expand Down Expand Up @@ -189,21 +193,21 @@ class DiaryViewModel @Inject constructor(
// nothing to do here
}
}
is DiaryEvent.WriteExport -> {
is DiaryEvent.WriteDBExport -> {
if (entries.isNotEmpty()) {
val timestamp = System.currentTimeMillis()
val currentDate =
SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(timestamp)
val filename = "OneShot_DB_$currentDate.json"
val success = fielManager?.writeJSONExport(filename, entries)
val success = fileManager?.writeJSONExport(filename, entries)
snackbarCause =
(if (success == true) SnackbarCause.SUCCESS else SnackbarCause.ERROR)
isSnackbarShowing = true
}
}
is DiaryEvent.ReadImport -> {
is DiaryEvent.ReadDBImport -> {
var success = false
fielManager?.let { fileManager ->
fileManager?.let { fileManager ->
currentImportDatabaseUri?.let { uri ->
Log.d(LOG_TAG, "Importing entries from $uri")
val importEntries = fileManager.readJSONExport(uri)
Expand Down Expand Up @@ -243,7 +247,7 @@ class DiaryViewModel @Inject constructor(
diaryUseCases.getSettingsString(imageBaseLocationKey).onEach { uriString ->
Log.d(LOG_TAG, "New element received in GetImageBaseLocationJob")
uriString?.let {
fielManager = DiaryFileManager(Uri.parse(uriString), context)
fileManager = DiaryFileManager(Uri.parse(uriString), context)
}
}.launchIn(viewModelScope)

Expand Down Expand Up @@ -289,16 +293,24 @@ class DiaryViewModel @Inject constructor(
* @return Uri that can be used to store an image
*/
fun createNewImageDummy(): Uri? {
return createNewImageDummyOrImport(copyImageUri = null)
}

private fun createNewImageDummyOrImport(copyImageUri: Uri? = null): Uri? {
val localDate = LocalDate.now()
val timestamp = System.currentTimeMillis()
val currentDate =
SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(timestamp)
val filename = "OneShot_$currentDate.jpg"

val uri = fielManager?.createNewImageDummy(filename)
val uri = copyImageUri?.let {
fileManager?.copyImage(srcUri = copyImageUri, filename = filename)
}?: run {
fileManager?.createNewImageDummy(filename)
}

uri?.let {
currentIsCapture = true
currentIsNewEntry = true
currentImageAddEditEntry = DiaryEntry(
localDate,
timestamp / 1000L,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.ptrlx.oneshot.R
import de.ptrlx.oneshot.feature_diary.domain.model.DiaryEntry
import de.ptrlx.oneshot.feature_diary.domain.util.HappinessType
import de.ptrlx.oneshot.feature_diary.presentation.diary.DiaryEvent
import de.ptrlx.oneshot.feature_diary.presentation.diary.DiaryViewModel
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId

@Composable
Expand Down Expand Up @@ -70,7 +72,7 @@ fun AddEditEntryModalBottomSheetContent(
.fillMaxWidth()
.height(670.dp)
) {
if (!viewModel.currentIsCapture)
if (!viewModel.currentIsNewEntry)
Icon(
Icons.Default.Delete,
contentDescription = "delete",
Expand Down Expand Up @@ -175,9 +177,17 @@ fun AddEditEntryModalBottomSheetContent(
fun AddEditOrSetLocationButton(
modifier: Modifier = Modifier,
viewModel: DiaryViewModel,
hideWhenCaptured: Boolean = false
) {
if (viewModel.fielManager == null)
SetLocation(modifier, viewModel = viewModel)
else
AddEntryButton(modifier, viewModel = viewModel)
var lastEntry: DiaryEntry? = null
try {
lastEntry = viewModel.entries.last()
} catch (e: NoSuchElementException) {}

if (viewModel.fileManager == null)
SetLocation(modifier.padding(top = 4.dp), viewModel = viewModel)
else if ((!hideWhenCaptured) || (lastEntry?.date != LocalDate.now()))
AddEntryButton(modifier.padding(top = 4.dp), viewModel = viewModel)

}

Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fun ImageCard(
) {
AsyncImage(
modifier = Modifier.fillMaxWidth(),
model = viewModel.fielManager?.resolveUri(entry.relativePath),
model = viewModel.fileManager?.resolveUri(entry.relativePath),
contentDescription = contentDescription,
error = painterResource(R.drawable.ic_baseline_error_24),
contentScale = if (!expanded) ContentScale.Crop else ContentScale.FillWidth,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package de.ptrlx.oneshot.feature_diary.presentation.diary.components.common

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import de.ptrlx.oneshot.feature_diary.presentation.diary.DiaryEvent
import de.ptrlx.oneshot.feature_diary.presentation.diary.DiaryViewModel

@Composable
fun ImportImageButton(
modifier: Modifier = Modifier,
viewModel: DiaryViewModel
) {
if (viewModel.fileManager != null) {
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let {
viewModel.onEvent(DiaryEvent.ImageImport(it))
}
}

RoundedButton(
modifier = modifier,
text = "Import image",
onClick = {
launcher.launch("image/*")
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fun SetLocation(
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
uri?.let {
viewModel.fielManager?.let {
viewModel.fileManager?.let {
val toast =
Toast.makeText(
context,
Expand All @@ -38,7 +38,7 @@ fun SetLocation(
RoundedButton(
modifier = modifier,
text = "Set image file path",
onClick = { launcher.launch(viewModel.fielManager?.baseLocation) },
onClick = { launcher.launch(viewModel.fileManager?.baseLocation) },
arrow = arrow
)
}
Loading

0 comments on commit bda091f

Please sign in to comment.