mirror of https://github.com/readrops/Readrops.git
Migrate database to new version
* Rename some Account fields * Migrate Account.type enum field from INTEGER to TEXT
This commit is contained in:
parent
fb68f1f492
commit
4753454a9d
|
@ -12,9 +12,9 @@ abstract class Credentials(val authorization: String?, val url: String) {
|
|||
|
||||
companion object {
|
||||
fun toCredentials(account: Account): Credentials {
|
||||
val endPoint = getEndPoint(account.accountType!!)
|
||||
val endPoint = getEndPoint(account.type!!)
|
||||
|
||||
return when (account.accountType) {
|
||||
return when (account.type) {
|
||||
AccountType.NEXTCLOUD_NEWS -> NextcloudNewsCredentials(account.login, account.password, account.url + endPoint)
|
||||
AccountType.FRESHRSS -> FreshRSSCredentials(account.token, account.url + endPoint)
|
||||
AccountType.FEVER -> FeverCredentials(account.login, account.password, account.url + endPoint)
|
||||
|
|
|
@ -22,7 +22,7 @@ class GetFoldersWithFeedsTest {
|
|||
|
||||
private lateinit var database: Database
|
||||
private lateinit var getFoldersWithFeeds: GetFoldersWithFeeds
|
||||
private val account = Account(accountType = AccountType.LOCAL)
|
||||
private val account = Account(type = AccountType.LOCAL)
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
|
|
|
@ -32,7 +32,7 @@ import kotlin.test.assertTrue
|
|||
class LocalRSSRepositoryTest : KoinTest {
|
||||
|
||||
private val mockServer: MockWebServer = MockWebServer()
|
||||
private val account = Account(accountType = AccountType.LOCAL)
|
||||
private val account = Account(type = AccountType.LOCAL)
|
||||
private lateinit var database: Database
|
||||
private lateinit var repository: LocalRSSRepository
|
||||
private lateinit var feeds: List<Feed>
|
||||
|
|
|
@ -32,20 +32,20 @@ class SyncAnalyzerTest {
|
|||
NullPointerException("Notification content shouldn't be null")
|
||||
|
||||
private val account1 = Account(
|
||||
accountName = "test account 1",
|
||||
accountType = AccountType.FRESHRSS,
|
||||
name = "test account 1",
|
||||
type = AccountType.FRESHRSS,
|
||||
isNotificationsEnabled = true
|
||||
)
|
||||
|
||||
private val account2 = Account(
|
||||
accountName = "test account 2",
|
||||
accountType = AccountType.NEXTCLOUD_NEWS,
|
||||
name = "test account 2",
|
||||
type = AccountType.NEXTCLOUD_NEWS,
|
||||
isNotificationsEnabled = false
|
||||
)
|
||||
|
||||
private val account3 = Account(
|
||||
accountName = "test account 3",
|
||||
accountType = AccountType.LOCAL,
|
||||
name = "test account 3",
|
||||
type = AccountType.LOCAL,
|
||||
isNotificationsEnabled = true
|
||||
)
|
||||
|
||||
|
@ -124,7 +124,7 @@ class SyncAnalyzerTest {
|
|||
|
||||
syncAnalyzer.getNotificationContent(mapOf(account1 to syncResult))?.let { content ->
|
||||
assertEquals(context.getString(R.string.new_items, 2), content.text)
|
||||
assertEquals(account1.accountName, content.title)
|
||||
assertEquals(account1.name, content.title)
|
||||
assertTrue(content.largeIcon != null)
|
||||
assertTrue(content.accountId > 0)
|
||||
} ?: throw nullContentException
|
||||
|
|
|
@ -59,7 +59,7 @@ val appModule = module {
|
|||
single { GetFoldersWithFeeds(get()) }
|
||||
|
||||
factory<BaseRepository> { (account: Account) ->
|
||||
when (account.accountType) {
|
||||
when (account.type) {
|
||||
AccountType.LOCAL -> LocalRSSRepository(get(), get(), account)
|
||||
AccountType.FRESHRSS -> FreshRSSRepository(
|
||||
database = get(),
|
||||
|
|
|
@ -178,8 +178,8 @@ class AccountScreenModel(
|
|||
fun createLocalAccount() {
|
||||
val context = get<Context>()
|
||||
val account = Account(
|
||||
accountName = context.getString(AccountType.LOCAL.typeName),
|
||||
accountType = AccountType.LOCAL,
|
||||
name = context.getString(AccountType.LOCAL.nameRes),
|
||||
type = AccountType.LOCAL,
|
||||
isCurrentAccount = true
|
||||
)
|
||||
|
||||
|
@ -212,7 +212,7 @@ class AccountScreenModel(
|
|||
|
||||
@Stable
|
||||
data class AccountState(
|
||||
val account: Account = Account(accountName = "account", accountType = AccountType.LOCAL),
|
||||
val account: Account = Account(name = "account", type = AccountType.LOCAL),
|
||||
val dialog: DialogState? = null,
|
||||
val synchronizationErrors: ErrorResult? = null,
|
||||
val error: Exception? = null,
|
||||
|
|
|
@ -192,7 +192,7 @@ object AccountTab : Tab {
|
|||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Image(
|
||||
painter = adaptiveIconPainterResource(id = state.account.accountType!!.iconRes),
|
||||
painter = adaptiveIconPainterResource(id = state.account.type!!.iconRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
@ -201,7 +201,7 @@ object AccountTab : Tab {
|
|||
|
||||
Column {
|
||||
Text(
|
||||
text = state.account.accountName!!,
|
||||
text = state.account.name!!,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
@ -224,7 +224,7 @@ object AccountTab : Tab {
|
|||
ThreeDotsMenu(
|
||||
items = mapOf(1 to stringResource(id = R.string.rename_account)),
|
||||
onItemClick = {
|
||||
screenModel.openDialog(DialogState.RenameAccount(state.account.accountName!!))
|
||||
screenModel.openDialog(DialogState.RenameAccount(state.account.name!!))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -303,8 +303,8 @@ object AccountTab : Tab {
|
|||
|
||||
for (account in state.accounts) {
|
||||
SelectableImageText(
|
||||
image = adaptiveIconPainterResource(id = account.accountType!!.iconRes),
|
||||
text = account.accountName!!,
|
||||
image = adaptiveIconPainterResource(id = account.type!!.iconRes),
|
||||
text = account.name!!,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
padding = MaterialTheme.spacing.mediumSpacing,
|
||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||
|
@ -358,8 +358,8 @@ object AccountTab : Tab {
|
|||
screenModel.createLocalAccount()
|
||||
} else {
|
||||
val account = Account(
|
||||
accountType = accountType,
|
||||
accountName = context.resources.getString(accountType.typeName)
|
||||
type = accountType,
|
||||
name = context.resources.getString(accountType.nameRes)
|
||||
)
|
||||
navigator.push(
|
||||
AccountCredentialsScreen(
|
||||
|
|
|
@ -117,7 +117,7 @@ class AccountCredentialsScreen(
|
|||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Image(
|
||||
painter = adaptiveIconPainterResource(id = account.accountType!!.iconRes),
|
||||
painter = adaptiveIconPainterResource(id = account.type!!.iconRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
@ -125,7 +125,7 @@ class AccountCredentialsScreen(
|
|||
ShortSpacer()
|
||||
|
||||
Text(
|
||||
text = stringResource(id = account.accountType!!.typeName),
|
||||
text = stringResource(id = account.type!!.nameRes),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
|
@ -155,7 +155,7 @@ class AccountCredentialsScreen(
|
|||
state.urlError != null -> {
|
||||
Text(text = state.urlError!!.errorText())
|
||||
}
|
||||
account.accountType == AccountType.FEVER -> {
|
||||
account.type == AccountType.FEVER -> {
|
||||
Text(text = stringResource(R.string.provide_full_url))
|
||||
}
|
||||
else -> {
|
||||
|
@ -211,7 +211,7 @@ class AccountCredentialsScreen(
|
|||
state.passwordError != null -> {
|
||||
Text(text = state.passwordError!!.errorText())
|
||||
}
|
||||
account.accountType == AccountType.FRESHRSS -> {
|
||||
account.type == AccountType.FRESHRSS -> {
|
||||
Text(text = stringResource(id = R.string.password_helper))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,14 +28,14 @@ class AccountCredentialsScreenModel(
|
|||
if (mode == AccountCredentialsScreenMode.EDIT_CREDENTIALS) {
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
name = account.accountName!!,
|
||||
name = account.name!!,
|
||||
url = account.url!!,
|
||||
login = account.login!!,
|
||||
password = account.password!!
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mutableState.update { it.copy(name = account.accountName!!) }
|
||||
mutableState.update { it.copy(name = account.name!!) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,10 +61,10 @@ class AccountCredentialsScreenModel(
|
|||
|
||||
val newAccount = account.copy(
|
||||
url = normalizedUrl,
|
||||
accountName = name,
|
||||
name = name,
|
||||
login = login,
|
||||
password = password,
|
||||
accountType = account.accountType,
|
||||
type = account.type,
|
||||
isCurrentAccount = true
|
||||
)
|
||||
|
||||
|
|
|
@ -22,11 +22,10 @@ fun AccountSelectionDialog(
|
|||
onDismiss = onDismiss
|
||||
) {
|
||||
AccountType.entries
|
||||
.filter { it != AccountType.FEEDLY }
|
||||
.forEach { type ->
|
||||
SelectableImageText(
|
||||
image = adaptiveIconPainterResource(id = type.iconRes),
|
||||
text = stringResource(id = type.typeName),
|
||||
text = stringResource(id = type.nameRes),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||
padding = MaterialTheme.spacing.shortSpacing,
|
||||
|
|
|
@ -95,8 +95,8 @@ class AccountSelectionScreen : AndroidScreen() {
|
|||
val accountType =
|
||||
(state.navState as NavState.GoToAccountCredentialsScreen).accountType
|
||||
val account = Account(
|
||||
accountType = accountType,
|
||||
accountName = stringResource(id = accountType.typeName)
|
||||
type = accountType,
|
||||
name = stringResource(id = accountType.nameRes)
|
||||
)
|
||||
|
||||
navigator.push(
|
||||
|
@ -159,7 +159,7 @@ class AccountSelectionScreen : AndroidScreen() {
|
|||
|
||||
SelectableImageText(
|
||||
image = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
|
||||
text = stringResource(id = AccountType.LOCAL.typeName),
|
||||
text = stringResource(id = AccountType.LOCAL.nameRes),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||
padding = MaterialTheme.spacing.mediumSpacing,
|
||||
|
@ -187,11 +187,10 @@ class AccountSelectionScreen : AndroidScreen() {
|
|||
|
||||
AccountType.entries
|
||||
.filter { it != AccountType.LOCAL }
|
||||
.filter { it != AccountType.FEEDLY }
|
||||
.forEach { accountType ->
|
||||
SelectableImageText(
|
||||
image = adaptiveIconPainterResource(id = accountType.iconRes),
|
||||
text = stringResource(id = accountType.typeName),
|
||||
text = stringResource(id = accountType.nameRes),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
imageSize = 24.dp,
|
||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||
|
|
|
@ -55,8 +55,8 @@ class AccountSelectionScreenModel(
|
|||
val context = get<Context>()
|
||||
val account = Account(
|
||||
url = null,
|
||||
accountName = context.getString(AccountType.LOCAL.typeName),
|
||||
accountType = AccountType.LOCAL,
|
||||
name = context.getString(AccountType.LOCAL.nameRes),
|
||||
type = AccountType.LOCAL,
|
||||
isCurrentAccount = true
|
||||
)
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ sealed class FolderAndFeedsState {
|
|||
|
||||
data class AddFeedDialogState(
|
||||
val url: String = "",
|
||||
val selectedAccount: Account = Account(accountName = ""),
|
||||
val selectedAccount: Account = Account(name = ""),
|
||||
val accounts: List<Account> = listOf(),
|
||||
val error: TextFieldError? = null,
|
||||
val exception: Exception? = null,
|
||||
|
|
|
@ -79,14 +79,14 @@ fun AddFeedDialog(
|
|||
) {
|
||||
for (account in state.accounts) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = account.accountName!!) },
|
||||
text = { Text(text = account.name!!) },
|
||||
onClick = {
|
||||
onAccountClick(account)
|
||||
},
|
||||
leadingIcon = {
|
||||
Image(
|
||||
painter = adaptiveIconPainterResource(
|
||||
id = account.accountType!!.iconRes
|
||||
id = account.type!!.iconRes
|
||||
),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
|
@ -97,7 +97,7 @@ fun AddFeedDialog(
|
|||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.selectedAccount.accountName!!,
|
||||
value = state.selectedAccount.name!!,
|
||||
readOnly = true,
|
||||
onValueChange = {},
|
||||
trailingIcon = {
|
||||
|
@ -106,7 +106,7 @@ fun AddFeedDialog(
|
|||
leadingIcon = {
|
||||
Image(
|
||||
painter = adaptiveIconPainterResource(
|
||||
id = state.selectedAccount.accountType!!.iconRes
|
||||
id = state.selectedAccount.type!!.iconRes
|
||||
),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
|
|
|
@ -52,7 +52,7 @@ class ItemScreenModel(
|
|||
.flatMapLatest { account ->
|
||||
this@ItemScreenModel.account = account!!
|
||||
|
||||
if (account.accountType == AccountType.FEVER) {
|
||||
if (account.type == AccountType.FEVER) {
|
||||
get<SharedPreferences>().apply {
|
||||
account.login = getString(account.loginKey, null)
|
||||
account.password = getString(account.passwordKey, null)
|
||||
|
|
|
@ -25,7 +25,7 @@ class FeverRepository(
|
|||
val authenticated = feverDataSource.login(account.login!!, account.password!!)
|
||||
|
||||
if (authenticated) {
|
||||
account.displayedName = account.accountType!!.name
|
||||
account.displayedName = account.type!!.name
|
||||
} else {
|
||||
throw LoginFailedException()
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ class LocalRSSRepository(
|
|||
private suspend fun insertFeed(feed: Feed): Feed {
|
||||
// TODO better handle this case
|
||||
require(!database.feedDao().feedExists(feed.url!!, account.id)) {
|
||||
"Feed already exists for account ${account.accountName}"
|
||||
"Feed already exists for account ${account.name}"
|
||||
}
|
||||
|
||||
return feed.apply {
|
||||
|
|
|
@ -67,11 +67,11 @@ class SyncAnalyzer(
|
|||
// multiple new items from several feeds
|
||||
feedsIdsForNewItems.size > 1 && itemCount > 1 -> {
|
||||
NotificationContent(
|
||||
title = account.accountName!!,
|
||||
title = account.name!!,
|
||||
text = context.getString(R.string.new_items, itemCount.toString()),
|
||||
largeIcon = ContextCompat.getDrawable(
|
||||
context,
|
||||
account.accountType!!.iconRes
|
||||
account.type!!.iconRes
|
||||
)!!.toBitmap(),
|
||||
accountId = account.id
|
||||
)
|
||||
|
|
|
@ -131,7 +131,7 @@ class SyncWorker(
|
|||
notificationBuilder.setContentTitle(
|
||||
applicationContext.resources.getString(
|
||||
R.string.updating_account,
|
||||
account.accountName
|
||||
account.name
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -212,7 +212,7 @@ class SyncWorker(
|
|||
if (result.second.isNotEmpty()) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"refreshing local account ${account.accountName}: ${result.second.size} errors"
|
||||
"refreshing local account ${account.name}: ${result.second.size} errors"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,526 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "0bac941f8b1b6003c35a6d0cdc1f2e13",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Feed",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `description` TEXT, `url` TEXT, `siteUrl` TEXT, `last_updated` TEXT, `color` INTEGER NOT NULL, `icon_url` TEXT, `etag` TEXT, `last_modified` TEXT, `folder_id` INTEGER, `remote_id` TEXT, `account_id` INTEGER NOT NULL, `notification_enabled` INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(`folder_id`) REFERENCES `Folder`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "siteUrl",
|
||||
"columnName": "siteUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdated",
|
||||
"columnName": "last_updated",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconUrl",
|
||||
"columnName": "icon_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "etag",
|
||||
"columnName": "etag",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastModified",
|
||||
"columnName": "last_modified",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "folderId",
|
||||
"columnName": "folder_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteId",
|
||||
"columnName": "remote_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "account_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isNotificationEnabled",
|
||||
"columnName": "notification_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Feed_folder_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"folder_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_folder_id` ON `${TABLE_NAME}` (`folder_id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_Feed_account_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"account_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_account_id` ON `${TABLE_NAME}` (`account_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Folder",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"folder_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "Account",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "Item",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "cleanDescription",
|
||||
"columnName": "clean_description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "link",
|
||||
"columnName": "link",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "imageLink",
|
||||
"columnName": "image_link",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pubDate",
|
||||
"columnName": "pub_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "feedId",
|
||||
"columnName": "feed_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "readTime",
|
||||
"columnName": "read_time",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRead",
|
||||
"columnName": "read",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStarred",
|
||||
"columnName": "starred",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteId",
|
||||
"columnName": "remote_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Item_feed_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"feed_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Item_feed_id` ON `${TABLE_NAME}` (`feed_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Feed",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"feed_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "Folder",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `remoteId` TEXT, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteId",
|
||||
"columnName": "remoteId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "account_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Folder_account_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"account_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Folder_account_id` ON `${TABLE_NAME}` (`account_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Account",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "Account",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT, `name` TEXT, `displayed_name` TEXT, `type` TEXT, `last_modified` INTEGER NOT NULL, `current_account` INTEGER NOT NULL, `token` TEXT, `write_token` TEXT, `notifications_enabled` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayedName",
|
||||
"columnName": "displayed_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastModified",
|
||||
"columnName": "last_modified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isCurrentAccount",
|
||||
"columnName": "current_account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "writeToken",
|
||||
"columnName": "write_token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isNotificationsEnabled",
|
||||
"columnName": "notifications_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "ItemStateChange",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "readChange",
|
||||
"columnName": "read_change",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "starChange",
|
||||
"columnName": "star_change",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "account_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Account",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "ItemState",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "read",
|
||||
"columnName": "read",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "starred",
|
||||
"columnName": "starred",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteId",
|
||||
"columnName": "remote_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "account_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ItemState_remote_id_account_id",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"remote_id",
|
||||
"account_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemState_remote_id_account_id` ON `${TABLE_NAME}` (`remote_id`, `account_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Account",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '0bac941f8b1b6003c35a6d0cdc1f2e13')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -54,4 +54,16 @@ class MigrationsTest {
|
|||
assertEquals("guid", remoteId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate4To5() {
|
||||
helper.createDatabase(dbName, 4).apply {
|
||||
execSQL("Insert Into Account(account_type, last_modified, current_account, notifications_enabled) Values(0, 0, 1, 0)")
|
||||
}
|
||||
|
||||
helper.runMigrationsAndValidate(dbName, 5, true, MigrationFrom4To5).apply {
|
||||
val type = compileStatement("Select type From Account").simpleQueryForString()
|
||||
assertEquals("LOCAL", type)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ class FeedDaoTest {
|
|||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build()
|
||||
|
||||
account = Account(accountType = AccountType.LOCAL).apply {
|
||||
account = Account(type = AccountType.LOCAL).apply {
|
||||
id = database.accountDao().insert(this).toInt()
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ class FolderDaoTest {
|
|||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build()
|
||||
|
||||
account = Account(accountType = AccountType.LOCAL).apply {
|
||||
account = Account(type = AccountType.LOCAL).apply {
|
||||
id = database.accountDao().insert(this).toInt()
|
||||
}
|
||||
|
||||
|
|
|
@ -17,12 +17,13 @@ import com.readrops.db.entities.Item
|
|||
import com.readrops.db.entities.ItemState
|
||||
import com.readrops.db.entities.ItemStateChange
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import com.readrops.db.util.Converters
|
||||
|
||||
@Database(
|
||||
entities = [Feed::class, Item::class, Folder::class, Account::class,
|
||||
ItemStateChange::class, ItemState::class],
|
||||
version = 4
|
||||
version = 5
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class Database : RoomDatabase() {
|
||||
|
@ -96,5 +97,29 @@ object MigrationFrom3To4 : Migration(3, 4) {
|
|||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_Feed_folder_id` ON `Feed` (`folder_id`)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_Feed_account_id` ON `Feed` (`account_id`)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object MigrationFrom4To5 : Migration(4, 5) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// rename account_name to name
|
||||
// rename account_type to type
|
||||
// rename writeToken to write_token
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_Account` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT, `name` TEXT, `displayed_name` TEXT, `type` TEXT, `last_modified` INTEGER NOT NULL, `current_account` INTEGER NOT NULL, `token` TEXT, `write_token` TEXT, `notifications_enabled` INTEGER NOT NULL)")
|
||||
db.execSQL("INSERT INTO `_new_Account` (`id`, `url`, `name`, `displayed_name`, `type`, `last_modified`, `current_account`, `token`, `write_token`, `notifications_enabled`) SELECT `id`, `url`, `account_name`, `displayed_name`, NULL, `last_modified`, `current_account`, `token`, `writeToken`, `notifications_enabled` FROM `Account`")
|
||||
|
||||
// migrate type from INTEGER to TEXT
|
||||
val cursor = db.query("SELECT `id`, `account_type` FROM `Account`")
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getInt(0)
|
||||
val ordinal = cursor.getInt(1)
|
||||
|
||||
val type = AccountType.entries[ordinal]
|
||||
|
||||
db.execSQL("UPDATE `_new_Account` SET `type` = \"${type.name}\" WHERE `id` = $id")
|
||||
}
|
||||
|
||||
db.execSQL("DROP TABLE IF EXISTS `Account`")
|
||||
db.execSQL("ALTER TABLE `_new_Account` RENAME TO `Account`")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,12 @@ val dbModule = module {
|
|||
|
||||
single(createdAtStart = true) {
|
||||
Room.databaseBuilder(get(), Database::class.java, "readrops-db")
|
||||
.addMigrations(MigrationFrom1To2, MigrationFrom2To3, MigrationFrom3To4)
|
||||
.addMigrations(
|
||||
MigrationFrom1To2,
|
||||
MigrationFrom2To3,
|
||||
MigrationFrom3To4,
|
||||
MigrationFrom4To5
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
|
@ -46,6 +46,6 @@ interface AccountDao : BaseDao<Account> {
|
|||
When id Is Not :accountId Then 0 End""")
|
||||
suspend fun updateCurrentAccount(accountId: Int)
|
||||
|
||||
@Query("Update Account set account_name = :name Where id = :accountId")
|
||||
@Query("Update Account set name = :name Where id = :accountId")
|
||||
suspend fun renameAccount(accountId: Int, name: String)
|
||||
}
|
|
@ -8,34 +8,31 @@ import java.io.Serializable
|
|||
|
||||
@Entity
|
||||
data class Account(
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0,
|
||||
var url: String? = null,
|
||||
@ColumnInfo(name = "account_name") var accountName: String? = null,
|
||||
@ColumnInfo(name = "displayed_name") var displayedName: String? = null,
|
||||
@ColumnInfo(name = "account_type") var accountType: AccountType? = null,
|
||||
@ColumnInfo(name = "last_modified") var lastModified: Long = 0,
|
||||
@ColumnInfo(name = "current_account") var isCurrentAccount: Boolean = false,
|
||||
var token: String? = null,
|
||||
var writeToken: String? = null, // TODO : see if there is a better solution to store specific service account fields
|
||||
@ColumnInfo(name = "notifications_enabled") var isNotificationsEnabled: Boolean = false,
|
||||
@Ignore var login: String? = null,
|
||||
@Ignore var password: String? = null,
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0,
|
||||
var url: String? = null,
|
||||
@ColumnInfo(name = "name") var name: String? = null,
|
||||
@ColumnInfo(name = "displayed_name") var displayedName: String? = null,
|
||||
@ColumnInfo(name = "type") var type: AccountType? = null,
|
||||
@ColumnInfo(name = "last_modified") var lastModified: Long = 0,
|
||||
@ColumnInfo(name = "current_account") var isCurrentAccount: Boolean = false,
|
||||
var token: String? = null,
|
||||
@ColumnInfo(name = "write_token") var writeToken: String? = null,
|
||||
@ColumnInfo(name = "notifications_enabled") var isNotificationsEnabled: Boolean = false,
|
||||
@Ignore var login: String? = null,
|
||||
@Ignore var password: String? = null,
|
||||
) : Serializable {
|
||||
|
||||
constructor(accountUrl: String?, accountName: String, accountType: AccountType):
|
||||
this(url = accountUrl, accountName = accountName, accountType = accountType)
|
||||
|
||||
val config: AccountConfig
|
||||
get() = accountType!!.accountConfig!!
|
||||
get() = type!!.config
|
||||
|
||||
val isLocal
|
||||
get() = accountType == AccountType.LOCAL
|
||||
get() = type == AccountType.LOCAL
|
||||
|
||||
fun `is`(accountType: AccountType) = this.accountType == accountType
|
||||
fun `is`(accountType: AccountType) = this.type == accountType
|
||||
|
||||
val loginKey
|
||||
get() = accountType!!.name + "_login_" + id
|
||||
get() = type!!.name + "_login_" + id
|
||||
|
||||
val passwordKey
|
||||
get() = accountType!!.name + "_password_" + id
|
||||
get() = type!!.name + "_password_" + id
|
||||
}
|
|
@ -5,7 +5,7 @@ data class AccountConfig(
|
|||
val addNoFolder: Boolean, // Add a "No folder" option when modifying a feed's folder
|
||||
val useSeparateState: Boolean, // Let know if it uses ItemState table to synchronize read/star state
|
||||
val canCreateFolder: Boolean, // Enable or disable folder creation in Feed Tab
|
||||
val canCreateFeed: Boolean = true,
|
||||
val canCreateFeed: Boolean = true,
|
||||
val canUpdateFolder: Boolean = true,
|
||||
val canUpdateFeed: Boolean = true,
|
||||
val canDeleteFeed: Boolean = true,
|
||||
|
|
|
@ -4,13 +4,13 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.StringRes
|
||||
import com.readrops.db.R
|
||||
|
||||
// TODO comment Feedly
|
||||
enum class AccountType(@DrawableRes val iconRes: Int,
|
||||
@StringRes val typeName: Int,
|
||||
val accountConfig: AccountConfig?) {
|
||||
enum class AccountType(
|
||||
@DrawableRes val iconRes: Int,
|
||||
@StringRes val nameRes: Int,
|
||||
val config: AccountConfig
|
||||
) {
|
||||
LOCAL(R.mipmap.ic_launcher, R.string.local_account, AccountConfig.LOCAL),
|
||||
NEXTCLOUD_NEWS(R.drawable.ic_nextcloud_news, R.string.nextcloud_news, AccountConfig.NEXTCLOUD_NEWS),
|
||||
FEEDLY(R.drawable.ic_feedly, R.string.feedly, null),
|
||||
FRESHRSS(R.drawable.ic_freshrss, R.string.freshrss, AccountConfig.FRESHRSS),
|
||||
FEVER(R.drawable.ic_fever, R.string.fever, AccountConfig.FEVER)
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package com.readrops.db.util
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class Converters {
|
||||
|
@ -15,15 +14,4 @@ class Converters {
|
|||
fun fromLocalDateTime(localDateTime: LocalDateTime): Long {
|
||||
return localDateTime.toInstant(DateUtils.defaultOffset).toEpochMilli()
|
||||
}
|
||||
|
||||
// TODO Use Room built-in enum converter, ordinal is not reliable
|
||||
@TypeConverter
|
||||
fun fromAccountTypeCode(ordinal: Int): AccountType {
|
||||
return AccountType.entries[ordinal]
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun getAccountTypeCode(accountType: AccountType): Int {
|
||||
return accountType.ordinal
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue