Skip to content

Commit

Permalink
feat: Add launcher shortcuts to quickly jump back to last read book (#…
Browse files Browse the repository at this point in the history
…183)

---------
Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam authored Jun 5, 2024
1 parent 13c472c commit 6f0bbf1
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 31 deletions.
116 changes: 116 additions & 0 deletions app/schemas/com.starry.myne.database.MyneDatabase/5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "a45c727254ac24ff3a2e67f604e06159",
"entities": [
{
"tableName": "book_library",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`book_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `authors` TEXT NOT NULL, `file_path` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `is_external_book` INTEGER NOT NULL DEFAULT false, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "bookId",
"columnName": "book_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "authors",
"columnName": "authors",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filePath",
"columnName": "file_path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isExternalBook",
"columnName": "is_external_book",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "reader_table",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`library_item_id` INTEGER NOT NULL, `last_chapter_index` INTEGER NOT NULL, `last_chapter_offset` INTEGER NOT NULL, `last_read_time` INTEGER NOT NULL DEFAULT 0, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "libraryItemId",
"columnName": "library_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastChapterIndex",
"columnName": "last_chapter_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastChapterOffset",
"columnName": "last_chapter_offset",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadTime",
"columnName": "last_read_time",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a45c727254ac24ff3a2e67f604e06159')"
]
}
}
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="myne_lc_shortcut" />
</intent-filter>

<meta-data
android:name="android.app.lib_name"
android:value="" />
Expand Down
28 changes: 27 additions & 1 deletion app/src/main/java/com/starry/myne/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package com.starry.myne

import android.content.pm.ShortcutManager
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
Expand Down Expand Up @@ -73,9 +75,33 @@ class MainActivity : AppCompatActivity() {
initial = NetworkObserver.Status.Unavailable
)

MainScreen(startDestination = startDestination, networkStatus = status)
MainScreen(
intent = intent,
startDestination = startDestination,
networkStatus = status
)
}
}
}
}

override fun onPause() {
super.onPause()
updateShortcuts()
}

private fun updateShortcuts() {
val shortcutManager = getSystemService(ShortcutManager::class.java)
mainViewModel.buildDynamicShortcuts(
context = this,
limit = shortcutManager.maxShortcutCountPerActivity,
onComplete = { shortcuts ->
try {
shortcutManager.dynamicShortcuts = shortcuts
} catch (e: IllegalArgumentException) {
Log.e("MainActivity", "Error setting dynamic shortcuts", e)
}
}
)
}
}
64 changes: 63 additions & 1 deletion app/src/main/java/com/starry/myne/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,34 @@

package com.starry.myne

import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Icon
import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.starry.myne.database.library.LibraryDao
import com.starry.myne.database.reader.ReaderDao
import com.starry.myne.ui.navigation.BottomBarScreen
import com.starry.myne.ui.navigation.Screens
import com.starry.myne.ui.screens.welcome.viewmodels.WelcomeDataStore
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(private val welcomeDataStore: WelcomeDataStore) :
class MainViewModel @Inject constructor(
private val welcomeDataStore: WelcomeDataStore,
private val libraryDao: LibraryDao,
private val readerDao: ReaderDao
) :
ViewModel() {
private val _isLoading: MutableState<Boolean> = mutableStateOf(true)
val isLoading: State<Boolean> = _isLoading
Expand All @@ -40,6 +53,17 @@ class MainViewModel @Inject constructor(private val welcomeDataStore: WelcomeDat
mutableStateOf(Screens.WelcomeScreen.route)
val startDestination: State<String> = _startDestination

companion object {
// Must be same as the one in AndroidManifest.xml
const val LAUNCHER_SHORTCUT_SCHEME = "myne_lc_shortcut"

// Key to get goalId from intent.
const val LC_SC_LIBRARY_ITEM_ID = "lc_shortcut_library_item_id"

// Key to detect new goal shortcut.
const val LC_SC_BOOK_LIBRARY = "lc_shortcut_book_library"
}

init {
viewModelScope.launch {
// Check if user has completed onboarding.
Expand All @@ -55,4 +79,42 @@ class MainViewModel @Inject constructor(private val welcomeDataStore: WelcomeDat
}
}
}

fun buildDynamicShortcuts(
context: Context,
limit: Int,
onComplete: (List<ShortcutInfo>) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
val libraryItems = readerDao.getAllReaderItems()
.sortedByDescending { it.lastReadTime }
.take(limit - 1).mapNotNull {
libraryDao.getItemById(it.libraryItemId)
}

val libraryShortcut = ShortcutInfo.Builder(context, "library").apply {
setShortLabel(context.getString(R.string.library_header))
setIcon(Icon.createWithResource(context, R.drawable.ic_nav_library))
setIntent(Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse("$LAUNCHER_SHORTCUT_SCHEME://library")
putExtra(LC_SC_BOOK_LIBRARY, true)
})
}.build()

val shortcuts = listOf(libraryShortcut) + libraryItems.map {
ShortcutInfo.Builder(context, "library_item_${it.id}").apply {
setShortLabel(it.title)
setIcon(Icon.createWithResource(context, R.drawable.ic_library_external_item))
setIntent(Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse("$LAUNCHER_SHORTCUT_SCHEME://library_item/${it.id}")
putExtra(LC_SC_LIBRARY_ITEM_ID, it.id)
})
}.build()
}

withContext(Dispatchers.Main) { onComplete(shortcuts) }
}
}
}
3 changes: 2 additions & 1 deletion app/src/main/java/com/starry/myne/database/MyneDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ import com.starry.myne.helpers.Constants

@Database(
entities = [LibraryItem::class, ReaderData::class],
version = 4,
version = 5,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 4, to = 5),
]
)
abstract class MyneDatabase : RoomDatabase() {
Expand Down
15 changes: 7 additions & 8 deletions app/src/main/java/com/starry/myne/database/reader/ReaderDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow


Expand All @@ -33,17 +34,15 @@ interface ReaderDao {
@Query("DELETE FROM reader_table WHERE library_item_id = :libraryItemId")
fun delete(libraryItemId: Int)

@Query(
"UPDATE reader_table SET "
+ "last_chapter_index = :lastChapterIndex,"
+ "last_chapter_offset = :lastChapterOffset"
+ " WHERE library_item_id = :libraryItemId"
)
fun update(libraryItemId: Int, lastChapterIndex: Int, lastChapterOffset: Int)
@Update
fun update(readerData: ReaderData)

@Query("SELECT * FROM reader_table WHERE library_item_id = :libraryItemId")
fun getReaderData(libraryItemId: Int): ReaderData?

@Query("SELECT * FROM reader_table")
fun getAllReaderItems(): List<ReaderData>

@Query("SELECT * FROM reader_table WHERE library_item_id = :libraryItemId")
fun getReaderDataAsFlow(libraryItemId: Int): Flow<ReaderData?>
fun getReaderDataAsFlow(libraryItemId: Int): Flow<ReaderData>?
}
14 changes: 12 additions & 2 deletions app/src/main/java/com/starry/myne/database/reader/ReaderData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,26 @@ package com.starry.myne.database.reader
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.Locale

@Entity(tableName = "reader_table")
data class ReaderData(
@ColumnInfo(name = "library_item_id") val libraryItemId: Int,
@ColumnInfo(name = "last_chapter_index") val lastChapterIndex: Int,
@ColumnInfo(name = "last_chapter_offset") val lastChapterOffset: Int
@ColumnInfo(name = "last_chapter_offset") val lastChapterOffset: Int,
// Added in database schema version 5
@ColumnInfo(
name = "last_read_time",
defaultValue = "0"
) val lastReadTime: Long = System.currentTimeMillis()
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0

fun getProgressPercent(totalChapters: Int) =
String.format("%.2f", ((lastChapterIndex + 1).toFloat() / totalChapters.toFloat()) * 100f)
String.format(
locale = Locale.US,
format = "%.2f",
((lastChapterIndex + 1).toFloat() / totalChapters.toFloat()) * 100f
)
}
Loading

0 comments on commit 6f0bbf1

Please sign in to comment.