DB caching. #33
| @@ -78,12 +78,6 @@ android { | ||||
|  | ||||
|         // tests | ||||
|         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||
|  | ||||
|         javaCompileOptions { | ||||
|             annotationProcessorOptions { | ||||
|                 arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     buildTypes { | ||||
|         getByName("release") { | ||||
| @@ -196,16 +190,14 @@ dependencies { | ||||
|  | ||||
|     implementation("androidx.core:core-ktx:1.8.0") | ||||
|  | ||||
|     implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") | ||||
|     implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1") | ||||
|     implementation("androidx.lifecycle:lifecycle-runtime:2.5.1") | ||||
|     // implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") | ||||
|     // implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1") | ||||
|     // implementation("androidx.lifecycle:lifecycle-runtime:2.5.1") | ||||
|     implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") | ||||
|  | ||||
|     implementation("androidx.room:room-ktx:2.4.0-beta01") | ||||
|     kapt("androidx.room:room-compiler:2.4.0-beta01") | ||||
|  | ||||
|     implementation("android.arch.work:work-runtime-ktx:1.0.1") | ||||
|  | ||||
|     // Network information | ||||
|      implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0") | ||||
|  | ||||
|     // SQLDELIGHT | ||||
|     implementation("com.squareup.sqldelight:android-driver:1.5.3") | ||||
| } | ||||
| @@ -1,96 +0,0 @@ | ||||
| { | ||||
|   "formatVersion": 1, | ||||
|   "database": { | ||||
|     "version": 1, | ||||
|     "identityHash": "08ca537d7ac9d4dd216e8e395d70801a", | ||||
|     "entities": [ | ||||
|       { | ||||
|         "tableName": "tags", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "tag", | ||||
|             "columnName": "tag", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "color", | ||||
|             "columnName": "color", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "tag" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "sources", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "spout", | ||||
|             "columnName": "spout", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "error", | ||||
|             "columnName": "error", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "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, \"08ca537d7ac9d4dd216e8e395d70801a\")" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
| @@ -1,176 +0,0 @@ | ||||
| { | ||||
|   "formatVersion": 1, | ||||
|   "database": { | ||||
|     "version": 2, | ||||
|     "identityHash": "6fa6944b04100d68eab61039876a8804", | ||||
|     "entities": [ | ||||
|       { | ||||
|         "tableName": "tags", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "tag", | ||||
|             "columnName": "tag", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "color", | ||||
|             "columnName": "color", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "tag" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "sources", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "spout", | ||||
|             "columnName": "spout", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "error", | ||||
|             "columnName": "error", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "items", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "datetime", | ||||
|             "columnName": "datetime", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "content", | ||||
|             "columnName": "content", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "starred", | ||||
|             "columnName": "starred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "thumbnail", | ||||
|             "columnName": "thumbnail", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "link", | ||||
|             "columnName": "link", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "sourcetitle", | ||||
|             "columnName": "sourcetitle", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "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, \"6fa6944b04100d68eab61039876a8804\")" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
| @@ -1,226 +0,0 @@ | ||||
| { | ||||
|   "formatVersion": 1, | ||||
|   "database": { | ||||
|     "version": 3, | ||||
|     "identityHash": "7ad9c4961992c13b670128485ebb3efc", | ||||
|     "entities": [ | ||||
|       { | ||||
|         "tableName": "tags", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "tag", | ||||
|             "columnName": "tag", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "color", | ||||
|             "columnName": "color", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "tag" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "sources", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "spout", | ||||
|             "columnName": "spout", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "error", | ||||
|             "columnName": "error", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "items", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "datetime", | ||||
|             "columnName": "datetime", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "content", | ||||
|             "columnName": "content", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "starred", | ||||
|             "columnName": "starred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "thumbnail", | ||||
|             "columnName": "thumbnail", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "link", | ||||
|             "columnName": "link", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "sourcetitle", | ||||
|             "columnName": "sourcetitle", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "actions", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "articleId", | ||||
|             "columnName": "articleid", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "read", | ||||
|             "columnName": "read", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "starred", | ||||
|             "columnName": "starred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unstarred", | ||||
|             "columnName": "unstarred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": true | ||||
|         }, | ||||
|         "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, \"7ad9c4961992c13b670128485ebb3efc\")" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
| @@ -1,226 +0,0 @@ | ||||
| { | ||||
|   "formatVersion": 1, | ||||
|   "database": { | ||||
|     "version": 4, | ||||
|     "identityHash": "9cf8b03d32f80dfd58160599a1df197d", | ||||
|     "entities": [ | ||||
|       { | ||||
|         "tableName": "tags", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "tag", | ||||
|             "columnName": "tag", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "color", | ||||
|             "columnName": "color", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "tag" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "sources", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "spout", | ||||
|             "columnName": "spout", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "error", | ||||
|             "columnName": "error", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "items", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "datetime", | ||||
|             "columnName": "datetime", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "content", | ||||
|             "columnName": "content", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "starred", | ||||
|             "columnName": "starred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "thumbnail", | ||||
|             "columnName": "thumbnail", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "link", | ||||
|             "columnName": "link", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "sourcetitle", | ||||
|             "columnName": "sourcetitle", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "actions", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "articleId", | ||||
|             "columnName": "articleid", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "read", | ||||
|             "columnName": "read", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "starred", | ||||
|             "columnName": "starred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unstarred", | ||||
|             "columnName": "unstarred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": true | ||||
|         }, | ||||
|         "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, \"9cf8b03d32f80dfd58160599a1df197d\")" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
| @@ -1,226 +0,0 @@ | ||||
| { | ||||
|   "formatVersion": 1, | ||||
|   "database": { | ||||
|     "version": 4, | ||||
|     "identityHash": "9cf8b03d32f80dfd58160599a1df197d", | ||||
|     "entities": [ | ||||
|       { | ||||
|         "tableName": "tags", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "tag", | ||||
|             "columnName": "tag", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "color", | ||||
|             "columnName": "color", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "tag" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "sources", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "spout", | ||||
|             "columnName": "spout", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "error", | ||||
|             "columnName": "error", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "items", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "datetime", | ||||
|             "columnName": "datetime", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "title", | ||||
|             "columnName": "title", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "content", | ||||
|             "columnName": "content", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "starred", | ||||
|             "columnName": "starred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "thumbnail", | ||||
|             "columnName": "thumbnail", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "icon", | ||||
|             "columnName": "icon", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "link", | ||||
|             "columnName": "link", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "sourcetitle", | ||||
|             "columnName": "sourcetitle", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tags", | ||||
|             "columnName": "tags", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "actions", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "articleId", | ||||
|             "columnName": "articleid", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "read", | ||||
|             "columnName": "read", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "starred", | ||||
|             "columnName": "starred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unstarred", | ||||
|             "columnName": "unstarred", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": true | ||||
|         }, | ||||
|         "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, '9cf8b03d32f80dfd58160599a1df197d')" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
| @@ -11,6 +11,7 @@ import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.Toppings | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.Config | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid | ||||
| import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import com.ftinc.scoop.Scoop | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @@ -109,33 +110,40 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
|         } | ||||
|  | ||||
|  | ||||
|         fun handleSpoutFailure(networkIssue: Boolean = false) { | ||||
|             Toast.makeText( | ||||
|                 this@AddSourceActivity, | ||||
|                 if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, | ||||
|                 Toast.LENGTH_SHORT | ||||
|             ).show() | ||||
|             mProgress.visibility = View.GONE | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             val items = repository.getSpouts() | ||||
|             if (items != null) { | ||||
|             try { | ||||
|                 val items = repository.getSpouts() | ||||
|                 if (items != null) { | ||||
|                     val itemsStrings = items.map { it.value.name } | ||||
|                     for ((key, value) in items) { | ||||
|                         spoutsKV[value.name] = key | ||||
|                     } | ||||
|  | ||||
|                 val itemsStrings = items.map { it.value.name } | ||||
|                 for ((key, value) in items) { | ||||
|                     spoutsKV[value.name] = key | ||||
|                     mProgress.visibility = View.GONE | ||||
|                     formContainer.visibility = View.VISIBLE | ||||
|  | ||||
|                     val spinnerArrayAdapter = | ||||
|                         ArrayAdapter( | ||||
|                             this@AddSourceActivity, | ||||
|                             android.R.layout.simple_spinner_item, | ||||
|                             itemsStrings | ||||
|                         ) | ||||
|                     spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) | ||||
|                     spoutsSpinner.adapter = spinnerArrayAdapter | ||||
|                 } else { | ||||
|                     handleSpoutFailure() | ||||
|                 } | ||||
|  | ||||
|                 mProgress.visibility = View.GONE | ||||
|                 formContainer.visibility = View.VISIBLE | ||||
|  | ||||
|                 val spinnerArrayAdapter = | ||||
|                     ArrayAdapter( | ||||
|                         this@AddSourceActivity, | ||||
|                         android.R.layout.simple_spinner_item, | ||||
|                         itemsStrings | ||||
|                     ) | ||||
|                 spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) | ||||
|                 spoutsSpinner.adapter = spinnerArrayAdapter | ||||
|             } else { | ||||
|                 Toast.makeText( | ||||
|                     this@AddSourceActivity, | ||||
|                     R.string.cant_get_spouts, | ||||
|                     Toast.LENGTH_SHORT | ||||
|                 ).show() | ||||
|                 mProgress.visibility = View.GONE | ||||
|             } catch (e: NetworkUnavailableException) { | ||||
|                 handleSpoutFailure(networkIssue = true) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -18,7 +18,6 @@ import androidx.appcompat.widget.SearchView | ||||
| import androidx.core.view.doOnNextLayout | ||||
| import androidx.drawerlayout.widget.DrawerLayout | ||||
| import androidx.recyclerview.widget.* | ||||
| import androidx.room.Room | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.ExistingPeriodicWorkPolicy | ||||
| import androidx.work.PeriodicWorkRequestBuilder | ||||
| @@ -28,15 +27,6 @@ import bou.amine.apps.readerforselfossv2.android.adapters.ItemListAdapter | ||||
| import bou.amine.apps.readerforselfossv2.android.adapters.ItemsAdapter | ||||
| import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getIcon | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabaseService | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 | ||||
| import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.Toppings | ||||
| @@ -44,13 +34,10 @@ import bou.amine.apps.readerforselfossv2.android.utils.Config | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView | ||||
| import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel | ||||
| import bou.amine.apps.readerforselfossv2.dao.ACTION | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.ItemType | ||||
| import bou.amine.apps.readerforselfossv2.utils.longHash | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.* | ||||
| import com.ashokvarma.bottomnavigation.BottomNavigationBar | ||||
| import com.ashokvarma.bottomnavigation.BottomNavigationItem | ||||
| import com.ashokvarma.bottomnavigation.TextBadgeItem | ||||
| @@ -83,8 +70,6 @@ import kotlin.concurrent.thread | ||||
|  | ||||
| class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware { | ||||
|  | ||||
|     private lateinit var dataBase: AndroidDeviceDatabase | ||||
|     private lateinit var dbService: AndroidDeviceDatabaseService | ||||
|     private val MENU_PREFERENCES = 12302 | ||||
|     private val DRAWER_ID_TAGS = 100101L | ||||
|     private val DRAWER_ID_HIDDEN_TAGS = 101100L | ||||
| @@ -129,8 +114,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|     private lateinit var tagsBadge: Map<Long, Int> | ||||
|  | ||||
|     private lateinit var db: AppDatabase | ||||
|  | ||||
|     private lateinit var config: Config | ||||
|  | ||||
|     override val di by closestDI() | ||||
| @@ -169,15 +152,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         binding.drawerContainer.addDrawerListener(mDrawerToggle) | ||||
|         mDrawerToggle.syncState() | ||||
|  | ||||
|         db = Room.databaseBuilder( | ||||
|             applicationContext, | ||||
|             AppDatabase::class.java, "selfoss-database" | ||||
|         ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() | ||||
|  | ||||
|         customTabActivityHelper = CustomTabActivityHelper() | ||||
|  | ||||
|         dataBase = AndroidDeviceDatabase(applicationContext) | ||||
|  | ||||
|         handleBottomBar() | ||||
|         handleDrawer() | ||||
|  | ||||
| @@ -186,6 +162,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         handleSettings() | ||||
|  | ||||
|         getElementsAccordingToTab() | ||||
|  | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             repository.tryToCacheItemsAndGetNewOnes() | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private fun handleSwipeRefreshLayout() { | ||||
| @@ -466,6 +448,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // TODO: refactor this. | ||||
|     private fun handleDrawerItems() { | ||||
|         tagsBadge = emptyMap() | ||||
|         fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) { | ||||
| @@ -486,7 +469,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|                 val drawerItem = PrimaryDrawerItem() | ||||
|                     .apply { | ||||
|                         nameText = it.getTitleDecoded() | ||||
|                         nameText = it.tag.getHtmlDecoded() | ||||
|                         identifier = it.tag.longHash() | ||||
|                         iconDrawable = gd | ||||
|                         badgeStyle = BadgeStyle().apply { | ||||
| @@ -562,7 +545,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|                 } else { | ||||
|                     for (source in maybeSources) { | ||||
|                         val item = PrimaryDrawerItem().apply { | ||||
|                             nameText = source.getTitleDecoded() | ||||
|                             nameText = source.title.getHtmlDecoded() | ||||
|                             identifier = source.id.toLong() | ||||
|                             iconUrl = source.getIcon(repository.baseUrl) | ||||
|                             onDrawerItemClickListener = { _,_,_ -> | ||||
| @@ -650,17 +633,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|                 if (!loadedFromCache) { | ||||
|                     if (maybeDrawerData.tags != null) { | ||||
|                         thread { | ||||
|                             val tagEntities = maybeDrawerData.tags.map { it.toEntity() } | ||||
|                             db.drawerDataDao().deleteAllTags() | ||||
|                             db.drawerDataDao().insertAllTags(*tagEntities.toTypedArray()) | ||||
|                             repository.resetDBTagsWithData(maybeDrawerData.tags) | ||||
|                         } | ||||
|                     } | ||||
|                     if (maybeDrawerData.sources != null) { | ||||
|                         thread { | ||||
|                             val sourceEntities = | ||||
|                                 maybeDrawerData.sources.map { it.toEntity() } | ||||
|                             db.drawerDataDao().deleteAllSources() | ||||
|                             db.drawerDataDao().insertAllSources(*sourceEntities.toTypedArray()) | ||||
|                             repository.resetDBSourcesWithData(maybeDrawerData.sources) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| @@ -718,8 +696,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         ) | ||||
|  | ||||
|         thread { | ||||
|             val drawerData = DrawerData(db.drawerDataDao().tags().map { it.toView() }, | ||||
|                                         db.drawerDataDao().sources().map { it.toView() }) | ||||
|             val drawerData = DrawerData(repository.getDBTags().map { it.toView() }, | ||||
|                                         repository.getDBSources().map { it.toView() }) | ||||
|             runOnUiThread { | ||||
|                 handleDrawerData(drawerData, loadedFromCache = true) | ||||
|                 drawerApiCalls(drawerData) | ||||
| @@ -894,7 +872,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|                         ItemCardAdapter( | ||||
|                             this, | ||||
|                             items, | ||||
|                             db, | ||||
|                             customTabActivityHelper, | ||||
|                             internalBrowser, | ||||
|                             articleViewer, | ||||
| @@ -909,7 +886,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|                         ItemListAdapter( | ||||
|                             this, | ||||
|                             items, | ||||
|                             db, | ||||
|                             customTabActivityHelper, | ||||
|                             internalBrowser, | ||||
|                             articleViewer, | ||||
| @@ -1106,23 +1082,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|     } | ||||
|  | ||||
|     private fun handleOfflineActions() { | ||||
|         fun doAndReportOnFail(success: Boolean, action: ActionEntity) { | ||||
|         fun doAndReportOnFail(success: Boolean, action: ACTION) { | ||||
|             if (success) { | ||||
|                 thread { | ||||
|                     db.actionsDao().delete(action) | ||||
|                     repository.deleteDBAction(action) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             val actions = db.actionsDao().actions() | ||||
|             val actions = repository.getDBActions() | ||||
|  | ||||
|             actions.forEach { action -> | ||||
|                 when { | ||||
|                     action.read -> doAndReportOnFail(repository.markAsReadById(action.articleId.toInt()), action) | ||||
|                     action.unread -> doAndReportOnFail(repository.unmarkAsReadById(action.articleId.toInt()), action) | ||||
|                     action.starred -> doAndReportOnFail(repository.starrById(action.articleId.toInt()), action) | ||||
|                     action.unstarred -> doAndReportOnFail(repository.unstarrById(action.articleId.toInt()), action) | ||||
|                     action.read -> doAndReportOnFail(repository.markAsReadById(action.articleid.toInt()), action) | ||||
|                     action.unread -> doAndReportOnFail(repository.unmarkAsReadById(action.articleid.toInt()), action) | ||||
|                     action.starred -> doAndReportOnFail(repository.starrById(action.articleid.toInt()), action) | ||||
|                     action.unstarred -> doAndReportOnFail(repository.unstarrById(action.articleid.toInt()), action) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -11,13 +11,14 @@ import android.widget.Toast | ||||
| import androidx.lifecycle.DefaultLifecycleObserver | ||||
| import androidx.lifecycle.LifecycleOwner | ||||
| import androidx.lifecycle.ProcessLifecycleOwner | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import androidx.multidex.MultiDexApplication | ||||
| import androidx.preference.PreferenceManager | ||||
| import bou.amine.apps.readerforselfossv2.DI.networkModule | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.Config | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth | ||||
| import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel | ||||
| import bou.amine.apps.readerforselfossv2.dao.DriverFactory | ||||
| import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| @@ -26,18 +27,20 @@ import com.github.ln_12.library.ConnectivityStatus | ||||
| import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader | ||||
| import com.mikepenz.materialdrawer.util.DrawerImageLoader | ||||
| import com.russhwolf.settings.Settings | ||||
| import io.github.aakira.napier.DebugAntilog | ||||
| import io.github.aakira.napier.Napier | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import io.github.aakira.napier.DebugAntilog | ||||
| import io.github.aakira.napier.Napier | ||||
| import org.kodein.di.* | ||||
|  | ||||
| class MyApp : MultiDexApplication(), DIAware { | ||||
|  | ||||
|     override val di by DI.lazy { | ||||
|         import(networkModule) | ||||
|         bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus) } | ||||
|         bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } | ||||
|         bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } | ||||
|         bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus, instance()) } | ||||
|         bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } | ||||
|         bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } | ||||
|     } | ||||
| @@ -45,6 +48,7 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|     private val repository: Repository by instance() | ||||
|     private val viewModel: AppViewModel by instance() | ||||
|     private val connectivityStatus: ConnectivityStatus by instance() | ||||
|     private val driverFactory: DriverFactory by instance() | ||||
|     private lateinit var config: Config | ||||
|     private lateinit var settings : Settings | ||||
|  | ||||
| @@ -73,6 +77,7 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|                 ).show() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private fun handleNotificationChannels() { | ||||
|   | ||||
| @@ -8,20 +8,14 @@ import android.view.MenuItem | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.room.Room | ||||
| import androidx.viewpager2.adapter.FragmentStateAdapter | ||||
| import androidx.viewpager2.widget.ViewPager2 | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ActivityReaderBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.fragments.ArticleFragment | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.Toppings | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.toggleStar | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import com.ftinc.scoop.Scoop | ||||
| import com.russhwolf.settings.Settings | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @@ -39,7 +33,6 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|     private lateinit var toolbarMenu: Menu | ||||
|  | ||||
|     private lateinit var db: AppDatabase | ||||
|     private lateinit var binding: ActivityReaderBinding | ||||
|  | ||||
|     private var activeAlignment: Int = 1 | ||||
| @@ -47,7 +40,7 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|     private val ALIGN_LEFT = 2 | ||||
|  | ||||
|     override val di by closestDI() | ||||
|     private val repository : Repository by instance() | ||||
|     private val repository: Repository by instance() | ||||
|  | ||||
|     private fun showMenuItem(willAddToFavorite: Boolean) { | ||||
|         if (willAddToFavorite) { | ||||
| @@ -75,11 +68,6 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|         setContentView(view) | ||||
|  | ||||
|         db = Room.databaseBuilder( | ||||
|             applicationContext, | ||||
|             AppDatabase::class.java, "selfoss-database" | ||||
|         ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() | ||||
|  | ||||
|         val scoop = Scoop.getInstance() | ||||
|         scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar) | ||||
|         scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) | ||||
| @@ -111,11 +99,11 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|     private fun readItem(item: SelfossModel.Item) { | ||||
|         if (markOnScroll) { | ||||
|                 CoroutineScope(Dispatchers.IO).launch { | ||||
|                     repository.markAsRead(item) | ||||
|                     // TODO: Handle failure | ||||
|                 } | ||||
|             CoroutineScope(Dispatchers.IO).launch { | ||||
|                 repository.markAsRead(item) | ||||
|                 // TODO: Handle failure | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onSaveInstanceState(oldInstanceState: Bundle) { | ||||
| @@ -128,19 +116,22 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|         override fun getItemCount(): Int = allItems.size | ||||
|  | ||||
|         override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position]) | ||||
|         override fun createFragment(position: Int): Fragment = | ||||
|             ArticleFragment.newInstance(allItems[position]) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { | ||||
|         return when (keyCode) { | ||||
|             KeyEvent.KEYCODE_VOLUME_DOWN -> { | ||||
|                 val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment | ||||
|                 val currentFragment = | ||||
|                     supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment | ||||
|                 currentFragment.scrollDown() | ||||
|                 true | ||||
|             } | ||||
|             KeyEvent.KEYCODE_VOLUME_UP -> { | ||||
|                 val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment | ||||
|                 val currentFragment = | ||||
|                     supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment | ||||
|                 currentFragment.scrollUp() | ||||
|                 true | ||||
|             } | ||||
| @@ -169,19 +160,19 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|         alignmentMenu() | ||||
|  | ||||
|         binding.pager.registerOnPageChangeCallback( | ||||
|                 object : ViewPager2.OnPageChangeCallback() { | ||||
|             object : ViewPager2.OnPageChangeCallback() { | ||||
|  | ||||
|                     override fun onPageSelected(position: Int) { | ||||
|                         super.onPageSelected(position) | ||||
|                 override fun onPageSelected(position: Int) { | ||||
|                     super.onPageSelected(position) | ||||
|  | ||||
|                         if (allItems[position].starred) { | ||||
|                             canRemoveFromFavorite() | ||||
|                         } else { | ||||
|                             canFavorite() | ||||
|                         } | ||||
|                         readItem(allItems[position]) | ||||
|                     if (allItems[position].starred) { | ||||
|                         canRemoveFromFavorite() | ||||
|                     } else { | ||||
|                         canFavorite() | ||||
|                     } | ||||
|                     readItem(allItems[position]) | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         return true | ||||
| @@ -190,7 +181,7 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         fun afterSave() { | ||||
|             allItems[binding.pager.currentItem] = | ||||
|                     allItems[binding.pager.currentItem].toggleStar() | ||||
|                 allItems[binding.pager.currentItem].toggleStar() | ||||
|             canRemoveFromFavorite() | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBind | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.Toppings | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import com.ftinc.scoop.Scoop | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
|   | ||||
| @@ -10,14 +10,16 @@ import androidx.recyclerview.widget.RecyclerView | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.* | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.* | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||
| import bou.amine.apps.readerforselfossv2.utils.getThumbnail | ||||
| import com.amulyakhare.textdrawable.TextDrawable | ||||
| import com.amulyakhare.textdrawable.util.ColorGenerator | ||||
| import com.bumptech.glide.Glide | ||||
| @@ -31,7 +33,6 @@ import org.kodein.di.instance | ||||
| class ItemCardAdapter( | ||||
|     override val app: Activity, | ||||
|     override var items: ArrayList<SelfossModel.Item>, | ||||
|     override val db: AppDatabase, | ||||
|     private val helper: CustomTabActivityHelper, | ||||
|     private val internalBrowser: Boolean, | ||||
|     private val articleViewer: Boolean, | ||||
| @@ -58,7 +59,7 @@ class ItemCardAdapter( | ||||
|             val itm = items[position] | ||||
|  | ||||
|             binding.favButton.isSelected = itm.starred | ||||
|             binding.title.text = itm.getTitleDecoded() | ||||
|             binding.title.text = itm.title.getHtmlDecoded() | ||||
|  | ||||
|             binding.title.setOnTouchListener(LinkOnTouchListener()) | ||||
|  | ||||
| @@ -81,13 +82,13 @@ class ItemCardAdapter( | ||||
|             } | ||||
|  | ||||
|             if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||
|                 val color = generator.getColor(itm.getSourceTitle()) | ||||
|                 val color = generator.getColor(itm.title.getHtmlDecoded()) | ||||
|  | ||||
|                 val drawable = | ||||
|                         TextDrawable | ||||
|                                 .builder() | ||||
|                                 .round() | ||||
|                                 .build(itm.getSourceTitle().toTextDrawableString(c), color) | ||||
|                                 .build(itm.title.getHtmlDecoded().toTextDrawableString(), color) | ||||
|                 binding.sourceImage.setImageDrawable(drawable) | ||||
|             } else { | ||||
|                 c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.sourceImage) | ||||
| @@ -128,7 +129,7 @@ class ItemCardAdapter( | ||||
|  | ||||
|                 binding.shareBtn.setOnClickListener { | ||||
|                     val item = items[bindingAdapterPosition] | ||||
|                     c.shareLink(item.getLinkDecoded(), item.getTitleDecoded()) | ||||
|                     c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded()) | ||||
|                 } | ||||
|  | ||||
|                 binding.browserBtn.setOnClickListener { | ||||
|   | ||||
| @@ -7,14 +7,16 @@ import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.* | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.* | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||
| import bou.amine.apps.readerforselfossv2.utils.getThumbnail | ||||
| import com.amulyakhare.textdrawable.TextDrawable | ||||
| import com.amulyakhare.textdrawable.util.ColorGenerator | ||||
| import org.kodein.di.DI | ||||
| @@ -24,7 +26,6 @@ import org.kodein.di.instance | ||||
| class ItemListAdapter( | ||||
|     override val app: Activity, | ||||
|     override var items: ArrayList<SelfossModel.Item>, | ||||
|     override val db: AppDatabase, | ||||
|     private val helper: CustomTabActivityHelper, | ||||
|     private val internalBrowser: Boolean, | ||||
|     private val articleViewer: Boolean, | ||||
| @@ -47,7 +48,7 @@ class ItemListAdapter( | ||||
|         with(holder) { | ||||
|             val itm = items[position] | ||||
|  | ||||
|             binding.title.text = itm.getTitleDecoded() | ||||
|             binding.title.text = itm.title.getHtmlDecoded() | ||||
|  | ||||
|             binding.title.setOnTouchListener(LinkOnTouchListener()) | ||||
|  | ||||
| @@ -58,13 +59,13 @@ class ItemListAdapter( | ||||
|             if (itm.getThumbnail(repository.baseUrl).isEmpty()) { | ||||
|  | ||||
|                 if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||
|                     val color = generator.getColor(itm.getSourceTitle()) | ||||
|                     val color = generator.getColor(itm.title.getHtmlDecoded()) | ||||
|  | ||||
|                     val drawable = | ||||
|                             TextDrawable | ||||
|                                     .builder() | ||||
|                                     .round() | ||||
|                                     .build(itm.getSourceTitle().toTextDrawableString(c), color) | ||||
|                                     .build(itm.title.getHtmlDecoded().toTextDrawableString(), color) | ||||
|  | ||||
|                     binding.itemImage.setImageDrawable(drawable) | ||||
|                 } else { | ||||
|   | ||||
| @@ -5,11 +5,10 @@ import android.graphics.Color | ||||
| import android.widget.TextView | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.Config | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.ItemType | ||||
| import com.google.android.material.snackbar.Snackbar | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @@ -20,7 +19,6 @@ import org.kodein.di.DIAware | ||||
| abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware { | ||||
|     abstract var items: ArrayList<SelfossModel.Item> | ||||
|     abstract val repository: Repository | ||||
|     abstract val db: AppDatabase | ||||
|     abstract val app: Activity | ||||
|     abstract val appColors: AppColors | ||||
|     abstract val config: Config | ||||
|   | ||||
| @@ -10,13 +10,13 @@ import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getIcon | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded | ||||
| import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.Config | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||
| import com.amulyakhare.textdrawable.TextDrawable | ||||
| import com.amulyakhare.textdrawable.util.ColorGenerator | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @@ -49,19 +49,19 @@ class SourcesListAdapter( | ||||
|         config = Config() | ||||
|  | ||||
|         if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||
|             val color = generator.getColor(itm.getTitleDecoded()) | ||||
|             val color = generator.getColor(itm.title.getHtmlDecoded()) | ||||
|  | ||||
|             val drawable = | ||||
|                 TextDrawable | ||||
|                     .builder() | ||||
|                     .round() | ||||
|                     .build(itm.getTitleDecoded().toTextDrawableString(c), color) | ||||
|                     .build(itm.title.getHtmlDecoded().toTextDrawableString(), color) | ||||
|             binding.itemImage.setImageDrawable(drawable) | ||||
|         } else { | ||||
|             c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage) | ||||
|         } | ||||
|  | ||||
|         binding.sourceTitle.text = itm.getTitleDecoded() | ||||
|         binding.sourceTitle.text = itm.title.getHtmlDecoded() | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount(): Int = items.size | ||||
|   | ||||
| @@ -8,22 +8,17 @@ import android.os.Build | ||||
| import androidx.core.app.NotificationCompat | ||||
| import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT | ||||
| import androidx.core.app.NotificationCompat.PRIORITY_LOW | ||||
| import androidx.room.Room | ||||
| import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import bou.amine.apps.readerforselfossv2.android.MainActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.MyApp | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.model.preloadImages | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.Config | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible | ||||
| import bou.amine.apps.readerforselfossv2.dao.ACTION | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.ItemType | ||||
| import com.russhwolf.settings.Settings | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @@ -36,7 +31,6 @@ import kotlin.concurrent.schedule | ||||
| import kotlin.concurrent.thread | ||||
|  | ||||
| class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), DIAware { | ||||
|     lateinit var db: AppDatabase | ||||
|  | ||||
|     override val di by lazy { (applicationContext as MyApp).di } | ||||
|     private val repository : Repository by instance() | ||||
| @@ -63,43 +57,31 @@ override fun doWork(): Result { | ||||
|  | ||||
|             val notifyNewItems = settings.getBoolean("notify_new_items", false) | ||||
|  | ||||
|             db = Room.databaseBuilder( | ||||
|                 applicationContext, | ||||
|                 AppDatabase::class.java, "selfoss-database" | ||||
|             ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3) | ||||
|                 .addMigrations(MIGRATION_3_4).build() | ||||
|  | ||||
|             val actions = db.actionsDao().actions() | ||||
|             val actions: List<ACTION> = repository.getDBActions() | ||||
|  | ||||
|             actions.forEach { action -> | ||||
|                 when { | ||||
|                     action.read -> doAndReportOnFail( | ||||
|                         repository.markAsReadById(action.articleId.toInt()), | ||||
|                         repository.markAsReadById(action.articleid.toInt()), | ||||
|                         action | ||||
|                     ) | ||||
|                     action.unread -> doAndReportOnFail( | ||||
|                         repository.unmarkAsReadById(action.articleId.toInt()), | ||||
|                         repository.unmarkAsReadById(action.articleid.toInt()), | ||||
|                         action | ||||
|                     ) | ||||
|                     action.starred -> doAndReportOnFail( | ||||
|                         repository.starrById(action.articleId.toInt()), | ||||
|                         repository.starrById(action.articleid.toInt()), | ||||
|                         action | ||||
|                     ) | ||||
|                     action.unstarred -> doAndReportOnFail( | ||||
|                         repository.unstarrById(action.articleId.toInt()), | ||||
|                         repository.unstarrById(action.articleid.toInt()), | ||||
|                         action | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             launch { | ||||
|                 try { | ||||
|                     val newItems = repository.allItems(ItemType.UNREAD) | ||||
|                     handleNewItemsNotification(newItems, notifyNewItems, notificationManager) | ||||
|                     val readItems = repository.allItems(ItemType.ALL) | ||||
|                     val starredItems = repository.allItems(ItemType.STARRED) | ||||
|                     // TODO: save all to DB | ||||
|                 } catch (e: Throwable) {} | ||||
|                 handleNewItemsNotification(repository.tryToCacheItemsAndGetNewOnes(), notifyNewItems, notificationManager) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -154,11 +136,10 @@ override fun doWork(): Result { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun doAndReportOnFail(result: Boolean, action: ActionEntity) { | ||||
|         // TODO: Failures should be reported | ||||
|     private fun doAndReportOnFail(result: Boolean, action: ACTION) { | ||||
|         if (result) { | ||||
|             thread { | ||||
|                 db.actionsDao().delete(action) | ||||
|                 repository.deleteDBAction(action) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -19,24 +19,22 @@ import androidx.browser.customtabs.CustomTabsIntent | ||||
| import androidx.core.content.res.ResourcesCompat | ||||
| import androidx.core.widget.NestedScrollView | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.room.Room | ||||
| import bou.amine.apps.readerforselfossv2.android.ImageActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi | ||||
| import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.* | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 | ||||
| import bou.amine.apps.readerforselfossv2.android.themes.AppColors | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.* | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getImages | ||||
| import bou.amine.apps.readerforselfossv2.utils.getThumbnail | ||||
| import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| @@ -71,7 +69,6 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     private lateinit var allImages : ArrayList<String> | ||||
|     private lateinit var fab: FloatingActionButton | ||||
|     private lateinit var appColors: AppColors | ||||
|     private lateinit var db: AppDatabase | ||||
|     private lateinit var textAlignment: String | ||||
|     private lateinit var config: Config | ||||
|     private var _binding: FragmentArticleBinding? = null | ||||
| @@ -103,11 +100,6 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|         val pi: ParecelableItem = requireArguments().getParcelable(ARG_ITEMS)!! | ||||
|  | ||||
|         item = pi.toModel() | ||||
|  | ||||
|         db = Room.databaseBuilder( | ||||
|             requireContext(), | ||||
|             AppDatabase::class.java, "selfoss-database" | ||||
|         ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() | ||||
|     } | ||||
|  | ||||
|     override fun onCreateView( | ||||
| @@ -120,7 +112,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|  | ||||
|             url = item.getLinkDecoded() | ||||
|             contentText = item.content | ||||
|             contentTitle = item.getTitleDecoded() | ||||
|             contentTitle = item.title.getHtmlDecoded() | ||||
|             contentImage = item.getThumbnail(repository.baseUrl) | ||||
|             contentSource = item.sourceAndDateText(repository.dateUtils) | ||||
|             allImages = item.getImages() | ||||
| @@ -134,7 +126,6 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 typeface = try { | ||||
|                     ResourcesCompat.getFont(requireContext(), resId)!! | ||||
|                 } catch (e: java.lang.Exception) { | ||||
|                     // ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext()) | ||||
|                     // Just to be sure | ||||
|                     null | ||||
|                 } | ||||
|   | ||||
| @@ -1,43 +1,12 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.model | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import android.text.Html | ||||
| import android.webkit.URLUtil | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.getImages | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import org.jsoup.Jsoup | ||||
| import java.util.* | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Items extension methods | ||||
|  */ | ||||
| fun SelfossModel.Item.getIcon(baseUrl: String): String { | ||||
|     return constructUrl(baseUrl, "favicons", icon) | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Item.getThumbnail(baseUrl: String): String { | ||||
|     return constructUrl(baseUrl, "thumbnails", thumbnail) | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Item.getImages() : ArrayList<String> { | ||||
|     val allImages = ArrayList<String>() | ||||
|  | ||||
|     for ( image in Jsoup.parse(content).getElementsByTag("img")) { | ||||
|         val url = image.attr("src") | ||||
|         if (url.lowercase(Locale.US).contains(".jpg") || | ||||
|             url.lowercase(Locale.US).contains(".jpeg") || | ||||
|             url.lowercase(Locale.US).contains(".png") || | ||||
|             url.lowercase(Locale.US).contains(".webp")) | ||||
|         { | ||||
|             allImages.add(url) | ||||
|         } | ||||
|     } | ||||
|     return allImages | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Item.preloadImages(context: Context) : Boolean { | ||||
|     val imageUrls = this.getImages() | ||||
| @@ -60,66 +29,13 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean { | ||||
|     return true | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Item.getTitleDecoded(): String { | ||||
|     return Html.fromHtml(title).toString() | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Item.getSourceTitle(): String { | ||||
|     return Html.fromHtml(sourcetitle).toString() | ||||
| } | ||||
|  | ||||
| // TODO: maybe find a better way to handle these kind of urls | ||||
| fun SelfossModel.Item.getLinkDecoded(): String { | ||||
|     var stringUrl: String | ||||
|     stringUrl = | ||||
|         if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { | ||||
|             if (link.contains("&url=")) { | ||||
|                 link.substringAfter("&url=") | ||||
|             } else { | ||||
|                 this.link.replace("&", "&") | ||||
|             } | ||||
|         } else { | ||||
|             this.link.replace("&", "&") | ||||
| fun String.toTextDrawableString(): String { | ||||
|     val textDrawable = StringBuilder() | ||||
|     for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) { | ||||
|         try { | ||||
|             textDrawable.append(s[0]) | ||||
|         } catch (e: StringIndexOutOfBoundsException) { | ||||
|         } | ||||
|  | ||||
|     // handle :443 => https | ||||
|     if (stringUrl.contains(":443")) { | ||||
|         stringUrl = stringUrl.replace(":443", "").replace("http://", "https://") | ||||
|     } | ||||
|  | ||||
|     // handle url not starting with http | ||||
|     if (stringUrl.startsWith("//")) { | ||||
|         stringUrl = "http:$stringUrl" | ||||
|     } | ||||
|  | ||||
|     return stringUrl | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Sources extension methods | ||||
|  */ | ||||
|  | ||||
| fun SelfossModel.Source.getIcon(baseUrl: String): String { | ||||
|     return constructUrl(baseUrl, "favicons", icon) | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Source.getTitleDecoded(): String { | ||||
|     return Html.fromHtml(title).toString() | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Common methods | ||||
|  */ | ||||
| private fun constructUrl(baseUrl: String, path: String, file: String?): String { | ||||
|     return if (file == null || file == "null" || file.isEmpty()) { | ||||
|         "" | ||||
|     } else { | ||||
|         val baseUriBuilder = Uri.parse(baseUrl).buildUpon() | ||||
|         baseUriBuilder.appendPath(path).appendPath(file) | ||||
|  | ||||
|         baseUriBuilder.toString() | ||||
|     } | ||||
|     return textDrawable.toString() | ||||
| } | ||||
| @@ -1,10 +1,8 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.model | ||||
|  | ||||
| import android.os.Build | ||||
| import android.os.Parcel | ||||
| import android.os.Parcelable | ||||
| import androidx.annotation.RequiresApi | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import com.google.gson.annotations.SerializedName | ||||
|  | ||||
| fun SelfossModel.Item.toParcelable() : ParecelableItem = | ||||
|   | ||||
| @@ -1,28 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.room.Room | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 | ||||
| import bou.amine.apps.readerforselfossv2.dao.DeviceDatabase | ||||
|  | ||||
| class AndroidDeviceDatabase(applicationContext: Context): DeviceDatabase<AndroidItemEntity> { | ||||
|     var db: AppDatabase = Room.databaseBuilder( | ||||
|         applicationContext, | ||||
|         AppDatabase::class.java, "selfoss-database" | ||||
|     ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() | ||||
|  | ||||
|  | ||||
|     override suspend fun items(): List<AndroidItemEntity> = db.itemsDao().items() | ||||
|  | ||||
|     override suspend fun insertAllItems(vararg items: AndroidItemEntity) = db.itemsDao().insertAllItems(*items) | ||||
|  | ||||
|     override suspend fun deleteAllItems() = db.itemsDao().deleteAllItems() | ||||
|  | ||||
|     override suspend fun delete(item: AndroidItemEntity) = db.itemsDao().delete(item) | ||||
|  | ||||
|     override suspend fun updateItem(item: AndroidItemEntity) = db.itemsDao().updateItem(item) | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.service.DeviceDataBaseService | ||||
| import bou.amine.apps.readerforselfossv2.service.SearchService | ||||
|  | ||||
| class AndroidDeviceDatabaseService(db: AndroidDeviceDatabase, searchService: SearchService) : | ||||
|     DeviceDataBaseService<AndroidItemEntity>(db, searchService) { | ||||
|     override suspend fun updateDatabase() { | ||||
|         if (itemsCaching) { | ||||
|             if (items.isEmpty()) { | ||||
|                 getFromDB() | ||||
|             } | ||||
|             db.deleteAllItems() | ||||
|             db.insertAllItems(*(items.map { it.toEntity() }).toTypedArray()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun clearDBItems() { | ||||
|         db.deleteAllItems() | ||||
|     } | ||||
|  | ||||
|     override fun appendNewItems(newItems: List<SelfossModel.Item>) { | ||||
|         var oldItems = items | ||||
|         if (oldItems != newItems) { | ||||
|             oldItems = oldItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<SelfossModel.Item> | ||||
|             oldItems.addAll(newItems) | ||||
|             items = oldItems | ||||
|  | ||||
|             sortItems() | ||||
|             getFocusedItems() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getFromDB() { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.dao | ||||
|  | ||||
| import androidx.room.Dao | ||||
| import androidx.room.Delete | ||||
| import androidx.room.Insert | ||||
| import androidx.room.OnConflictStrategy | ||||
| import androidx.room.Query | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity | ||||
|  | ||||
| @Dao | ||||
| interface ActionsDao { | ||||
|     @Query("SELECT * FROM actions order by id asc") | ||||
|     suspend fun actions(): List<ActionEntity> | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     fun insertAllActions(vararg actions: ActionEntity) | ||||
|  | ||||
|     @Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1") | ||||
|     fun deleteReadActionForArticle(article_id: String) | ||||
|  | ||||
|     @Delete | ||||
|     fun delete(action: ActionEntity) | ||||
| } | ||||
| @@ -1,36 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.dao | ||||
|  | ||||
| import androidx.room.Delete | ||||
| import androidx.room.Dao | ||||
| import androidx.room.Insert | ||||
| import androidx.room.OnConflictStrategy | ||||
| import androidx.room.Query | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity | ||||
|  | ||||
| @Dao | ||||
| interface DrawerDataDao { | ||||
|     @Query("SELECT * FROM tags") | ||||
|     fun tags(): List<TagEntity> | ||||
|  | ||||
|     @Query("SELECT * FROM sources") | ||||
|     fun sources(): List<SourceEntity> | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     fun insertAllTags(vararg tags: TagEntity) | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     fun insertAllSources(vararg sources: SourceEntity) | ||||
|  | ||||
|     @Query("DELETE FROM tags") | ||||
|     fun deleteAllTags() | ||||
|  | ||||
|     @Query("DELETE FROM sources") | ||||
|     fun deleteAllSources() | ||||
|  | ||||
|     @Delete | ||||
|     fun deleteTag(tag: TagEntity) | ||||
|  | ||||
|     @Delete | ||||
|     fun deleteSource(source: SourceEntity) | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.dao | ||||
|  | ||||
| import androidx.room.Dao | ||||
| import androidx.room.Delete | ||||
| import androidx.room.Insert | ||||
| import androidx.room.OnConflictStrategy | ||||
| import androidx.room.Query | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity | ||||
| import androidx.room.Update | ||||
|  | ||||
|  | ||||
|  | ||||
| @Dao | ||||
| interface ItemsDao { | ||||
|     @Query("SELECT * FROM items order by id desc") | ||||
|     suspend fun items(): List<AndroidItemEntity> | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     suspend fun insertAllItems(vararg items: AndroidItemEntity) | ||||
|  | ||||
|     @Query("DELETE FROM items") | ||||
|     suspend fun deleteAllItems() | ||||
|  | ||||
|     @Delete | ||||
|     suspend fun delete(item: AndroidItemEntity) | ||||
|  | ||||
|     @Update | ||||
|     suspend fun updateItem(item: AndroidItemEntity) | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.database | ||||
|  | ||||
| import androidx.room.RoomDatabase | ||||
| import androidx.room.Database | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.dao.ActionsDao | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.dao.DrawerDataDao | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.dao.ItemsDao | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity | ||||
|  | ||||
| @Database(entities = [TagEntity::class, SourceEntity::class, AndroidItemEntity::class, ActionEntity::class], version = 4) | ||||
| abstract class AppDatabase : RoomDatabase() { | ||||
|     abstract fun drawerDataDao(): DrawerDataDao | ||||
|  | ||||
|     abstract fun itemsDao(): ItemsDao | ||||
|  | ||||
|     abstract fun actionsDao(): ActionsDao | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.entities | ||||
|  | ||||
| import androidx.room.ColumnInfo | ||||
| import androidx.room.Entity | ||||
| import androidx.room.PrimaryKey | ||||
|  | ||||
| @Entity(tableName = "actions") | ||||
| data class ActionEntity( | ||||
|     @ColumnInfo(name = "articleid") | ||||
|     val articleId: String, | ||||
|     @ColumnInfo(name = "read") | ||||
|     val read: Boolean, | ||||
|     @ColumnInfo(name = "unread") | ||||
|     val unread: Boolean, | ||||
|     @ColumnInfo(name = "starred") | ||||
|     var starred: Boolean, | ||||
|     @ColumnInfo(name = "unstarred") | ||||
|     var unstarred: Boolean | ||||
| ) { | ||||
|     @PrimaryKey(autoGenerate = true) | ||||
|     var id: Int = 0 | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.entities | ||||
|  | ||||
| import androidx.room.ColumnInfo | ||||
| import androidx.room.Entity | ||||
| import androidx.room.PrimaryKey | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
|  | ||||
| @Entity(tableName = "items") | ||||
| data class AndroidItemEntity( | ||||
|     @PrimaryKey | ||||
|     @ColumnInfo(name = "id") | ||||
|     val id: String, | ||||
|     @ColumnInfo(name = "datetime") | ||||
|     val datetime: String, | ||||
|     @ColumnInfo(name = "title") | ||||
|     val title: String, | ||||
|     @ColumnInfo(name = "content") | ||||
|     val content: String, | ||||
|     @ColumnInfo(name = "unread") | ||||
|     val unread: Boolean, | ||||
|     @ColumnInfo(name = "starred") | ||||
|     var starred: Boolean, | ||||
|     @ColumnInfo(name = "thumbnail") | ||||
|     val thumbnail: String?, | ||||
|     @ColumnInfo(name = "icon") | ||||
|     val icon: String?, | ||||
|     @ColumnInfo(name = "link") | ||||
|     val link: String, | ||||
|     @ColumnInfo(name = "sourcetitle") | ||||
|     val sourcetitle: String, | ||||
|     @ColumnInfo(name = "tags") | ||||
|     val tags: String | ||||
| ) | ||||
| @@ -1,33 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.entities | ||||
|  | ||||
| import androidx.room.ColumnInfo | ||||
| import androidx.room.Entity | ||||
| import androidx.room.PrimaryKey | ||||
|  | ||||
| @Entity(tableName = "tags") | ||||
| data class TagEntity( | ||||
|     @PrimaryKey | ||||
|     @ColumnInfo(name = "tag") | ||||
|     val tag: String, | ||||
|     @ColumnInfo(name = "color") | ||||
|     val color: String, | ||||
|     @ColumnInfo(name = "unread") | ||||
|     val unread: Int | ||||
| ) | ||||
|  | ||||
| @Entity(tableName = "sources") | ||||
| data class SourceEntity( | ||||
|     @PrimaryKey | ||||
|     @ColumnInfo(name = "id") | ||||
|     val id: String, | ||||
|     @ColumnInfo(name = "title") | ||||
|     val title: String, | ||||
|     @ColumnInfo(name = "tags") | ||||
|     val tags: String, | ||||
|     @ColumnInfo(name = "spout") | ||||
|     val spout: String, | ||||
|     @ColumnInfo(name = "error") | ||||
|     val error: String, | ||||
|     @ColumnInfo(name = "icon") | ||||
|     val icon: String | ||||
| ) | ||||
| @@ -1,34 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.persistence.migrations | ||||
|  | ||||
| import androidx.sqlite.db.SupportSQLiteDatabase | ||||
| import androidx.room.migration.Migration | ||||
|  | ||||
| val MIGRATION_1_2: Migration = object : Migration(1, 2) { | ||||
|     override fun migrate(database: SupportSQLiteDatabase) { | ||||
|         database.execSQL("CREATE TABLE IF NOT EXISTS `items` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))") | ||||
|     } | ||||
| } | ||||
|  | ||||
| val MIGRATION_2_3: Migration = object : Migration(2, 3) { | ||||
|     override fun migrate(database: SupportSQLiteDatabase) { | ||||
|         database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))") | ||||
|     } | ||||
| } | ||||
|  | ||||
| val MIGRATION_3_4: Migration = object : Migration(3, 4) { | ||||
|     override fun migrate(database: SupportSQLiteDatabase) { | ||||
|         // @see https://stackoverflow.com/questions/57392015/how-to-migrate-not-null-table-column-into-null-in-android-room-database | ||||
|         // Create the new table | ||||
|         database.execSQL("CREATE TABLE IF NOT EXISTS `itemstmp` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))") | ||||
|  | ||||
|         // Copy the data | ||||
|         database.execSQL( | ||||
|                 "INSERT INTO itemstmp (`id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags`) SELECT `id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags` FROM items") | ||||
|  | ||||
|         // Remove the old table | ||||
|         database.execSQL("DROP TABLE items") | ||||
|  | ||||
|         // Change the table name to the correct one | ||||
|         database.execSQL("ALTER TABLE itemstmp RENAME TO items") | ||||
|     } | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.utils | ||||
|  | ||||
| import android.content.Context | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.DateUtils | ||||
| import bou.amine.apps.readerforselfossv2.utils.parseRelativeDate | ||||
|  | ||||
| fun String.toTextDrawableString(c: Context): String { | ||||
|     val textDrawable = StringBuilder() | ||||
|     for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) { | ||||
|         try { | ||||
|             textDrawable.append(s[0]) | ||||
|         } catch (e: StringIndexOutOfBoundsException) { | ||||
|         } | ||||
|     } | ||||
|     return textDrawable.toString() | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Item.sourceAndDateText(dateUtils: DateUtils): String { | ||||
|     val formattedDate = parseRelativeDate(dateUtils) | ||||
|  | ||||
|     return getSourceTitle() + formattedDate | ||||
| } | ||||
|  | ||||
| fun SelfossModel.Item.toggleStar(): SelfossModel.Item { | ||||
|     this.starred = !this.starred | ||||
|     return this | ||||
| } | ||||
| @@ -18,9 +18,8 @@ import android.widget.Toast | ||||
| import androidx.browser.customtabs.CustomTabsIntent | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.ReaderActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getLinkDecoded | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrlOrNull | ||||
|  | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.utils | ||||
|  | ||||
| import android.content.res.Resources | ||||
|  | ||||
| val Int.toPx: Int | ||||
|     get() = (this * Resources.getSystem().displayMetrics.density).toInt() | ||||
|  | ||||
| val Int.toDp: Int | ||||
|     get() = (this / Resources.getSystem().displayMetrics.density).toInt() | ||||
| @@ -1,72 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.utils.persistence | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle | ||||
| import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity | ||||
| import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
|  | ||||
| fun TagEntity.toView(): SelfossModel.Tag = | ||||
|         SelfossModel.Tag( | ||||
|             this.tag, | ||||
|             this.color, | ||||
|             this.unread | ||||
|         ) | ||||
|  | ||||
| fun SourceEntity.toView(): SelfossModel.Source = | ||||
|         SelfossModel.Source( | ||||
|             this.id.toInt(), | ||||
|             this.title, | ||||
|             this.tags.split(","), | ||||
|             this.spout, | ||||
|             this.error, | ||||
|             this.icon | ||||
|         ) | ||||
|  | ||||
| fun SelfossModel.Source.toEntity(): SourceEntity = | ||||
|         SourceEntity( | ||||
|             this.id.toString(), | ||||
|             this.getTitleDecoded(), | ||||
|             this.tags.joinToString(","), | ||||
|             this.spout, | ||||
|             this.error, | ||||
|             this.icon.orEmpty() | ||||
|         ) | ||||
|  | ||||
| fun SelfossModel.Tag.toEntity(): TagEntity = | ||||
|         TagEntity( | ||||
|             this.tag, | ||||
|             this.color, | ||||
|             this.unread | ||||
|         ) | ||||
|  | ||||
| fun AndroidItemEntity.toView(): SelfossModel.Item = | ||||
|         SelfossModel.Item( | ||||
|             this.id.toInt(), | ||||
|             this.datetime, | ||||
|             this.title, | ||||
|             this.content, | ||||
|             this.unread, | ||||
|             this.starred, | ||||
|             this.thumbnail, | ||||
|             this.icon, | ||||
|             this.link, | ||||
|             this.sourcetitle, | ||||
|             this.tags.split(",") | ||||
|         ) | ||||
|  | ||||
| fun SelfossModel.Item.toEntity(): AndroidItemEntity = | ||||
|     AndroidItemEntity( | ||||
|         this.id.toString(), | ||||
|         this.datetime, | ||||
|         this.getTitleDecoded(), | ||||
|         this.content, | ||||
|         this.unread, | ||||
|         this.starred, | ||||
|         this.thumbnail, | ||||
|         this.icon, | ||||
|         this.link, | ||||
|         this.getSourceTitle(), | ||||
|         this.tags.joinToString(",") | ||||
|     ) | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">La barre sera affichée</string> | ||||
|     <string name="reader_static_bar_off">La barre sera affichée grâce au bouton</string> | ||||
|     <string name="remove_source">Supprimer la source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">A barra inferior mostrarase sempre</string> | ||||
|     <string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string> | ||||
|     <string name="remove_source">Eliminar fonte</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">底部栏将始终显示</string> | ||||
|     <string name="reader_static_bar_off">底部栏可以通过浮动按钮显示</string> | ||||
|     <string name="remove_source">删除源</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -168,4 +168,5 @@ | ||||
|     <string name="reader_static_bar_on">The bottom bar will always be displayed</string> | ||||
|     <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> | ||||
|     <string name="remove_source">Remove source</string> | ||||
|     <string name="cant_get_spouts_no_network">Can\'t get spouts list because of a network issue.</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -39,7 +39,8 @@ | ||||
|     <string name="addStringNoUrl">"Log in to add sources."</string> | ||||
|     <string name="cant_get_sources">"Can't get sources list."</string> | ||||
|     <string name="cant_create_source">"Can't create source."</string> | ||||
|     <string name="cant_get_spouts">"Can't get spouts list."</string> | ||||
|     <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string> | ||||
|     <string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> | ||||
|     <string name="form_not_complete">"The form is not complete"</string> | ||||
|     <string name="pref_header_links">"Links"</string> | ||||
|     <string name="issue_tracker_link">"Issue Tracker"</string> | ||||
|   | ||||
| @@ -6,10 +6,13 @@ buildscript { | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") | ||||
|         classpath("com.android.tools.build:gradle:7.2.1") | ||||
|         classpath("com.android.tools.build:gradle:7.2.2") | ||||
|  | ||||
|         // sonarquve | ||||
|         classpath("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513") | ||||
|  | ||||
|         // SqlDelight | ||||
|         classpath("com.squareup.sqldelight:gradle-plugin:1.5.3") | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,14 @@ | ||||
| object SqlDelight { | ||||
|     const val runtime = "com.squareup.sqldelight:runtime:1.5.3" | ||||
|     const val android = "com.squareup.sqldelight:android-driver:1.5.3" | ||||
|     const val native = "com.squareup.sqldelight:native-driver:1.5.3" | ||||
|  | ||||
| } | ||||
|  | ||||
| plugins { | ||||
|     kotlin("multiplatform") | ||||
|     id("com.android.library") | ||||
|     id("com.squareup.sqldelight") | ||||
|     kotlin("plugin.serialization") version "1.4.10" | ||||
| } | ||||
|  | ||||
| @@ -39,6 +47,9 @@ kotlin { | ||||
|  | ||||
|                 // Network information | ||||
|                 implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0") | ||||
|  | ||||
|                 // Sql | ||||
|                 implementation(SqlDelight.runtime) | ||||
|             } | ||||
|         } | ||||
|         val commonTest by getting { | ||||
| @@ -50,6 +61,9 @@ kotlin { | ||||
|         val androidMain by getting { | ||||
|             dependencies { | ||||
|                 implementation("io.ktor:ktor-client-android:2.0.1") | ||||
|  | ||||
|                 // Sql | ||||
|                 implementation(SqlDelight.android) | ||||
|             } | ||||
|         } | ||||
|         val androidTest by getting { | ||||
| @@ -66,6 +80,11 @@ kotlin { | ||||
|             iosX64Main.dependsOn(this) | ||||
|             iosArm64Main.dependsOn(this) | ||||
|             //iosSimulatorArm64Main.dependsOn(this) | ||||
|  | ||||
|             // Sql | ||||
|             dependencies { | ||||
|                 implementation(SqlDelight.native) | ||||
|             } | ||||
|         } | ||||
|         val iosX64Test by getting | ||||
|         val iosArm64Test by getting | ||||
| @@ -93,4 +112,12 @@ android { | ||||
|         sourceCompatibility = JavaVersion.VERSION_1_8 | ||||
|         targetCompatibility = JavaVersion.VERSION_1_8 | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| sqldelight { | ||||
|     database("ReaderForSelfossDB") { | ||||
|         packageName = "bou.amine.apps.readerforselfossv2.dao" | ||||
|         sourceFolders = listOf("sqldelight") | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| package bou.amine.apps.readerforselfossv2.dao | ||||
| import android.content.Context | ||||
| import com.squareup.sqldelight.android.AndroidSqliteDriver | ||||
| import com.squareup.sqldelight.db.SqlDriver | ||||
|  | ||||
| actual class DriverFactory(private val context: Context) { | ||||
|     actual fun createDriver(): SqlDriver { | ||||
|         return AndroidSqliteDriver(ReaderForSelfossDB.Schema, context, "ReaderForSelfossV2-android.db") | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,5 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.text.format.DateUtils | ||||
| import java.time.Instant | ||||
| import java.time.LocalDateTime | ||||
|   | ||||
| @@ -0,0 +1,51 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import android.net.Uri | ||||
| import android.text.Html | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import org.jsoup.Jsoup | ||||
| import java.util.* | ||||
| import kotlin.collections.ArrayList | ||||
|  | ||||
| actual fun String.getHtmlDecoded(): String { | ||||
|     return Html.fromHtml(this).toString() | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getIcon(baseUrl: String): String { | ||||
|     return constructUrl(baseUrl, "favicons", icon) | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String { | ||||
|     return constructUrl(baseUrl, "thumbnails", thumbnail) | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getImages(): ArrayList<String> { | ||||
|     val allImages = ArrayList<String>() | ||||
|  | ||||
|     for ( image in Jsoup.parse(content).getElementsByTag("img")) { | ||||
|         val url = image.attr("src") | ||||
|         if (url.lowercase(Locale.US).contains(".jpg") || | ||||
|             url.lowercase(Locale.US).contains(".jpeg") || | ||||
|             url.lowercase(Locale.US).contains(".png") || | ||||
|             url.lowercase(Locale.US).contains(".webp")) | ||||
|         { | ||||
|             allImages.add(url) | ||||
|         } | ||||
|     } | ||||
|     return allImages | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Source.getIcon(baseUrl: String): String { | ||||
|     return constructUrl(baseUrl, "favicons", icon) | ||||
| } | ||||
|  | ||||
| actual fun constructUrl(baseUrl: String, path: String, file: String?): String { | ||||
|     return if (file == null || file == "null" || file.isEmpty()) { | ||||
|         "" | ||||
|     } else { | ||||
|         val baseUriBuilder = Uri.parse(baseUrl).buildUpon() | ||||
|         baseUriBuilder.appendPath(path).appendPath(file) | ||||
|  | ||||
|         baseUriBuilder.toString() | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +1,7 @@ | ||||
| package bou.amine.apps.readerforselfossv2.dao | ||||
|  | ||||
| interface DeviceDatabase<ItemEntity> { | ||||
|     suspend fun items(): List<ItemEntity> | ||||
|     suspend fun insertAllItems(vararg items: ItemEntity) | ||||
|     suspend fun deleteAllItems() | ||||
|     suspend fun delete(item: ItemEntity) | ||||
|     suspend fun updateItem(item: ItemEntity) | ||||
| } | ||||
| import com.squareup.sqldelight.db.SqlDriver | ||||
|  | ||||
| expect class DriverFactory { | ||||
|     fun createDriver(): SqlDriver | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| package bou.amine.apps.readerforselfossv2.model | ||||
|  | ||||
| class NetworkUnavailableException : Exception() | ||||
| @@ -1,5 +1,7 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
| package bou.amine.apps.readerforselfossv2.model | ||||
| 
 | ||||
| import bou.amine.apps.readerforselfossv2.utils.DateUtils | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| class SelfossModel { | ||||
| @@ -9,11 +11,7 @@ class SelfossModel { | ||||
|         val tag: String, | ||||
|         val color: String, | ||||
|         val unread: Int | ||||
|     ) { | ||||
|         fun getTitleDecoded(): String { | ||||
|             return tag // TODO Html.fromHtml(tag).toString() | ||||
|         } | ||||
|     } | ||||
|     ) | ||||
| 
 | ||||
|     @Serializable | ||||
|     class SuccessResponse(val success: Boolean) { | ||||
| @@ -71,5 +69,40 @@ class SelfossModel { | ||||
|         val link: String, | ||||
|         val sourcetitle: String, | ||||
|         val tags: List<String> | ||||
|     ) | ||||
|     ) { | ||||
|         // TODO: maybe find a better way to handle these kind of urls | ||||
|         fun getLinkDecoded(): String { | ||||
|             var stringUrl: String | ||||
|             stringUrl = | ||||
|                 if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { | ||||
|                     if (link.contains("&url=")) { | ||||
|                         link.substringAfter("&url=") | ||||
|                     } else { | ||||
|                         this.link.replace("&", "&") | ||||
|                     } | ||||
|                 } else { | ||||
|                     this.link.replace("&", "&") | ||||
|                 } | ||||
| 
 | ||||
|             // handle :443 => https | ||||
|             if (stringUrl.contains(":443")) { | ||||
|                 stringUrl = stringUrl.replace(":443", "").replace("http://", "https://") | ||||
|             } | ||||
| 
 | ||||
|             // handle url not starting with http | ||||
|             if (stringUrl.startsWith("//")) { | ||||
|                 stringUrl = "http:$stringUrl" | ||||
|             } | ||||
| 
 | ||||
|             return stringUrl | ||||
|         } | ||||
| 
 | ||||
|         fun sourceAndDateText(dateUtils: DateUtils): String = | ||||
|             this.sourcetitle.getHtmlDecoded() + dateUtils.parseRelativeDate(this.datetime) | ||||
| 
 | ||||
|         fun toggleStar(): Item { | ||||
|             this.starred = !this.starred | ||||
|             return this | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +1,11 @@ | ||||
| package bou.amine.apps.readerforselfossv2.repository | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.dao.* | ||||
| import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.service.ApiDetailsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.DateUtils | ||||
| import bou.amine.apps.readerforselfossv2.utils.ItemType | ||||
| import bou.amine.apps.readerforselfossv2.utils.* | ||||
| import com.github.ln_12.library.ConnectivityStatus | ||||
| import com.russhwolf.settings.Settings | ||||
| import io.github.aakira.napier.Napier | ||||
| @@ -12,7 +13,7 @@ import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
|  | ||||
| class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService, val connectivityStatus: ConnectivityStatus) { | ||||
| class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService, private val connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) { | ||||
|     val settings = Settings() | ||||
|  | ||||
|     var items = ArrayList<SelfossModel.Item>() | ||||
| @@ -62,11 +63,18 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|                 null | ||||
|             ) | ||||
|         } else { | ||||
|             // TODO: Get items from the database | ||||
|             if (itemsCaching) { | ||||
|                 fetchedItems = getDBItems().filter { | ||||
|                     displayedItems == ItemType.ALL || | ||||
|                             (it.unread && displayedItems == ItemType.UNREAD) || | ||||
|                             (it.starred && displayedItems == ItemType.STARRED) | ||||
|                 }.map { it.toView() } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (fetchedItems != null) { | ||||
|             items = ArrayList(fetchedItems) | ||||
|             sortItems() | ||||
|         } | ||||
|         return items | ||||
|     } | ||||
| @@ -84,17 +92,16 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|                 searchFilter, | ||||
|                 null | ||||
|             ) | ||||
|         } else { | ||||
|             // TODO: Get items from the database | ||||
|         } | ||||
|         } // When using the db cache, we load everything the first time, so there should be nothing more to load. | ||||
|  | ||||
|         if (fetchedItems != null) { | ||||
|             appendItems(fetchedItems) | ||||
|             items.addAll(fetchedItems) | ||||
|             sortItems() | ||||
|         } | ||||
|         return items | ||||
|     } | ||||
|  | ||||
|     suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? { | ||||
|     suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item>? { | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.getItems( | ||||
|                 itemType.type, | ||||
| @@ -106,21 +113,11 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|                 null | ||||
|             ) | ||||
|         } else { | ||||
|             // TODO: Provide an error message | ||||
|             null | ||||
|             emptyList() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun appendItems(fetchedItems: List<SelfossModel.Item>) { | ||||
|         // TODO: Store in DB if enabled by user | ||||
|         val fetchedIDS = fetchedItems.map { it.id } | ||||
|         val tmpItems = ArrayList(items.filterNot { it.id in fetchedIDS }) | ||||
|         tmpItems.addAll(fetchedItems) | ||||
|         sortItems(tmpItems) | ||||
|         items = tmpItems | ||||
|     } | ||||
|  | ||||
|     private fun sortItems(items: ArrayList<SelfossModel.Item>) { | ||||
|     private fun sortItems() { | ||||
|         items.sortByDescending { dateUtils.parseDate(it.datetime) } | ||||
|     } | ||||
|  | ||||
| @@ -135,38 +132,37 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|                 success = true | ||||
|             } | ||||
|         } else { | ||||
|             // TODO: Compute badges from database | ||||
|             // TODO: do this differently, because it's not efficient | ||||
|             val dbItems = getDBItems() | ||||
|             badgeUnread = dbItems.filter { item -> item.unread }.size | ||||
|             badgeStarred = dbItems.filter { item -> item.starred }.size | ||||
|             badgeAll = items.size | ||||
|         } | ||||
|         return success | ||||
|     } | ||||
|  | ||||
|     suspend fun getTags(): List<SelfossModel.Tag>? { | ||||
|         // TODO: Store in DB | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.tags() | ||||
|         } else { | ||||
|             // TODO: Compute from database | ||||
|             null | ||||
|             getDBTags().map { it.toView() } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun getSpouts(): Map<String, SelfossModel.Spout>? { | ||||
|         // TODO: Store in DB | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.spouts() | ||||
|         } else { | ||||
|             // TODO: Compute from database | ||||
|             null | ||||
|             throw NetworkUnavailableException() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun getSources(): ArrayList<SelfossModel.Source>? { | ||||
|         // TODO: Store in DB | ||||
|  | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.sources() | ||||
|         } else { | ||||
|             // TODO: Compute from database | ||||
|             null | ||||
|             ArrayList(getDBSources().map { it.toView() }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -179,8 +175,14 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|         return success | ||||
|     } | ||||
|  | ||||
|     suspend fun markAsReadById(id: Int): Boolean = | ||||
|         isNetworkAvailable() && api.markAsRead(id.toString())?.isSuccess == true | ||||
|     suspend fun markAsReadById(id: Int): Boolean { | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.markAsRead(id.toString())?.isSuccess == true | ||||
|         } else { | ||||
|             insertDBAction(id.toString(), read = true) | ||||
|             true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { | ||||
| @@ -192,8 +194,14 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|         return success | ||||
|     } | ||||
|  | ||||
|     suspend fun unmarkAsReadById(id: Int): Boolean = | ||||
|         isNetworkAvailable() && api.unmarkAsRead(id.toString())?.isSuccess == true | ||||
|     suspend fun unmarkAsReadById(id: Int): Boolean { | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.unmarkAsRead(id.toString())?.isSuccess == true | ||||
|         } else { | ||||
|             insertDBAction(id.toString(), unread = true) | ||||
|             true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun starr(item: SelfossModel.Item): Boolean { | ||||
|         val success = starrById(item.id) | ||||
| @@ -204,8 +212,14 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|         return success | ||||
|     } | ||||
|  | ||||
|     suspend fun starrById(id: Int): Boolean = | ||||
|         isNetworkAvailable() && api.starr(id.toString())?.isSuccess == true | ||||
|     suspend fun starrById(id: Int): Boolean { | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.starr(id.toString())?.isSuccess == true | ||||
|         } else { | ||||
|             insertDBAction(id.toString(), starred = true) | ||||
|             true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun unstarr(item: SelfossModel.Item): Boolean { | ||||
|         val success = unstarrById(item.id) | ||||
| @@ -216,9 +230,14 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|         return success | ||||
|     } | ||||
|  | ||||
|     suspend fun unstarrById(id: Int): Boolean = | ||||
|         isNetworkAvailable() && api.unstarr(id.toString())?.isSuccess == true | ||||
|  | ||||
|     suspend fun unstarrById(id: Int): Boolean { | ||||
|         return if (isNetworkAvailable()) { | ||||
|             api.unstarr(id.toString())?.isSuccess == true | ||||
|         } else { | ||||
|             insertDBAction(id.toString(), starred = true) | ||||
|             true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { | ||||
|         var success = false | ||||
| @@ -233,35 +252,47 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|     } | ||||
|  | ||||
|     private fun markAsReadLocally(item: SelfossModel.Item) { | ||||
|         // TODO: Mark also in the database | ||||
|         if (item.unread) { | ||||
|             item.unread = false | ||||
|             badgeUnread -= 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             updateDBItem(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun unmarkAsReadLocally(item: SelfossModel.Item) { | ||||
|         // TODO: Mark also in the database | ||||
|         if (!item.unread) { | ||||
|             item.unread = true | ||||
|             badgeUnread += 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             updateDBItem(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun starrLocally(item: SelfossModel.Item) { | ||||
|         // TODO: Mark also in the database | ||||
|         if (!item.starred) { | ||||
|             item.starred = true | ||||
|             badgeStarred += 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             updateDBItem(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun unstarrLocally(item: SelfossModel.Item) { | ||||
|         // TODO: Mark also in the database | ||||
|         if (item.starred) { | ||||
|             item.starred = false | ||||
|             badgeStarred -= 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             updateDBItem(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun createSource( | ||||
| @@ -287,7 +318,6 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|     } | ||||
|  | ||||
|     suspend fun deleteSource(id: Int): Boolean { | ||||
|         // TODO: Store in DB | ||||
|         var success = false | ||||
|         if (isNetworkAvailable()) { | ||||
|             val response = api.deleteSource(id) | ||||
| @@ -342,5 +372,61 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails | ||||
|  | ||||
|     fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride | ||||
|  | ||||
|     // TODO: Handle offline actions | ||||
|     fun getDBActions(): List<ACTION> = | ||||
|         db.actionsQueries.actions().executeAsList() | ||||
|  | ||||
|     fun deleteDBAction(action: ACTION) = | ||||
|         db.actionsQueries.deleteAction(action.id) | ||||
|  | ||||
|     fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList() | ||||
|  | ||||
|     fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList() | ||||
|  | ||||
|     fun resetDBTagsWithData(tagEntities: List<SelfossModel.Tag>) { | ||||
|         db.tagsQueries.deleteAllTags() | ||||
|  | ||||
|         db.tagsQueries.transaction { | ||||
|             tagEntities.forEach { tag -> | ||||
|                 db.tagsQueries.insertTag(tag.toEntity()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun resetDBSourcesWithData(sources: List<SelfossModel.Source>) { | ||||
|         db.sourcesQueries.deleteAllSources() | ||||
|  | ||||
|         db.sourcesQueries.transaction { | ||||
|             sources.forEach { source -> | ||||
|                 db.sourcesQueries.insertSource(source.toEntity()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun insertDBItems(items: List<SelfossModel.Item>) { | ||||
|         db.itemsQueries.transaction { | ||||
|             items.forEach { item -> | ||||
|                 db.itemsQueries.insertItem(item.toEntity()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getDBItems(): List<ITEM> = db.itemsQueries.items().executeAsList() | ||||
|  | ||||
|     private fun insertDBAction(articleid: String, read: Boolean = false, unread: Boolean = false, starred: Boolean = false, unstarred: Boolean = false) = | ||||
|         db.actionsQueries.insertAction(articleid, read, unread, starred, unstarred) | ||||
|  | ||||
|     private fun updateDBItem(item: SelfossModel.Item) = | ||||
|         db.itemsQueries.updateItem(item.datetime, item.title.getHtmlDecoded(), item.content, item.unread, item.starred, item.thumbnail, item.icon, item.link, item.sourcetitle, item.tags.joinToString(","), item.id.toString()) | ||||
|  | ||||
|  | ||||
|     suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item>? { | ||||
|         try { | ||||
|             val newItems = getMaxItemsForBackground(ItemType.UNREAD) | ||||
|             val allItems = getMaxItemsForBackground(ItemType.ALL) | ||||
|             val starredItems = getMaxItemsForBackground(ItemType.STARRED) | ||||
|             insertDBItems(newItems.orEmpty() + allItems.orEmpty() + starredItems.orEmpty()) | ||||
|             return newItems | ||||
|         } catch (e: Throwable) {} | ||||
|         return emptyList() | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.service.ApiDetailsService | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.call.* | ||||
|   | ||||
| @@ -1,34 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.service | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.dao.DeviceDatabase | ||||
| import bou.amine.apps.readerforselfossv2.utils.parseDate | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
|  | ||||
| abstract class DeviceDataBaseService<ItemEntity>(val db: DeviceDatabase<ItemEntity>, private val searchService: SearchService) { | ||||
|     var itemsCaching = false | ||||
|     var items: ArrayList<SelfossModel.Item> = arrayListOf() | ||||
|         get() { | ||||
|             return ArrayList(field) | ||||
|         } | ||||
|         set(value) { | ||||
|             field = ArrayList(value) | ||||
|         } | ||||
|  | ||||
|     abstract suspend fun updateDatabase() | ||||
|     abstract suspend fun clearDBItems() | ||||
|     abstract fun appendNewItems(items: List<SelfossModel.Item>) | ||||
|     abstract fun getFromDB() | ||||
|  | ||||
|     fun sortItems() { | ||||
|         val tmpItems = ArrayList(items.sortedByDescending { it.parseDate(searchService.dateUtils) }) | ||||
|         items = tmpItems | ||||
|     } | ||||
|  | ||||
|     // This filtered items from items val. Do not use | ||||
|     fun getFocusedItems() {} | ||||
|     fun computeBadges() { | ||||
|         searchService.badgeUnread = items.filter { item -> item.unread }.size | ||||
|         searchService.badgeStarred = items.filter { item -> item.starred }.size | ||||
|         searchService.badgeAll = items.size | ||||
|     } | ||||
| } | ||||
| @@ -1,14 +1,11 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
|  | ||||
|  | ||||
| fun SelfossModel.Item.parseDate(dateUtils: DateUtils): Long = | ||||
|     dateUtils.parseDate(this.datetime) | ||||
|  | ||||
| fun SelfossModel.Item.parseRelativeDate(dateUtils: DateUtils): String = | ||||
|     dateUtils.parseRelativeDate(this.datetime) | ||||
|  | ||||
| expect class DateUtils(apiMajorVersion: Int) { | ||||
|     fun parseDate(dateString: String): Long | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,70 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.dao.ITEM | ||||
| import bou.amine.apps.readerforselfossv2.dao.SOURCE | ||||
| import bou.amine.apps.readerforselfossv2.dao.TAG | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
|  | ||||
| fun TAG.toView(): SelfossModel.Tag = | ||||
|     SelfossModel.Tag( | ||||
|         this.name, | ||||
|         this.color, | ||||
|         this.unread.toInt() | ||||
|     ) | ||||
|  | ||||
| fun SOURCE.toView(): SelfossModel.Source = | ||||
|     SelfossModel.Source( | ||||
|         this.id.toInt(), | ||||
|         this.title, | ||||
|         this.tags.split(","), | ||||
|         this.spout, | ||||
|         this.error, | ||||
|         this.icon | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Source.toEntity(): SOURCE = | ||||
|     SOURCE( | ||||
|         this.id.toString(), | ||||
|         this.title.getHtmlDecoded(), | ||||
|         this.tags.joinToString(","), | ||||
|         this.spout, | ||||
|         this.error, | ||||
|         this.icon.orEmpty() | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Tag.toEntity(): TAG = | ||||
|     TAG( | ||||
|         this.tag, | ||||
|         this.color, | ||||
|         this.unread.toLong() | ||||
|     ) | ||||
|  | ||||
| fun ITEM.toView(): SelfossModel.Item = | ||||
|     SelfossModel.Item( | ||||
|         this.id.toInt(), | ||||
|         this.datetime, | ||||
|         this.title, | ||||
|         this.content, | ||||
|         this.unread, | ||||
|         this.starred, | ||||
|         this.thumbnail, | ||||
|         this.icon, | ||||
|         this.link, | ||||
|         this.sourcetitle, | ||||
|         this.tags.split(",") | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Item.toEntity(): ITEM = | ||||
|     ITEM( | ||||
|         this.id.toString(), | ||||
|         this.datetime, | ||||
|         this.title.getHtmlDecoded(), | ||||
|         this.content, | ||||
|         this.unread, | ||||
|         this.starred, | ||||
|         this.thumbnail, | ||||
|         this.icon, | ||||
|         this.link, | ||||
|         this.title.getHtmlDecoded(), | ||||
|         this.tags.joinToString(",") | ||||
|     ) | ||||
| @@ -0,0 +1,15 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
|  | ||||
| expect fun String.getHtmlDecoded(): String | ||||
|  | ||||
| expect fun SelfossModel.Item.getIcon(baseUrl: String): String | ||||
|  | ||||
| expect fun SelfossModel.Item.getThumbnail(baseUrl: String): String | ||||
|  | ||||
| expect fun SelfossModel.Item.getImages(): ArrayList<String> | ||||
|  | ||||
| expect fun SelfossModel.Source.getIcon(baseUrl: String): String | ||||
|  | ||||
| expect fun constructUrl(baseUrl: String, path: String, file: String?): String | ||||
| @@ -0,0 +1,18 @@ | ||||
| CREATE TABLE `ACTION` ( | ||||
|     `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | ||||
|     `articleid` TEXT NOT NULL, | ||||
|     `read` INTEGER AS Boolean DEFAULT 0 NOT NULL, | ||||
|     `unread` INTEGER AS Boolean DEFAULT 0 NOT NULL, | ||||
|     `starred` INTEGER AS Boolean DEFAULT 0 NOT NULL, | ||||
|     `unstarred` INTEGER AS Boolean DEFAULT 0 NOT NULL | ||||
| ); | ||||
|  | ||||
| actions: | ||||
| SELECT * | ||||
| FROM `ACTION`; | ||||
|  | ||||
| insertAction: | ||||
| INSERT OR REPLACE INTO `ACTION` (`articleid`, `read`, `unread`, `starred`, `unstarred`) VALUES (?, ?, ?, ?, ?); | ||||
|  | ||||
| deleteAction: | ||||
| DELETE FROM `ACTION` WHERE id = ?; | ||||
| @@ -0,0 +1,30 @@ | ||||
| CREATE TABLE ITEM ( | ||||
|     `id` TEXT NOT NULL, | ||||
|     `datetime` TEXT NOT NULL, | ||||
|     `title` TEXT NOT NULL, | ||||
|     `content` TEXT NOT NULL, | ||||
|     `unread` INTEGER AS Boolean DEFAULT 0 NOT NULL, | ||||
|     `starred` INTEGER AS Boolean DEFAULT 0 NOT NULL, | ||||
|     `thumbnail` TEXT, | ||||
|     `icon` TEXT, | ||||
|     `link` TEXT NOT NULL, | ||||
|     `sourcetitle` TEXT NOT NULL, | ||||
|     `tags` TEXT NOT NULL, | ||||
|     PRIMARY KEY(`id`) | ||||
| ); | ||||
|  | ||||
| CREATE INDEX item_title ON ITEM(`title`); | ||||
| CREATE INDEX item_source ON ITEM(`sourcetitle`); | ||||
|  | ||||
|  | ||||
| items: | ||||
| SELECT * FROM ITEM ORDER BY `id` DESC; | ||||
|  | ||||
| insertItem: | ||||
| INSERT OR REPLACE INTO ITEM VALUES ?; | ||||
|  | ||||
| deleteItem: | ||||
| DELETE FROM ITEM WHERE `id` = ?; | ||||
|  | ||||
| updateItem: | ||||
| UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ? WHERE `id` = ?; | ||||
| @@ -0,0 +1,21 @@ | ||||
| CREATE TABLE SOURCE ( | ||||
|     `id` TEXT NOT NULL, | ||||
|     `title` TEXT NOT NULL, | ||||
|     `tags` TEXT NOT NULL, | ||||
|     `spout` TEXT NOT NULL, | ||||
|     `error` TEXT NOT NULL, | ||||
|     `icon` TEXT NOT NULL, | ||||
|     PRIMARY KEY(`id`) | ||||
| ); | ||||
|  | ||||
| sources: | ||||
| SELECT * FROM SOURCE; | ||||
|  | ||||
| insertSource: | ||||
| INSERT OR REPLACE INTO SOURCE VALUES ?; | ||||
|  | ||||
| deleteAllSources: | ||||
| DELETE FROM SOURCE; | ||||
|  | ||||
| deleteSource: | ||||
| DELETE FROM SOURCE WHERE `id` = ?; | ||||
| @@ -0,0 +1,20 @@ | ||||
| CREATE TABLE TAG ( | ||||
|     `name` TEXT NOT NULL, | ||||
|     `color` TEXT NOT NULL, | ||||
|     `unread` INTEGER NOT NULL, | ||||
|     PRIMARY KEY(`name`) | ||||
| ); | ||||
|  | ||||
| CREATE INDEX tag_name ON TAG(`name`); | ||||
|  | ||||
| tags: | ||||
| SELECT * FROM TAG; | ||||
|  | ||||
| insertTag: | ||||
| INSERT OR REPLACE INTO TAG VALUES ?; | ||||
|  | ||||
| deleteAllTags: | ||||
| DELETE FROM TAG; | ||||
|  | ||||
| deleteTag: | ||||
| DELETE FROM TAG WHERE `name` = ? AND `color` = ?; | ||||
| @@ -0,0 +1,10 @@ | ||||
| package bou.amine.apps.readerforselfossv2.dao | ||||
|  | ||||
| import com.squareup.sqldelight.db.SqlDriver | ||||
| import com.squareup.sqldelight.drivers.native.NativeSqliteDriver | ||||
|  | ||||
| actual class DriverFactory { | ||||
|     actual fun createDriver(): SqlDriver { | ||||
|         return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| actual class DateUtils actual constructor(apiMajorVersion: Int) { | ||||
|     actual fun parseDate(dateString: String): Long { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
|  | ||||
|     actual fun parseRelativeDate(dateString: String): String { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
|  | ||||
| actual fun String.getHtmlDecoded(): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getIcon(baseUrl: String): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getImages(): ArrayList<String> { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Source.getIcon(baseUrl: String): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun constructUrl(baseUrl: String, path: String, file: String?): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| package bou.amine.apps.readerforselfossv2.dao | ||||
|  | ||||
| import com.squareup.sqldelight.db.SqlDriver | ||||
| import com.squareup.sqldelight.drivers.native.NativeSqliteDriver | ||||
|  | ||||
| actual class DriverFactory { | ||||
|     actual fun createDriver(): SqlDriver { | ||||
|         return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| actual class DateUtils actual constructor(apiMajorVersion: Int) { | ||||
|     actual fun parseDate(dateString: String): Long { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
|  | ||||
|     actual fun parseRelativeDate(dateString: String): String { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
|  | ||||
| actual fun String.getHtmlDecoded(): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getIcon(baseUrl: String): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Item.getImages(): ArrayList<String> { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun SelfossModel.Source.getIcon(baseUrl: String): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
|  | ||||
| actual fun constructUrl(baseUrl: String, path: String, file: String?): String { | ||||
|     TODO("Not yet implemented") | ||||
| } | ||||
		Reference in New Issue
	
	Block a user