Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d1e4906
Add a setting to open the app settings page on Android
alexgallardo11 Apr 10, 2025
cce0471
Add API Key settings to manage API keys
alexgallardo11 Apr 11, 2025
3eefd7c
Refactor CreateScopeBottomSheet
alexgallardo11 Apr 11, 2025
b24069d
Refactor App Settings Handlers
alexgallardo11 Apr 11, 2025
3e4140c
Store API Key in Encrypted Storage ai repository
alexgallardo11 Apr 11, 2025
749c084
wip
alexgallardo11 Apr 14, 2025
81d9db8
small fixes
alexgallardo11 Apr 14, 2025
5b536f2
Refactor AI message creation and enhance input component
alexgallardo11 Apr 22, 2025
da14165
Refactor to solve issues
alexgallardo11 Apr 23, 2025
b286784
- Add two new icons `ic_ai_enabled` and `ic_ai_disabled` for AI mode
alexgallardo11 Apr 23, 2025
cc0ac54
Add AI message loading indicator
alexgallardo11 Apr 23, 2025
22b5328
Refactor Create Scope screen and bottom sheet to improve title editing
alexgallardo11 Apr 25, 2025
a4f8158
Add AI error message display and handling
alexgallardo11 Apr 25, 2025
1d91c88
Change the schedule identifier type from `Long` to `Uuid` and remove …
alexgallardo11 Apr 25, 2025
b0bc00f
remove ai error state
alexgallardo11 Apr 25, 2025
9b00053
Adjust surface container color and add a high container color
alexgallardo11 Apr 28, 2025
57ac85a
Refactor: Enhance Create Scope and Chat Input UI
alexgallardo11 Apr 29, 2025
e295082
remove "-" in ScopeTitleEditor
alexgallardo11 Apr 30, 2025
d3bc924
Fix navigation from notification error in iOS
alexgallardo11 May 2, 2025
de2f4c2
Refactor AI message loading state to use a dedicated `AiMessageState`…
alexgallardo11 May 5, 2025
635e89f
Improve the handling of shared URLs in the ShareViewController to dis…
alexgallardo11 May 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ kotlin {
implementation(libs.filekit.dialogs.compose)
implementation(libs.filekit.core)
implementation(libs.filekit.coil)
implementation(libs.open.ai)

// About
implementation(libs.aboutLibrariesCore)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
package io.middlepoint.morestuff.shared.di

import android.content.Context
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import androidx.work.WorkManager
import co.touchlab.kermit.Logger
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SharedPreferencesSettings
import io.middlepoint.morestuff.shared.data.DriverFactory
import io.middlepoint.morestuff.shared.data.NotifierImpl
import io.middlepoint.morestuff.shared.data.SchedulerImpl
import io.middlepoint.morestuff.shared.data.DriverFactory
import io.middlepoint.morestuff.shared.data.VoiceToTextParser
import io.middlepoint.morestuff.shared.data.VoiceToTextParserImpl
import io.middlepoint.morestuff.shared.data.repository.LlmRepositoryImpl
import io.middlepoint.morestuff.shared.domain.service.Notifier
import io.middlepoint.morestuff.shared.domain.service.Scheduler
import io.middlepoint.morestuff.shared.ui.utils.SharedFunctionsHandler
import org.koin.android.ext.koin.androidApplication
import org.koin.core.module.Module
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module

Expand All @@ -28,15 +32,40 @@ actual val platformModule: Module = module {

factoryOf(::VoiceToTextParserImpl) bind VoiceToTextParser::class
singleOf(::SchedulerImpl) bind Scheduler::class
singleOf(::NotifierImpl) bind Notifier::class
single<Notifier> {
NotifierImpl(
context = get(),
notificationManager = get(),
logger = get(),
settings = get(named(SharedSettings.Unencrypted))
)
}
singleOf(::SharedFunctionsHandler)

single { WorkManager.getInstance(androidApplication()) }
single { NotificationManagerCompat.from(androidApplication()) }

factory<Settings> {

single<Settings>(named(SharedSettings.Encrypted)) {
SharedPreferencesSettings(
EncryptedSharedPreferences.create(
LlmRepositoryImpl.ENCRYPTED_DATABASE_NAME,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
get<Context>(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
),
false
)
}

single<Settings>(named(SharedSettings.Unencrypted)) {
SharedPreferencesSettings(
PreferenceManager.getDefaultSharedPreferences(androidApplication())
androidApplication().getSharedPreferences(
"UNENCRYPTED_SETTINGS",
Context.MODE_PRIVATE
),
false
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="24dp"
android:viewportWidth="25"
android:viewportHeight="24">
<path
android:pathData="M1.033,1.034C1.207,0.858 1.437,0.758 1.678,0.75L17.578,0.75C17.578,0.75 17.578,0.75 17.578,0.75C19.244,0.75 20.842,1.42 22.022,2.614C23.203,3.808 23.868,5.43 23.868,7.123L23.868,16.918C23.857,18.604 23.188,20.215 22.01,21.4C20.83,22.586 19.237,23.25 17.578,23.25L7.039,23.25C7.039,23.25 7.039,23.25 7.039,23.25C5.374,23.25 3.775,22.58 2.595,21.386C1.415,20.192 0.75,18.57 0.75,16.877L0.75,1.698C0.758,1.447 0.86,1.209 1.033,1.034Z"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#696969"/>
<path
android:pathData="M17.55,17.094L15.939,17.094L15.939,7.231L17.55,7.231L17.55,17.094Z"
android:fillColor="#696969"/>
<path
android:pathData="M14.635,17.093L12.919,17.093L11.938,14.358L8.16,14.358L7.201,17.093L5.5,17.093L9.187,7.193L10.941,7.193L14.635,17.093ZM8.633,12.964L11.51,12.964C10.746,10.731 10.324,9.487 10.244,9.232C10.164,8.977 10.101,8.76 10.057,8.58C9.937,9.124 9.759,9.729 9.524,10.393L8.633,12.964Z"
android:fillColor="#696969"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="25dp"
android:height="24dp"
android:viewportWidth="25"
android:viewportHeight="24">
<path
android:pathData="M1.033,1.034C1.207,0.858 1.437,0.758 1.678,0.75L17.578,0.75C17.578,0.75 17.578,0.75 17.578,0.75C19.244,0.75 20.842,1.42 22.022,2.614C23.203,3.808 23.868,5.43 23.868,7.123L23.868,16.918C23.857,18.604 23.188,20.215 22.01,21.4C20.83,22.586 19.237,23.25 17.578,23.25L7.039,23.25C7.039,23.25 7.039,23.25 7.039,23.25C5.374,23.25 3.775,22.58 2.595,21.386C1.415,20.192 0.75,18.57 0.75,16.877L0.75,1.698C0.758,1.447 0.86,1.209 1.033,1.034Z"
android:strokeWidth="1.5"
android:fillColor="#00000000">
<aapt:attr name="android:strokeColor">
<gradient
android:startX="-3"
android:startY="0"
android:endX="29.437"
android:endY="22.686"
android:type="linear">
<item android:offset="0" android:color="#FF9CAAFF"/>
<item android:offset="1" android:color="#FFE299FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M17.55,17.094L15.939,17.094L15.939,7.231L17.55,7.231L17.55,17.094Z"
android:fillColor="#696969"/>
<path
android:pathData="M17.55,17.094L15.939,17.094L15.939,7.231L17.55,7.231L17.55,17.094Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="15.743"
android:startY="7.231"
android:endX="18.865"
android:endY="7.579"
android:type="linear">
<item android:offset="0" android:color="#FF9CAAFF"/>
<item android:offset="1" android:color="#FFE299FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M14.635,17.093L12.919,17.093L11.938,14.358L8.16,14.358L7.201,17.093L5.5,17.093L9.187,7.193L10.941,7.193L14.635,17.093ZM8.633,12.964L11.51,12.964C10.746,10.731 10.324,9.487 10.244,9.232C10.164,8.977 10.101,8.76 10.057,8.58C9.937,9.124 9.759,9.729 9.524,10.393L8.633,12.964Z"
android:fillColor="#696969"/>
<path
android:pathData="M14.635,17.093L12.919,17.093L11.938,14.358L8.16,14.358L7.201,17.093L5.5,17.093L9.187,7.193L10.941,7.193L14.635,17.093ZM8.633,12.964L11.51,12.964C10.746,10.731 10.324,9.487 10.244,9.232C10.164,8.977 10.101,8.76 10.057,8.58C9.937,9.124 9.759,9.729 9.524,10.393L8.633,12.964Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="4.387"
android:startY="7.193"
android:endX="17.228"
android:endY="15.273"
android:type="linear">
<item android:offset="0" android:color="#FF9CAAFF"/>
<item android:offset="1" android:color="#FFE299FF"/>
</gradient>
</aapt:attr>
</path>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@
<string name="task_schedule_deletion_warning_plural">Algunas de estas tareas tienen programaciones que también serán eliminadas.</string>

<string name="scope_name">Nombre del scope</string>
<string name="api_key_title">API Key</string>
<string name="api_key_label">Añade API Key</string>
<string name="api_key_not_set">No establecida</string>


<string name="sign_in_with_email">Iniciar sesión con Email</string>
<string name="enter_your_email">Introduce tu email</string>
Expand All @@ -290,4 +294,6 @@
<string name="error_invalid_email">El email introducido no es válido o no existe.</string>
<string name="error_generic">Ha ocurrido un error. Inténtalo de nuevo.</string>
<string name="delete_scope">Eliminar Scope</string>

<string name="chat_whit_ai">Habla con MoreStuff IA.</string>
</resources>
4 changes: 4 additions & 0 deletions composeApp/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@
<string name="edit_message">Edit message</string>
<string name="task_schedule_deletion_warning_singular">This task has a schedule that will also be deleted.</string>
<string name="task_schedule_deletion_warning_plural">Some of these tasks have schedules that will also be deleted.</string>
<string name="api_key_title">API Key</string>
<string name="api_key_label">Enter API Key</string>
<string name="api_key_not_set">Not set</string>


<string name="sign_in_with_email">Sign in with Email</string>
Expand All @@ -307,4 +310,5 @@
<string name="verify">Verify</string>
<string name="error_invalid_email">The email address is invalid or does not exist.</string>
<string name="error_generic">An error occurred. Please try again.</string>
<string name="chat_whit_ai">Chat with MoreStuff AI.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ object Constants {
const val KEY_REVIEW_TIME = "key_review_time"
const val KEY_REVIEW_HINT = "key_review_hint"
const val KEY_LANGUAGE_INPUT = "key_language_input"
const val UNENCRYPTED_SETTINGS_NAME = "APP_SETTINGS"
const val ENCRYPTED_SETTINGS_NAME = "ENCRYPTED_SETTINGS"
const val KEY_API_KEY = "key_api_key"


// Developer keys
Expand All @@ -25,4 +28,8 @@ object Constants {
const val TELEGRAM_INVITE_LINK = "https://t.me/+hzE7jInTlSRiOGVk"

const val PRIVACY_POLICY_LINK = "https://bit.ly/3P4Sd3I"


const val IA_MODEL_OPEN_AI = "gpt-4o-mini"
const val IA_MODEL_DEEPSEEK = "deepseek-chat"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import io.middlepoint.morestuff.shared.domain.enums.ContentType
import io.middlepoint.morestuff.shared.domain.redux.state.AppState
import io.middlepoint.morestuff.shared.domain.redux.Middleware
import io.middlepoint.morestuff.shared.domain.redux.action.MessageAction
import io.middlepoint.morestuff.shared.domain.redux.action.MessageAction.CreateAITaskMessageAction
import io.middlepoint.morestuff.shared.domain.redux.action.MessageAction.CreateScheduleMessageAction
import io.middlepoint.morestuff.shared.domain.redux.action.NotificationAction
import io.middlepoint.morestuff.shared.domain.redux.action.TaskAction
import io.middlepoint.morestuff.shared.domain.redux.store.Action
import io.middlepoint.morestuff.shared.domain.redux.store.Dispatch
import io.middlepoint.morestuff.shared.domain.redux.store.Next
import io.middlepoint.morestuff.shared.domain.redux.store.NoOp
import io.middlepoint.morestuff.shared.domain.usecase.ia.CreateAIMessageUseCase
import io.middlepoint.morestuff.shared.domain.usecase.message.CreateMediaMessageUseCase
import io.middlepoint.morestuff.shared.domain.usecase.message.CreateMessageUseCase
import io.middlepoint.morestuff.shared.domain.usecase.message.CreatePDFMessageUseCase
Expand All @@ -30,6 +32,7 @@ class MessageMiddleware(
private val createPDFMessageUseCase: CreatePDFMessageUseCase,
private val deleteMessageUseCase: DeleteMessageUseCase,
private val updateMessageContentUseCase: UpdateMessageContentUseCase,
private val createAIMessageUseCase: CreateAIMessageUseCase,
) : Middleware<AppState> {

val logger = Logger.withTag("MessageMiddleware")
Expand Down Expand Up @@ -60,7 +63,6 @@ class MessageMiddleware(

is MessageAction.CreateAppTaskMessageAction -> scope.launch {
logger.d { "Creating app task message: ${action.content}" }

createMessageUseCase(
action.taskId,
action.content,
Expand All @@ -69,9 +71,25 @@ class MessageMiddleware(
scheduleId = null
)
logger.d { "App task message created" }
}

is CreateAITaskMessageAction -> scope.launch {
logger.d { "AI request started for task=${action.taskId}" }
dispatch(MessageAction.AIRequestStarted(action.taskId))
val result = createAIMessageUseCase(action.taskId, action.prompt)
result.fold(
{ failure ->
logger.e { "AI request failed: $failure" }
dispatch(MessageAction.AIRequestFailed(action.taskId, failure.toString()))
},
{ message ->
logger.d { "AI request finished, got message id=${message.id}" }
dispatch(MessageAction.AIRequestFinished(action.taskId))
}
)
}


is MessageAction.CreateFileMessageAction -> scope.launch {
createMediaMessageUseCase(
action.taskId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,60 +15,62 @@ import io.middlepoint.morestuff.shared.domain.redux.store.Dispatch
import io.middlepoint.morestuff.shared.domain.redux.store.InitStoreAction
import io.middlepoint.morestuff.shared.domain.redux.store.Next
import io.middlepoint.morestuff.shared.domain.redux.store.NoOp
import io.middlepoint.morestuff.shared.domain.usecase.settings.GetApiKeyUseCase
import io.middlepoint.morestuff.shared.domain.usecase.settings.GetAppSettingsUseCase
import io.middlepoint.morestuff.shared.domain.usecase.settings.SaveApiKeyUseCase
import io.middlepoint.morestuff.shared.domain.usecase.settings.SaveUserSettingUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

class SettingsMiddleware(
private val getAppSettingsUseCase: GetAppSettingsUseCase,
private val saveUserSettingUseCase: SaveUserSettingUseCase,
) : Middleware<AppState> {
private val getAppSettingsUseCase: GetAppSettingsUseCase,
private val saveUserSettingUseCase: SaveUserSettingUseCase,
) : Middleware<AppState> {

override fun invoke(
state: AppState,
action: Action,
dispatch: Dispatch,
next: Next<AppState>,
scope: CoroutineScope,
): Action {
when (action) {
is InitStoreAction -> scope.launch {
dispatch(InitSettings(getAppSettingsUseCase()))
}
override fun invoke(
state: AppState,
action: Action,
dispatch: Dispatch,
next: Next<AppState>,
scope: CoroutineScope,
): Action {
when (action) {
is InitStoreAction -> scope.launch {
dispatch(InitSettings(getAppSettingsUseCase()))
}

is EnableDevSettings -> scope.launch {
saveUserSettingUseCase(AppSetting.DevSettings, action.enable)
}
is EnableDevSettings -> scope.launch {
saveUserSettingUseCase(AppSetting.DevSettings, action.enable)
}

is SetSnoozeLimit -> scope.launch {
saveUserSettingUseCase(AppSetting.SnoozeLimit, action.amount)
}
is SetSnoozeLimit -> scope.launch {
saveUserSettingUseCase(AppSetting.SnoozeLimit, action.amount)
}

is SetAppTheme -> scope.launch {
saveUserSettingUseCase(AppSetting.Theme, action.theme.name)
}
is SetAppTheme -> scope.launch {
saveUserSettingUseCase(AppSetting.Theme, action.theme.name)
}

is OnBoardingComplete -> scope.launch {
saveUserSettingUseCase(AppSetting.FirstTime, false)
dispatch(TaskAction.CreateHintTask)
}
is OnBoardingComplete -> scope.launch {
saveUserSettingUseCase(AppSetting.FirstTime, false)
dispatch(TaskAction.CreateHintTask)
}

/*is SettingAction.SetReviewTimeAction -> scope.launch {
saveUserSettingUseCase(AppSetting.ReviewTime, Pair(action.hour, action.minute))
}*/
/*is SettingAction.SetReviewTimeAction -> scope.launch {
saveUserSettingUseCase(AppSetting.ReviewTime, Pair(action.hour, action.minute))
}*/

is SettingAction.EnableReviewHint -> scope.launch {
saveUserSettingUseCase(AppSetting.ShowHintArrowPriority, action.enable)
}
is SettingAction.EnableReviewHint -> scope.launch {
saveUserSettingUseCase(AppSetting.ShowHintArrowPriority, action.enable)
}

is SettingAction.SetVoiceLanguage -> scope.launch {
saveUserSettingUseCase(AppSetting.VoiceInputLanguage, action.language.name)
}
is SettingAction.SetVoiceLanguage -> scope.launch {
saveUserSettingUseCase(AppSetting.VoiceInputLanguage, action.language.name)
}

else -> NoOp
}

return next(state, action, dispatch)
else -> NoOp
}

return next(state, action, dispatch)
}
}
Loading
Loading