commit
1e85b14759
@ -0,0 +1,897 @@ |
||||
{ |
||||
"formatVersion": 1, |
||||
"database": { |
||||
"version": 25, |
||||
"identityHash": "0a5f8f196d357a01b8b571098ea32431", |
||||
"entities": [ |
||||
{ |
||||
"tableName": "TootEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "uid", |
||||
"columnName": "uid", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "text", |
||||
"columnName": "text", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "urls", |
||||
"columnName": "urls", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "descriptions", |
||||
"columnName": "descriptions", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "contentWarning", |
||||
"columnName": "contentWarning", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "inReplyToId", |
||||
"columnName": "inReplyToId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "inReplyToText", |
||||
"columnName": "inReplyToText", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "inReplyToUsername", |
||||
"columnName": "inReplyToUsername", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "visibility", |
||||
"columnName": "visibility", |
||||
"affinity": "INTEGER", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "poll", |
||||
"columnName": "poll", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "formattingSyntax", |
||||
"columnName": "formattingSyntax", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "markdownMode", |
||||
"columnName": "markdownMode", |
||||
"affinity": "INTEGER", |
||||
"notNull": false |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"uid" |
||||
], |
||||
"autoGenerate": true |
||||
}, |
||||
"indices": [], |
||||
"foreignKeys": [] |
||||
}, |
||||
{ |
||||
"tableName": "AccountEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsStreamingEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "id", |
||||
"columnName": "id", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "domain", |
||||
"columnName": "domain", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "accessToken", |
||||
"columnName": "accessToken", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "isActive", |
||||
"columnName": "isActive", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "accountId", |
||||
"columnName": "accountId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "username", |
||||
"columnName": "username", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "displayName", |
||||
"columnName": "displayName", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "profilePictureUrl", |
||||
"columnName": "profilePictureUrl", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsEnabled", |
||||
"columnName": "notificationsEnabled", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsStreamingEnabled", |
||||
"columnName": "notificationsStreamingEnabled", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsMentioned", |
||||
"columnName": "notificationsMentioned", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsFollowed", |
||||
"columnName": "notificationsFollowed", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsFollowRequested", |
||||
"columnName": "notificationsFollowRequested", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsReblogged", |
||||
"columnName": "notificationsReblogged", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsFavorited", |
||||
"columnName": "notificationsFavorited", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsPolls", |
||||
"columnName": "notificationsPolls", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsEmojiReactions", |
||||
"columnName": "notificationsEmojiReactions", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsChatMessages", |
||||
"columnName": "notificationsChatMessages", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationSound", |
||||
"columnName": "notificationSound", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationVibration", |
||||
"columnName": "notificationVibration", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationLight", |
||||
"columnName": "notificationLight", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "defaultPostPrivacy", |
||||
"columnName": "defaultPostPrivacy", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "defaultMediaSensitivity", |
||||
"columnName": "defaultMediaSensitivity", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "alwaysShowSensitiveMedia", |
||||
"columnName": "alwaysShowSensitiveMedia", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "alwaysOpenSpoiler", |
||||
"columnName": "alwaysOpenSpoiler", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "mediaPreviewEnabled", |
||||
"columnName": "mediaPreviewEnabled", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastNotificationId", |
||||
"columnName": "lastNotificationId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "activeNotifications", |
||||
"columnName": "activeNotifications", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "emojis", |
||||
"columnName": "emojis", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "tabPreferences", |
||||
"columnName": "tabPreferences", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "notificationsFilter", |
||||
"columnName": "notificationsFilter", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "defaultFormattingSyntax", |
||||
"columnName": "defaultFormattingSyntax", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"id" |
||||
], |
||||
"autoGenerate": true |
||||
}, |
||||
"indices": [ |
||||
{ |
||||
"name": "index_AccountEntity_domain_accountId", |
||||
"unique": true, |
||||
"columnNames": [ |
||||
"domain", |
||||
"accountId" |
||||
], |
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" |
||||
} |
||||
], |
||||
"foreignKeys": [] |
||||
}, |
||||
{ |
||||
"tableName": "InstanceEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, `chatLimit` INTEGER, PRIMARY KEY(`instance`))", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "instance", |
||||
"columnName": "instance", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "emojiList", |
||||
"columnName": "emojiList", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "maximumTootCharacters", |
||||
"columnName": "maximumTootCharacters", |
||||
"affinity": "INTEGER", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "maxPollOptions", |
||||
"columnName": "maxPollOptions", |
||||
"affinity": "INTEGER", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "maxPollOptionLength", |
||||
"columnName": "maxPollOptionLength", |
||||
"affinity": "INTEGER", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "version", |
||||
"columnName": "version", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "chatLimit", |
||||
"columnName": "chatLimit", |
||||
"affinity": "INTEGER", |
||||
"notNull": false |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"instance" |
||||
], |
||||
"autoGenerate": false |
||||
}, |
||||
"indices": [], |
||||
"foreignKeys": [] |
||||
}, |
||||
{ |
||||
"tableName": "TimelineStatusEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `pleroma` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "serverId", |
||||
"columnName": "serverId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "url", |
||||
"columnName": "url", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "timelineUserId", |
||||
"columnName": "timelineUserId", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "authorServerId", |
||||
"columnName": "authorServerId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "inReplyToId", |
||||
"columnName": "inReplyToId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "inReplyToAccountId", |
||||
"columnName": "inReplyToAccountId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "content", |
||||
"columnName": "content", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "createdAt", |
||||
"columnName": "createdAt", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "emojis", |
||||
"columnName": "emojis", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "reblogsCount", |
||||
"columnName": "reblogsCount", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "favouritesCount", |
||||
"columnName": "favouritesCount", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "reblogged", |
||||
"columnName": "reblogged", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "bookmarked", |
||||
"columnName": "bookmarked", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "favourited", |
||||
"columnName": "favourited", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "sensitive", |
||||
"columnName": "sensitive", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "spoilerText", |
||||
"columnName": "spoilerText", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "visibility", |
||||
"columnName": "visibility", |
||||
"affinity": "INTEGER", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "attachments", |
||||
"columnName": "attachments", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "mentions", |
||||
"columnName": "mentions", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "application", |
||||
"columnName": "application", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "reblogServerId", |
||||
"columnName": "reblogServerId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "reblogAccountId", |
||||
"columnName": "reblogAccountId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "poll", |
||||
"columnName": "poll", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "pleroma", |
||||
"columnName": "pleroma", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"serverId", |
||||
"timelineUserId" |
||||
], |
||||
"autoGenerate": false |
||||
}, |
||||
"indices": [ |
||||
{ |
||||
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId", |
||||
"unique": false, |
||||
"columnNames": [ |
||||
"authorServerId", |
||||
"timelineUserId" |
||||
], |
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" |
||||
} |
||||
], |
||||
"foreignKeys": [ |
||||
{ |
||||
"table": "TimelineAccountEntity", |
||||
"onDelete": "NO ACTION", |
||||
"onUpdate": "NO ACTION", |
||||
"columns": [ |
||||
"authorServerId", |
||||
"timelineUserId" |
||||
], |
||||
"referencedColumns": [ |
||||
"serverId", |
||||
"timelineUserId" |
||||
] |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
"tableName": "TimelineAccountEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "serverId", |
||||
"columnName": "serverId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "timelineUserId", |
||||
"columnName": "timelineUserId", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "localUsername", |
||||
"columnName": "localUsername", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "username", |
||||
"columnName": "username", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "displayName", |
||||
"columnName": "displayName", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "url", |
||||
"columnName": "url", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "avatar", |
||||
"columnName": "avatar", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "emojis", |
||||
"columnName": "emojis", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "bot", |
||||
"columnName": "bot", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"serverId", |
||||
"timelineUserId" |
||||
], |
||||
"autoGenerate": false |
||||
}, |
||||
"indices": [], |
||||
"foreignKeys": [] |
||||
}, |
||||
{ |
||||
"tableName": "ConversationEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "accountId", |
||||
"columnName": "accountId", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "id", |
||||
"columnName": "id", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "accounts", |
||||
"columnName": "accounts", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "unread", |
||||
"columnName": "unread", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.id", |
||||
"columnName": "s_id", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.url", |
||||
"columnName": "s_url", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.inReplyToId", |
||||
"columnName": "s_inReplyToId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.inReplyToAccountId", |
||||
"columnName": "s_inReplyToAccountId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.account", |
||||
"columnName": "s_account", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.content", |
||||
"columnName": "s_content", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.createdAt", |
||||
"columnName": "s_createdAt", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.emojis", |
||||
"columnName": "s_emojis", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.favouritesCount", |
||||
"columnName": "s_favouritesCount", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.favourited", |
||||
"columnName": "s_favourited", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.bookmarked", |
||||
"columnName": "s_bookmarked", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.sensitive", |
||||
"columnName": "s_sensitive", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.spoilerText", |
||||
"columnName": "s_spoilerText", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.attachments", |
||||
"columnName": "s_attachments", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.mentions", |
||||
"columnName": "s_mentions", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.showingHiddenContent", |
||||
"columnName": "s_showingHiddenContent", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.expanded", |
||||
"columnName": "s_expanded", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.collapsible", |
||||
"columnName": "s_collapsible", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.collapsed", |
||||
"columnName": "s_collapsed", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.poll", |
||||
"columnName": "s_poll", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"id", |
||||
"accountId" |
||||
], |
||||
"autoGenerate": false |
||||
}, |
||||
"indices": [], |
||||
"foreignKeys": [] |
||||
}, |
||||
{ |
||||
"tableName": "ChatEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `lastMessageId` TEXT, PRIMARY KEY(`localId`, `chatId`))", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "localId", |
||||
"columnName": "localId", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "chatId", |
||||
"columnName": "chatId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "accountId", |
||||
"columnName": "accountId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "unread", |
||||
"columnName": "unread", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "updatedAt", |
||||
"columnName": "updatedAt", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastMessageId", |
||||
"columnName": "lastMessageId", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"localId", |
||||
"chatId" |
||||
], |
||||
"autoGenerate": false |
||||
}, |
||||
"indices": [], |
||||
"foreignKeys": [] |
||||
}, |
||||
{ |
||||
"tableName": "ChatMessageEntity", |
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `messageId` TEXT NOT NULL, `content` TEXT, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `attachment` TEXT, `emojis` TEXT NOT NULL, PRIMARY KEY(`localId`, `messageId`))", |
||||
"fields": [ |
||||
{ |
||||
"fieldPath": "localId", |
||||
"columnName": "localId", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "messageId", |
||||
"columnName": "messageId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "content", |
||||
"columnName": "content", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "chatId", |
||||
"columnName": "chatId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "accountId", |
||||
"columnName": "accountId", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "createdAt", |
||||
"columnName": "createdAt", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "attachment", |
||||
"columnName": "attachment", |
||||
"affinity": "TEXT", |
||||
"notNull": false |
||||
}, |
||||
{ |
||||
"fieldPath": "emojis", |
||||
"columnName": "emojis", |
||||
"affinity": "TEXT", |
||||
"notNull": true |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"localId", |
||||
"messageId" |
||||
], |
||||
"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, '0a5f8f196d357a01b8b571098ea32431')" |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
<resources> |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="action_login">ⵇⵇⴻⵏ ⵖⴻⵔ ⵎⴰⵚⵟⵓⴷⵓⵏ</string> |
||||
|
||||
|
||||
<string name="add_account_description">ⵔⵏⵓ ⵢⵉⵡⴻⵏ ⵏ ⵓⵎⵉⴹⴰⵏ ⴰⵎⴰⵢⵏⵓⵝ ⵏ ⵎⴰⵚⵟⵓⴷⵓⵏ</string> |
||||
|
||||
|
||||
|
||||
</resources> |
@ -0,0 +1,56 @@ |
||||
<resources> |
||||
|
||||
<string name="about_tusky_account">Próifíl Husky</string> |
||||
|
||||
|
||||
<string name="about_tusky_license">Is bogearraí foinse oscailte agus saor in aisce é Husky. Tá sé ceadúnaithe faoi Leagan 3. Ceadúnas Poiblí Ginearálta GNU 3. Is féidir leat an ceadúnas a fheiceáil anseo: https://www.gnu.org/licenses/gpl-3.0.en.html</string> |
||||
|
||||
|
||||
<string name="about_powered_by_tusky">Cumhachtaithe ag Husky</string> |
||||
|
||||
|
||||
<string name="about_tusky_version">Husky %s</string> |
||||
|
||||
|
||||
<string name="restart_emoji">Beidh ort Husky a atosú chun na hathruithe seo a chur i bhfeidhm</string> |
||||
|
||||
|
||||
<string name="license_description">Tá cód agus sócmhainní ó na tionscadail foinse oscailte seo a leanas i Husky:</string> |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="about_project_site">Suíomh Gréasáin an tionscadail: |
||||
\n https://huskyapp.dev</string> |
||||
|
||||
|
||||
|
||||
|
||||
<string name="about_bug_feature_request_site">Tuarascálacha ar fhabhtanna & iarratais ar ghnéithe: |
||||
\n https://git.mentality.rip/FWGS/Husky/issues</string> |
||||
|
||||
|
||||
|
||||
|
||||
<string name="action_login">Logáil isteach le Pleroma</string> |
||||
|
||||
|
||||
<string name="add_account_description">Cuir Cuntas Pleroma nua leis</string> |
||||
|
||||
|
||||
<string name="warning_scheduling_interval">Tá eatramh sceidealaithe íosta 5 nóiméad ag Pleroma.</string> |
||||
|
||||
|
||||
|
||||
|
||||
<string name="dialog_whats_an_instance">Is féidir seoladh nó fearann aon cháis a iontráil anseo, mar shampla shitposter.club, blob.cat, expired.mentality.rip, agus <a href="https://fediverse.network/pleroma?count=peers"> níos mó! </a> |
||||
\n |
||||
\nMura bhfuil cuntas agat fós, is féidir leat ainm an cháis ar mhaith leat a bheith páirteach ann agus cuntas a chruthú ann. |
||||
\n |
||||
\nIs áit amháin é sampla ina ndéantar do chuntas a óstáil, ach is féidir leat cumarsáid a dhéanamh go héasca le daoine eile agus iad a leanúint ar chásanna eile mar a bheadh tú ar an suíomh céanna. |
||||
\n |
||||
\nIs féidir tuilleadh faisnéise a fháil ag <a href="https://joinmastodon.org"> joinmastodon.org </a>. </string> |
||||
|
||||
|
||||
</resources> |
@ -0,0 +1,11 @@ |
||||
<resources> |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="action_login">Clàraich a-steach le Pleroma</string> |
||||
|
||||
|
||||
|
||||
</resources> |
@ -0,0 +1,56 @@ |
||||
<resources> |
||||
|
||||
<string name="about_tusky_license">Husky là phần mềm mã nguồn mở, được phân phối với giấy phép GNU General Public License Version 3. Bạn có thể tham khảo thêm tại: https://www.gnu.org/licenses/gpl-3.0.en.html</string> |
||||
|
||||
|
||||
<string name="about_powered_by_tusky">Powered by Husky</string> |
||||
|
||||
|
||||
<string name="about_tusky_version">Husky %s</string> |
||||
|
||||
|
||||
<string name="about_tusky_account">Trang cá nhân Husky</string> |
||||
|
||||
|
||||
<string name="license_description">Husky có sử dụng mã nguồn từ những dự án mã nguồn mở sau:</string> |
||||
|
||||
|
||||
<string name="restart_emoji">Bạn cần khởi động lại Husky để áp dụng các thiết lập</string> |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="about_project_site">Trang chủ |
||||
\nhttps://huskyapp.dev</string> |
||||
|
||||
|
||||
|
||||
|
||||
<string name="about_bug_feature_request_site">Báo lỗi và đề xuất tính năng |
||||
\nhttps://git.mentality.rip/FWGS/Husky/issues</string> |
||||
|
||||
|
||||
|
||||
|
||||
<string name="action_login">Đăng nhập Pleroma</string> |
||||
|
||||
|
||||
<string name="warning_scheduling_interval">Pleroma giới hạn tối thiểu 5 phút.</string> |
||||
|
||||
|
||||
<string name="add_account_description">Thêm tài khoản Pleroma</string> |
||||
|
||||
|
||||
|
||||
|
||||
<string name="dialog_whats_an_instance">Bạn phải nhập một tên miền, ví dụ shitposter.club, blob.cat, expired.mentality.rip, và <a href="https://fediverse.network/pleroma?count=peers">nhiều hơn nữa!</a> |
||||
\n |
||||
\nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó. |
||||
\n |
||||
\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể giao tiếp và theo dõi mọi người trên các máy chủ khác một cách dễ dàng. |
||||
\n |
||||
\nTham khảo thêm tại <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> |
||||
|
||||
|
||||
</resources> |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 6.5 KiB |
@ -1,291 +0,0 @@ |
||||
package com.keylesspalace.tusky; |
||||
|
||||
import android.app.AlarmManager; |
||||
import android.app.PendingIntent; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.os.Build; |
||||
import android.util.AttributeSet; |
||||
import android.util.Log; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.widget.ImageButton; |
||||
import android.widget.ImageView; |
||||
import android.widget.ProgressBar; |
||||
import android.widget.RadioButton; |
||||
import android.widget.TextView; |
||||
import android.widget.Toast; |
||||
|
||||
import androidx.appcompat.app.AlertDialog; |
||||
import androidx.preference.Preference; |
||||
import androidx.preference.PreferenceManager; |
||||
|
||||
import com.keylesspalace.tusky.util.EmojiCompatFont; |
||||
|
||||
import java.util.ArrayList; |
||||
|
||||
/** |
||||
* This Preference lets the user select their preferred emoji font |
||||
*/ |
||||
public class EmojiPreference extends Preference { |
||||
private static final String TAG = "EmojiPreference"; |
||||
private EmojiCompatFont selected, original; |
||||
static final String FONT_PREFERENCE = "selected_emoji_font"; |
||||
private static final EmojiCompatFont[] FONTS = EmojiCompatFont.FONTS; |
||||
// Please note that this array should be sorted in the same way as their fonts.
|
||||
private static final int[] viewIds = { |
||||
R.id.item_nomoji, |
||||
R.id.item_blobmoji, |
||||
R.id.item_twemoji, |
||||
R.id.item_notoemoji}; |
||||
|
||||
private ArrayList<RadioButton> radioButtons = new ArrayList<>(); |
||||
|
||||
private boolean updated, currentNeedsUpdate; |
||||
|
||||
public EmojiPreference(Context context) { |
||||
super(context); |
||||
|
||||
// Find out which font is currently active
|
||||
this.selected = EmojiCompatFont.byId(PreferenceManager |
||||
.getDefaultSharedPreferences(context) |
||||
.getInt(FONT_PREFERENCE, 0)); |
||||
// We'll use this later to determine if anything has changed
|
||||
this.original = this.selected; |
||||
|
||||
setSummary(selected.getDisplay(context)); |
||||
} |
||||
|
||||
public EmojiPreference(Context context, AttributeSet attrs) { |
||||
super(context, attrs); |
||||
|
||||
// Find out which font is currently active
|
||||
this.selected = EmojiCompatFont.byId(PreferenceManager |
||||
.getDefaultSharedPreferences(context) |
||||
.getInt(FONT_PREFERENCE, 0)); |
||||
// We'll use this later to determine if anything has changed
|
||||
this.original = this.selected; |
||||
|
||||
setSummary(selected.getDisplay(context)); |
||||
} |
||||
|
||||
@Override |
||||
protected void onClick() { |
||||
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_emojicompat, null); |
||||
|
||||
for (int i = 0; i < viewIds.length; i++) { |
||||
setupItem(view.findViewById(viewIds[i]), FONTS[i]); |
||||
} |
||||
|
||||
new AlertDialog.Builder(getContext()) |
||||
.setView(view) |
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> onDialogOk()) |
||||
.setNegativeButton(android.R.string.cancel, null) |
||||
.show(); |
||||
} |
||||
|
||||
private void setupItem(View container, EmojiCompatFont font) { |
||||
Context context = container.getContext(); |
||||
|
||||
TextView title = container.findViewById(R.id.emojicompat_name); |
||||
TextView caption = container.findViewById(R.id.emojicompat_caption); |
||||
ImageView thumb = container.findViewById(R.id.emojicompat_thumb); |
||||
ImageButton download = container.findViewById(R.id.emojicompat_download); |
||||
ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel); |
||||
RadioButton radio = container.findViewById(R.id.emojicompat_radio); |
||||
|
||||
// Initialize all the views
|
||||
title.setText(font.getDisplay(context)); |
||||
caption.setText(font.getCaption(context)); |
||||
thumb.setImageDrawable(font.getThumb(context)); |
||||
|
||||
// There needs to be a list of all the radio buttons in order to uncheck them when one is selected
|
||||
radioButtons.add(radio); |
||||
|
||||
updateItem(font, container); |
||||
|
||||
// Set actions
|
||||
download.setOnClickListener((downloadButton) -> |
||||
startDownload(font, container)); |
||||
|
||||
cancel.setOnClickListener((cancelButton) -> |
||||
cancelDownload(font, container)); |
||||
|
||||
radio.setOnClickListener((radioButton) -> |
||||
select(font, (RadioButton) radioButton)); |
||||
|
||||
container.setOnClickListener((containterView) -> |
||||
select(font, |
||||
containterView.findViewById(R.id.emojicompat_radio |
||||
))); |
||||
} |
||||
|
||||
private void startDownload(EmojiCompatFont font, View container) { |
||||
ImageButton download = container.findViewById(R.id.emojicompat_download); |
||||
TextView caption = container.findViewById(R.id.emojicompat_caption); |
||||
|
||||
ProgressBar progressBar = container.findViewById(R.id.emojicompat_progress); |
||||
ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel); |
||||
|
||||
// Switch to downloading style
|
||||
download.setVisibility(View.GONE); |
||||
caption.setVisibility(View.INVISIBLE); |
||||
progressBar.setVisibility(View.VISIBLE); |
||||
cancel.setVisibility(View.VISIBLE); |
||||
|
||||
|
||||
font.downloadFont(getContext(), new EmojiCompatFont.Downloader.EmojiDownloadListener() { |
||||
@Override |
||||
public void onDownloaded(EmojiCompatFont font) { |
||||
finishDownload(font, container); |
||||
} |
||||
|
||||
@Override |
||||
public void onProgress(float progress) { |
||||
// The progress is returned as a float between 0 and 1
|
||||
progress *= progressBar.getMax(); |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
||||
progressBar.setProgress((int) progress, true); |
||||
} else { |
||||
progressBar.setProgress((int) progress); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onFailed() { |
||||
Toast.makeText(getContext(), R.string.download_failed, Toast.LENGTH_SHORT).show(); |
||||
updateItem(font, container); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private void cancelDownload(EmojiCompatFont font, View container) { |
||||
font.cancelDownload(); |
||||
updateItem(font, container); |
||||
} |
||||
|
||||
private void finishDownload(EmojiCompatFont font, View container) { |
||||
select(font, container.findViewById(R.id.emojicompat_radio)); |
||||
updateItem(font, container); |
||||
// Set the flag to restart the app (because an update has been downloaded)
|
||||
if (selected == original && currentNeedsUpdate) { |
||||
updated = true; |
||||
currentNeedsUpdate = false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Select a font both visually and logically |
||||
* |
||||
* @param font The font to be selected |
||||
* @param radio The radio button associated with it's visual item |
||||
*/ |
||||
private void select(EmojiCompatFont font, RadioButton radio) { |
||||
selected = font; |
||||
// Uncheck all the other buttons
|
||||
for (RadioButton other : radioButtons) { |
||||
if (other != radio) { |
||||
other.setChecked(false); |
||||
} |
||||
} |
||||
radio.setChecked(true); |
||||
} |
||||
|
||||
/** |
||||
* Called when a "consistent" state is reached, i.e. it's not downloading the font |
||||
* |
||||
* @param font The font to be displayed |
||||
* @param container The ConstraintLayout containing the item |
||||
*/ |
||||
private void updateItem(EmojiCompatFont font, View container) { |
||||
// Assignments
|
||||
ImageButton download = container.findViewById(R.id.emojicompat_download); |
||||
TextView caption = container.findViewById(R.id.emojicompat_caption); |
||||
|
||||
ProgressBar progress = container.findViewById(R.id.emojicompat_progress); |
||||
ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel); |
||||
|
||||
RadioButton radio = container.findViewById(R.id.emojicompat_radio); |
||||
|
||||
// There's no download going on
|
||||
progress.setVisibility(View.GONE); |
||||
cancel.setVisibility(View.GONE); |
||||
caption.setVisibility(View.VISIBLE); |
||||
|
||||
if (font.isDownloaded(getContext())) { |
||||
// Make it selectable
|
||||
download.setVisibility(View.GONE); |
||||
radio.setVisibility(View.VISIBLE); |
||||
container.setClickable(true); |
||||
} else { |
||||
// Make it downloadable
|
||||
download.setVisibility(View.VISIBLE); |
||||
radio.setVisibility(View.GONE); |
||||
container.setClickable(false); |
||||
} |
||||
|
||||
// Select it if necessary
|
||||
if (font == selected) { |
||||
radio.setChecked(true); |
||||
// Update available
|
||||
if (!font.isDownloaded(getContext())) { |
||||
currentNeedsUpdate = true; |
||||
} |
||||
} else { |
||||
radio.setChecked(false); |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* In order to be able to use this font later on, it needs to be saved first. |
||||
*/ |
||||
private void saveSelectedFont() { |
||||
int index = selected.getId(); |
||||
Log.i(TAG, "saveSelectedFont: Font ID: " + index); |
||||
// It's saved using the key FONT_PREFERENCE
|
||||
PreferenceManager |
||||
.getDefaultSharedPreferences(getContext()) |
||||
.edit() |
||||
.putInt(FONT_PREFERENCE, index) |
||||
.apply(); |
||||
setSummary(selected.getDisplay(getContext())); |
||||
} |
||||
|
||||
/** |
||||
* That's it. The user doesn't want to switch between these amazing radio buttons anymore! |
||||
* That means, the selected font can be saved (if the user hit OK) |
||||
*/ |
||||
private void onDialogOk() { |
||||
saveSelectedFont(); |
||||
if (selected != original || updated) { |
||||
new AlertDialog.Builder(getContext()) |
||||
.setTitle(R.string.restart_required) |
||||
.setMessage(R.string.restart_emoji) |
||||
.setNegativeButton(R.string.later, null) |
||||
.setPositiveButton(R.string.restart, ((dialog, which) -> { |
||||
// Restart the app
|
||||
// From https://stackoverflow.com/a/17166729/5070653
|
||||
Intent launchIntent = new Intent(getContext(), SplashActivity.class); |
||||
PendingIntent mPendingIntent = PendingIntent.getActivity( |
||||
getContext(), |
||||
// This is the codepoint of the party face emoji :D
|
||||
0x1f973, |
||||
launchIntent, |
||||
PendingIntent.FLAG_CANCEL_CURRENT); |
||||
AlarmManager mgr = |
||||
(AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); |
||||
if (mgr != null) { |
||||
mgr.set( |
||||
AlarmManager.RTC, |
||||
System.currentTimeMillis() + 100, |
||||
mPendingIntent); |
||||
} |
||||
System.exit(0); |
||||
})).show(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,227 @@ |
||||
package com.keylesspalace.tusky.adapter |
||||
|
||||
import android.content.Context |
||||
import android.graphics.drawable.ColorDrawable |
||||
import android.text.TextUtils |
||||
import android.text.format.DateUtils |
||||
import android.view.LayoutInflater |
||||
import android.view.View |
||||
import android.view.ViewGroup |
||||
import android.widget.* |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import com.bumptech.glide.Glide |
||||
import com.keylesspalace.tusky.R |
||||
import com.keylesspalace.tusky.entity.Attachment |
||||
import com.keylesspalace.tusky.interfaces.ChatActionListener |
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions |
||||
import com.keylesspalace.tusky.util.ThemeUtils |
||||
import com.keylesspalace.tusky.util.TimestampUtils |
||||
import com.keylesspalace.tusky.util.emojify |
||||
import com.keylesspalace.tusky.view.MediaPreviewImageView |
||||
import com.keylesspalace.tusky.viewdata.ChatMessageViewData |
||||
import java.text.SimpleDateFormat |
||||
import java.util.* |
||||
import kotlin.math.roundToInt |
||||
|
||||
class ChatMessagesViewHolder(view: View) : RecyclerView.ViewHolder(view) { |
||||
object Key { |
||||
const val KEY_CREATED = "created" |
||||
} |
||||
|
||||
private val content: TextView = view.findViewById(R.id.content) |
||||
private val timestamp: TextView = view.findViewById(R.id.datetime) |
||||
private val attachmentView: MediaPreviewImageView = view.findViewById(R.id.attachment) |
||||
private val mediaOverlay: ImageView = view.findViewById(R.id.mediaOverlay) |
||||
private val attachmentLayout: FrameLayout = view.findViewById(R.id.attachmentLayout) |
||||
|
||||
private val sdf = SimpleDateFormat("HH:mm", Locale.getDefault()) |
||||
|
||||
private val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(itemView.context, R.attr.colorBackgroundAccent)) |
||||
|
||||
fun setupWithChatMessage(msg: ChatMessageViewData.Concrete, chatActionListener: ChatActionListener, statusDisplayOptions: StatusDisplayOptions, payload: Any?) { |
||||
if(payload == null) { |
||||
if(msg.content != null) |
||||
content.text = msg.content.emojify(msg.emojis, content) |
||||
|
||||
setAttachment(msg.attachment, chatActionListener) |
||||
setCreatedAt(msg.createdAt) |
||||
} else { |
||||
if(payload is List<*>) { |
||||
for (item in payload) { |
||||
if (ChatsViewHolder.Key.KEY_CREATED == item) { |
||||
setCreatedAt(msg.createdAt) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun loadImage(imageView: MediaPreviewImageView, |
||||
previewUrl: String?, |
||||
meta: Attachment.MetaData?) { |
||||
if (TextUtils.isEmpty(previewUrl)) { |
||||
imageView.removeFocalPoint() |
||||
Glide.with(imageView) |
||||
.load(mediaPreviewUnloaded) |
||||
.centerInside() |
||||
.into(imageView) |
||||
} else { |
||||
val focus = meta?.focus |
||||
if (focus != null) { // If there is a focal point for this attachment: |
||||
imageView.setFocalPoint(focus) |
||||
Glide.with(imageView) |
||||
.load(previewUrl) |
||||
.placeholder(mediaPreviewUnloaded) |
||||
.centerInside() |
||||
.addListener(imageView) |
||||
.into(imageView) |
||||
} else { |
||||
imageView.removeFocalPoint() |
||||
Glide.with(imageView) |
||||
.load(previewUrl) |
||||
.placeholder(mediaPreviewUnloaded) |
||||
.centerInside() |
||||
.into(imageView) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun formatDuration(durationInSeconds: Double): String? { |
||||
val seconds = durationInSeconds.roundToInt().toInt() % 60 |
||||
val minutes = durationInSeconds.toInt() % 3600 / 60 |
||||
val hours = durationInSeconds.toInt() / 3600 |
||||
return String.format("%d:%02d:%02d", hours, minutes, seconds) |
||||
} |
||||
|
||||
private fun getAttachmentDescription(context: Context, attachment: Attachment): CharSequence { |
||||
var duration = "" |
||||
if (attachment.meta?.duration != null && attachment.meta.duration > 0) { |
||||
duration = formatDuration(attachment.meta.duration.toDouble()) + " " |
||||
} |
||||
return if (TextUtils.isEmpty(attachment.description)) { |
||||
duration + context.getString(R.string.description_status_media_no_description_placeholder) |
||||
} else { |
||||
duration + attachment.description |
||||
} |
||||
} |
||||
|
||||
|
||||
private fun setAttachmentClickListener(view: View, listener: ChatActionListener, attachment: Attachment, animateTransition: Boolean) { |
||||
view.setOnClickListener { v: View? -> |
||||
val position = adapterPosition |
||||
if (position != RecyclerView.NO_POSITION) { |
||||
listener.onViewMedia(position, if (animateTransition) v else null) |
||||
} |
||||
} |
||||
view.setOnLongClickListener { v: View? -> |
||||
val description = getAttachmentDescription(view.context, attachment) |
||||
Toast.makeText(view.context, description, Toast.LENGTH_LONG).show() |
||||
true |
||||
} |
||||
} |
||||
|
||||
|
||||
private fun setAttachment(attachment: Attachment?, listener: ChatActionListener) { |
||||
if(attachment == null) { |
||||
attachmentLayout.visibility = View.GONE |
||||
} else { |
||||
attachmentLayout.visibility = View.VISIBLE |
||||
|
||||
val previewUrl: String = attachment.previewUrl |
||||
val description: String? = attachment.description |
||||
|
||||
if(description != null && TextUtils.isEmpty(description) ) { |
||||
attachmentView.contentDescription = description |
||||
} else { |
||||
attachmentView.contentDescription = attachmentView.context |
||||
.getString(R.string.action_view_media) |
||||
} |
||||
|
||||
loadImage(attachmentView, previewUrl, attachment.meta) |
||||
|
||||
when(attachment.type) { |
||||
Attachment.Type.VIDEO, Attachment.Type.GIFV -> { |
||||
mediaOverlay.visibility = View.VISIBLE |
||||
} |
||||
else -> { |
||||
mediaOverlay.visibility = View.GONE |
||||
} |
||||
} |
||||
|
||||
setAttachmentClickListener(attachmentView, listener, attachment, true) |
||||
} |
||||
} |
||||
|
||||
private fun setCreatedAt(createdAt: Date) { |
||||
timestamp.text = sdf.format(createdAt) |
||||
} |
||||
} |
||||
|
||||
class ChatMessagesAdapter(private val dataSource : TimelineAdapter.AdapterDataSource<ChatMessageViewData>, |
||||
private val chatActionListener: ChatActionListener, |
||||
private val statusDisplayOptions: StatusDisplayOptions, |
||||
private val localUserId: String) |
||||
: RecyclerView.Adapter<RecyclerView.ViewHolder>() { |
||||
|
||||
private val VIEW_TYPE_OUR_MESSAGE = 0 |
||||
private val VIEW_TYPE_THEIR_MESSAGE = 1 |
||||
private val VIEW_TYPE_PLACEHOLDER = 2 |
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { |
||||
when(viewType) { |
||||
VIEW_TYPE_OUR_MESSAGE -> { |
||||
val view = LayoutInflater.from(parent.context) |
||||
.inflate(R.layout.item_our_message, parent, false) |
||||
return ChatMessagesViewHolder(view) |
||||
} |
||||
VIEW_TYPE_THEIR_MESSAGE -> { |
||||
val view = LayoutInflater.from(parent.context) |
||||
.inflate(R.layout.item_their_message, parent, false) |
||||
return ChatMessagesViewHolder(view) |
||||
} |
||||
else -> { |
||||
val view = LayoutInflater.from(parent.context) |
||||
.inflate(R.layout.item_status_placeholder, parent, false) |
||||
return PlaceholderViewHolder(view) |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun getItemCount(): Int { |
||||
return dataSource.itemCount |
||||
} |
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { |
||||
bindViewHolder(holder, position, null) |
||||
} |
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList<Any>) { |
||||
bindViewHolder(holder, position, payload) |
||||
} |
||||
|
||||
private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>?) { |
||||
val chat: ChatMessageViewData = dataSource.getItemAt(position) |
||||
if(holder is PlaceholderViewHolder) { |
||||
holder.setup(chatActionListener, (chat as ChatMessageViewData.Placeholder).isLoading) |
||||
} else if(holder is ChatMessagesViewHolder) { |
||||
holder.setupWithChatMessage(chat as ChatMessageViewData.Concrete, chatActionListener, statusDisplayOptions, |
||||
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null) |
||||
} |
||||
} |
||||
|
||||
override fun getItemViewType(position: Int): Int { |
||||
if(dataSource.getItemAt(position) is ChatMessageViewData.Concrete) { |
||||
val msg = dataSource.getItemAt(position) as ChatMessageViewData.Concrete |
||||
|
||||
if(msg.accountId == localUserId) { |
||||
return VIEW_TYPE_OUR_MESSAGE |
||||
} |
||||
return VIEW_TYPE_THEIR_MESSAGE |
||||
} |
||||
return VIEW_TYPE_PLACEHOLDER |
||||
} |
||||
|
||||
override fun getItemId(position: Int): Long { |
||||
return dataSource.getItemAt(position).getViewDataId().toLong() |
||||
} |
||||
} |
@ -0,0 +1,209 @@ |
||||
package com.keylesspalace.tusky.adapter |
||||
|
||||
import android.graphics.Typeface |
||||
import android.opengl.Visibility |
||||
import android.text.SpannableStringBuilder |
||||
import android.text.TextUtils |
||||
import android.text.format.DateUtils |
||||
import android.view.LayoutInflater |
||||
import android.view.View |
||||
import android.view.ViewGroup |
||||
import android.widget.ImageView |
||||
import android.widget.TextView |
||||
import androidx.core.text.toSpanned |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import at.connyduck.sparkbutton.helpers.Utils |
||||
import com.bumptech.glide.Glide |
||||
import com.keylesspalace.tusky.R |
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener |
||||
import com.keylesspalace.tusky.interfaces.ChatActionListener |
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions |
||||
import com.keylesspalace.tusky.util.TimestampUtils |
||||
import com.keylesspalace.tusky.util.emojify |
||||
import com.keylesspalace.tusky.util.loadAvatar |
||||
import com.keylesspalace.tusky.viewdata.ChatViewData |
||||
import java.text.SimpleDateFormat |
||||
import java.util.* |
||||
|
||||
class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) { |
||||
object Key { |
||||
const val KEY_CREATED = "created" |
||||
} |
||||
|
||||
private val avatar: ImageView = view.findViewById(R.id.status_avatar) |
||||
private val avatarInset: ImageView = view.findViewById(R.id.status_avatar_inset) |
||||
private val displayName: TextView = view.findViewById(R.id.status_display_name) |
||||
private val userName: TextView = view.findViewById(R.id.status_username) |
||||
private val timestamp: TextView = view.findViewById(R.id.status_timestamp_info) |
||||
private val content: TextView = view.findViewById(R.id.status_content) |
||||
private val unread: TextView = view.findViewById(R.id.chat_unread) |
||||
|
||||
private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) |
||||
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()) |
||||
|
||||
fun setupWithChat(chat: ChatViewData.Concrete, |
||||
listener: ChatActionListener, |
||||
statusDisplayOptions: StatusDisplayOptions, |
||||
localUserId: String, |
||||
payload: Any?) { |
||||
if (payload == null) { |
||||
displayName.text = chat.account.displayName?.emojify(chat.account.emojis, displayName, true) |
||||
?: "" |
||||
userName.text = userName.context.getString(R.string.status_username_format, chat.account.username) |
||||
setUpdatedAt(chat.updatedAt, statusDisplayOptions) |
||||
setAvatar(chat.account.avatar, chat.account.bot, statusDisplayOptions) |
||||
if (chat.unread <= 0) { |
||||
unread.visibility = View.GONE |
||||
} else if (chat.unread > 99) { |
||||
unread.text = ":)" |
||||
} else { |
||||
unread.text = chat.unread.toString() |
||||
} |
||||
avatar.setOnClickListener { listener.onViewAccount(chat.account.id) } |
||||
val onLongClickListener = View.OnLongClickListener { |
||||
listener.onMore(chat.id, it) |
||||
true |
||||
} |
||||
val onClickListener = View.OnClickListener { |
||||
val pos = adapterPosition |
||||
if (pos != RecyclerView.NO_POSITION) |
||||
listener.openChat(pos) |
||||
} |
||||
|
||||
content.setOnLongClickListener(onLongClickListener) |
||||
itemView.setOnLongClickListener(onLongClickListener) |
||||
content.setOnClickListener(onClickListener) |
||||
itemView.setOnClickListener(onClickListener) |
||||
|
||||
if(chat.lastMessage != null) { |
||||
var text = if (chat.lastMessage.content != null) { |
||||
content.setTypeface(null, Typeface.NORMAL) |
||||
|
||||
chat.lastMessage.content.emojify(chat.lastMessage.emojis, content, true) |
||||
} else if (chat.lastMessage.attachment != null) { |
||||
content.setTypeface(null, Typeface.ITALIC) |
||||
|
||||
content.resources.getString(chat.lastMessage.attachment.describeAttachmentType()) |
||||
} else if (chat.lastMessage.card != null) { |
||||
content.setTypeface(null, Typeface.ITALIC) |
||||
|
||||
content.resources.getString(R.string.link) |
||||
} else "" |
||||
|
||||
content.text = if(chat.lastMessage.accountId == localUserId) { |
||||
SpannableStringBuilder.valueOf(content.resources.getText(R.string.chat_our_last_message)) |
||||
.append(": $text") |
||||
} else text |
||||
|
||||
} else { |
||||
content.text = "" |
||||
} |
||||
} else { |
||||
if(payload is List<*>) { |
||||
for (item in payload as List<*>) { |
||||
if (Key.KEY_CREATED == item) { |
||||
setUpdatedAt(chat.updatedAt, statusDisplayOptions) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun setAvatar(url: String, |
||||
isBot: Boolean, |
||||
statusDisplayOptions: StatusDisplayOptions) { |
||||
avatar.setPaddingRelative(0, 0, 0, 0) |
||||
if (statusDisplayOptions.showBotOverlay && isBot) { |
||||
avatarInset.visibility = View.VISIBLE |
||||
avatarInset.setBackgroundColor(0x50ffffff) |
||||
Glide.with(avatarInset) |
||||
.load(R.drawable.ic_bot_24dp) |
||||
.into(avatarInset) |
||||
} else { |
||||
avatarInset.visibility = View.GONE |
||||
} |
||||
val avatarRadius = itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp); |
||||
loadAvatar(url, avatar, avatarRadius, |
||||
statusDisplayOptions.animateAvatars) |
||||
} |
||||
|
||||
|
||||
private fun getAbsoluteTime(createdAt: Date?): String? { |
||||
if (createdAt == null) { |
||||
return "??:??:??" |
||||
} |
||||
return if (DateUtils.isToday(createdAt.time)) { |
||||
shortSdf.format(createdAt) |
||||
} else { |
||||
longSdf.format(createdAt) |
||||
} |
||||
} |
||||
|
||||
private fun setUpdatedAt(updatedAt: Date, statusDisplayOptions: StatusDisplayOptions) { |
||||
if (statusDisplayOptions.useAbsoluteTime) { |
||||
timestamp.text = getAbsoluteTime(updatedAt) |
||||
} else { |
||||
val then = updatedAt.time |
||||
val now = System.currentTimeMillis() |
||||
val readout = TimestampUtils.getRelativeTimeSpanString(timestamp.context, then, now) |
||||
timestamp.text = readout |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
class ChatsAdapter(private val dataSource: TimelineAdapter.AdapterDataSource<ChatViewData>, |
||||
val statusDisplayOptions: StatusDisplayOptions, |
||||
private val chatActionListener: ChatActionListener, |
||||
val localUserId: String) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { |
||||
|
||||
private val VIEW_TYPE_CHAT = 0 |
||||
private val VIEW_TYPE_PLACEHOLDER = 1 |
||||
|
||||
override fun getItemCount(): Int { |
||||
return dataSource.itemCount |
||||
} |
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { |
||||
bindViewHolder(holder, position, null) |
||||
} |
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList<Any>) { |
||||
bindViewHolder(holder, position, payload) |
||||
} |
||||
|
||||
private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>?) { |
||||
val chat: ChatViewData = dataSource.getItemAt(position) |
||||
if(holder is PlaceholderViewHolder) { |
||||
holder.setup(chatActionListener, (chat as ChatViewData.Placeholder).isLoading) |
||||
} else if(holder is ChatsViewHolder) { |
||||
holder.setupWithChat(chat as ChatViewData.Concrete, chatActionListener, |
||||
statusDisplayOptions, localUserId, |
||||
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null) |
||||
} |
||||
} |
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { |
||||
if(viewType == VIEW_TYPE_CHAT ) { |
||||
val view = LayoutInflater.from(parent.context) |
||||
.inflate(R.layout.item_chat, parent, false) |
||||
return ChatsViewHolder(view) |
||||
} |
||||
// else VIEW_TYPE_PLACEHOLDER |
||||
|
||||
val view = LayoutInflater.from(parent.context) |
||||
.inflate(R.layout.item_status_placeholder, parent, false) |
||||
return PlaceholderViewHolder(view) |
||||
} |
||||
|
||||
override fun getItemViewType(position: Int): Int { |
||||
if(dataSource.getItemAt(position) is ChatViewData.Concrete) |
||||
return VIEW_TYPE_CHAT |
||||
|
||||
return VIEW_TYPE_PLACEHOLDER |
||||
} |
||||
|
||||
override fun getItemId(position: Int): Long { |
||||
return dataSource.getItemAt(position).getViewDataId().toLong() |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@ |
||||
package com.keylesspalace.tusky.components.chat |
||||
|
||||
import com.keylesspalace.tusky.components.common.CommonComposeViewModel |
||||
import com.keylesspalace.tusky.components.common.MediaUploader |
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity |
||||
import com.keylesspalace.tusky.db.AccountManager |
||||
import com.keylesspalace.tusky.db.AppDatabase |
||||
import com.keylesspalace.tusky.network.MastodonApi |
||||
import com.keylesspalace.tusky.service.ServiceClient |
||||
import com.keylesspalace.tusky.util.* |
||||
import javax.inject.Inject |
||||
|
||||
open class ChatViewModel |
||||
@Inject constructor( |
||||
private val api: MastodonApi, |
||||
private val accountManager: AccountManager, |
||||
private val mediaUploader: MediaUploader, |
||||
private val serviceClient: ServiceClient, |
||||
private val saveTootHelper: SaveTootHelper, |
||||
private val db: AppDatabase |
||||
) : CommonComposeViewModel(api, accountManager, mediaUploader, db) { |
||||
|
||||
fun getSingleMedia() : ComposeActivity.QueuedMedia? { |
||||
return if(media.value?.isNotEmpty() == true) |
||||
media.value?.get(0) |
||||
else null |
||||
} |
||||
|
||||
} |
@ -0,0 +1,378 @@ |
||||
/* Copyright 2019 Tusky Contributors |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */ |
||||
package com.keylesspalace.tusky.components.common |
||||
|
||||
import android.net.Uri |
||||
import android.util.Log |
||||
import androidx.lifecycle.LiveData |
||||
import androidx.lifecycle.MutableLiveData |
||||
import androidx.lifecycle.Observer |
||||
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter |
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia |
||||
import com.keylesspalace.tusky.components.search.SearchType |
||||
import com.keylesspalace.tusky.db.AccountManager |
||||
import com.keylesspalace.tusky.db.AppDatabase |
||||
import com.keylesspalace.tusky.db.InstanceEntity |
||||
import com.keylesspalace.tusky.entity.* |
||||
import com.keylesspalace.tusky.network.MastodonApi |
||||
import com.keylesspalace.tusky.util.* |
||||
import io.reactivex.Single |
||||
import io.reactivex.disposables.Disposable |
||||
import io.reactivex.rxkotlin.Singles |
||||
import retrofit2.Response |
||||
import java.util.* |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* Throw when trying to add an image when video is already present or the other way around |
||||
*/ |
||||
class VideoOrImageException : Exception() |
||||
|
||||
open class CommonComposeViewModel( |
||||
private val api: MastodonApi, |
||||
private val accountManager: AccountManager, |
||||
private val mediaUploader: MediaUploader, |
||||
private val db: AppDatabase |
||||
) : RxAwareViewModel() { |
||||
|
||||
protected val instance: MutableLiveData<InstanceEntity?> = MutableLiveData(null) |
||||
protected val nodeinfo: MutableLiveData<NodeInfo?> = MutableLiveData(null) |
||||
protected val stickers: MutableLiveData<Array<StickerPack>> = MutableLiveData(emptyArray()) |
||||
val haveStickers: MutableLiveData<Boolean> = MutableLiveData(false) |
||||
var tryFetchStickers = false |
||||
var anonymizeNames = true |
||||
var hasNoAttachmentLimits = false |
||||
|
||||
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance -> |
||||
ComposeInstanceParams( |
||||
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, |
||||
chatLimit = instance?.chatLimit ?: DEFAULT_CHARACTER_LIMIT, |
||||
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, |
||||
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, |
||||
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false |
||||
) |
||||
} |
||||
val instanceMetadata: LiveData<ComposeInstanceMetadata> = nodeinfo.map { nodeinfo -> |
||||
val software = nodeinfo?.software?.name ?: "mastodon" |
||||
|
||||
if(software.equals("pleroma")) { |
||||
hasNoAttachmentLimits = true |
||||
ComposeInstanceMetadata( |
||||
software = "pleroma", |
||||
supportsMarkdown = nodeinfo?.metadata?.postFormats?.contains("text/markdown") ?: false, |
||||
supportsBBcode = nodeinfo?.metadata?.postFormats?.contains("text/bbcode") ?: false, |
||||
supportsHTML = nodeinfo?.metadata?.postFormats?.contains("text/html") ?: false, |
||||
videoLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_VIDEO_SIZE_LIMIT, |
||||
imageLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_IMAGE_SIZE_LIMIT |
||||
) |
||||
} else if(software.equals("pixelfed")) { |
||||
ComposeInstanceMetadata( |
||||
software = "pixelfed", |
||||
supportsMarkdown = false, |
||||
supportsBBcode = false, |
||||
supportsHTML = false, |
||||
videoLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_VIDEO_SIZE_LIMIT, |
||||
imageLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_IMAGE_SIZE_LIMIT |
||||
) |
||||
} else { |
||||
ComposeInstanceMetadata( |
||||
software = "mastodon", |
||||
supportsMarkdown = nodeinfo?.software?.version?.contains("+glitch") ?: false, |
||||
supportsBBcode = false, |
||||
supportsHTML = nodeinfo?.software?.version?.contains("+glitch") ?: false, |
||||
videoLimit = STATUS_VIDEO_SIZE_LIMIT, |
||||
imageLimit = STATUS_IMAGE_SIZE_LIMIT |
||||
) |
||||
} |
||||
} |
||||
val instanceStickers: LiveData<Array<StickerPack>> = stickers // .map { stickers -> HashMap<String,String>(stickers) } |
||||
|
||||
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData() |
||||
|
||||
val media = mutableLiveData<List<QueuedMedia>>(listOf()) |
||||
val uploadError = MutableLiveData<Throwable>() |
||||
|
||||
protected val mediaToDisposable = mutableMapOf<Long, Disposable>() |
||||
|
||||
init { |
||||
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance -> |
||||
InstanceEntity( |
||||
instance = accountManager.activeAccount?.domain!!, |
||||
emojiList = emojis, |
||||
maximumTootCharacters = instance.maxTootChars, |
||||
maxPollOptions = instance.pollLimits?.maxOptions, |
||||
maxPollOptionLength = instance.pollLimits?.maxOptionChars, |
||||
version = instance.version, |
||||
chatLimit = instance.chatLimit |
||||
) |
||||
} |
||||
.doOnSuccess { |
||||
db.instanceDao().insertOrReplace(it) |
||||
} |
||||
.onErrorResumeNext( |
||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) |
||||
) |
||||
.subscribe ({ instanceEntity -> |
||||
emoji.postValue(instanceEntity.emojiList) |
||||
instance.postValue(instanceEntity) |
||||
}, { throwable -> |
||||
// this can happen on network error when no cached data is available |
||||
Log.w(TAG, "error loading instance data", throwable) |
||||
}) |
||||
.autoDispose() |
||||
|
||||
|
||||
api.getNodeinfoLinks().subscribe({ |
||||
links -> if(links.links.isNotEmpty()) { |
||||
api.getNodeinfo(links.links[0].href).subscribe({ |
||||
ni -> nodeinfo.postValue(ni) |
||||
}, { |
||||
err -> Log.d(TAG, "Failed to get nodeinfo", err) |
||||
}).autoDispose() |
||||
} |
||||
}, { err -> |
||||
Log.d(TAG, "Failed to get nodeinfo links", err) |
||||
}).autoDispose() |
||||
} |
||||
|
||||
fun pickMedia(uri: Uri, filename: String?): LiveData<Either<Throwable, QueuedMedia>> { |
||||
// We are not calling .toLiveData() here because we don't want to stop the process when |
||||
// the Activity goes away temporarily (like on screen rotation). |
||||
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>() |
||||
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT |
||||
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT |
||||
|
||||
mediaUploader.prepareMedia(uri, videoLimit, imageLimit, filename) |
||||
.map { (type, uri, size) -> |
||||
val mediaItems = media.value!! |
||||
if (!hasNoAttachmentLimits |
||||
&& type != QueuedMedia.Type.IMAGE |
||||
&& mediaItems.isNotEmpty() |
||||
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) { |
||||
throw VideoOrImageException() |
||||
} else { |
||||
addMediaToQueue(type, uri, size, filename ?: "unknown", anonymizeNames) |
||||
} |
||||
} |
||||
.subscribe({ queuedMedia -> |
||||
liveData.postValue(Either.Right(queuedMedia)) |
||||
}, { error -> |
||||
liveData.postValue(Either.Left(error)) |
||||
}) |
||||
.autoDispose() |
||||
return liveData |
||||
} |
||||
|
||||
private fun addMediaToQueue(type: Int, uri: Uri, mediaSize: Long, filename: String, anonymizeNames: Boolean): QueuedMedia { |
||||
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize, filename, |
||||
hasNoAttachmentLimits, anonymizeNames) |
||||
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT |
||||
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT |
||||
|
||||
media.value = media.value!! + mediaItem |
||||
mediaToDisposable[mediaItem.localId] = mediaUploader |
||||
.uploadMedia(mediaItem, videoLimit, imageLimit ) |
||||
.subscribe ({ event -> |
||||
val item = media.value?.find { it.localId == mediaItem.localId } |
||||
?: return@subscribe |
||||
val newMediaItem = when (event) { |
||||
is UploadEvent.ProgressEvent -> |
||||
item.copy(uploadPercent = event.percentage) |
||||
is UploadEvent.FinishedEvent -> |
||||
item.copy(id = event.attachment.id, uploadPercent = -1) |
||||
} |
||||
synchronized(media) { |
||||
val mediaValue = media.value!! |
||||
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId } |
||||
media.postValue(if (index == -1) { |
||||
mediaValue + newMediaItem |
||||
} else { |
||||
mediaValue.toMutableList().also { it[index] = newMediaItem } |
||||
}) |
||||
} |
||||
}, { error -> |
||||
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) |
||||
uploadError.postValue(error) |
||||
}) |
||||
return mediaItem |
||||
} |
||||
|
||||
protected fun addUploadedMedia(id: String, type: Int, uri: Uri, description: String?) { |
||||
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, "unknown", |
||||
hasNoAttachmentLimits, anonymizeNames, -1, id, description) |
||||
media.value = media.value!! + mediaItem |
||||
} |
||||
|
||||
fun removeMediaFromQueue(item: QueuedMedia) { |
||||
mediaToDisposable[item.localId]?.dispose() |
||||
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } |
||||
} |
||||
|
||||
fun updateDescription(localId: Long, description: String): LiveData<Boolean> { |
||||
val newList = media.value!!.toMutableList() |
||||
val index = newList.indexOfFirst { it.localId == localId } |
||||
if (index != -1) { |
||||
newList[index] = newList[index].copy(description = description) |
||||
} |
||||
media.value = newList |
||||
val completedCaptioningLiveData = MutableLiveData<Boolean>() |
||||
media.observeForever(object : Observer<List<QueuedMedia>> { |
||||
override fun onChanged(mediaItems: List<QueuedMedia>) { |
||||
val updatedItem = mediaItems.find { it.localId == localId } |
||||
if (updatedItem == null) { |
||||
media.removeObserver(this) |
||||
} else if (updatedItem.id != null) { |
||||
api.updateMedia(updatedItem.id, description) |
||||
.subscribe({ |
||||
completedCaptioningLiveData.postValue(true) |
||||
}, { |
||||
completedCaptioningLiveData.postValue(false) |
||||
}) |
||||
.autoDispose() |
||||
media.removeObserver(this) |
||||
} |
||||
} |
||||
}) |
||||
return completedCaptioningLiveData |
||||
} |
||||
|
||||
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> { |
||||
when (token[0]) { |
||||
'@' -> { |
||||
return try { |
||||
api.searchAccounts(query = token.substring(1), limit = 10) |
||||
.blockingGet() |
||||
.map { ComposeAutoCompleteAdapter.AccountResult(it) } |
||||
} catch (e: Throwable) { |
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) |
||||
emptyList() |
||||
} |
||||
} |
||||
'#' -> { |
||||
return try { |
||||
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) |
||||
.blockingGet() |
||||
.hashtags |
||||
.map { ComposeAutoCompleteAdapter.HashtagResult(it) } |
||||
} catch (e: Throwable) { |
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) |
||||
emptyList() |
||||
} |
||||
} |
||||
':' -> { |
||||
val emojiList = emoji.value ?: return emptyList() |
||||
|
||||
val incomplete = token.substring(1).toLowerCase(Locale.ROOT) |
||||
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>() |
||||
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>() |
||||
for (emoji in emojiList) { |
||||
val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT) |
||||
if (shortcode.startsWith(incomplete)) { |
||||
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji)) |
||||
} else if (shortcode.indexOf(incomplete, 1) != -1) { |
||||
resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji)) |
||||
} |
||||
} |
||||
if (results.isNotEmpty() && resultsInside.isNotEmpty()) { |
||||
results.add(ComposeAutoCompleteAdapter.ResultSeparator()) |
||||
} |
||||
results.addAll(resultsInside) |
||||
return results |
||||
} |
||||
else -> { |
||||
Log.w(TAG, "Unexpected autocompletion token: $token") |
||||
return emptyList() |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun onCleared() { |
||||
for (uploadDisposable in mediaToDisposable.values) { |
||||
uploadDisposable.dispose() |
||||
} |
||||
super.onCleared() |
||||
} |
||||
|
||||
private fun getStickers() { |
||||
if(!tryFetchStickers) |
||||
return |
||||
|
||||
api.getStickers().subscribe({ stickers -> |
||||
if (stickers.isNotEmpty()) { |
||||
haveStickers.postValue(true) |
||||
|
||||
val singles = mutableListOf<Single<Response<StickerPack>>>() |
||||
|
||||
for(entry in stickers) { |
||||
val url = entry.value.removePrefix("/").removeSuffix("/") + "/pack.json"; |
||||
singles += api.getStickerPack(url) |
||||
} |
||||
|
||||
Single.zip(singles) { |
||||
it.map { |
||||
it as Response<StickerPack> |
||||
it.body()!!.internal_url = it.raw().request.url.toString().removeSuffix("pack.json") |
||||
it.body()!! |
||||
} |
||||
}.onErrorReturn { |
||||
Log.d(TAG, "Failed to get sticker pack.json", it) |
||||
emptyList() |
||||
}.subscribe() { pack -> |
||||
if(pack.isNotEmpty()) { |
||||
val array = pack.toTypedArray() |
||||
array.sort() |
||||
this.stickers.postValue(array) |
||||
} |
||||
}.autoDispose() |
||||
} |
||||
}, { |
||||
err -> Log.d(TAG, "Failed to get sticker.json", err) |
||||
}).autoDispose() |
||||
} |
||||
|
||||
fun setup() { |
||||
getStickers() // early as possible |
||||
} |
||||
|
||||
private companion object { |
||||
const val TAG = "CCVM" |
||||
} |
||||
|
||||
} |
||||
|
||||
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default } |
||||
|
||||
const val DEFAULT_CHARACTER_LIMIT = 500 |
||||
const val DEFAULT_MAX_OPTION_COUNT = 4 |
||||
const val DEFAULT_MAX_OPTION_LENGTH = 25 |
||||
const val STATUS_VIDEO_SIZE_LIMIT : Long = 41943040 // 40MiB |
||||
const val STATUS_IMAGE_SIZE_LIMIT : Long = 8388608 // 8MiB |
||||
|
||||
data class ComposeInstanceParams( |
||||
val maxChars: Int, |
||||
val chatLimit: Int, |
||||
val pollMaxOptions: Int, |
||||
val pollMaxLength: Int, |
||||
val supportsScheduled: Boolean |
||||
) |
||||
|
||||
data class ComposeInstanceMetadata( |
||||
val software: String, |
||||
val supportsMarkdown: Boolean, |
||||
val supportsBBcode: Boolean, |
||||
val supportsHTML: Boolean, |
||||
val videoLimit: Long, |
||||
val imageLimit: Long |
||||
) |
@ -0,0 +1,84 @@ |
||||
package com.keylesspalace.tusky.components.notifications |
||||
|
||||
import android.util.Log |
||||
import com.keylesspalace.tusky.db.AccountEntity |
||||
import com.keylesspalace.tusky.db.AccountManager |
||||
import com.keylesspalace.tusky.entity.Marker |
||||
import com.keylesspalace.tusky.entity.Notification |
||||
import com.keylesspalace.tusky.network.MastodonApi |
||||
import com.keylesspalace.tusky.util.isLessThan |
||||
import javax.inject.Inject |
||||
|
||||
class NotificationFetcher @Inject constructor( |
||||
private val mastodonApi: MastodonApi, |
||||
private val accountManager: AccountManager, |
||||
private val notifier: Notifier |
||||
) { |
||||
fun fetchAndShow() { |
||||
for (account in accountManager.getAllAccountsOrderedByActive()) { |
||||
if (account.notificationsEnabled) { |
||||
try { |
||||
val notifications = fetchNotifications(account) |
||||
notifications.forEachIndexed { index, notification -> |
||||
notifier.show(notification, account, index == 0) |
||||
} |
||||
accountManager.saveAccount(account) |
||||
} catch (e: Exception) { |
||||
Log.w(TAG, "Error while fetching notifications", e) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun fetchNotifications(account: AccountEntity): MutableList<Notification> { |
||||
val authHeader = String.format("Bearer %s", account.accessToken) |
||||
// We fetch marker to not load/show notifications which user has already seen |
||||
val marker = fetchMarker(authHeader, account) |
||||
if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) { |
||||
account.lastNotificationId = marker.lastReadId |
||||
} |
||||
Log.d(TAG, "getting Notifications for " + account.fullName) |
||||
val notifications = mastodonApi.notificationsWithAuth( |
||||
authHeader, |
||||
account.domain, |
||||
account.lastNotificationId, |
||||
true, |
||||
Notification.Type.asStringList |
||||
).blockingGet() |
||||
|
||||
val newId = account.lastNotificationId |
||||
var newestId = "" |
||||
val result = mutableListOf<Notification>() |
||||
for (notification in notifications.reversed()) { |
||||
val currentId = notification.id |
||||
if (newestId.isLessThan(currentId)) { |
||||
newestId = currentId |
||||
account.lastNotificationId = currentId |
||||
} |
||||
if (newId.isLessThan(currentId)) { |
||||
result.add(notification) |
||||
} |
||||
} |
||||
return result |
||||
} |
||||
|
||||
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? { |
||||
return try { |
||||
val allMarkers = mastodonApi.markersWithAuth( |
||||
authHeader, |
||||
account.domain, |
||||
listOf("notifications") |
||||
).blockingGet() |
||||
val notificationMarker = allMarkers["notifications"] |
||||
Log.d(TAG, "Fetched marker: $notificationMarker") |
||||
notificationMarker |
||||
} catch (e: Exception) { |
||||
Log.e(TAG, "Failed to fetch marker", e) |
||||
null |
||||
} |
||||
} |
||||
|
||||
companion object { |
||||
const val TAG = "NotificationFetcher" |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
package com.keylesspalace.tusky.components.notifications |
||||
|
||||
import android.content.Context |
||||
import com.keylesspalace.tusky.db.AccountEntity |
||||
import com.keylesspalace.tusky.entity.Notification |
||||
|
||||
/** |
||||
* Shows notifications. |
||||
*/ |
||||
interface Notifier { |
||||
fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) |
||||
} |
||||
|
||||
class SystemNotifier( |
||||
private val context: Context |
||||
) : Notifier { |
||||
override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) { |
||||
NotificationHelper.make(context, notification, account, isFirstInBatch) |
||||
} |
||||
} |
@ -0,0 +1,258 @@ |
||||
package com.keylesspalace.tusky.components.preference |
||||
|
||||
import android.app.AlarmManager |
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.os.Build |
||||
import android.util.Log |
||||
import android.view.LayoutInflater |
||||
import android.view.View |
||||
import android.widget.* |
||||
import androidx.appcompat.app.AlertDialog |
||||
import androidx.preference.Preference |
||||
import androidx.preference.PreferenceManager |
||||
import com.keylesspalace.tusky.R |
||||
import com.keylesspalace.tusky.SplashActivity |
||||
import com.keylesspalace.tusky.util.EmojiCompatFont |
||||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS |
||||
import io.reactivex.android.schedulers.AndroidSchedulers |
||||
import io.reactivex.disposables.Disposable |
||||
import okhttp3.OkHttpClient |
||||
import kotlin.system.exitProcess |
||||
|
||||
/** |
||||
* This Preference lets the user select their preferred emoji font |
||||
*/ |
||||
class EmojiPreference( |
||||
context: Context, |
||||
private val okHttpClient: OkHttpClient |
||||
) : Preference(context) { |
||||
|
||||
private lateinit var selected: EmojiCompatFont |
||||
private lateinit var original: EmojiCompatFont |
||||
private val radioButtons = mutableListOf<RadioButton>() |
||||
private var updated = false |
||||
private var currentNeedsUpdate = false |
||||
|
||||
private val downloadDisposables = MutableList<Disposable?>(FONTS.size) { null } |
||||
|
||||
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) { |
||||
super.onAttachedToHierarchy(preferenceManager) |
||||
|
||||
// Find out which font is currently active |
||||
selected = EmojiCompatFont.byId( |
||||
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0) |
||||
) |
||||
// We'll use this later to determine if anything has changed |
||||
original = selected |
||||
summary = selected.getDisplay(context) |
||||
} |
||||
|
||||
override fun onClick() { |
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null) |
||||
viewIds.forEachIndexed { index, viewId -> |
||||
setupItem(view.findViewById(viewId), FONTS[index]) |
||||
} |
||||
AlertDialog.Builder(context) |
||||
.setView(view) |
||||
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } |
||||
.setNegativeButton(android.R.string.cancel, null) |
||||
.show() |
||||
} |
||||
|
||||
private fun setupItem(container: View, font: EmojiCompatFont) { |
||||
val title: TextView = container.findViewById(R.id.emojicompat_name) |
||||
val caption: TextView = container.findViewById(R.id.emojicompat_caption) |
||||
val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb) |
||||
val download: ImageButton = container.findViewById(R.id.emojicompat_download) |
||||
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) |
||||
val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) |
||||
|
||||
// Initialize all the views |
||||
title.text = font.getDisplay(container.context) |
||||
caption.setText(font.caption) |
||||
thumb.setImageResource(font.img) |
||||
|
||||
// There needs to be a list of all the radio buttons in order to uncheck them when one is selected |
||||
radioButtons.add(radio) |
||||
updateItem(font, container) |
||||
|
||||
// Set actions |
||||
download.setOnClickListener { startDownload(font, container) } |
||||
cancel.setOnClickListener { cancelDownload(font, container) } |
||||
radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } |
||||
container.setOnClickListener { containerView: View -> |
||||
select(font, containerView.findViewById(R.id.emojicompat_radio)) |
||||
} |
||||
} |
||||
|
||||
private fun startDownload(font: EmojiCompatFont, container: View) { |
||||
val download: ImageButton = container.findViewById(R.id.emojicompat_download) |
||||
val caption: TextView = container.findViewById(R.id.emojicompat_caption) |
||||
val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress) |
||||
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) |
||||
|
||||
// Switch to downloading style |
||||
download.visibility = View.GONE |
||||
caption.visibility = View.INVISIBLE |
||||
progressBar.visibility = View.VISIBLE |
||||
progressBar.progress = 0 |
||||
cancel.visibility = View.VISIBLE |
||||
font.downloadFontFile(context, okHttpClient) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe( |
||||
{ progress -> |
||||
// The progress is returned as a float between 0 and 1, or -1 if it could not determined |
||||
if (progress >= 0) { |
||||
progressBar.isIndeterminate = false |
||||
val max = progressBar.max.toFloat() |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
||||
progressBar.setProgress((max * progress).toInt(), true) |
||||
} else { |
||||
progressBar.progress = (max * progress).toInt() |
||||
} |
||||
} else { |
||||
progressBar.isIndeterminate = true |
||||
} |
||||
}, |
||||
{ |
||||
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() |
||||
updateItem(font, container) |
||||
}, |
||||
{ |
||||
finishDownload(font, container) |
||||
} |
||||
).also { downloadDisposables[font.id] = it } |
||||
|
||||
|
||||
} |
||||
|
||||
private fun cancelDownload(font: EmojiCompatFont, container: View) { |
||||
font.deleteDownloadedFile(container.context) |
||||
downloadDisposables[font.id]?.dispose() |
||||
downloadDisposables[font.id] = null |
||||
updateItem(font, container) |
||||
} |
||||
|
||||
private fun finishDownload(font: EmojiCompatFont, container: View) { |
||||
select(font, container.findViewById(R.id.emojicompat_radio)) |
||||
updateItem(font, container) |
||||
// Set the flag to restart the app (because an update has been downloaded) |
||||
if (selected === original && currentNeedsUpdate) { |
||||
updated = true |
||||
currentNeedsUpdate = false |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Select a font both visually and logically |
||||
* |
||||
* @param font The font to be selected |
||||
* @param radio The radio button associated with it's visual item |
||||
*/ |
||||
private fun select(font: EmojiCompatFont, radio: RadioButton) { |
||||
selected = font |
||||
// Uncheck all the other buttons |
||||
for (other in radioButtons) { |
||||
if (other !== radio) { |
||||
other.isChecked = false |
||||
} |
||||
} |
||||
radio.isChecked = true |
||||
} |
||||
|
||||
/** |
||||
* Called when a "consistent" state is reached, i.e. it's not downloading the font |
||||
* |
||||
* @param font The font to be displayed |
||||
* @param container The ConstraintLayout containing the item |
||||
*/ |
||||
private fun updateItem(font: EmojiCompatFont, container: View) { |
||||
// Assignments |
||||
val download: ImageButton = container.findViewById(R.id.emojicompat_download) |
||||
val caption: TextView = container.findViewById(R.id.emojicompat_caption) |
||||
val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress) |
||||
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) |
||||
val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) |
||||
|
||||
// There's no download going on |
||||
progress.visibility = View.GONE |
||||
cancel.visibility = View.GONE |
||||
caption.visibility = View.VISIBLE |
||||
if (font.isDownloaded(context)) { |
||||
// Make it selectable |
||||
download.visibility = View.GONE |
||||
radio.visibility = View.VISIBLE |
||||
container.isClickable = true |
||||
} else { |
||||
// Make it downloadable |
||||
download.visibility = View.VISIBLE |
||||
radio.visibility = View.GONE |
||||
container.isClickable = false |
||||
} |
||||
|
||||
// Select it if necessary |
||||
if (font === selected) { |
||||
radio.isChecked = true |
||||
// Update available |
||||
if (!font.isDownloaded(context)) { |
||||
currentNeedsUpdate = true |
||||
} |
||||
} else { |
||||
radio.isChecked = false |
||||
} |
||||
} |
||||
|
||||
private fun saveSelectedFont() { |
||||
val index = selected.id |
||||
Log.i(TAG, "saveSelectedFont: Font ID: $index") |
||||
PreferenceManager |
||||
.getDefaultSharedPreferences(context) |
||||
.edit() |
||||
.putInt(key, index) |
||||
.apply() |
||||
summary = selected.getDisplay(context) |
||||
} |
||||
|
||||
/** |
||||
* User clicked ok -> save the selected font and offer to restart the app if something changed |
||||
*/ |
||||
private fun onDialogOk() { |
||||
saveSelectedFont() |
||||
if (selected !== original || updated) { |
||||
AlertDialog.Builder(context) |
||||
.setTitle(R.string.restart_required) |
||||
.setMessage(R.string.restart_emoji) |
||||
.setNegativeButton(R.string.later, null) |
||||
.setPositiveButton(R.string.restart) { _, _ -> |
||||
// Restart the app |
||||
// From https://stackoverflow.com/a/17166729/5070653 |
||||
val launchIntent = Intent(context, SplashActivity::class.java) |
||||
val mPendingIntent = PendingIntent.getActivity( |
||||
context, |
||||
0x1f973, // This is the codepoint of the party face emoji :D |
||||
launchIntent, |
||||
PendingIntent.FLAG_CANCEL_CURRENT) |
||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager |
||||
mgr.set( |
||||
AlarmManager.RTC, |
||||
System.currentTimeMillis() + 100, |
||||
mPendingIntent) |
||||
exitProcess(0) |
||||
}.show() |
||||
} |
||||
} |
||||
|
||||
companion object { |
||||
private const val TAG = "EmojiPreference" |
||||
|
||||
// Please note that this array must sorted in the same way as the fonts. |
||||
private val viewIds = intArrayOf( |
||||
R.id.item_nomoji, |
||||
R.id.item_blobmoji, |
||||
R.id.item_twemoji, |
||||
R.id.item_notoemoji |
||||
) |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue