Skip to content
Closed
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
4 changes: 4 additions & 0 deletions capy/src/main/java/com/jocmp/capy/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@ data class Account(
feedRecords.updateShowUnreadBadge(feedID, enabled)
}

suspend fun updateFeedImportance(feedID: String, importance: FeedImportance) {
feedRecords.updateImportance(feedID, importance)
}

suspend fun toggleAllFeedUnreadBadges(enabled: Boolean) {
feedRecords.toggleAllShowUnreadBadge(enabled)
}
Expand Down
1 change: 1 addition & 0 deletions capy/src/main/java/com/jocmp/capy/Feed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ data class Feed(
val priority: FeedPriority? = null,
val showUnreadBadge: Boolean = true,
val isReadLater: Boolean = false,
val importance: FeedImportance = FeedImportance.NORMAL,
): Countable
25 changes: 25 additions & 0 deletions capy/src/main/java/com/jocmp/capy/FeedImportance.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.jocmp.capy

/**
* Feed importance bucket, modeled after Fraidycat.
*
* Higher importance = check more often, place higher in the surfaced feed list.
*/
enum class FeedImportance {
REAL_TIME,
DAILY,
NORMAL,
WEEKLY,
MONTHLY,
YEARLY;

val storageValue: String
get() = name

companion object {
fun parse(value: String?): FeedImportance {
if (value.isNullOrBlank()) return NORMAL
return entries.firstOrNull { it.name == value } ?: NORMAL
}
}
}
10 changes: 10 additions & 0 deletions capy/src/main/java/com/jocmp/capy/persistence/FeedRecords.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.jocmp.capy.persistence
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import com.jocmp.capy.Feed
import com.jocmp.capy.FeedImportance
import com.jocmp.capy.FeedPriority
import com.jocmp.capy.Folder
import com.jocmp.capy.common.withIOContext
Expand Down Expand Up @@ -122,6 +123,13 @@ internal class FeedRecords(private val database: Database) {
database.feedsQueries.toggleAllNotifications(enabled = enabled)
}

suspend fun updateImportance(feedID: String, importance: FeedImportance) = withIOContext {
database.feedsQueries.updateImportance(
importance = importance.storageValue,
feedID = feedID,
)
}

suspend fun updateShowUnreadBadge(feedID: String, enabled: Boolean) = withIOContext {
database.feedsQueries.updateShowUnreadBadge(
enabled = enabled,
Expand Down Expand Up @@ -186,6 +194,7 @@ internal class FeedRecords(private val database: Database) {
etag: String? = null,
lastModified: String? = null,
conditionalGetRefreshedAt: Long? = null,
importance: String? = null,
folderName: String? = "",
expanded: Boolean? = false,
) = Feed(
Expand All @@ -205,5 +214,6 @@ internal class FeedRecords(private val database: Database) {
priority = FeedPriority.parse(priority),
showUnreadBadge = showUnreadBadge,
isReadLater = readLater,
importance = FeedImportance.parse(importance),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE feeds ADD COLUMN importance TEXT;
3 changes: 3 additions & 0 deletions capy/src/main/sqldelight/com/jocmp/capy/db/feeds.sq
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ UPDATE feeds SET enable_notifications = :enabled WHERE feeds.id = :feedID;
toggleAllNotifications:
UPDATE feeds SET enable_notifications = :enabled;

updateImportance:
UPDATE feeds SET importance = :importance WHERE id = :feedID;

updateShowUnreadBadge:
UPDATE feeds SET show_unread_badge = :enabled WHERE id = :feedID;

Expand Down
117 changes: 117 additions & 0 deletions lite/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import java.util.Properties

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
kotlin("plugin.serialization") version libs.versions.kotlin
alias(libs.plugins.compose.compiler)
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
}

val secrets = Properties()

if (rootProject.file("secrets.properties").exists()) {
secrets.load(rootProject.file("secrets.properties").inputStream())
}

android {
namespace = "com.capyreader.lite"
compileSdk = 36

defaultConfig {
applicationId = "com.capyreader.lite"
minSdk = 30
targetSdk = 36
versionCode = 1
versionName = "0.1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

dependenciesInfo {
includeInApk = false
includeInBundle = false
}

signingConfigs {
getByName("debug") {
storeFile = file("${project.rootDir}/debug.keystore")
}
}

buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
applicationIdSuffix = ".debug"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures {
compose = true
buildConfig = true
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
excludes += "/META-INF/versions/9/OSGI-INF/MANIFEST.MF"
}
}
}

dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.browser)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.material)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.preferences)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.webkit)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
implementation(libs.coil.svg)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.androidx.workmanager)
implementation(platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp.client)
implementation(libs.sqldelight.android.driver)
implementation(platform(libs.androidx.compose.bom))
implementation(project(":capy"))
implementation(project(":feedfinder"))
testImplementation(libs.tests.junit)
testImplementation(libs.tests.kotlinx.coroutines)
testImplementation(libs.tests.mockk.mockk)
testImplementation(libs.tests.robolectric)
debugImplementation(libs.androidx.ui.tooling)
}
1 change: 1 addition & 0 deletions lite/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Add project-specific ProGuard rules here.
38 changes: 38 additions & 0 deletions lite/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>

<application
android:name=".LiteApplication"
android:allowBackup="false"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CapyLite"
tools:targetApi="33">

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/Theme.CapyLite">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
68 changes: 68 additions & 0 deletions lite/src/main/java/com/capyreader/lite/CoreModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.capyreader.lite

import android.webkit.WebSettings
import com.capyreader.lite.common.AndroidClientCertManager
import com.capyreader.lite.common.AndroidDatabaseProvider
import com.capyreader.lite.common.AppFaviconPolicy
import com.capyreader.lite.common.SharedPreferenceStoreProvider
import com.capyreader.lite.preferences.LitePreferences
import com.capyreader.lite.ui.feeds.FraidycatViewModel
import com.capyreader.lite.ui.login.LiteLoginViewModel
import com.jocmp.capy.Account
import com.jocmp.capy.AccountManager
import com.jocmp.capy.ClientCertManager
import com.jocmp.capy.DatabaseProvider
import com.jocmp.capy.PreferenceStoreProvider
import com.jocmp.capy.accounts.httpClientBuilder
import com.jocmp.capy.db.Database
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.loadKoinModules
import org.koin.core.context.unloadKoinModules
import org.koin.dsl.module
import java.util.Locale

internal val coreModule = module {
single<OkHttpClient> {
httpClientBuilder(cachePath = androidContext().cacheDir.toURI()).build()
}
single<PreferenceStoreProvider> { SharedPreferenceStoreProvider(get()) }
single<DatabaseProvider> { AndroidDatabaseProvider(context = get()) }
single<ClientCertManager> { AndroidClientCertManager(context = get()) }
single {
AccountManager(
rootFolder = androidContext().filesDir.toURI(),
databaseProvider = get(),
cacheDirectory = androidContext().cacheDir.toURI(),
preferenceStoreProvider = get(),
faviconPolicy = AppFaviconPolicy(get()),
clientCertManager = get(),
userAgent = WebSettings.getDefaultUserAgent(androidContext()),
acceptLanguage = Locale.getDefault().toLanguageTag(),
)
}
single { LitePreferences(get()) }
viewModel { LiteLoginViewModel(accountManager = get(), litePreferences = get()) }
}

internal val accountModule = module {
single<Database> {
get<DatabaseProvider>().build(accountID = get<LitePreferences>().accountID.get())
}
single<Account> {
get<AccountManager>().findByID(
id = get<LitePreferences>().accountID.get(),
database = get<Database>()
)!!
}
single { FraidycatViewModel(account = get()) }
}

fun loadLiteAccountModules() {
loadKoinModules(accountModule)
}

fun unloadLiteAccountModules() {
unloadKoinModules(accountModule)
}
24 changes: 24 additions & 0 deletions lite/src/main/java/com/capyreader/lite/LiteApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.capyreader.lite

import android.app.Application
import com.capyreader.lite.preferences.LitePreferences
import com.google.android.material.color.DynamicColors
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class LiteApplication : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)

startKoin {
androidContext(this@LiteApplication)
modules(coreModule)
}

if (get<LitePreferences>().isLoggedIn) {
loadLiteAccountModules()
}
}
}
22 changes: 22 additions & 0 deletions lite/src/main/java/com/capyreader/lite/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.capyreader.lite

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.capyreader.lite.preferences.LitePreferences
import com.capyreader.lite.theme.CapyLiteTheme
import com.capyreader.lite.ui.LiteApp
import org.koin.android.ext.android.inject

class MainActivity : ComponentActivity() {
private val preferences by inject<LitePreferences>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CapyLiteTheme {
LiteApp(startLoggedIn = preferences.isLoggedIn)
}
}
}
}
Loading
Loading