,
43 | ) = Thread.setDefaultUncaughtExceptionHandler(
44 | GlobalExceptionHandler(
45 | applicationContext = applicationContext,
46 | defaultHandler = Thread.getDefaultUncaughtExceptionHandler()!!,
47 | activityToBeLaunched = activityToBeLaunched
48 | )
49 | )
50 | }
51 | }
52 |
53 | private const val INTENT_DATA_NAME = "GlobalExceptionHandler"
54 | private const val defFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
55 | Intent.FLAG_ACTIVITY_NEW_TASK or
56 | Intent.FLAG_ACTIVITY_CLEAR_TASK
57 |
58 | abstract class CrashHandler : ComponentActivity() {
59 | fun getCrashReason(): String = intent.getStringExtra(INTENT_DATA_NAME) ?: ""
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/components/buttons/RichTextStyleButton.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.components.buttons
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.animation.animateColorAsState
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.draw.clip
13 | import androidx.compose.ui.focus.focusProperties
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.unit.dp
17 | import com.jobik.shkiper.ui.theme.AppTheme
18 | import androidx.compose.material3.*
19 | import androidx.compose.ui.Alignment
20 |
21 | @Composable
22 | fun RichTextStyleButton(
23 | isActive: Boolean,
24 | onClick: () -> Unit,
25 | @DrawableRes icon: Int,
26 | contentDescription: String = "",
27 | ) {
28 | val buttonContentColor: Color by animateColorAsState(
29 | targetValue = if (isActive) AppTheme.colors.onPrimary else AppTheme.colors.onSecondaryContainer,
30 | label = "buttonContentColor"
31 | )
32 |
33 | val buttonBackgroundColor: Color by animateColorAsState(
34 | targetValue = if (isActive) AppTheme.colors.primary else Color.Transparent,
35 | label = "buttonBackgroundColor"
36 | )
37 |
38 | Card(
39 | modifier = Modifier
40 | .size(30.dp)
41 | .clip(RoundedCornerShape(5.dp))
42 | .clickable(onClick = onClick)
43 | .focusProperties { canFocus = false },
44 | shape = RoundedCornerShape(5.dp),
45 | border = null,
46 | colors = CardDefaults.cardColors(contentColor = buttonContentColor, containerColor = buttonBackgroundColor),
47 | ) {
48 | Box(
49 | modifier = Modifier.fillMaxSize(),
50 | contentAlignment = Alignment.Center
51 | ) {
52 | Icon(
53 | modifier = Modifier.padding(2.dp),
54 | painter = painterResource(id = icon),
55 | contentDescription = contentDescription,
56 | tint = buttonContentColor
57 | )
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [//]: # ()
2 |
3 | [//]: # (
)
4 |
5 | [//]: # ( Shkiper)
6 |
7 | [//]: # (
)
8 |
9 |

10 |
11 |
12 | ## Description
13 |
14 |
15 |
16 | The skipper is designed to make your life easier and organize your daily activities. Take notes, schedule reminders with
17 | flexible repeat modes (daily, weekly, monthly, yearly) and don't forget important events.
18 |
19 | [
](https://play.google.com/store/apps/details?id=com.jobik.shkiper)
20 |
21 | ### Key features
22 |
23 | - Create notes and group them with tags for easy organization.
24 | - View a preview of the links if they are in the note.
25 | - Go back to similar actions when editing notes, so you don't lose anything.
26 | - Customize the themes of the app to make it prestigious and customized to your style.
27 | - Back up your data or download it for quick transitions.
28 | - View application usage statistics, because it can be interesting.
29 | - Convenient reminders for important occasions.
30 | - Task calendar with all reminders and date range.
31 | - Create widgets for quick access and viewing.
32 | - Styling notes and text formatting.
33 | - Ability to share a note and statistics as an image.
34 |
35 | ### App features
36 |
37 | - Switch language (localization)
38 | - Push notifications
39 | - Custom color themes
40 | - In-app updates
41 | - In-app purchases
42 | - Glance app widgets
43 | - Image share
44 | - Text formatting
45 |
46 |
47 | ⭐ Shkiper is still under development, if you have any questions or suggestions, please contact me! We value all of your feedback.
48 |
49 |
50 | ### Design & Screenshots
51 |
52 |
53 |

54 |
55 |
56 | ### Do you like this app? 💜
57 |
58 | Support it by joining __[stargazers](https://github.com/Efimj/Shkiper/stargazers)__ for this repository. ⭐
59 |
60 |
61 | Also, support me
62 | with
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/helpers/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.helpers
2 |
3 | import androidx.compose.runtime.*
4 | import com.jobik.shkiper.database.models.Reminder
5 | import com.jobik.shkiper.helpers.DateHelper
6 | import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
7 | import com.kizitonwose.calendar.core.Week
8 | import kotlinx.coroutines.flow.filter
9 | import java.time.LocalDateTime
10 | import java.time.Month
11 | import java.time.YearMonth
12 | import java.time.format.TextStyle
13 | import java.util.*
14 |
15 | /**
16 | * Find first visible week in a paged week calendar **after** scrolling stops.
17 | */
18 | @Composable
19 | fun rememberFirstVisibleWeekAfterScroll(state: WeekCalendarState): Week {
20 | val visibleWeek = remember(state) { mutableStateOf(state.firstVisibleWeek) }
21 | LaunchedEffect(state) {
22 | snapshotFlow { state.isScrollInProgress }
23 | .filter { scrolling -> !scrolling }
24 | .collect { visibleWeek.value = state.firstVisibleWeek }
25 | }
26 | return visibleWeek.value
27 | }
28 |
29 | /**
30 | * To display month name.
31 | */
32 | fun YearMonth.displayText(): String {
33 | return "${this.month.getDisplayName(TextStyle.FULL, Locale.getDefault())} ${this.year}"
34 | }
35 |
36 | @Composable
37 | fun rememberNextReminder(
38 | reminders: List,
39 | pointDate: LocalDateTime = LocalDateTime.now()
40 | ): Reminder? {
41 | val nextReminder = remember { mutableStateOf(null) }
42 |
43 | LaunchedEffect(reminders) {
44 | nextReminder.value = DateHelper.sortReminders(reminders = reminders, pointDate = pointDate).firstOrNull()
45 | }
46 |
47 | return nextReminder.value
48 | }
49 |
50 | /**
51 | * To display month name.
52 | */
53 | fun Month.displayText(short: Boolean = true): String {
54 | val style = if (short) TextStyle.SHORT else TextStyle.FULL
55 | return getDisplayName(style, Locale.getDefault())
56 | }
57 |
58 | fun splitIntoTriple(input: List): Triple, List, List> {
59 | val list1 = mutableListOf()
60 | val list2 = mutableListOf()
61 | val list3 = mutableListOf()
62 |
63 | input.forEachIndexed { index, element ->
64 | when (index % 3) {
65 | 0 -> list1.add(element)
66 | 1 -> list2.add(element)
67 | 2 -> list3.add(element)
68 | }
69 | }
70 |
71 | return Triple(list1, list2, list3)
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/widgets/handlers/NotesHandler.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.widgets.handlers
2 |
3 | import android.app.PendingIntent
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.datastore.preferences.core.Preferences
7 | import androidx.glance.appwidget.GlanceAppWidgetManager
8 | import androidx.glance.appwidget.state.updateAppWidgetState
9 | import androidx.glance.appwidget.updateIf
10 | import com.jobik.shkiper.SharedPreferencesKeys
11 | import com.jobik.shkiper.database.models.Note
12 | import com.jobik.shkiper.helpers.TextHelper
13 | import com.jobik.shkiper.widgets.WidgetKeys
14 | import com.jobik.shkiper.widgets.WidgetKeys.Prefs.noteId
15 | import com.jobik.shkiper.widgets.services.NoteWidgetReceiver
16 | import com.jobik.shkiper.widgets.services.PinWidgetReceiver
17 | import com.jobik.shkiper.widgets.widgets.NoteWidget
18 | import com.mohamedrejeb.richeditor.model.RichTextState
19 | import org.mongodb.kbson.ObjectId
20 |
21 | suspend fun handleNoteWidgetPin(context: Context, noteId: String) {
22 | val intent = Intent(context, PinWidgetReceiver::class.java)
23 | intent.putExtra(SharedPreferencesKeys.NoteIdExtra, noteId)
24 | val pendingIntent = PendingIntent.getBroadcast(
25 | context,
26 | ObjectId(noteId).timestamp,
27 | intent,
28 | PendingIntent.FLAG_IMMUTABLE
29 | )
30 | GlanceAppWidgetManager(context).requestPinGlanceAppWidget(
31 | NoteWidgetReceiver::class.java,
32 | successCallback = pendingIntent
33 | )
34 | }
35 |
36 | suspend fun GlanceAppWidgetManager.mapNoteToWidget(context: Context, note: Note) =
37 | getGlanceIds(NoteWidget::class.java)
38 | .forEach { glanceId ->
39 | updateAppWidgetState(context, glanceId) { prefs ->
40 | if (prefs[noteId] == note._id.toHexString()) {
41 | val richBody = RichTextState()
42 | richBody.setHtml(note.body)
43 |
44 | prefs[WidgetKeys.Prefs.noteHeader] = note.header
45 | prefs[WidgetKeys.Prefs.noteBody] = TextHelper.removeMarkdownStyles(richBody.toMarkdown())
46 | prefs[WidgetKeys.Prefs.noteLastUpdate] = note.updateDateString
47 | NoteWidget().update(context, glanceId)
48 | }
49 | }
50 | NoteWidget().updateIf(context) {
51 | it[noteId] == note._id.toHexString()
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/modifiers/sharedTransition.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.modifiers
2 |
3 | import androidx.compose.animation.ExperimentalSharedTransitionApi
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.composed
7 | import com.jobik.shkiper.ui.components.cards.NoteSharedElementKey
8 | import com.jobik.shkiper.ui.components.cards.NoteSharedElementType
9 | import com.jobik.shkiper.ui.helpers.LocalNavAnimatedVisibilityScope
10 | import com.jobik.shkiper.ui.helpers.LocalSharedElementKey
11 | import com.jobik.shkiper.ui.helpers.LocalSharedTransitionScope
12 |
13 | @OptIn(ExperimentalSharedTransitionApi::class)
14 | @Composable
15 | fun Modifier.skipToLookaheadSize() = composed {
16 | val sharedTransitionScope = LocalSharedTransitionScope.current
17 | val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
18 |
19 | val sharedTransitionModifier = sharedTransitionScope?.let { scope ->
20 | animatedVisibilityScope?.let { visibilityScope ->
21 | with(scope) {
22 | Modifier
23 | .skipToLookaheadSize()
24 | }
25 | }
26 | } ?: Modifier
27 | this.then(sharedTransitionModifier)
28 | }
29 |
30 | @OptIn(ExperimentalSharedTransitionApi::class)
31 | @Composable
32 | fun Modifier.sharedNoteTransitionModifier(
33 | noteId: String
34 | ) = composed {
35 | val sharedTransitionScope = LocalSharedTransitionScope.current
36 | val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
37 | val sharedElementKey = LocalSharedElementKey.current
38 |
39 | val sharedTransitionModifier = sharedTransitionScope?.let { scope ->
40 | animatedVisibilityScope?.let { visibilityScope ->
41 | with(scope) {
42 | Modifier
43 | .skipToLookaheadSize()
44 | .sharedBounds(
45 | rememberSharedContentState(
46 | key = NoteSharedElementKey(
47 | noteId = noteId,
48 | origin = sharedElementKey,
49 | type = NoteSharedElementType.Bounds
50 | )
51 | ),
52 | animatedVisibilityScope = visibilityScope
53 | )
54 | }
55 | }
56 | } ?: Modifier
57 | this.then(sharedTransitionModifier)
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/util/settings/SettingsManager.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.util.settings
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.mutableStateOf
6 | import com.google.gson.Gson
7 | import com.jobik.shkiper.SharedPreferencesKeys.ApplicationSettings
8 | import com.jobik.shkiper.SharedPreferencesKeys.ApplicationStorageName
9 |
10 | object SettingsManager {
11 | private var _settings: MutableState = mutableStateOf(SettingsState())
12 | var state: MutableState
13 | get() = _settings
14 | private set(value) {
15 | _settings = value
16 | }
17 |
18 | val settings: SettingsState
19 | get() = _settings.value
20 |
21 |
22 | fun init(context: Context) {
23 | state.value = restore(context = context)
24 | }
25 |
26 | fun update(context: Context, settings: SettingsState) {
27 | updateState(settings)
28 | saveToSharedPreferences(settings = settings, context = context)
29 | }
30 |
31 | fun update(context: Context, settings: (SettingsState) -> Unit): Boolean {
32 | val updatedSettings = _settings.value.apply {
33 | settings(_settings.value)
34 | }
35 | updateState(updatedSettings)
36 | saveToSharedPreferences(settings = updatedSettings, context = context)
37 | return true
38 | }
39 |
40 | private fun updateState(settings: SettingsState) {
41 | _settings.value = settings
42 | }
43 |
44 | private fun saveToSharedPreferences(
45 | settings: SettingsState,
46 | context: Context
47 | ) {
48 | val storedUiThemeString = Gson().toJson(settings, SettingsState::class.java)
49 | val store = context.getSharedPreferences(ApplicationStorageName, Context.MODE_PRIVATE)
50 | store.edit().putString(ApplicationSettings, storedUiThemeString.toString()).apply()
51 | }
52 |
53 | private fun restore(context: Context): SettingsState {
54 | val store = context.getSharedPreferences(ApplicationStorageName, Context.MODE_PRIVATE)
55 | val savedSettings = store.getString(ApplicationSettings, "")
56 | return if (savedSettings.isNullOrEmpty()) {
57 | SettingsState()
58 | } else {
59 | try {
60 | Gson().fromJson(savedSettings, SettingsState::class.java)
61 | } catch (e: Exception) {
62 | SettingsState()
63 | }
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/database/models/Note.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.database.models
2 |
3 | import androidx.annotation.Keep
4 | import io.realm.kotlin.ext.realmListOf
5 | import io.realm.kotlin.ext.realmSetOf
6 | import io.realm.kotlin.types.RealmList
7 | import io.realm.kotlin.types.RealmObject
8 | import io.realm.kotlin.types.RealmSet
9 | import io.realm.kotlin.types.annotations.Index
10 | import io.realm.kotlin.types.annotations.PrimaryKey
11 | import org.mongodb.kbson.ObjectId
12 | import java.time.LocalDateTime
13 |
14 | @Keep
15 | enum class NotePosition {
16 | MAIN,
17 | ARCHIVE,
18 | DELETE,
19 | }
20 |
21 | class Note : RealmObject {
22 | @PrimaryKey
23 | var _id: ObjectId = ObjectId.invoke()
24 |
25 | @Index
26 | var header: String = ""
27 |
28 | @Index
29 | var body: String = ""
30 | var hashtags: RealmSet = realmSetOf()
31 | var creationDateString: String = ""
32 | var updateDateString: String = ""
33 | var deletionDateString: String? = null
34 | var photos: RealmList = realmListOf()
35 |
36 | @Index
37 | var isPinned: Boolean = false
38 | var isTaskList: Boolean = false
39 | var linkPreviewEnabled: Boolean = true
40 |
41 | @Index
42 | var positionString: String = NotePosition.MAIN.name
43 |
44 | var position: NotePosition
45 | get() = NotePosition.valueOf(positionString)
46 | set(value) {
47 | positionString = value.name
48 | }
49 |
50 | var creationDate: LocalDateTime
51 | get() {
52 | return try {
53 | LocalDateTime.parse(creationDateString)
54 | } catch (e: Exception) {
55 | LocalDateTime.now()
56 | }
57 | }
58 | set(value) {
59 | creationDateString = value.toString()
60 | }
61 |
62 | var updateDate: LocalDateTime
63 | get() {
64 | return try {
65 | LocalDateTime.parse(updateDateString)
66 | } catch (e: Exception) {
67 | LocalDateTime.now()
68 | }
69 | }
70 | set(value) {
71 | updateDateString = value.toString()
72 | }
73 |
74 | var deletionDate: LocalDateTime?
75 | get() {
76 | return try {
77 | LocalDateTime.parse(deletionDateString)
78 | } catch (e: Exception) {
79 | null
80 | }
81 | }
82 | set(value) {
83 | deletionDateString = value.toString()
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/components/layouts/Counter.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.components.layouts
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.text.TextStyle
14 | import androidx.compose.ui.text.style.TextAlign
15 | import androidx.compose.ui.unit.dp
16 | import com.jobik.shkiper.ui.theme.AppTheme
17 |
18 | @Composable
19 | fun Counter(
20 | count: Int,
21 | style: TextStyle = MaterialTheme.typography.bodyMedium,
22 | color: Color = AppTheme.colors.text
23 | ) {
24 | Row(
25 | modifier = Modifier.animateContentSize(),
26 | horizontalArrangement = Arrangement.Center,
27 | verticalAlignment = Alignment.CenterVertically,
28 | ) {
29 | count.toString()
30 | .mapIndexed { index, c -> Digit(c, count, index) }
31 | .forEach { digit ->
32 | AnimatedContent(
33 | modifier = Modifier.width(10.dp),
34 | targetState = digit,
35 | transitionSpec = {
36 | if (targetState > initialState) {
37 | slideInVertically { -it } togetherWith slideOutVertically { it }
38 | } else {
39 | slideInVertically { it } togetherWith slideOutVertically { -it }
40 | }
41 | },
42 | label = "Counter"
43 | ) { digit ->
44 | Text(
45 | text = "${digit.digitChar}",
46 | style = style,
47 | textAlign = TextAlign.Center,
48 | color = color
49 | )
50 | }
51 | }
52 | }
53 | }
54 |
55 | private data class Digit(val digitChar: Char, val fullNumber: Int, val place: Int) {
56 | override fun equals(other: Any?): Boolean {
57 | return when (other) {
58 | is Digit -> digitChar == other.digitChar
59 | else -> super.equals(other)
60 | }
61 | }
62 | }
63 |
64 | private operator fun Digit.compareTo(other: Digit): Int {
65 | return fullNumber.compareTo(other.fullNumber)
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/navigation/NavigationHelpers.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.navigation
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.navigation.NavController
6 | import androidx.navigation.NavDestination.Companion.hasRoute
7 | import androidx.navigation.NavDestination.Companion.hierarchy
8 | import androidx.navigation.NavGraph.Companion.findStartDestination
9 |
10 | class NavigationHelpers {
11 | companion object {
12 |
13 | fun NavController.canNavigate(): Boolean {
14 | return this.currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED
15 | }
16 |
17 | fun NavController.navigateToMain(destination: Screen) = run {
18 | if (canNavigate().not()) return@run
19 | if (checkIsDestinationCurrent(destination)) return@run
20 | navigate(destination) {
21 | // Pop up to the start destination of the graph to
22 | // avoid building up a large stack of destinations
23 | // on the back stack as users select items
24 | try {
25 | popUpTo(graph.findStartDestination().id) {
26 | saveState = true
27 | }
28 | } catch (e: Exception) {
29 | Log.e("navigateToMain", "findStartDestination", e)
30 | }
31 | // Avoid multiple copies of the same destination when
32 | // reselecting the same item
33 | launchSingleTop = true
34 | // Restore state when reselecting a previously selected item
35 | restoreState = true
36 | }
37 | }
38 |
39 | fun NavController.navigateToSecondary(destination: Screen) = run {
40 | if (canNavigate().not()) return@run
41 | if (checkIsDestinationCurrent(destination)) return@run
42 | navigate(destination) {
43 | // Avoid multiple copies of the same destination when
44 | // reselecting the same item
45 | launchSingleTop = true
46 | // Restore state when reselecting a previously selected item
47 | restoreState = true
48 | }
49 | }
50 |
51 | private fun NavController.checkIsDestinationCurrent(destination: Screen): Boolean {
52 | val backStackEntry = this.currentBackStackEntry ?: return false
53 | return backStackEntry.destination.hierarchy.any {
54 | it.hasRoute(destination::class)
55 | }
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/util/ContextUtils.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.util
2 |
3 | import android.app.UiModeManager
4 | import android.content.Context
5 | import android.net.ConnectivityManager
6 | import android.net.NetworkCapabilities
7 | import android.os.Build
8 | import com.jobik.shkiper.util.settings.Localization
9 | import java.util.Locale
10 |
11 | object ContextUtils {
12 | fun Context.adjustFontSize(
13 | scale: Float?
14 | ): Context {
15 | val configuration = resources.configuration
16 | configuration.fontScale = scale ?: resources.configuration.fontScale
17 | return createConfigurationContext(configuration)
18 | }
19 |
20 | fun Context.isInstalledFromPlayStore(): Boolean = verifyInstallerId(
21 | listOf(
22 | "com.android.vending",
23 | "com.google.android.feedback"
24 | )
25 | )
26 |
27 | private fun Context.verifyInstallerId(
28 | validInstallers: List
29 | ): Boolean = validInstallers.contains(getInstallerPackageName(packageName))
30 |
31 | private fun Context.getInstallerPackageName(packageName: String): String? {
32 | kotlin.runCatching {
33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
34 | return packageManager.getInstallSourceInfo(packageName).installingPackageName
35 | @Suppress("DEPRECATION")
36 | return packageManager.getInstallerPackageName(packageName)
37 | }
38 | return null
39 | }
40 |
41 | fun isDarkModeEnabled(context: Context): Boolean {
42 | val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
43 | return uiModeManager.nightMode == UiModeManager.MODE_NIGHT_YES
44 | }
45 |
46 | fun setLocale(context: Context, language: Localization): Context? {
47 | val locale = Locale(language.localeKey)
48 | Locale.setDefault(locale)
49 | val configuration = context.resources.configuration
50 | configuration.setLocale(locale)
51 | configuration.setLayoutDirection(locale)
52 | return context.createConfigurationContext(configuration)
53 | }
54 |
55 | fun hasInternetConnection(context: Context): Boolean {
56 | val connectivityManager =
57 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
58 | val activeNetwork = connectivityManager.activeNetwork ?: return false
59 | val networkCapabilities =
60 | connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
61 | return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/widgets/services/PinWidgetReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.widgets.services
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.glance.GlanceId
7 | import androidx.glance.appwidget.GlanceAppWidgetManager
8 | import androidx.glance.appwidget.state.updateAppWidgetState
9 | import com.jobik.shkiper.SharedPreferencesKeys
10 | import com.jobik.shkiper.database.data.note.NoteMongoRepository
11 | import com.jobik.shkiper.database.models.Note
12 | import com.jobik.shkiper.helpers.TextHelper
13 | import com.jobik.shkiper.widgets.WidgetKeys
14 | import com.jobik.shkiper.widgets.WidgetKeys.Prefs.noteBody
15 | import com.jobik.shkiper.widgets.WidgetKeys.Prefs.noteHeader
16 | import com.jobik.shkiper.widgets.WidgetKeys.Prefs.noteId
17 | import com.jobik.shkiper.widgets.WidgetKeys.Prefs.noteLastUpdate
18 | import com.jobik.shkiper.widgets.widgets.NoteWidget
19 | import com.mohamedrejeb.richeditor.model.RichTextState
20 | import dagger.hilt.android.AndroidEntryPoint
21 | import kotlinx.coroutines.CoroutineScope
22 | import kotlinx.coroutines.delay
23 | import kotlinx.coroutines.launch
24 | import org.mongodb.kbson.ObjectId
25 | import javax.inject.Inject
26 | import kotlin.coroutines.EmptyCoroutineContext
27 |
28 | @AndroidEntryPoint
29 | class PinWidgetReceiver : BroadcastReceiver() {
30 |
31 | @Inject
32 | lateinit var repository: NoteMongoRepository
33 |
34 | override fun onReceive(context: Context, intent: Intent) {
35 | val noteId = intent.getStringExtra(SharedPreferencesKeys.NoteIdExtra)
36 | if(noteId.isNullOrEmpty()) return
37 | CoroutineScope(EmptyCoroutineContext).launch {
38 | val note = repository.getNote(ObjectId(noteId)) ?: return@launch
39 | delay(3000)
40 | val glanceManager = GlanceAppWidgetManager(context)
41 | val lastAddedGlanceId = glanceManager.getGlanceIds(NoteWidget::class.java).last()
42 | mapNoteToWidget(context, lastAddedGlanceId, note)
43 | }
44 | }
45 |
46 | private suspend fun mapNoteToWidget(context: Context, lastAddedGlanceId: GlanceId, note: Note) {
47 | updateAppWidgetState(context, lastAddedGlanceId) { prefs ->
48 | val richBody = RichTextState()
49 | richBody.setHtml(note.body)
50 |
51 | prefs[noteId] = note._id.toHexString()
52 | prefs[noteHeader] = note.header
53 | prefs[noteBody] = TextHelper.removeMarkdownStyles(richBody.toMarkdown())
54 | prefs[noteLastUpdate] = note.updateDateString
55 | }
56 | NoteWidget().update(context, lastAddedGlanceId)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/components/layouts/SettingsGroup.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.components.layouts
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.animation.core.animateDpAsState
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.border
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.ColumnScope
9 | import androidx.compose.foundation.layout.PaddingValues
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.clip
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.unit.dp
21 | import com.jobik.shkiper.ui.theme.AppTheme
22 |
23 | @Composable
24 | fun SettingsGroup(
25 | header: String? = null,
26 | accent: Boolean = false,
27 | paddingValues: PaddingValues = PaddingValues(vertical = 8.dp),
28 | columnScope: @Composable ColumnScope.() -> Unit
29 | ) {
30 | val accentColor =
31 | animateColorAsState(
32 | targetValue = if (accent) AppTheme.colors.primary else Color.Transparent,
33 | label = "accentColor"
34 | )
35 |
36 | val borderWidth =
37 | animateDpAsState(
38 | targetValue = if (accent) 2.dp else 0.dp,
39 | label = "borderWidth"
40 | )
41 |
42 | Column(
43 | modifier = Modifier
44 | .clip(AppTheme.shapes.large)
45 | .background(AppTheme.colors.container)
46 | .border(
47 | width = borderWidth.value,
48 | shape = AppTheme.shapes.large,
49 | color = accentColor.value
50 | )
51 | .padding(paddingValues),
52 | horizontalAlignment = Alignment.CenterHorizontally
53 | ) {
54 | if (!header.isNullOrBlank()) {
55 | Text(
56 | modifier = Modifier
57 | .fillMaxWidth()
58 | .padding(top = 7.dp)
59 | .padding(horizontal = 20.dp)
60 | .padding(bottom = 8.dp),
61 | color = AppTheme.colors.primary,
62 | text = header,
63 | fontWeight = FontWeight.SemiBold,
64 | style = MaterialTheme.typography.titleLarge,
65 | )
66 | }
67 | columnScope()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/components/cards/ThemePreview.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.components.cards
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.shape.CircleShape
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.outlined.Check
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.clip
17 | import androidx.compose.ui.unit.dp
18 | import com.jobik.shkiper.ui.theme.CustomThemeColors
19 |
20 | @Composable
21 | fun ThemePreview(
22 | colors: CustomThemeColors,
23 | selected: Boolean = false,
24 | onClick: () -> Unit = {}
25 | ) {
26 | Box(
27 | modifier = Modifier
28 | .size(60.dp)
29 | .clip(CircleShape)
30 | .clickable { onClick() },
31 | contentAlignment = Alignment.Center
32 | ) {
33 | Column(Modifier.fillMaxSize()) {
34 | Column(
35 | Modifier
36 | .weight(1f)
37 | .background(colors.primary)
38 | ) {
39 | Row(modifier = Modifier.fillMaxSize()) {}
40 | }
41 | Row(Modifier.weight(1f)) {
42 | Box(
43 | Modifier
44 | .weight(1f)
45 | .background(colors.container)
46 | ) {
47 | Row(modifier = Modifier.fillMaxSize()) {}
48 | }
49 | Box(
50 | Modifier
51 | .weight(1f)
52 | .background(colors.background)
53 | ) {
54 | Row(modifier = Modifier.fillMaxSize()) {}
55 | }
56 | }
57 | }
58 | AnimatedVisibility(visible = selected, enter = fadeIn(), exit = fadeOut()) {
59 | Box(
60 | modifier = Modifier
61 | .size(35.dp)
62 | .clip(CircleShape)
63 | .background(colors.secondaryContainer),
64 | contentAlignment = Alignment.Center
65 | ) {
66 | Icon(
67 | imageVector = Icons.Outlined.Check,
68 | contentDescription = null,
69 | tint = colors.onSecondaryContainer
70 | )
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/animation/AnimateVerticalSwitch.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.animation
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 |
7 |
8 | @Composable
9 | fun AnimateVerticalSwitch(
10 | modifier: Modifier,
11 | state: Boolean,
12 | directionUp: Boolean = true,
13 | topComponent: @Composable () -> Unit,
14 | bottomComponent: @Composable () -> Unit
15 | ) {
16 | AnimatedContent(
17 | modifier = modifier,
18 | targetState = state,
19 | transitionSpec = {
20 | if (directionUp) {
21 | // Compare the incoming number with the previous number.
22 | if (targetState > initialState) {
23 | // If the target number is larger, it slides up and fades in
24 | // while the initial (smaller) number slides up and fades out.
25 | (slideInVertically { height -> height } + fadeIn()).togetherWith(slideOutVertically { height -> -height } + fadeOut())
26 | } else {
27 | // If the target number is smaller, it slides down and fades in
28 | // while the initial number slides down and fades out.
29 | (slideInVertically { height -> -height } + fadeIn()).togetherWith(slideOutVertically { height -> height } + fadeOut())
30 | }.using(
31 | // Disable clipping since the faded slide-in/out should
32 | // be displayed out of bounds.
33 | SizeTransform(clip = false)
34 | )
35 | } else {
36 | // Compare the incoming number with the previous number.
37 | if (targetState < initialState) {
38 | // If the target number is larger, it slides up and fades in
39 | // while the initial (smaller) number slides up and fades out.
40 | (slideInVertically { height -> height } + fadeIn()).togetherWith(slideOutVertically { height -> -height } + fadeOut())
41 | } else {
42 | // If the target number is smaller, it slides down and fades in
43 | // while the initial number slides down and fades out.
44 | (slideInVertically { height -> -height } + fadeIn()).togetherWith(slideOutVertically { height -> height } + fadeOut())
45 | }.using(
46 | // Disable clipping since the faded slide-in/out should
47 | // be displayed out of bounds.
48 | SizeTransform(clip = false)
49 | )
50 | }
51 | }, label = ""
52 | ) { value ->
53 | if (value) {
54 | bottomComponent()
55 | } else {
56 | topComponent()
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jobik/shkiper/ui/components/layouts/VerticalIndicator.kt:
--------------------------------------------------------------------------------
1 | package com.jobik.shkiper.ui.components.layouts
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.pager.PagerState
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.geometry.CornerRadius
8 | import androidx.compose.ui.geometry.RoundRect
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.Path
11 | import androidx.compose.ui.graphics.drawscope.DrawScope
12 | import androidx.compose.ui.graphics.lerp
13 | import androidx.compose.ui.unit.Dp
14 | import androidx.compose.ui.unit.dp
15 | import com.jobik.shkiper.ui.theme.AppTheme
16 |
17 | @Composable
18 | fun VerticalIndicator(
19 | pagerState: PagerState,
20 | color: Color = AppTheme.colors.secondaryContainer,
21 | activeColor: Color = AppTheme.colors.primary,
22 | spacing: Dp = 15.dp,
23 | dotWidth: Dp = 8.dp,
24 | dotHeight: Dp = 25.dp,
25 | dotActiveHeight: Dp = 58.dp,
26 | ) {
27 | // To get scroll offset
28 | val pageOffset = pagerState.currentPage + pagerState.currentPageOffsetFraction
29 |
30 | Canvas(modifier = Modifier) {
31 | val spacing = spacing.toPx()
32 | val dotWidth = dotWidth.toPx()
33 | val dotHeight = dotHeight.toPx()
34 |
35 | val activeDotHeight = dotActiveHeight.toPx()
36 | var y = 0f
37 | val x = center.x
38 |
39 | repeat(pagerState.pageCount) { i ->
40 | val posOffset = pageOffset
41 | val dotOffset = posOffset % 1
42 | val current = posOffset.toInt()
43 |
44 | val factor = (dotOffset * (activeDotHeight - dotHeight))
45 |
46 | val calculatedHeight = when {
47 | i == current -> activeDotHeight - factor
48 | i - 1 == current || (i == 0 && posOffset > pagerState.pageCount - 1) -> dotHeight + factor
49 | else -> dotHeight
50 | }
51 |
52 | val currentColor = lerp(color, activeColor, calculatedHeight / activeDotHeight)
53 |
54 | drawIndicator(
55 | x = x,
56 | y = y,
57 | width = dotWidth,
58 | height = calculatedHeight,
59 | radius = CornerRadius(2000f),
60 | color = currentColor
61 | )
62 | y += calculatedHeight + spacing
63 | }
64 | }
65 | }
66 |
67 | private fun DrawScope.drawIndicator(
68 | x: Float,
69 | y: Float,
70 | width: Float,
71 | height: Float,
72 | radius: CornerRadius,
73 | color: Color
74 | ) {
75 | val rect = RoundRect(
76 | left = x - width / 2,
77 | top = y,
78 | right = x + width / 2,
79 | bottom = y + height,
80 | cornerRadius = radius
81 | )
82 | val path = Path().apply { addRoundRect(rect) }
83 | drawPath(path = path, color = color)
84 | }
--------------------------------------------------------------------------------