Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 0 additions & 36 deletions .github/workflows/master-apk.yml

This file was deleted.

17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@
- Import/Export links
- QR Code support: Generate and scan
- Organize links by tags
- Manage multiple profiles
- Manage multiple profiles. When the user is inside a profile list of links, clicking the Profile icon at the bottom bar or using the device back button will send them back to the profiles selection grid.
- Save link by sharing from other app (eg: chrome, etc.)
- **Silent save option:** Save links from share sheet without opening the app
- Save links to markdown file in local storage. (can be used for obsidian)
- **Local network server:** Access and manage links from other devices on the same network

### Build Variant specific features

| Feature | Github Release | Play Store | F-droid | Pro Version on Play Store |
|-----------------------------------|----------------|------------|---------|-----------|
| Firebase Analytics | ❌ | | ❌ | ✅ |
| Google Drive Backup | ❌ | ❌ | ❌ | ✅ |
| Profile speicific themes | ❌ | ❌ | ❌ | ✅ |
| Feature | Github Release | F-droid | Play Store | Pro Version on Play Store |
|-----------------------------------|----------------|---------|------------|-----------|
| Firebase Analytics | ❌ | | ✅ | ✅ |
| Google Drive Backup | ❌ | ❌ | ❌ | ✅ |
| Profile speicific themes | ❌ | ❌ | ❌ | ✅ |
Comment on lines +35 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Two text errors in the changed table rows.

  1. Line 35/39: "Github" → "GitHub" (official branding; flagged by static analysis).
  2. Line 39: "Profile speicific themes" → "Profile specific themes" (transposed letters).
✏️ Proposed fix
-| Feature                           | Github Release | F-droid | Play Store | Pro Version on Play Store |
-|-----------------------------------|----------------|---------|------------|-----------|
-| Firebase Analytics                | ❌              | ❌       | ✅          | ✅  |
-| Google Drive Backup | ❌              | ❌       | ❌         | ✅  |
-| Profile speicific themes | ❌              | ❌       | ❌          | ✅  |
+| Feature                           | GitHub Release | F-droid | Play Store | Pro Version on Play Store |
+|-----------------------------------|----------------|---------|------------|-----------|
+| Firebase Analytics                | ❌              | ❌       | ✅          | ✅  |
+| Google Drive Backup               | ❌              | ❌       | ❌          | ✅  |
+| Profile specific themes           | ❌              | ❌       | ❌          | ✅  |
🧰 Tools
🪛 LanguageTool

[uncategorized] ~35-~35: The official name of this software platform is spelled with a capital “H”.
Context: ... | Feature | Github Release | F-droid | Play Store | Pro Ve...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 35 - 39, Update the markdown table rows to fix two
typos: change the string "Github" in the column header/cell to the correct brand
casing "GitHub", and correct the feature label "Profile speicific themes" to
"Profile specific themes" so the table reads exactly "GitHub" and "Profile
specific themes" where those exact strings appear.




Expand All @@ -58,11 +58,10 @@ You can download from any of the sources mentioned below.
All these sources supports cross platform app updates. for eg: if you've installed app from F-Droid
then you can update the app from any of the sources.

- F-Droid : [Download](https://f-droid.org/packages/com.yogeshpaliyal.deepr/)
- Github Release : [Download](https://github.com/yogeshpaliyal/Deepr/releases/latest)
- F-Droid : [Download](https://f-droid.org/packages/com.yogeshpaliyal.deepr/)
- Play Store: [Download](https://play.google.com/store/apps/details?id=com.yogeshpaliyal.deepr)
- Play Store (All features
unlocked) : [Download](https://play.google.com/store/apps/details?id=com.yogeshpaliyal.deepr.pro)
- Play Store (All features unlocked) : [Download](https://play.google.com/store/apps/details?id=com.yogeshpaliyal.deepr.pro)



Expand Down
12 changes: 9 additions & 3 deletions app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ class DeeprApplication : Application() {
single<SqlDriver> {
LogSqliteDriver(
AndroidSqliteDriver(
DeeprDB.Schema,
this@DeeprApplication,
"deepr.db",
schema = DeeprDB.Schema,
context = this@DeeprApplication,
name = "deepr.db",
callback = object : AndroidSqliteDriver.Callback(DeeprDB.Schema) {
override fun onOpen(db: androidx.sqlite.db.SupportSQLiteDatabase) {
super.onOpen(db)
db.setForeignKeyConstraintsEnabled(true)
}
},
),
) {
Log.d("loggingDB", it)
Expand Down
33 changes: 25 additions & 8 deletions app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,13 @@ fun Dashboard(
val context = LocalContext.current
val viewModel: AccountViewModel = koinViewModel()

// Collect clipboard link detection preference
val clipboardLinkDetectionEnabled by viewModel.clipboardLinkDetectionEnabled.collectAsStateWithLifecycle()

// Clipboard link detection
var clipboardLink by remember { mutableStateOf<ClipboardLink?>(null) }

LaunchedEffect(clipboardLinkDetectionEnabled) {
if (clipboardLinkDetectionEnabled) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
androidx.compose.runtime.DisposableEffect(Unit) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager

val listener = ClipboardManager.OnPrimaryClipChangedListener {
val clipData = clipboard.primaryClip
if (clipData != null && clipData.itemCount > 0) {
val text = clipData.getItemAt(0).text?.toString()
Expand All @@ -213,8 +211,24 @@ fun Dashboard(
}
}
}
} else {
clipboardLink = null
}

// Initial check
val clipData = clipboard.primaryClip
if (clipData != null && clipData.itemCount > 0) {
val text = clipData.getItemAt(0).text?.toString()
if (!text.isNullOrBlank()) {
val normalizedLink = normalizeLink(text)
if (isValidDeeplink(normalizedLink)) {
clipboardLink = ClipboardLink(normalizedLink)
}
}
}

clipboard.addPrimaryClipChangedListener(listener)

onDispose {
clipboard.removePrimaryClipChangedListener(listener)
}
}

Expand All @@ -239,6 +253,9 @@ fun Dashboard(
selected = isSelected,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick)
if (topLevelRoute is Dashboard2) {
viewModel.setShowProfilesGrid(true)
}
backStack.addTopLevel(topLevelRoute)
},
label = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class CsvBookmarkImporter(
profileName?.let {
val profile = deeprQueries.getProfileByName(it).executeAsOneOrNull()
if (profile == null) {
deeprQueries.insertProfile(it)
deeprQueries.insertProfile(it, 0L)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "maxPriority|insertProfile" --type=kotlin -A 2 -B 1

Repository: yogeshpaliyal/Deepr

Length of output: 6312


🏁 Script executed:

rg -n "maxPriority\|insertProfile" --include="*.sq" -A 2 -B 1

Repository: yogeshpaliyal/Deepr

Length of output: 504


🏁 Script executed:

# Also check the CsvBookmarkImporter.kt file directly to see the context around line 71
fd "CsvBookmarkImporter.kt" --type=f -x cat -n {}

Repository: yogeshpaliyal/Deepr

Length of output: 7603


Imported profiles all receive priority = 0, producing indeterminate grid ordering.

When a CSV contains multiple new profiles, they are all inserted with priority = 0L at line 71. Since the profile grid orders by priority, imported profiles with identical priority values will appear in undefined order relative to each other.

The codebase already follows the correct pattern elsewhere (see AccountViewModel.insertProfile() which queries maxPriority and assigns maxPriority + 1). Apply the same approach here—query maxPriority for each new profile within the transaction loop and assign sequential priorities.

💡 Suggested approach
 if (profile == null) {
-    deeprQueries.insertProfile(it, 0L)
+    val nextPriority = (deeprQueries.maxPriority().executeAsOneOrNull()?.MAX ?: 0L) + 1L
+    deeprQueries.insertProfile(it, nextPriority)
     deeprQueries.lastInsertRowId().executeAsOneOrNull()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt`
at line 71, Imported profiles are all saved with priority = 0L at the
deeprQueries.insertProfile(it, 0L) call in CsvBookmarkImporter, causing
indeterminate ordering; modify the import transaction so it queries the current
max priority (using the same pattern as AccountViewModel.insertProfile(), i.e.,
call deeprQueries.maxPriority() or equivalent) before inserting each new
profile, set the profile's priority to maxPriority + 1, and increment that in
the loop for subsequent inserts so each imported profile gets a sequentially
higher priority rather than 0.

deeprQueries.lastInsertRowId().executeAsOneOrNull()
} else {
profile.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.yogeshpaliyal.deepr.Tags

interface LinkRepository {
// Profile operations
suspend fun insertProfile(name: String)
suspend fun insertProfile(name: String, priority: Long = 0)

fun getAllProfiles(): Query<Profile>

Expand All @@ -23,6 +23,10 @@ interface LinkRepository {
id: Long,
)

suspend fun updateProfilePriority(id: Long, priority: Long)

suspend fun getMaxPriority(): Long

suspend fun deleteProfile(id: Long)

fun countProfiles(): Query<Long>
Expand Down Expand Up @@ -68,6 +72,7 @@ interface LinkRepository {
searchQuery1: String,
searchQuery2: String,
searchQuery3: String,
searchQuery4: String,
favouriteFilter1: Long,
favouriteFilter2: Long,
tagIdsString1: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ class LinkRepositoryImpl(
}

// Profile operations
override suspend fun insertProfile(name: String) {
override suspend fun insertProfile(name: String, priority: Long) {
withContext(Dispatchers.IO) {
deeprQueries.insertProfile(name)
deeprQueries.insertProfile(name, priority)
}
scheduleAutoBackup()
}
Expand Down Expand Up @@ -54,6 +54,18 @@ class LinkRepositoryImpl(
scheduleAutoBackup()
}

override suspend fun updateProfilePriority(id: Long, priority: Long) {
withContext(Dispatchers.IO) {
deeprQueries.updateProfilePriority(priority, id)
}
scheduleAutoBackup()
}

override suspend fun getMaxPriority(): Long =
withContext(Dispatchers.IO) {
deeprQueries.maxPriority().executeAsOneOrNull()?.MAX ?: 0L
}

override suspend fun deleteProfile(id: Long) {
withContext(Dispatchers.IO) {
deeprQueries.deleteProfile(id)
Expand Down Expand Up @@ -148,6 +160,7 @@ class LinkRepositoryImpl(
searchQuery1: String,
searchQuery2: String,
searchQuery3: String,
searchQuery4: String,
favouriteFilter1: Long,
favouriteFilter2: Long,
tagIdsString1: String,
Expand All @@ -162,6 +175,7 @@ class LinkRepositoryImpl(
searchQuery1,
searchQuery2,
searchQuery3,
searchQuery4,
favouriteFilter1,
favouriteFilter2,
tagIdsString1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class AppPreferenceDataStore(

val getThemeMode: Flow<String> =
context.appDataStore.data.map { preferences ->
preferences[THEME_MODE] ?: "light" // Default to light theme
preferences[THEME_MODE] ?: "system" // Default to system theme
}

val getShowOpenCounter: Flow<Boolean> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.net.wifi.WifiManager
import android.util.Log
import com.yogeshpaliyal.deepr.BuildConfig
import com.yogeshpaliyal.deepr.DeeprQueries
import com.yogeshpaliyal.deepr.R
import com.yogeshpaliyal.deepr.Tags
import com.yogeshpaliyal.deepr.analytics.AnalyticsManager
import com.yogeshpaliyal.deepr.data.NetworkRepository
Expand Down Expand Up @@ -120,11 +121,62 @@ open class LocalServerRepositoryImpl(
routing {
get("/") {
try {
val htmlContent =
var htmlContent =
context.assets
.open("index.html")
.bufferedReader()
.use { it.readText() }

// Replace placeholders with translations
val placeholders =
mapOf(
"{{WEB_TITLE}}" to R.string.web_title,
"{{WEB_SUBTITLE}}" to R.string.web_subtitle,
"{{WEB_PROFILE_LABEL}}" to R.string.web_profile_label,
"{{WEB_NEW_PROFILE}}" to R.string.web_new_profile,
"{{WEB_ADD_NEW_LINK}}" to R.string.web_add_new_link,
"{{WEB_LINK_URL}}" to R.string.web_link_url,
"{{WEB_LINK_NAME}}" to R.string.web_link_name,
"{{WEB_OPTIONAL}}" to R.string.web_optional,
"{{WEB_NOTES_OPTIONAL}}" to R.string.web_notes_optional,
"{{WEB_TAGS_OPTIONAL}}" to R.string.web_tags_optional,
"{{WEB_ADD_LINK_BUTTON}}" to R.string.web_add_link_button,
"{{WEB_YOUR_LINKS}}" to R.string.web_your_links,
"{{WEB_REFRESH}}" to R.string.web_refresh,
"{{WEB_SEARCH_PLACEHOLDER}}" to R.string.web_search_placeholder,
"{{WEB_ALL_TAGS}}" to R.string.web_all_tags,
"{{WEB_SORT_DATE}}" to R.string.web_sort_date,
"{{WEB_SORT_NAME}}" to R.string.web_sort_name,
"{{WEB_SORT_OPENS}}" to R.string.web_sort_opens,
"{{WEB_CLEAR}}" to R.string.web_clear,
"{{WEB_LOADING_LINKS}}" to R.string.web_loading_links,
"{{WEB_LOADING_DESCRIPTION}}" to R.string.web_loading_description,
"{{WEB_CREATE_PROFILE_TITLE}}" to R.string.web_create_profile_title,
"{{WEB_PROFILE_NAME_LABEL}}" to R.string.web_profile_name_label,
"{{WEB_CANCEL}}" to R.string.web_cancel,
"{{WEB_CREATE_PROFILE_BUTTON}}" to R.string.web_create_profile_button,
"{{WEB_STATUS_CREATING}}" to R.string.web_status_creating,
"{{WEB_STATUS_ADDED}}" to R.string.web_status_added,
"{{WEB_SUCCESS_LINK_ADDED}}" to R.string.web_success_link_added,
"{{WEB_ERROR_ADD_LINK}}" to R.string.web_error_add_link,
"{{WEB_ERROR_LOADING}}" to R.string.web_error_loading,
"{{WEB_ERROR_REFRESH}}" to R.string.web_error_refresh,
"{{WEB_ERROR_FETCH}}" to R.string.web_error_fetch,
"{{WEB_MSG_SWITCHED}}" to R.string.web_msg_switched,
"{{WEB_MSG_FILTERED}}" to R.string.web_msg_filtered,
"{{WEB_MSG_FILTERS_CLEARED}}" to R.string.web_msg_filters_cleared,
"{{WEB_NO_LINKS}}" to R.string.web_no_links,
"{{WEB_NO_LINKS_DESCRIPTION}}" to R.string.web_no_links_description,
"{{WEB_NO_MATCHING}}" to R.string.web_no_matching,
"{{WEB_NO_MATCHING_DESCRIPTION}}" to R.string.web_no_matching_description,
"{{WEB_OPEN_LINK}}" to R.string.web_open_link,
"{{WEB_OPENS}}" to R.string.web_opens,
)

placeholders.forEach { (placeholder, resId) ->
htmlContent = htmlContent.replace(placeholder, context.getString(resId))
}
Comment on lines +124 to +178
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

HTML asset is re-read and all 43 placeholders replaced on every GET / request

The full file I/O (assets.open("index.html")) and the 43-entry forEach replacement loop run on every web page load. Since the content doesn't change during the server's lifetime, this work should be done once at server startup and the result cached.

♻️ Proposed refactor
+    // Cache the localized HTML content at server start
+    private var cachedHtmlContent: String? = null
+
+    private fun buildLocalizedHtml(): String {
+        val html = context.assets.open("index.html").bufferedReader().use { it.readText() }
+        val placeholders = mapOf(
+            "{{WEB_TITLE}}" to R.string.web_title,
+            // ... (same map as today)
+        )
+        return placeholders.entries.fold(html) { acc, (key, resId) ->
+            acc.replace(key, context.getString(resId))
+        }
+    }

Then in startServer:

+    cachedHtmlContent = buildLocalizedHtml()
     server = embeddedServer(CIO, host = "0.0.0.0", port = port) {
         ...
         get("/") {
             try {
-                var htmlContent = context.assets.open("index.html")...
-                placeholders.forEach { ... }
+                val htmlContent = cachedHtmlContent ?: buildLocalizedHtml()
                 call.respondText(htmlContent, ContentType.Text.Html)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`
around lines 124 - 178, The GET handler currently opens assets and runs the
43-entry placeholders replacement on every request (see htmlContent and
placeholders in LocalServerRepositoryImpl); move that work into server
initialization (e.g., in startServer or the class constructor) so the file is
read once, replacements performed once, and the resulting string stored in a
cached property (e.g., cachedIndexHtml) that the GET / handler returns directly;
update the GET handler to use cachedIndexHtml instead of re-reading assets and
looping through placeholders.


call.respondText(htmlContent, ContentType.Text.Html)
} catch (e: Exception) {
Log.e("LocalServer", "Error reading HTML asset", e)
Expand Down Expand Up @@ -166,7 +218,7 @@ open class LocalServerRepositoryImpl(
post("/api/profiles") {
try {
val request = call.receive<AddProfileRequest>()
deeprQueries.insertProfile(request.name)
deeprQueries.insertProfile(request.name, 0L)
call.respond(
HttpStatusCode.Created,
SuccessResponse("Profile created successfully"),
Expand All @@ -191,6 +243,7 @@ open class LocalServerRepositoryImpl(
"",
"",
"",
"",
-1L,
-1L,
"",
Expand Down Expand Up @@ -267,6 +320,7 @@ open class LocalServerRepositoryImpl(
"",
"",
"",
"",
-1L,
-1L,
tag.id.toString(),
Expand Down Expand Up @@ -328,6 +382,24 @@ open class LocalServerRepositoryImpl(
)
}
}

post("/api/links/increment-count") {
try {
val id = call.request.queryParameters["id"]?.toLongOrNull()
if (id != null) {
accountViewModel.incrementOpenedCount(id)
call.respond(HttpStatusCode.OK, SuccessResponse("Count incremented"))
} else {
call.respond(HttpStatusCode.BadRequest, ErrorResponse("Invalid link ID"))
}
} catch (e: Exception) {
Log.e("LocalServer", "Error incrementing count", e)
call.respond(
HttpStatusCode.InternalServerError,
ErrorResponse("Error incrementing count: ${e.message}"),
)
}
}
}
}

Expand Down Expand Up @@ -554,4 +626,4 @@ data class QRTransferInfo(
val ip: String,
val port: Int,
val appVersion: String,
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ fun NoteViewDialog(
SelectionContainer {
ClickableText(
text = annotatedString,
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
onClick = { offset ->
// Check if clicked position has a URL annotation
annotatedString
Expand Down
Loading
Loading