WIP: Add database/networking and login
This commit is contained in:
parent
ec96e39580
commit
7cc1ae892f
30 changed files with 1738 additions and 21 deletions
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
|
|
@ -2,6 +2,7 @@ plugins {
|
|||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
|
@ -52,7 +53,7 @@ android {
|
|||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.get()
|
||||
kotlinCompilerVersion = libs.versions.kotlin.get()
|
||||
kotlinCompilerVersion = rootProject.extra["kotlinVersion"] as String
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,11 +63,20 @@ dependencies {
|
|||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.preference)
|
||||
implementation(libs.lifecycle)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.room.ktx)
|
||||
kapt(libs.room.kapt)
|
||||
implementation(libs.moshi.core)
|
||||
implementation(libs.bundles.networking)
|
||||
implementation(libs.bundles.moshi)
|
||||
kapt(libs.moshi.kapt)
|
||||
implementation(libs.hilt.android.core)
|
||||
kapt(libs.hilt.android.kapt)
|
||||
implementation(libs.hilt.work.core)
|
||||
kapt(libs.hilt.work.kapt)
|
||||
implementation(libs.work.core)
|
||||
testImplementation(libs.work.test)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.room.test)
|
||||
androidTestImplementation(libs.test.ext)
|
||||
|
|
432
app/schemas/com.wbrawner.nanoflux.data.NanofluxDatabase/1.json
Normal file
432
app/schemas/com.wbrawner.nanoflux.data.NanofluxDatabase/1.json
Normal file
|
@ -0,0 +1,432 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "a3ba65cc1674ea74c169d8f134ff3c42",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Feed",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `title` TEXT NOT NULL, `siteUrl` TEXT NOT NULL, `feedUrl` TEXT NOT NULL, `checkedAt` INTEGER NOT NULL, `etagHeader` TEXT NOT NULL, `lastModifiedHeader` TEXT NOT NULL, `parsingErrorMessage` TEXT NOT NULL, `parsingErrorCount` INTEGER NOT NULL, `scraperRules` TEXT NOT NULL, `rewriteRules` TEXT NOT NULL, `crawler` INTEGER NOT NULL, `blocklistRules` TEXT NOT NULL, `keeplistRules` TEXT NOT NULL, `userAgent` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `disabled` INTEGER NOT NULL, `ignoreHttpCache` INTEGER NOT NULL, `fetchViaProxy` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `iconId` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "siteUrl",
|
||||
"columnName": "siteUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "feedUrl",
|
||||
"columnName": "feedUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "etagHeader",
|
||||
"columnName": "etagHeader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastModifiedHeader",
|
||||
"columnName": "lastModifiedHeader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parsingErrorMessage",
|
||||
"columnName": "parsingErrorMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parsingErrorCount",
|
||||
"columnName": "parsingErrorCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scraperRules",
|
||||
"columnName": "scraperRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rewriteRules",
|
||||
"columnName": "rewriteRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "crawler",
|
||||
"columnName": "crawler",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "blocklistRules",
|
||||
"columnName": "blocklistRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keeplistRules",
|
||||
"columnName": "keeplistRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userAgent",
|
||||
"columnName": "userAgent",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "disabled",
|
||||
"columnName": "disabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ignoreHttpCache",
|
||||
"columnName": "ignoreHttpCache",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fetchViaProxy",
|
||||
"columnName": "fetchViaProxy",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconId",
|
||||
"columnName": "iconId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Entry",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `feedId` INTEGER NOT NULL, `status` TEXT NOT NULL, `hash` TEXT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `commentsUrl` TEXT NOT NULL, `publishedAt` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `content` TEXT NOT NULL, `author` TEXT NOT NULL, `shareCode` TEXT NOT NULL, `starred` INTEGER NOT NULL, `readingTime` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "feedId",
|
||||
"columnName": "feedId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hash",
|
||||
"columnName": "hash",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "commentsUrl",
|
||||
"columnName": "commentsUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "publishedAt",
|
||||
"columnName": "publishedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shareCode",
|
||||
"columnName": "shareCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "starred",
|
||||
"columnName": "starred",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "readingTime",
|
||||
"columnName": "readingTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Category",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Icon",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feedId` INTEGER NOT NULL, `iconId` INTEGER NOT NULL, PRIMARY KEY(`iconId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "feedId",
|
||||
"columnName": "feedId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconId",
|
||||
"columnName": "iconId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"iconId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "User",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `username` TEXT NOT NULL, `admin` INTEGER NOT NULL, `theme` TEXT NOT NULL, `language` TEXT NOT NULL, `timezone` TEXT NOT NULL, `entrySortingDirection` TEXT NOT NULL, `stylesheet` TEXT NOT NULL, `googleId` TEXT NOT NULL, `openidConnectId` TEXT NOT NULL, `entriesPerPage` INTEGER NOT NULL, `keyboardShortcuts` INTEGER NOT NULL, `showReadingTime` INTEGER NOT NULL, `entrySwipe` INTEGER NOT NULL, `lastLoginAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "admin",
|
||||
"columnName": "admin",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "theme",
|
||||
"columnName": "theme",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timezone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entrySortingDirection",
|
||||
"columnName": "entrySortingDirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "stylesheet",
|
||||
"columnName": "stylesheet",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "googleId",
|
||||
"columnName": "googleId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "openidConnectId",
|
||||
"columnName": "openidConnectId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entriesPerPage",
|
||||
"columnName": "entriesPerPage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyboardShortcuts",
|
||||
"columnName": "keyboardShortcuts",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showReadingTime",
|
||||
"columnName": "showReadingTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entrySwipe",
|
||||
"columnName": "entrySwipe",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastLoginAt",
|
||||
"columnName": "lastLoginAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"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, 'a3ba65cc1674ea74c169d8f134ff3c42')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,14 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.wbrawner.nanoflux">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:name=".NanofluxApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Nanoflux">
|
||||
android:theme="@style/Theme.Nanoflux"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="m">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
@ -20,6 +25,10 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -3,21 +3,37 @@ package com.wbrawner.nanoflux
|
|||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.wbrawner.nanoflux.data.model.Entry
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
import com.wbrawner.nanoflux.data.viewmodel.AuthViewModel
|
||||
import com.wbrawner.nanoflux.ui.MainScreen
|
||||
import com.wbrawner.nanoflux.ui.auth.AuthScreen
|
||||
import com.wbrawner.nanoflux.ui.theme.NanofluxTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
val authViewModel: AuthViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
NanofluxTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Greeting("Android")
|
||||
val authenticated = authViewModel.state.collectAsState()
|
||||
NanofluxApp {
|
||||
if (authenticated.value is AuthViewModel.AuthState.Authenticated) {
|
||||
MainScreen(authViewModel)
|
||||
} else {
|
||||
AuthScreen(authViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +41,31 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String) {
|
||||
Text(text = "Hello $name!")
|
||||
fun NanofluxApp(content: @Composable () -> Unit) {
|
||||
NanofluxTheme {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
NanofluxTheme {
|
||||
Greeting("Android")
|
||||
fun AppPreview() {
|
||||
NanofluxApp {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EntryListItem(
|
||||
entry: Entry,
|
||||
feed: Feed,
|
||||
onEntryItemClicked: (entry: Entry) -> Unit,
|
||||
onFeedClicked: (feed: Feed) -> Unit,
|
||||
onToggleReadClicked: (entry: Entry) -> Unit,
|
||||
onStarClicked: (entry: Entry) -> Unit,
|
||||
onExternalLinkClicked: (entry: Entry) -> Unit
|
||||
) {
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.wbrawner.nanoflux
|
||||
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.room.Room
|
||||
import androidx.work.Configuration
|
||||
import com.wbrawner.nanoflux.data.NanofluxDatabase
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class NanofluxApplication : Application(), Configuration.Provider {
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
}
|
36
app/src/main/java/com/wbrawner/nanoflux/SyncWorker.kt
Normal file
36
app/src/main/java/com/wbrawner/nanoflux/SyncWorker.kt
Normal file
|
@ -0,0 +1,36 @@
|
|||
package com.wbrawner.nanoflux
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.wbrawner.nanoflux.data.MinifluxApiService
|
||||
import com.wbrawner.nanoflux.data.repository.EntryRepository
|
||||
import com.wbrawner.nanoflux.data.repository.FeedRepository
|
||||
import com.wbrawner.nanoflux.data.repository.UserRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltWorker
|
||||
class SyncWorker @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParams: WorkerParameters
|
||||
) : Worker(context, workerParams) {
|
||||
@Inject
|
||||
lateinit var entryRepository: EntryRepository
|
||||
@Inject
|
||||
lateinit var feedRepository: FeedRepository
|
||||
|
||||
override fun doWork(): Result {
|
||||
runBlocking {
|
||||
feedRepository.getAll()
|
||||
entryRepository.getAll()
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package com.wbrawner.nanoflux.data
|
||||
|
||||
import com.wbrawner.nanoflux.data.model.Category
|
||||
import com.wbrawner.nanoflux.data.model.Entry
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
import com.wbrawner.nanoflux.data.model.User
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.*
|
||||
|
||||
interface MinifluxApiService {
|
||||
// @POST("discover")
|
||||
// suspend fun discoverSubscriptions(@Body url: String): DiscoverResponse
|
||||
|
||||
@GET("feeds")
|
||||
suspend fun getFeeds(): List<Feed>
|
||||
|
||||
@GET("categories/{categoryId}/feeds")
|
||||
suspend fun getCategoryFeeds(@Path("categoryId") categoryId: Long): List<Feed>
|
||||
|
||||
@GET("feeds/{id}")
|
||||
suspend fun getFeed(@Path("id") id: Long): Feed
|
||||
|
||||
@GET("feeds/{feedId}/icon")
|
||||
suspend fun getFeedIcon(@Path("feedId") feedId: Long): Feed.Icon
|
||||
|
||||
// @POST("feeds")
|
||||
// suspend fun createFeed(feed: FeedRequest): CreateFeedResponse
|
||||
|
||||
// @PUT("feeds/{id}")
|
||||
// suspend fun updateFeed(@Path("id") id: Long, @Body request: FeedRequest): Feed
|
||||
|
||||
@PUT("feeds/refresh")
|
||||
suspend fun refreshFeeds(): Response
|
||||
|
||||
@PUT("feeds/{id}/refresh")
|
||||
suspend fun refreshFeed(@Path("id") id: Long): Response
|
||||
|
||||
@DELETE("feeds/{id}")
|
||||
suspend fun deleteFeed(@Path("id") id: Long): Response
|
||||
|
||||
@GET("feeds/{feedId}/entries")
|
||||
suspend fun getFeedEntries(
|
||||
@Path("feedId") feedId: Long,
|
||||
@Query("status") status: List<Entry.Status>? = null,
|
||||
@Query("offset") offset: Long? = null,
|
||||
@Query("limit") limit: Long? = null,
|
||||
@Query("order") order: Entry.Order? = null,
|
||||
@Query("direction") direction: Entry.SortDirection? = null,
|
||||
@Query("before") before: Long? = null,
|
||||
@Query("after") after: Long? = null,
|
||||
@Query("before_entry_id") beforeEntryId: Long? = null,
|
||||
@Query("after_entry_id") afterEntryId: Long? = null,
|
||||
@Query("starred") starred: Boolean? = null,
|
||||
@Query("search") search: String? = null,
|
||||
@Query("category_id") categoryId: Long? = null,
|
||||
): List<Entry>
|
||||
|
||||
@GET("feeds/{feedId}/entries/{entryId}")
|
||||
suspend fun getFeedEntry(@Path("feedId") feedId: Long, @Path("entryId") entryId: Long): Entry
|
||||
|
||||
@GET("entries")
|
||||
suspend fun getEntries(
|
||||
@Query("status") status: List<Entry.Status>? = null,
|
||||
@Query("offset") offset: Long? = null,
|
||||
@Query("limit") limit: Long? = null,
|
||||
@Query("order") order: Entry.Order? = null,
|
||||
@Query("direction") direction: Entry.SortDirection? = null,
|
||||
@Query("before") before: Long? = null,
|
||||
@Query("after") after: Long? = null,
|
||||
@Query("before_entry_id") beforeEntryId: Long? = null,
|
||||
@Query("after_entry_id") afterEntryId: Long? = null,
|
||||
@Query("starred") starred: Boolean? = null,
|
||||
@Query("search") search: String? = null,
|
||||
@Query("category_id") categoryId: Long? = null,
|
||||
): List<Entry>
|
||||
|
||||
@GET("entries/{entryId}")
|
||||
suspend fun getEntry(@Path("entryId") entryId: Long): Entry
|
||||
|
||||
@PUT("feeds/{id}/mark-all-as-read")
|
||||
suspend fun markFeedEntriesAsRead(@Path("id") id: Long): Response
|
||||
|
||||
//{
|
||||
// "entry_ids": [1234, 4567],
|
||||
// "status": "read"
|
||||
//}
|
||||
// @PUT("entries")
|
||||
// suspend fun updateEntries(request: UpdateEntriesRequest): Response
|
||||
|
||||
@PUT("entries/{id}/bookmark")
|
||||
suspend fun toggleEntryBookmark(@Path("id") id: Long): Response
|
||||
|
||||
@GET("categories")
|
||||
suspend fun getCategories(): List<Category>
|
||||
|
||||
@POST("categories")
|
||||
suspend fun createCategory(@Body title: String): Category
|
||||
|
||||
@PUT("categories/{id}")
|
||||
suspend fun updateCategory(@Path("id") id: Long, @Body title: String): Category
|
||||
|
||||
@DELETE("categories/{id}")
|
||||
suspend fun deleteCategory(@Path("id") id: Long): Response
|
||||
|
||||
@PUT("categories/{id}/mark-all-as-read")
|
||||
suspend fun markCategoryEntriesAsRead(@Path("id") id: Long): Response
|
||||
|
||||
@GET("export")
|
||||
@Streaming
|
||||
suspend fun opmlExport(): retrofit2.Response<ResponseBody>
|
||||
|
||||
@POST("import")
|
||||
@Streaming
|
||||
suspend fun opmlImport(@Body opml: RequestBody): retrofit2.Response<ResponseBody>
|
||||
|
||||
// @POST("users")
|
||||
// suspend fun createUser(@Body user: UserRequest): User
|
||||
|
||||
@PUT("users/{id}")
|
||||
suspend fun updateUser(@Path("id") id: Long, @Body user: User): User
|
||||
|
||||
@GET("me")
|
||||
suspend fun getCurrentUser(): User
|
||||
|
||||
@GET("users/{id}")
|
||||
suspend fun getUser(@Path("id") id: Long): User
|
||||
|
||||
@GET("users/{name}")
|
||||
suspend fun getUserByName(@Path("name") name: String): User
|
||||
|
||||
@GET("users")
|
||||
suspend fun getUses(): List<User>
|
||||
|
||||
@DELETE("users/{id}")
|
||||
suspend fun deleteUser(@Path("id") id: Long)
|
||||
|
||||
@PUT("users/{id}/mark-all-as-read")
|
||||
suspend fun markUserEntriesAsRead(@Path("id") id: Long): Response
|
||||
|
||||
@GET("/healthcheck")
|
||||
suspend fun healthcheck(): retrofit2.Response<String>
|
||||
|
||||
@GET("/version")
|
||||
suspend fun version(): retrofit2.Response<String>
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.wbrawner.nanoflux.data
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import com.wbrawner.nanoflux.data.dao.*
|
||||
import com.wbrawner.nanoflux.data.model.Category
|
||||
import com.wbrawner.nanoflux.data.model.Entry
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
import com.wbrawner.nanoflux.data.model.User
|
||||
import java.util.*
|
||||
|
||||
@Database(
|
||||
entities = [Feed::class, Entry::class, Category::class, Feed.Icon::class, User::class],
|
||||
version = 1,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(DateTypeConverter::class)
|
||||
abstract class NanofluxDatabase: RoomDatabase() {
|
||||
abstract fun feedDao(): FeedDao
|
||||
abstract fun entryDao(): EntryDao
|
||||
abstract fun categoryDao(): CategoryDao
|
||||
abstract fun iconDao(): IconDao
|
||||
abstract fun userDao(): UserDao
|
||||
}
|
||||
|
||||
class DateTypeConverter {
|
||||
@TypeConverter
|
||||
fun fromTimestamp(timestamp: Long) = Date(timestamp)
|
||||
|
||||
@TypeConverter
|
||||
fun toTimestamp(date: Date) = date.time
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.wbrawner.nanoflux.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.wbrawner.nanoflux.data.model.Category
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
import com.wbrawner.nanoflux.data.model.FeedCategoryIcon
|
||||
|
||||
@Dao
|
||||
interface CategoryDao {
|
||||
@Query("SELECT * FROM Category")
|
||||
fun getAll(): List<Category>
|
||||
|
||||
@Query("SELECT * FROM Category WHERE id in (:ids)")
|
||||
fun getAllByIds(vararg ids: Long): List<Category>
|
||||
|
||||
@Insert
|
||||
fun insertAll(vararg categories: Category)
|
||||
|
||||
@Delete
|
||||
fun delete(category: Category)
|
||||
}
|
34
app/src/main/java/com/wbrawner/nanoflux/data/dao/EntryDao.kt
Normal file
34
app/src/main/java/com/wbrawner/nanoflux/data/dao/EntryDao.kt
Normal file
|
@ -0,0 +1,34 @@
|
|||
package com.wbrawner.nanoflux.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.wbrawner.nanoflux.data.model.Entry
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
import com.wbrawner.nanoflux.data.model.FeedCategoryIcon
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface EntryDao {
|
||||
@Query("SELECT * FROM Entry")
|
||||
fun getAll(): Flow<List<Entry>>
|
||||
|
||||
@Query("SELECT * FROM Entry WHERE status = \"UNREAD\"")
|
||||
fun getAllUnread(): Flow<List<Entry>>
|
||||
|
||||
@Query("SELECT * FROM Entry WHERE id in (:ids)")
|
||||
fun getAllByIds(vararg ids: Long): List<Entry>
|
||||
|
||||
@Query("SELECT * FROM Entry WHERE feedId in (:ids)")
|
||||
fun getAllByFeedIds(vararg ids: Long): Flow<List<Entry>>
|
||||
|
||||
@Query("SELECT * FROM Entry WHERE status = \"UNREAD\" AND feedId in (:ids)")
|
||||
fun getAllUnreadByFeedIds(vararg ids: Long): Flow<List<Entry>>
|
||||
|
||||
@Insert
|
||||
fun insertAll(entries: List<Entry>)
|
||||
|
||||
@Delete
|
||||
fun delete(entry: Entry)
|
||||
}
|
23
app/src/main/java/com/wbrawner/nanoflux/data/dao/FeedDao.kt
Normal file
23
app/src/main/java/com/wbrawner/nanoflux/data/dao/FeedDao.kt
Normal file
|
@ -0,0 +1,23 @@
|
|||
package com.wbrawner.nanoflux.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
import com.wbrawner.nanoflux.data.model.FeedCategoryIcon
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface FeedDao {
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Feed")
|
||||
fun getAll(): Flow<List<FeedCategoryIcon>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Feed WHERE id in (:ids)")
|
||||
fun getAllByIds(vararg ids: Long): List<FeedCategoryIcon>
|
||||
|
||||
@Insert
|
||||
fun insertAll(feeds: List<Feed>)
|
||||
|
||||
@Delete
|
||||
fun delete(feed: Feed)
|
||||
}
|
22
app/src/main/java/com/wbrawner/nanoflux/data/dao/IconDao.kt
Normal file
22
app/src/main/java/com/wbrawner/nanoflux/data/dao/IconDao.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
package com.wbrawner.nanoflux.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
|
||||
@Dao
|
||||
interface IconDao {
|
||||
@Query("SELECT * FROM Icon")
|
||||
fun getAll(): List<Feed.Icon>
|
||||
|
||||
@Query("SELECT * FROM Icon WHERE iconId in (:ids)")
|
||||
fun getAllByIds(vararg ids: Long): List<Feed.Icon>
|
||||
|
||||
@Insert
|
||||
fun insertAll(vararg icons: Feed.Icon)
|
||||
|
||||
@Delete
|
||||
fun delete(icon: Feed.Icon)
|
||||
}
|
26
app/src/main/java/com/wbrawner/nanoflux/data/dao/UserDao.kt
Normal file
26
app/src/main/java/com/wbrawner/nanoflux/data/dao/UserDao.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
package com.wbrawner.nanoflux.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.wbrawner.nanoflux.data.model.Feed
|
||||
import com.wbrawner.nanoflux.data.model.User
|
||||
|
||||
@Dao
|
||||
interface UserDao {
|
||||
@Query("SELECT * FROM User")
|
||||
suspend fun getAll(): List<User>
|
||||
|
||||
@Query("SELECT * FROM User WHERE id in (:ids)")
|
||||
suspend fun getAllByIds(vararg ids: Long): List<User>
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(vararg users: User)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(user: User)
|
||||
|
||||
@Query("DELETE FROM User")
|
||||
suspend fun deleteAll()
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.wbrawner.nanoflux.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Category(
|
||||
@PrimaryKey
|
||||
@Json(name = "id")
|
||||
val id: Long,
|
||||
@Json(name = "title")
|
||||
val title: String,
|
||||
@Json(name = "user_id")
|
||||
val userId: Long,
|
||||
)
|
62
app/src/main/java/com/wbrawner/nanoflux/data/model/Entry.kt
Normal file
62
app/src/main/java/com/wbrawner/nanoflux/data/model/Entry.kt
Normal file
|
@ -0,0 +1,62 @@
|
|||
package com.wbrawner.nanoflux.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Entry(
|
||||
@Json(name = "id")
|
||||
@PrimaryKey
|
||||
val id: Long,
|
||||
@Json(name = "user_id")
|
||||
val userId: Long,
|
||||
@Json(name = "feed_id")
|
||||
val feedId: Long,
|
||||
@Json(name = "status")
|
||||
val status: Status,
|
||||
@Json(name = "hash")
|
||||
val hash: String,
|
||||
@Json(name = "title")
|
||||
val title: String,
|
||||
@Json(name = "url")
|
||||
val url: String,
|
||||
@Json(name = "comments_url")
|
||||
val commentsUrl: String,
|
||||
@Json(name = "published_at")
|
||||
val publishedAt: Date,
|
||||
@Json(name = "created_at")
|
||||
val createdAt: Date,
|
||||
@Json(name = "content")
|
||||
val content: String,
|
||||
@Json(name = "author")
|
||||
val author: String,
|
||||
@Json(name = "share_code")
|
||||
val shareCode: String,
|
||||
@Json(name = "starred")
|
||||
val starred: Boolean,
|
||||
@Json(name = "reading_time")
|
||||
val readingTime: Int,
|
||||
) {
|
||||
enum class Status {
|
||||
READ,
|
||||
UNREAD,
|
||||
REMOVED
|
||||
}
|
||||
|
||||
enum class Order {
|
||||
ID,
|
||||
STATUS,
|
||||
PUBLISHED_AT,
|
||||
CATEGORY_TITLE,
|
||||
CATEGORY_ID
|
||||
}
|
||||
|
||||
enum class SortDirection {
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
}
|
63
app/src/main/java/com/wbrawner/nanoflux/data/model/Feed.kt
Normal file
63
app/src/main/java/com/wbrawner/nanoflux/data/model/Feed.kt
Normal file
|
@ -0,0 +1,63 @@
|
|||
package com.wbrawner.nanoflux.data.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
data class Feed(
|
||||
@PrimaryKey
|
||||
val id: Long,
|
||||
val userId: Long,
|
||||
val title: String,
|
||||
val siteUrl: String,
|
||||
val feedUrl: String,
|
||||
val checkedAt: Date,
|
||||
val etagHeader: String,
|
||||
val lastModifiedHeader: String,
|
||||
val parsingErrorMessage: String,
|
||||
val parsingErrorCount: Int,
|
||||
val scraperRules: String,
|
||||
val rewriteRules: String,
|
||||
val crawler: Boolean,
|
||||
val blocklistRules: String,
|
||||
val keeplistRules: String,
|
||||
val userAgent: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val disabled: Boolean,
|
||||
val ignoreHttpCache: Boolean,
|
||||
val fetchViaProxy: Boolean,
|
||||
val categoryId: Long,
|
||||
val iconId: Long?
|
||||
) {
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Icon(
|
||||
@Json(name = "feed_id")
|
||||
val feedId: Long,
|
||||
@PrimaryKey
|
||||
@Json(name = "icon_id")
|
||||
val iconId: Long
|
||||
)
|
||||
}
|
||||
|
||||
data class FeedCategoryIcon(
|
||||
@Embedded
|
||||
val feed: Feed,
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "feedId"
|
||||
)
|
||||
val icon: Feed.Icon?,
|
||||
@Relation(
|
||||
parentColumn = "categoryId",
|
||||
entityColumn = "id",
|
||||
entity = Category::class
|
||||
)
|
||||
val category: Category
|
||||
)
|
43
app/src/main/java/com/wbrawner/nanoflux/data/model/User.kt
Normal file
43
app/src/main/java/com/wbrawner/nanoflux/data/model/User.kt
Normal file
|
@ -0,0 +1,43 @@
|
|||
package com.wbrawner.nanoflux.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class User(
|
||||
@Json(name = "id")
|
||||
@PrimaryKey
|
||||
val id: Long,
|
||||
@Json(name = "username")
|
||||
val username: String,
|
||||
@Json(name = "is_admin")
|
||||
val admin: Boolean,
|
||||
@Json(name = "theme")
|
||||
val theme: String,
|
||||
@Json(name = "language")
|
||||
val language: String,
|
||||
@Json(name = "timezone")
|
||||
val timezone: String,
|
||||
@Json(name = "entry_sorting_direction")
|
||||
val entrySortingDirection: String,
|
||||
@Json(name = "stylesheet")
|
||||
val stylesheet: String,
|
||||
@Json(name = "google_id")
|
||||
val googleId: String,
|
||||
@Json(name = "openid_connect_id")
|
||||
val openidConnectId: String,
|
||||
@Json(name = "entries_per_page")
|
||||
val entriesPerPage: Int,
|
||||
@Json(name = "keyboard_shortcuts")
|
||||
val keyboardShortcuts: Boolean,
|
||||
@Json(name = "show_reading_time")
|
||||
val showReadingTime: Boolean,
|
||||
@Json(name = "entry_swipe")
|
||||
val entrySwipe: Boolean,
|
||||
@Json(name = "last_login_at")
|
||||
val lastLoginAt: Date,
|
||||
)
|
|
@ -0,0 +1,27 @@
|
|||
package com.wbrawner.nanoflux.data.repository
|
||||
|
||||
import com.wbrawner.nanoflux.data.MinifluxApiService
|
||||
import com.wbrawner.nanoflux.data.dao.EntryDao
|
||||
import com.wbrawner.nanoflux.data.dao.FeedDao
|
||||
import com.wbrawner.nanoflux.data.model.Entry
|
||||
import com.wbrawner.nanoflux.data.model.FeedCategoryIcon
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class EntryRepository @Inject constructor(
|
||||
private val apiService: MinifluxApiService,
|
||||
private val entryDao: EntryDao,
|
||||
private val logger: Timber.Tree
|
||||
) {
|
||||
private val entries: Flow<List<Entry>> = entryDao.getAll()
|
||||
|
||||
fun getAll(): Flow<List<Entry>> {
|
||||
GlobalScope.launch {
|
||||
entryDao.insertAll(apiService.getEntries())
|
||||
}
|
||||
return entries
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.wbrawner.nanoflux.data.repository
|
||||
|
||||
import com.wbrawner.nanoflux.data.MinifluxApiService
|
||||
import com.wbrawner.nanoflux.data.dao.FeedDao
|
||||
import com.wbrawner.nanoflux.data.model.FeedCategoryIcon
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class FeedRepository @Inject constructor(
|
||||
private val apiService: MinifluxApiService,
|
||||
private val feedDao: FeedDao,
|
||||
private val logger: Timber.Tree
|
||||
) {
|
||||
private val feeds: Flow<List<FeedCategoryIcon>> = feedDao.getAll()
|
||||
|
||||
fun getAll(): Flow<List<FeedCategoryIcon>> {
|
||||
GlobalScope.launch {
|
||||
feedDao.insertAll(apiService.getFeeds())
|
||||
}
|
||||
return feeds
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package com.wbrawner.nanoflux.data.repository
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.core.content.edit
|
||||
import com.wbrawner.nanoflux.data.MinifluxApiService
|
||||
import com.wbrawner.nanoflux.data.dao.UserDao
|
||||
import com.wbrawner.nanoflux.data.model.User
|
||||
import com.wbrawner.nanoflux.di.PREF_KEY_BASE_URL
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
const val PREF_KEY_AUTH_TOKEN = "authToken"
|
||||
const val PREF_KEY_CURRENT_USER = "currentUser"
|
||||
|
||||
class UserRepository @Inject constructor(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val apiService: MinifluxApiService,
|
||||
private val userDao: UserDao,
|
||||
private val logger: Timber.Tree
|
||||
) {
|
||||
private val _currentUser: MutableSharedFlow<User> = MutableSharedFlow(replay = 1)
|
||||
|
||||
init {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
getCurrentUser()
|
||||
} catch (e: Exception) {
|
||||
logger.e(e, "No previously logged in user")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun login(server: String, username: String, password: String) {
|
||||
var correctedServer = if (server.startsWith("http")) {
|
||||
server
|
||||
} else {
|
||||
"http://$server"
|
||||
}
|
||||
if (!correctedServer.endsWith("/v1")) {
|
||||
correctedServer = if (correctedServer.endsWith("/")) {
|
||||
"${correctedServer}v1"
|
||||
} else {
|
||||
"$correctedServer/v1"
|
||||
}
|
||||
}
|
||||
val credentials = Base64.encode(
|
||||
"$username:$password".encodeToByteArray(),
|
||||
Base64.DEFAULT or Base64.NO_WRAP
|
||||
)
|
||||
.decodeToString()
|
||||
sharedPreferences.edit {
|
||||
putString(PREF_KEY_BASE_URL, correctedServer)
|
||||
putString(PREF_KEY_AUTH_TOKEN, "Basic $credentials")
|
||||
}
|
||||
try {
|
||||
val user = apiService.getCurrentUser()
|
||||
_currentUser.emit(user)
|
||||
userDao.insertAll(user)
|
||||
sharedPreferences.edit {
|
||||
putLong(PREF_KEY_CURRENT_USER, user.id)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
sharedPreferences.edit {
|
||||
remove(PREF_KEY_BASE_URL)
|
||||
remove(PREF_KEY_AUTH_TOKEN)
|
||||
remove(PREF_KEY_CURRENT_USER)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
userDao.deleteAll()
|
||||
sharedPreferences.edit {
|
||||
remove(PREF_KEY_BASE_URL)
|
||||
remove(PREF_KEY_AUTH_TOKEN)
|
||||
remove(PREF_KEY_CURRENT_USER)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrentUser(): User {
|
||||
return _currentUser.replayCache.firstOrNull()
|
||||
?: sharedPreferences.getLong(PREF_KEY_CURRENT_USER, -1L).let {
|
||||
if (it == -1L) {
|
||||
null
|
||||
} else {
|
||||
userDao.getAllByIds(it).firstOrNull()
|
||||
}
|
||||
}
|
||||
?: apiService.getCurrentUser().also {
|
||||
userDao.insertAll(it)
|
||||
_currentUser.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package com.wbrawner.nanoflux.data.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wbrawner.nanoflux.data.model.User
|
||||
import com.wbrawner.nanoflux.data.repository.UserRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthViewModel @Inject constructor(
|
||||
private val userRepository: UserRepository,
|
||||
private val logger: Timber.Tree
|
||||
) : ViewModel() {
|
||||
private val _state = MutableStateFlow<AuthState>(AuthState.Loading)
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
_state.emit(AuthState.Authenticated(userRepository.getCurrentUser()))
|
||||
} catch (e: Exception) {
|
||||
logger.e(e, "Unable to login user")
|
||||
_state.emit(AuthState.Unauthenticated())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(server: String, username: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
if (server.isBlank()) {
|
||||
_state.emit(
|
||||
AuthState.Unauthenticated(
|
||||
server,
|
||||
username,
|
||||
password,
|
||||
"Please enter a valid server URL"
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
if (username.isBlank()) {
|
||||
_state.emit(
|
||||
AuthState.Unauthenticated(
|
||||
server,
|
||||
username,
|
||||
password,
|
||||
"Please enter a valid username"
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
if (password.isBlank()) {
|
||||
_state.emit(
|
||||
AuthState.Unauthenticated(
|
||||
server,
|
||||
username,
|
||||
password,
|
||||
"Please enter a valid password"
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
_state.emit(AuthState.Loading)
|
||||
try {
|
||||
userRepository.login(server.trim(), username.trim(), password.trim())
|
||||
_state.emit(AuthState.Authenticated(userRepository.getCurrentUser()))
|
||||
} catch (e: Exception) {
|
||||
logger.e(e, "Login failed")
|
||||
_state.emit(AuthState.Unauthenticated(server, username, password, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
userRepository.logout()
|
||||
_state.emit(AuthState.Unauthenticated("", "", ""))
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AuthState {
|
||||
object Loading : AuthState()
|
||||
class Authenticated(val user: User) : AuthState()
|
||||
class Unauthenticated(
|
||||
val server: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val errorMessage: String? = null
|
||||
) : AuthState()
|
||||
}
|
||||
}
|
66
app/src/main/java/com/wbrawner/nanoflux/di/NetworkModule.kt
Normal file
66
app/src/main/java/com/wbrawner/nanoflux/di/NetworkModule.kt
Normal file
|
@ -0,0 +1,66 @@
|
|||
package com.wbrawner.nanoflux.di
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import com.wbrawner.nanoflux.BuildConfig
|
||||
import com.wbrawner.nanoflux.data.MinifluxApiService
|
||||
import com.wbrawner.nanoflux.data.repository.PREF_KEY_AUTH_TOKEN
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.util.*
|
||||
|
||||
const val PREF_KEY_BASE_URL = "BASE_URL"
|
||||
private const val DEFAULT_BASE_URL = "https://base_url"
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
@Provides
|
||||
fun providesOkHttpClient(prefs: SharedPreferences): OkHttpClient {
|
||||
val client = OkHttpClient.Builder()
|
||||
.addInterceptor {
|
||||
val old = it.request()
|
||||
val newUrl = prefs.getString(PREF_KEY_BASE_URL, null)
|
||||
if (old.url.host != "base_url" || newUrl == null) {
|
||||
return@addInterceptor it.proceed(it.request())
|
||||
}
|
||||
val new = old.newBuilder()
|
||||
val url = old.url.toString().replace(DEFAULT_BASE_URL, newUrl).toHttpUrl()
|
||||
new.header("Authorization", prefs.getString(PREF_KEY_AUTH_TOKEN, null) ?: "")
|
||||
it.proceed(new.url(url).build())
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
client.addInterceptor(HttpLoggingInterceptor(logger = HttpLoggingInterceptor.Logger.DEFAULT).also {
|
||||
it.setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
})
|
||||
}
|
||||
return client.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesMoshi(): Moshi = Moshi.Builder()
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
fun providesRetrofit(moshi: Moshi, okHttpClient: OkHttpClient) = Retrofit.Builder()
|
||||
.baseUrl(DEFAULT_BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
fun provideMinifluxApiService(retrofit: Retrofit) =
|
||||
retrofit.create(MinifluxApiService::class.java)
|
||||
}
|
47
app/src/main/java/com/wbrawner/nanoflux/di/StorageModule.kt
Normal file
47
app/src/main/java/com/wbrawner/nanoflux/di/StorageModule.kt
Normal file
|
@ -0,0 +1,47 @@
|
|||
package com.wbrawner.nanoflux.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.room.Room
|
||||
import com.wbrawner.nanoflux.data.NanofluxDatabase
|
||||
import com.wbrawner.nanoflux.data.dao.*
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object StorageModule {
|
||||
@Provides
|
||||
fun providesSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesNanofluxDatabase(@ApplicationContext context: Context): NanofluxDatabase =
|
||||
Room.databaseBuilder(context, NanofluxDatabase::class.java, "nanoflux").build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesCategoryDao(nanofluxDatabase: NanofluxDatabase): CategoryDao = nanofluxDatabase.categoryDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesEntryDao(nanofluxDatabase: NanofluxDatabase): EntryDao = nanofluxDatabase.entryDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFeedDao(nanofluxDatabase: NanofluxDatabase): FeedDao = nanofluxDatabase.feedDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIconDao(nanofluxDatabase: NanofluxDatabase): IconDao = nanofluxDatabase.iconDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesUserDao(nanofluxDatabase: NanofluxDatabase): UserDao = nanofluxDatabase.userDao()
|
||||
}
|
20
app/src/main/java/com/wbrawner/nanoflux/di/UtilityModule.kt
Normal file
20
app/src/main/java/com/wbrawner/nanoflux/di/UtilityModule.kt
Normal file
|
@ -0,0 +1,20 @@
|
|||
package com.wbrawner.nanoflux.di
|
||||
|
||||
import com.wbrawner.nanoflux.BuildConfig
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import timber.log.Timber
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object UtilityModule {
|
||||
@Provides
|
||||
fun providesLogger(): Timber.Tree {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
return Timber.asTree()
|
||||
}
|
||||
}
|
81
app/src/main/java/com/wbrawner/nanoflux/ui/MainScreen.kt
Normal file
81
app/src/main/java/com/wbrawner/nanoflux/ui/MainScreen.kt
Normal file
|
@ -0,0 +1,81 @@
|
|||
package com.wbrawner.nanoflux.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.wbrawner.nanoflux.NanofluxApp
|
||||
import com.wbrawner.nanoflux.data.viewmodel.AuthViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MainScreen(authViewModel: AuthViewModel) {
|
||||
MainScaffold(
|
||||
authViewModel::logout
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScaffold(
|
||||
onLogoutClicked: () -> Unit
|
||||
) {
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = {
|
||||
TopAppBar(navigationIcon = {
|
||||
IconButton(onClick = { coroutineScope.launch { scaffoldState.drawerState.open() } }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = "Menu"
|
||||
)
|
||||
}
|
||||
}, title = { Text(text = "Unread") })
|
||||
},
|
||||
drawerContent = {
|
||||
TextButton(onClick = onLogoutClicked, modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Image(
|
||||
// imageVector = icon,
|
||||
// contentDescription = null, // decorative
|
||||
// colorFilter = ColorFilter.tint(textIconColor),
|
||||
// alpha = imageAlpha
|
||||
// )
|
||||
// Spacer(Modifier.width(16.dp))
|
||||
Text(
|
||||
text = "Logout",
|
||||
modifier = Modifier.padding(8.dp),
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun MainScaffold_Preview() {
|
||||
NanofluxApp {
|
||||
MainScaffold {}
|
||||
}
|
||||
}
|
202
app/src/main/java/com/wbrawner/nanoflux/ui/auth/AuthScreen.kt
Normal file
202
app/src/main/java/com/wbrawner/nanoflux/ui/auth/AuthScreen.kt
Normal file
|
@ -0,0 +1,202 @@
|
|||
package com.wbrawner.nanoflux.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.wbrawner.nanoflux.NanofluxApp
|
||||
import com.wbrawner.nanoflux.data.viewmodel.AuthViewModel
|
||||
import com.wbrawner.nanoflux.ui.theme.NanofluxTheme
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun AuthScreen(authViewModel: AuthViewModel) {
|
||||
val vmState = authViewModel.state.collectAsState()
|
||||
val state = vmState.value
|
||||
if (state is AuthViewModel.AuthState.Loading) {
|
||||
CircularProgressIndicator()
|
||||
} else if (state is AuthViewModel.AuthState.Unauthenticated) {
|
||||
AuthForm(
|
||||
state.server,
|
||||
state.username,
|
||||
state.password,
|
||||
authViewModel::login,
|
||||
state.errorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthForm(
|
||||
initialServer: String,
|
||||
initialUsername: String,
|
||||
initialPassword: String,
|
||||
onLoginClicked: (String, String, String) -> Unit,
|
||||
error: String? = null,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val serverFocus = remember { FocusRequester() }
|
||||
val usernameFocus = remember { FocusRequester() }
|
||||
val passwordFocus = remember { FocusRequester() }
|
||||
val (server: String, setServer: (String) -> Unit) = remember { mutableStateOf(initialServer) }
|
||||
val (username: String, setUsername: (String) -> Unit) = remember {
|
||||
mutableStateOf(
|
||||
initialUsername
|
||||
)
|
||||
}
|
||||
val (password: String, setPassword: (String) -> Unit) = remember {
|
||||
mutableStateOf(
|
||||
initialPassword
|
||||
)
|
||||
}
|
||||
fun handleKeyEvent(
|
||||
previousFocusRequester: FocusRequester?,
|
||||
nextFocusRequester: FocusRequester?
|
||||
): (KeyEvent) -> Boolean = {
|
||||
when (it.key) {
|
||||
Key.Tab -> {
|
||||
if (it.isShiftPressed) {
|
||||
previousFocusRequester?.requestFocus()
|
||||
Timber.d("Shift+Tab pressed")
|
||||
} else {
|
||||
nextFocusRequester?.requestFocus()
|
||||
Timber.d("Tab pressed")
|
||||
}
|
||||
true
|
||||
}
|
||||
Key.Enter -> {
|
||||
Timber.d("Enter pressed")
|
||||
onLoginClicked(server, username, password)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(state = scrollState)
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val commonModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
if (!error.isNullOrBlank()) {
|
||||
BasicText(
|
||||
text = error,
|
||||
style = TextStyle.Default.copy(
|
||||
color = MaterialTheme.colors.error,
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
modifier = commonModifier
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = server,
|
||||
onValueChange = setServer,
|
||||
label = {
|
||||
BasicText(
|
||||
text = "Miniflux Server URL",
|
||||
style = TextStyle.Default.copy(color = MaterialTheme.colors.onSurface)
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Uri,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(onNext = { usernameFocus.requestFocus() }),
|
||||
modifier = commonModifier
|
||||
.focusRequester(serverFocus)
|
||||
.onPreviewKeyEvent(handleKeyEvent(null, usernameFocus)),
|
||||
maxLines = 1
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = setUsername,
|
||||
label = {
|
||||
BasicText(
|
||||
text = "Username",
|
||||
style = TextStyle.Default.copy(color = MaterialTheme.colors.onSurface)
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(onNext = { passwordFocus.requestFocus() }),
|
||||
modifier = commonModifier
|
||||
.focusRequester(usernameFocus)
|
||||
.onPreviewKeyEvent(handleKeyEvent(serverFocus, passwordFocus)),
|
||||
maxLines = 1
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = setPassword,
|
||||
label = {
|
||||
BasicText(
|
||||
text = "Password",
|
||||
style = TextStyle.Default.copy(color = MaterialTheme.colors.onSurface)
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go
|
||||
),
|
||||
keyboardActions = KeyboardActions(onGo = {
|
||||
onLoginClicked(server, username, password)
|
||||
}),
|
||||
modifier = commonModifier
|
||||
.focusRequester(passwordFocus)
|
||||
.onPreviewKeyEvent(handleKeyEvent(usernameFocus, null)),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
maxLines = 1
|
||||
)
|
||||
Button(
|
||||
onClick = { onLoginClicked(server, username, password) },
|
||||
modifier = commonModifier,
|
||||
) {
|
||||
BasicText(
|
||||
"Login",
|
||||
style = TextStyle.Default.copy(color = MaterialTheme.colors.onPrimary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun Preview_AuthScreen() {
|
||||
NanofluxApp {
|
||||
AuthForm("https://example.org", "myuser", "mypass", { _, _, _ -> }, "Invalid password")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun Preview_Night_AuthScreen() {
|
||||
NanofluxTheme(darkTheme = true) {
|
||||
Surface(color = MaterialTheme.colors.surface) {
|
||||
AuthForm("https://example.org", "myuser", "mypass", { _, _, _ -> }, "Invalid password")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,13 +4,12 @@ buildscript {
|
|||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
val kotlinVersion by extra<String>("1.5.0")
|
||||
val hiltVersion by extra("2.35")
|
||||
val kotlinVersion by extra("1.4.32")
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.0.0-alpha15")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle.kts files
|
||||
classpath("com.google.dagger:hilt-android-gradle-plugin:$hiltVersion")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,15 @@ androidx-core = "1.3.2"
|
|||
androidx-appcompat = "1.2.0"
|
||||
compose = "1.0.0-beta05"
|
||||
espresso = "3.3.0"
|
||||
kotlin = "1.5.0"
|
||||
hilt-android = "2.35"
|
||||
hilt-work = "1.0.0"
|
||||
kotlin = "1.4.32"
|
||||
material = "1.3.0"
|
||||
moshi = "1.12.0"
|
||||
okhttp = "4.9.1"
|
||||
retrofit = "2.9.0"
|
||||
room = "2.3.0"
|
||||
work = "2.5.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
|
@ -17,17 +22,31 @@ compose-material = { module = "androidx.compose.material:material", version.ref
|
|||
compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
||||
compose-activity = { module = "androidx.activity:activity-compose", version = "1.3.0-alpha07" }
|
||||
compose-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
|
||||
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
||||
lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.3.1" }
|
||||
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi"}
|
||||
moshi-kapt = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi"}
|
||||
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
|
||||
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
|
||||
hilt-work-core = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" }
|
||||
hilt-work-kapt = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt-work" }
|
||||
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" }
|
||||
moshi-kapt = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
|
||||
junit = { module = "junit:junit", version = "4.12" }
|
||||
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
room-kapt = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
room-test = { module = "androidx.room:room-testing", version.ref = "room" }
|
||||
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" }
|
||||
test-ext = { module = "androidx.text.ext:junit", version = "1.1.2" }
|
||||
espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
||||
work-core = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
|
||||
work-test = { module = "androidx.work:work-testing", version.ref = "work" }
|
||||
|
||||
[bundles]
|
||||
compose = ["compose-ui", "compose-material", "compose-tooling", "compose-activity"]
|
||||
moshi = ["moshi-core", "moshi-adapters"]
|
||||
networking = ["retrofit-core", "retrofit-moshi", "okhttp-logging"]
|
||||
|
|
|
@ -4,7 +4,6 @@ dependencyResolutionManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter() // Warning: this repository is going to shut down soon
|
||||
}
|
||||
}
|
||||
rootProject.name = "Nanoflux"
|
||||
|
|
Loading…
Reference in a new issue