diff --git a/app/build.gradle b/app/build.gradle index faa3001a..4f63e86d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,6 +50,9 @@ android { versionName "1.0.beta" + versionCode //TODO add resConfigs("en", "fr", "ja",...) ? + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } testInstrumentationRunner "org.pixeldroid.app.testUtility.TestRunner" testInstrumentationRunnerArguments clearPackageData: 'true' diff --git a/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/6.json b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/6.json new file mode 100644 index 00000000..3e54bb67 --- /dev/null +++ b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/6.json @@ -0,0 +1,710 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "365491e03fadf81e596b11716640518c", + "entities": [ + { + "tableName": "instances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxStatusChars", + "columnName": "maxStatusChars", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxPhotoSize", + "columnName": "maxPhotoSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxVideoSize", + "columnName": "maxVideoSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "albumLimit", + "columnName": "albumLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoEnabled", + "columnName": "videoEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pixelfed", + "columnName": "pixelfed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "display_name", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar_static", + "columnName": "avatar_static", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_users_instance_uri", + "unique": false, + "columnNames": [ + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "instances", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instance_uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "homePosts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spoiler_text", + "columnName": "spoiler_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media_attachments", + "columnName": "media_attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogs_count", + "columnName": "reblogs_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favourites_count", + "columnName": "favourites_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replies_count", + "columnName": "replies_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_id", + "columnName": "in_reply_to_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_account", + "columnName": "in_reply_to_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblog", + "columnName": "reblog", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_homePosts_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "publicPosts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spoiler_text", + "columnName": "spoiler_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media_attachments", + "columnName": "media_attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogs_count", + "columnName": "reblogs_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favourites_count", + "columnName": "favourites_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replies_count", + "columnName": "replies_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_id", + "columnName": "in_reply_to_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_account", + "columnName": "in_reply_to_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblog", + "columnName": "reblog", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_publicPosts_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_notifications_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + } + ], + "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, '365491e03fadf81e596b11716640518c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/7.json b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/7.json new file mode 100644 index 00000000..1ad440d1 --- /dev/null +++ b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/7.json @@ -0,0 +1,781 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "cd0310b10eff0e3e961b3cd7a8172b81", + "entities": [ + { + "tableName": "instances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxStatusChars", + "columnName": "maxStatusChars", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxPhotoSize", + "columnName": "maxPhotoSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxVideoSize", + "columnName": "maxVideoSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "albumLimit", + "columnName": "albumLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoEnabled", + "columnName": "videoEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pixelfed", + "columnName": "pixelfed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "display_name", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar_static", + "columnName": "avatar_static", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_users_instance_uri", + "unique": false, + "columnNames": [ + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "instances", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instance_uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "homePosts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spoiler_text", + "columnName": "spoiler_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media_attachments", + "columnName": "media_attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogs_count", + "columnName": "reblogs_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favourites_count", + "columnName": "favourites_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replies_count", + "columnName": "replies_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_id", + "columnName": "in_reply_to_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_account", + "columnName": "in_reply_to_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblog", + "columnName": "reblog", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_homePosts_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "publicPosts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spoiler_text", + "columnName": "spoiler_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media_attachments", + "columnName": "media_attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogs_count", + "columnName": "reblogs_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favourites_count", + "columnName": "favourites_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replies_count", + "columnName": "replies_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_id", + "columnName": "in_reply_to_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_account", + "columnName": "in_reply_to_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblog", + "columnName": "reblog", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_publicPosts_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_notifications_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "tabsChecked", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `tab` TEXT NOT NULL, `checked` INTEGER NOT NULL, PRIMARY KEY(`index`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tab", + "columnName": "tab", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checked", + "columnName": "checked", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "index", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_tabsChecked_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tabsChecked_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + } + ], + "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, 'cd0310b10eff0e3e961b3cd7a8172b81')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt index d1cfa6ac..9f474460 100644 --- a/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt @@ -9,6 +9,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.util.Log +import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageView @@ -23,6 +24,9 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.GravityCompat import androidx.core.view.children import androidx.core.view.isVisible +import androidx.core.view.children +import androidx.core.view.get +import androidx.core.view.isVisible import androidx.core.view.marginEnd import androidx.core.view.marginTop import androidx.fragment.app.Fragment @@ -37,6 +41,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.bumptech.glide.Glide +import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.color.DynamicColors import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationView @@ -67,12 +72,15 @@ import org.pixeldroid.app.profile.ProfileActivity import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment import org.pixeldroid.app.settings.SettingsActivity import org.pixeldroid.app.utils.BaseActivity +import org.pixeldroid.app.utils.Tab import org.pixeldroid.app.utils.api.objects.Notification import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.updateUserInfoDb import org.pixeldroid.app.utils.hasInternet +import org.pixeldroid.app.utils.loadDefaultMenuTabs +import org.pixeldroid.app.utils.loadDbMenuTabs import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.USER_NOTIFICATION_TAG @@ -95,7 +103,6 @@ class MainActivity : BaseActivity() { private lateinit var binding: ActivityMainBinding - @OptIn(ExperimentalPagingApi::class) override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen().setOnExitAnimationListener { it.remove() @@ -121,24 +128,8 @@ class MainActivity : BaseActivity() { sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this) setupDrawer() - val tabs: List<() -> Fragment> = listOf( - { - PostFeedFragment() - .apply { - arguments = Bundle().apply { putBoolean("home", true) } - } - }, - { SearchDiscoverFragment() }, - { CameraFragment() }, - { NotificationsFragment() }, - { - PostFeedFragment() - .apply { - arguments = Bundle().apply { putBoolean("home", false) } - } - } - ) - setupTabs(tabs) + + setupTabs() val showNotification: Boolean = intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false) @@ -465,29 +456,90 @@ class MainActivity : BaseActivity() { } } - private fun setupTabs(tab_array: List<() -> Fragment>){ + @OptIn(ExperimentalPagingApi::class) + private fun setupTabs() { + val tabsCheckedDbEntry = with (db.userDao().getActiveUser()!!) { + db.tabsDao().getTabsChecked(user_id, instance_uri) + } + val pageIds = listOf(R.id.page_1, R.id.page_2, R.id.page_3, R.id.page_4, R.id.page_5) + + fun Tab.getFragment(): (() -> Fragment) { + return when (this) { + Tab.HOME_FEED -> { { + PostFeedFragment() + .apply { + arguments = Bundle().apply { putBoolean("home", true) } + } + } } + Tab.SEARCH_DISCOVER_FEED -> { { SearchDiscoverFragment() } } + Tab.CREATE_FEED -> { { CameraFragment() } } + Tab.NOTIFICATIONS_FEED -> { { NotificationsFragment() } } + Tab.PUBLIC_FEED -> { { + PostFeedFragment() + .apply { + arguments = Bundle().apply { putBoolean("home", false) } + } + } } + } + } + + val tabs = if (tabsCheckedDbEntry.isEmpty()) { + // Load default menu + loadDefaultMenuTabs(applicationContext, binding.root) + } else { + // Get current menu visibility and order from settings + val tabsChecked = loadDbMenuTabs(applicationContext, tabsCheckedDbEntry).filter { it.second }.map { it.first } + + val bottomNavigationMenu: Menu? = (binding.tabs as? NavigationBarView)?.menu?.apply { + clear() + } + ?: binding.navigation?.menu?.apply { + removeGroup(R.id.tabsId) + } + + tabsChecked.zip(pageIds).forEach { (tabId, pageId) -> + with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(baseContext))) { + val tabIcon = tabId.getDrawable(applicationContext) + if (tabIcon != null) { + this?.icon = tabIcon + } + } + } + + tabsChecked + } + + val tabArray: List<() -> Fragment> = tabs.map { it.getFragment() } binding.viewPager.reduceDragSensitivity() binding.viewPager.adapter = object : FragmentStateAdapter(this) { override fun createFragment(position: Int): Fragment { - return tab_array[position]() + return tabArray[position]() } override fun getItemCount(): Int { - return tab_array.size + return tabArray.size } } + val notificationId = tabs.zip(pageIds).find { + it.first == Tab.NOTIFICATIONS_FEED + }?.second + + fun doAtPageId(pageId: Int): Int { + if (notificationId != null && pageId == notificationId) { + setNotificationBadge(false) + } + return pageId + } + binding.viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { override fun onPageSelected(position: Int) { val selected = when(position){ - 0 -> R.id.page_1 - 1 -> R.id.page_2 - 2 -> R.id.page_3 - 3 -> { - setNotificationBadge(false) - R.id.page_4 - } - 4 -> R.id.page_5 + 0 -> doAtPageId(R.id.page_1) + 1 -> doAtPageId(R.id.page_2) + 2 -> doAtPageId(R.id.page_3) + 3 -> doAtPageId(R.id.page_4) + 4 -> doAtPageId(R.id.page_5) else -> null } if (selected != null) { diff --git a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt new file mode 100644 index 00000000..1de40d77 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt @@ -0,0 +1,142 @@ +package org.pixeldroid.app.settings + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.button.MaterialButton +import com.google.android.material.checkbox.MaterialCheckBox +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import org.pixeldroid.app.R +import org.pixeldroid.app.utils.db.AppDatabase +import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity +import javax.inject.Inject + +@AndroidEntryPoint +class ArrangeTabsFragment: DialogFragment() { + + @Inject + lateinit var db: AppDatabase + + private val model: ArrangeTabsViewModel by viewModels { ArrangeTabsViewModelFactory(requireContext(), db) } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + val inflater: LayoutInflater = requireActivity().layoutInflater + val dialogView: View = inflater.inflate(R.layout.layout_tabs_arrange, null) + + model.initTabsChecked(dialogView) + + val listFeed: RecyclerView = dialogView.findViewById(R.id.tabs) + val listAdapter = ListViewAdapter(model) + listFeed.adapter = listAdapter + listFeed.layoutManager = LinearLayoutManager(requireActivity()) + val callback = object: ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + source: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + listAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + // Do nothing, all items should remain in the list + } + } + val itemTouchHelper = ItemTouchHelper(callback) + itemTouchHelper.attachToRecyclerView(listFeed) + + val dialog = MaterialAlertDialogBuilder(requireContext()).apply { + setIcon(R.drawable.outline_bottom_navigation) + setTitle(R.string.arrange_tabs_summary) + setView(dialogView) + setNegativeButton(android.R.string.cancel) { _, _ -> } + setPositiveButton(android.R.string.ok) { _, _ -> + // Save values into preferences + val tabsChecked = listAdapter.model.uiState.value.tabsChecked.toList() + val tabsDbEntity = tabsChecked.mapIndexed { index, (tab, checked) -> with (db.userDao().getActiveUser()!!) { + TabsDatabaseEntity(index, user_id, instance_uri, tab.name, checked) + } } + lifecycleScope.launch { + db.tabsDao().clearAndRefill(tabsDbEntity, model.uiState.value.userId, model.uiState.value.instanceUri) + } + } + }.create() + + return dialog + } + + inner class ListViewAdapter(val model: ArrangeTabsViewModel): + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = FrameLayout.inflate(context, R.layout.layout_tab, null) + + // Make sure the layout occupies full width + view.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ) + + return object: RecyclerView.ViewHolder(view) {} + } + + @SuppressLint("ClickableViewAccessibility") + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val textView: MaterialButton = holder.itemView.findViewById(R.id.textView) + val checkBox: MaterialCheckBox = holder.itemView.findViewById(R.id.checkBox) + val dragHandle: ImageView = holder.itemView.findViewById(R.id.dragHandle) + + // Set content of each entry + textView.text = model.uiState.value.tabsChecked[position].first.toLanguageString(requireContext()) + checkBox.isChecked = model.uiState.value.tabsChecked[position].second + + // Also interact with checkbox when button is clicked + textView.setOnClickListener { + val isCheckedNew = !model.uiState.value.tabsChecked[position].second + model.tabsCheckReplace(position, Pair(model.uiState.value.tabsChecked[position].first, isCheckedNew)) + checkBox.isChecked = isCheckedNew + + // Disable OK button when no tab is selected or when strictly more than 5 tabs are selected + val maxItemCount = BottomNavigationView(requireContext()).maxItemCount // = 5 + (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = + with (model.uiState.value.tabsChecked.count { (_, v) -> v }) { this in 1..maxItemCount} + } + + // Also highlight button when checkbox is clicked + checkBox.setOnTouchListener { _, motionEvent -> + textView.dispatchTouchEvent(motionEvent) + } + + // Do not highlight the button when the drag handle is touched + dragHandle.setOnTouchListener { _, _ -> true } + } + + override fun getItemCount(): Int { + return model.uiState.value.tabsChecked.size + } + + fun onItemMove(from: Int, to: Int) { + val previous = model.tabsCheckedRemove(from) + model.tabsCheckedAdd(to, previous) + notifyItemMoved(from, to) + notifyItemChanged(to) // necessary to avoid checkBox issues + } + } +} diff --git a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt new file mode 100644 index 00000000..4e103a11 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt @@ -0,0 +1,114 @@ +package org.pixeldroid.app.settings + +import android.content.Context +import android.view.View +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.pixeldroid.app.utils.Tab +import org.pixeldroid.app.utils.db.AppDatabase +import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity +import org.pixeldroid.app.utils.loadDbMenuTabs +import org.pixeldroid.app.utils.loadDefaultMenuTabs + + +class ArrangeTabsViewModelFactory( + private val context: Context, private val db: AppDatabase +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(ArrangeTabsViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return ArrangeTabsViewModel(context, db) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} + +class ArrangeTabsViewModel( + private val fragmentContext: Context, + private val db: AppDatabase +): ViewModel() { + + private val _uiState = MutableStateFlow(ArrangeTabsUiState()) + val uiState: StateFlow = _uiState + + private var oldTabsChecked: MutableList> = mutableListOf() + + init { + initTabsDbEntities() + } + + private fun initTabsDbEntities() { + val user = db.userDao().getActiveUser()!! + _uiState.update { currentUiState -> + currentUiState.copy( + userId = user.user_id, + instanceUri = user.instance_uri, + ) + } + _uiState.update { currentUiState -> + currentUiState.copy( + tabsDbEntities = db.tabsDao().getTabsChecked(_uiState.value.userId, _uiState.value.instanceUri) + ) + } + } + + fun initTabsChecked(view: View) { + if (oldTabsChecked.isEmpty()) { + // Only load tabsChecked if the model has not been updated + _uiState.update { currentUiState -> + currentUiState.copy( + tabsChecked = if (_uiState.value.tabsDbEntities.isEmpty()) { + // Load default menu + val list = loadDefaultMenuTabs(fragmentContext, view) + list.zip(List(list.size){true}.toTypedArray()).toList() + } else { + // Get current menu visibility and order from settings + loadDbMenuTabs(fragmentContext, _uiState.value.tabsDbEntities).toList() + } + ) + } + } + } + + fun tabsCheckReplace(position: Int, pair: Pair) { + oldTabsChecked = _uiState.value.tabsChecked.toMutableList() + oldTabsChecked[position] = pair + _uiState.update { currentUiState -> + currentUiState.copy( + tabsChecked = oldTabsChecked.toList() + ) + } + } + + fun tabsCheckedRemove(position: Int): Pair { + oldTabsChecked = _uiState.value.tabsChecked.toMutableList() + val removedPair = oldTabsChecked.removeAt(position) + _uiState.update { currentUiState -> + currentUiState.copy( + tabsChecked = oldTabsChecked.toList() + ) + } + return removedPair + } + + fun tabsCheckedAdd(position: Int, pair: Pair) { + oldTabsChecked = _uiState.value.tabsChecked.toMutableList() + oldTabsChecked.add(position, pair) + _uiState.update { currentUiState -> + currentUiState.copy( + tabsChecked = oldTabsChecked.toList() + ) + } + } +} + +data class ArrangeTabsUiState( + val userId: String = "", + val instanceUri: String = "", + val tabsDbEntities: List = listOf(), + val tabsChecked: List> = listOf() +) \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/settings/LanguageSettingFragment.kt b/app/src/main/java/org/pixeldroid/app/settings/LanguageSettingFragment.kt new file mode 100644 index 00000000..e7200167 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/settings/LanguageSettingFragment.kt @@ -0,0 +1,56 @@ +package org.pixeldroid.app.settings + +import android.app.Dialog +import android.content.res.XmlResourceParser +import android.os.Bundle +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.pixeldroid.app.R + +class LanguageSettingFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val list: MutableList = mutableListOf() + // IDE doesn't find it, but compiling works apparently? + resources.getXml(R.xml._generated_res_locale_config).use { + var eventType = it.eventType + while (eventType != XmlResourceParser.END_DOCUMENT) { + when (eventType) { + XmlResourceParser.START_TAG -> { + if (it.name == "locale") { + list.add(it.getAttributeValue(0)) + } + } + } + eventType = it.next() + } + } + val locales = AppCompatDelegate.getApplicationLocales() + val checkedItem: Int = + if(locales.isEmpty) 0 + else { + // For some reason we get a bit inconsistent language tags. This normalises it for + // the currently used languages, but it might break in the future if we add some + val index = list.indexOf(locales.get(0)?.toLanguageTag()?.lowercase()?.replace('_', '-')) + // If found, we want to compensate for the first in the list being the default + if(index == -1) -1 + else index + 1 + } + + return MaterialAlertDialogBuilder(requireContext()).apply { + setIcon(R.drawable.translate_black_24dp) + setTitle(R.string.language) + setSingleChoiceItems((mutableListOf(getString(R.string.default_system)) + list.map { + val appLocale = LocaleListCompat.forLanguageTags(it) + appLocale.get(0)!!.getDisplayName(appLocale.get(0)!!) + }).toTypedArray(), checkedItem) { dialog, which -> + val languageTag = if(which in 1..list.size) list[which - 1] else null + dialog.dismiss() + AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag)) + } + setNegativeButton(android.R.string.ok) { _, _ -> } + }.create() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt index 93f4e8eb..ebcff40d 100644 --- a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt @@ -1,28 +1,27 @@ package org.pixeldroid.app.settings -import android.app.Dialog import android.content.Intent import android.content.SharedPreferences -import android.content.res.XmlResourceParser import android.os.Build import android.os.Bundle import androidx.activity.addCallback import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.LocaleListCompat import androidx.fragment.app.DialogFragment import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.pixeldroid.app.main.MainActivity +import dagger.hilt.android.AndroidEntryPoint import org.pixeldroid.app.R import org.pixeldroid.app.databinding.SettingsBinding -import org.pixeldroid.common.ThemedActivity +import org.pixeldroid.app.main.MainActivity import org.pixeldroid.app.utils.setThemeFromPreferences +import org.pixeldroid.common.ThemedActivity +@AndroidEntryPoint class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener { + private var restartMainOnExit = false override fun onCreate(savedInstanceState: Bundle?) { @@ -103,6 +102,8 @@ class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceC dialogFragment = ColorPreferenceDialog((preference as ColorPreference?)!!) } else if(preference.key == "language"){ dialogFragment = LanguageSettingFragment() + } else if (preference.key == "arrange_tabs") { + dialogFragment = ArrangeTabsFragment() } if (dialogFragment != null) { dialogFragment.setTargetFragment(this, 0) @@ -129,49 +130,4 @@ class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceC } } } - -} -class LanguageSettingFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val list: MutableList = mutableListOf() - // IDE doesn't find it, but compiling works apparently? - resources.getXml(R.xml._generated_res_locale_config).use { - var eventType = it.eventType - while (eventType != XmlResourceParser.END_DOCUMENT) { - when (eventType) { - XmlResourceParser.START_TAG -> { - if (it.name == "locale") { - list.add(it.getAttributeValue(0)) - } - } - } - eventType = it.next() - } - } - val locales = AppCompatDelegate.getApplicationLocales() - val checkedItem: Int = - if(locales.isEmpty) 0 - else { - // For some reason we get a bit inconsistent language tags. This normalises it for - // the currently used languages, but it might break in the future if we add some - val index = list.indexOf(locales.get(0)?.toLanguageTag()?.lowercase()?.replace('_', '-')) - // If found, we want to compensate for the first in the list being the default - if(index == -1) -1 - else index + 1 - } - - return MaterialAlertDialogBuilder(requireContext()).apply { - setIcon(R.drawable.translate_black_24dp) - setTitle(R.string.language) - setSingleChoiceItems((mutableListOf(getString(R.string.default_system)) + list.map { - val appLocale = LocaleListCompat.forLanguageTags(it) - appLocale.get(0)!!.getDisplayName(appLocale.get(0)!!) - }).toTypedArray(), checkedItem) { dialog, which -> - val languageTag = if(which in 1..list.size) list[which - 1] else null - dialog.dismiss() - AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag)) - } - setNegativeButton(android.R.string.ok) { _, _ -> } - }.create() - } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt index 071c93c2..94891ab3 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -7,15 +7,19 @@ import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources import android.graphics.Color +import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.Uri import android.os.Build import android.util.DisplayMetrics +import android.view.View import android.view.WindowManager import android.webkit.MimeTypeMap import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.PopupMenu import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver @@ -29,6 +33,7 @@ import com.google.gson.JsonPrimitive import com.google.gson.JsonSerializer import okhttp3.HttpUrl import org.pixeldroid.app.R +import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import java.time.Instant import java.time.format.DateTimeFormatter import java.util.Locale @@ -192,4 +197,67 @@ fun Fragment.bindingLifecycleAware(): ReadWriteProperty = binding = value this@bindingLifecycleAware.viewLifecycleOwner.lifecycle.addObserver(this) } - } \ No newline at end of file + } + +fun loadDefaultMenuTabs(context: Context, anchor: View): List { + return with(PopupMenu(context, anchor)) { + val menu = this.menu + menuInflater.inflate(R.menu.navigation_main, menu) + menu.removeGroup(R.id.bottomNavigationGroup) + (0 until menu.size()).map { Tab.fromLanguageString(context, menu.getItem(it).title.toString()) } + } +} + +fun loadDbMenuTabs(ctx: Context, tabsDbEntry: List): List> { + return tabsDbEntry.map { + Pair(Tab.fromName(it.tab), it.checked) + } +} + +enum class Tab { + HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED; + + fun toLanguageString(ctx: Context): String { + return ctx.getString( + when (this) { + HOME_FEED -> R.string.home_feed + SEARCH_DISCOVER_FEED -> R.string.search_discover_feed + CREATE_FEED -> R.string.create_feed + NOTIFICATIONS_FEED -> R.string.notifications_feed + PUBLIC_FEED -> R.string.public_feed + } + ) + } + + fun toName(): String { + return this.name + } + + fun getDrawable(ctx: Context): Drawable? { + val resId = when (this) { + HOME_FEED -> R.drawable.selector_home_feed + SEARCH_DISCOVER_FEED -> R.drawable.ic_search_white_24dp + CREATE_FEED -> R.drawable.selector_camera + NOTIFICATIONS_FEED -> R.drawable.selector_notifications + PUBLIC_FEED -> R.drawable.ic_filter_black_24dp + } + return AppCompatResources.getDrawable(ctx, resId) + } + + companion object { + fun fromLanguageString(ctx: Context, name: String): Tab { + return when (name) { + ctx.getString(R.string.home_feed) -> HOME_FEED + ctx.getString(R.string.search_discover_feed) -> SEARCH_DISCOVER_FEED + ctx.getString(R.string.create_feed) -> CREATE_FEED + ctx.getString(R.string.notifications_feed) -> NOTIFICATIONS_FEED + ctx.getString(R.string.public_feed) -> PUBLIC_FEED + else -> HOME_FEED + } + } + + fun fromName(name: String): Tab { + return entries.filter { it.name == name }.getOrElse(0) { HOME_FEED } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt b/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt index 3158e18e..7c05d2ed 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt @@ -1,5 +1,6 @@ package org.pixeldroid.app.utils.db +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters @@ -14,15 +15,20 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.api.objects.Notification +import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity @Database(entities = [ InstanceDatabaseEntity::class, UserDatabaseEntity::class, HomeStatusDatabaseEntity::class, PublicFeedStatusDatabaseEntity::class, - Notification::class + Notification::class, + TabsDatabaseEntity::class + ], + version = 7, + autoMigrations = [ + AutoMigration(from = 6, to = 7) ], - version = 6 ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -31,6 +37,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun homePostDao(): HomePostDao abstract fun publicPostDao(): PublicPostDao abstract fun notificationDao(): NotificationDao + abstract fun tabsDao(): TabsDao } val MIGRATION_3_4 = object : Migration(3, 4) { @@ -49,4 +56,4 @@ val MIGRATION_5_6 = object : Migration(5, 6) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1") } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/TabsDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/TabsDao.kt new file mode 100644 index 00000000..3e899aa8 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/TabsDao.kt @@ -0,0 +1,46 @@ +package org.pixeldroid.app.utils.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity + +@Dao +interface TabsDao { + @Query("SELECT * FROM tabsChecked WHERE `index`=:index AND `user_id`=:userId AND `instance_uri`=:instanceUri") + fun getTabChecked(index: Int, userId: String, instanceUri: String): TabsDatabaseEntity + + @Query("SELECT * FROM tabsChecked WHERE `user_id`=:userId AND `instance_uri`=:instanceUri") + fun getTabsChecked(userId: String, instanceUri: String): List + + @Query("DELETE FROM tabsChecked WHERE `index`=:index AND `user_id`=:userId AND `instance_uri`=:instanceUri") + fun deleteTabChecked(index: Int, userId: String, instanceUri: String) + + @Query("DELETE FROM tabsChecked WHERE `user_id`=:userId AND `instance_uri`=:instanceUri") + fun deleteTabsChecked(userId: String, instanceUri: String) + + /** + * Insert a tab, if it already exists return -1 + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertTabChecked(tabChecked: TabsDatabaseEntity): Long + + @Update + suspend fun updateTabChecked(tabChecked: TabsDatabaseEntity) + + @Transaction + suspend fun insertOrUpdate(tabChecked: TabsDatabaseEntity) { + if (insertTabChecked(tabChecked) == -1L) { + updateTabChecked(tabChecked) + } + } + + @Transaction + suspend fun clearAndRefill(tabsChecked: List, userId: String, instanceUri: String) { + deleteTabsChecked(userId, instanceUri) + tabsChecked.forEach { insertTabChecked(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/entities/TabsDatabaseEntity.kt b/app/src/main/java/org/pixeldroid/app/utils/db/entities/TabsDatabaseEntity.kt new file mode 100644 index 00000000..afc53a67 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/db/entities/TabsDatabaseEntity.kt @@ -0,0 +1,25 @@ +package org.pixeldroid.app.utils.db.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Entity( + tableName = "tabsChecked", + primaryKeys = ["index", "user_id", "instance_uri"], + foreignKeys = [ForeignKey( + entity = UserDatabaseEntity::class, + parentColumns = arrayOf("user_id", "instance_uri"), + childColumns = arrayOf("user_id", "instance_uri"), + onUpdate = ForeignKey.CASCADE, + onDelete = ForeignKey.CASCADE + )], + indices = [Index(value = ["user_id", "instance_uri"])] +) +data class TabsDatabaseEntity( + var index: Int, + var user_id: String, + var instance_uri: String, + var tab: String, + var checked: Boolean = true, +) \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_bottom_navigation.xml b/app/src/main/res/drawable/outline_bottom_navigation.xml new file mode 100644 index 00000000..3b41aec3 --- /dev/null +++ b/app/src/main/res/drawable/outline_bottom_navigation.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/rounded_colors.xml b/app/src/main/res/drawable/rounded_colors.xml new file mode 100644 index 00000000..d8f08936 --- /dev/null +++ b/app/src/main/res/drawable/rounded_colors.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/rounded_drag_handle.xml b/app/src/main/res/drawable/rounded_drag_handle.xml new file mode 100644 index 00000000..467a37b8 --- /dev/null +++ b/app/src/main/res/drawable/rounded_drag_handle.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/layout_tab.xml b/app/src/main/res/layout/layout_tab.xml new file mode 100644 index 00000000..d31e0e50 --- /dev/null +++ b/app/src/main/res/layout/layout_tab.xml @@ -0,0 +1,57 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_tabs_arrange.xml b/app/src/main/res/layout/layout_tabs_arrange.xml new file mode 100644 index 00000000..39a76697 --- /dev/null +++ b/app/src/main/res/layout/layout_tabs_arrange.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/menu-sw600dp-land/navigation_main.xml b/app/src/main/res/menu-sw600dp-land/navigation_main.xml index e71b8f94..03d08ab5 100644 --- a/app/src/main/res/menu-sw600dp-land/navigation_main.xml +++ b/app/src/main/res/menu-sw600dp-land/navigation_main.xml @@ -27,7 +27,9 @@ android:icon="@drawable/ic_filter_black_24dp" android:title="@string/public_feed"/> - + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c49fbe5..47505ae1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -348,4 +348,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Continue Pictures after the first were removed but can be restored by switching back to creating a Post Story Duration + Arrange tabs + Change visibility and order of tabs + Content diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 8b248f33..346a5135 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -17,54 +17,64 @@ android:title="@string/accentColorTitle" android:key="themeColor" android:defaultValue="0" - android:summary="@string/accentColorSummary" /> + android:summary="@string/accentColorSummary" + app:icon="@drawable/rounded_colors"/> + + - + + - + - - - - - + - + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + +