diff --git a/app/build.gradle b/app/build.gradle index cbc5dce..5c67bff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +androidExtensions { + experimental = true +} + android { compileSdkVersion 26 buildToolsVersion "26.0.1" @@ -96,10 +101,6 @@ dependencies { compile 'com.jakewharton.timber:timber:4.5.1' // Misc - compile 'nz.bradcampbell:paperparcel:2.0.1' - compile 'nz.bradcampbell:paperparcel-kotlin:2.0.1' - kapt 'nz.bradcampbell:paperparcel-compiler:2.0.1' - compile('com.mikepenz:materialdrawer:5.9.0@aar') { transitive = true } @@ -109,6 +110,8 @@ dependencies { compile 'uk.co.chrisjenx:calligraphy:2.2.0' compile 'com.vanniktech:emoji-one:0.5.1' + + compile 'commons-io:commons-io:2.5' } repositories { jcenter() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cee9f37..cf074a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,16 @@ android:name=".view.activity.SettingActivity" android:label="@string/title_activity_setting" android:windowSoftInputMode="adjustResize" /> + + + + + + + diff --git a/app/src/main/assets/fonts/Ricty-Regular.ttf b/app/src/main/assets/fonts/Ricty-Regular.ttf new file mode 100755 index 0000000..a41f63c Binary files /dev/null and b/app/src/main/assets/fonts/Ricty-Regular.ttf differ diff --git a/app/src/main/java/com/geckour/egret/App.kt b/app/src/main/java/com/geckour/egret/App.kt index ea04bac..cda28af 100644 --- a/app/src/main/java/com/geckour/egret/App.kt +++ b/app/src/main/java/com/geckour/egret/App.kt @@ -12,11 +12,8 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.one.EmojiOneProvider -import paperparcel.Adapter -import paperparcel.ProcessorConfig import timber.log.Timber -@ProcessorConfig(adapters = arrayOf(Adapter(SpannedTypeAdapter::class))) class App: Application() { companion object { diff --git a/app/src/main/java/com/geckour/egret/api/MastodonClient.kt b/app/src/main/java/com/geckour/egret/api/MastodonClient.kt index 6c4944a..ccd1e5c 100644 --- a/app/src/main/java/com/geckour/egret/api/MastodonClient.kt +++ b/app/src/main/java/com/geckour/egret/api/MastodonClient.kt @@ -41,9 +41,11 @@ class MastodonClient(baseUrl: String) { password: String ): Single = service.authUser(clientId, clientSecret, username, password) - fun getSelfAccount(): Single = service.getSelfAccount() + fun getOwnAccount(): Single = service.getOwnAccount() - fun getAccount(accountId: Long): Observable = service.getAccount(accountId) + fun getAccount(accountId: Long): Single = service.getAccount(accountId) + + fun updateOwnAccount(displayName: String? = null, note: String? = null, avatarUrl: String? = null, headerUrl: String? = null): Single = service.updateOwnAccount(displayName, note, avatarUrl, headerUrl) fun getPublicTimelineAsStream(): Observable = streamService.getPublicTimelineAsStream() @@ -63,6 +65,8 @@ class MastodonClient(baseUrl: String) { fun getNotificationTimeline(maxId: Long? = null, sinceId: Long? = null): Single>> = service.getNotificationTimeline(maxId, sinceId) + fun getFavouriteTimeline(maxId: Long? = null, sinceId: Long? = null): Single>> = service.getFavouriteTimeline(maxId, sinceId) + fun getAccountAllToots(accountId: Long, maxId: Long? = null, sinceId: Long? = null): Single>> = service.getAccountAllToots(accountId, maxId, sinceId) fun favoriteByStatusId(statusId: Long): Single = service.favoriteStatusById(statusId) diff --git a/app/src/main/java/com/geckour/egret/api/model/Account.kt b/app/src/main/java/com/geckour/egret/api/model/Account.kt index 8643a42..4899241 100644 --- a/app/src/main/java/com/geckour/egret/api/model/Account.kt +++ b/app/src/main/java/com/geckour/egret/api/model/Account.kt @@ -30,19 +30,19 @@ data class Account( @SerializedName("statuses_count") var statusesCount: Long, - val note: String, + var note: String, val url: URL, @SerializedName("avatar") - val avatarUrl: String, + var avatarUrl: String, @SerializedName("avatar_static") - val avatarImg: String, + var avatarUrlStatic: String, @SerializedName("header") - val headerUrl: String, + var headerUrl: String, @SerializedName("header_static") - val headerImg: String + var headerUrlStatic: String ): Serializable \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/api/model/Attachment.kt b/app/src/main/java/com/geckour/egret/api/model/Attachment.kt index 3917485..68647f9 100644 --- a/app/src/main/java/com/geckour/egret/api/model/Attachment.kt +++ b/app/src/main/java/com/geckour/egret/api/model/Attachment.kt @@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName data class Attachment( val id: Long, - var type: String, + var type: Type, var url: String, @@ -18,9 +18,10 @@ data class Attachment( @SerializedName("text_url") var urlInText: String? ) { - enum class Type(val rowValue: Int) { - image(0), - video(1), - gifv(2) - } + + enum class Type { + image, + video, + gifv + } } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/api/model/Relationship.kt b/app/src/main/java/com/geckour/egret/api/model/Relationship.kt index eeb73a8..ad39bfc 100644 --- a/app/src/main/java/com/geckour/egret/api/model/Relationship.kt +++ b/app/src/main/java/com/geckour/egret/api/model/Relationship.kt @@ -16,5 +16,5 @@ data class Relationship( var muting: Boolean, @SerializedName("requested") - var requestedAllowToFollow: Boolean + var hasSendFollowRequest: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/api/service/MastodonService.kt b/app/src/main/java/com/geckour/egret/api/service/MastodonService.kt index 695d95f..8b64131 100644 --- a/app/src/main/java/com/geckour/egret/api/service/MastodonService.kt +++ b/app/src/main/java/com/geckour/egret/api/service/MastodonService.kt @@ -56,13 +56,29 @@ interface MastodonService { ): Single @GET("api/v1/accounts/verify_credentials") - fun getSelfAccount(): Single + fun getOwnAccount(): Single @GET("api/v1/accounts/{id}") fun getAccount( @Path("id") accountId: Long - ): Observable + ): Single + + @FormUrlEncoded + @PATCH("api/v1/accounts/update_credentials") + fun updateOwnAccount( + @Field("display_name") + displayName: String? = null, + + @Field("note") + note: String? = null, + + @Field("avatar") + avatar: String? = null, + + @Field("header") + headeer: String? = null + ): Single @GET("api/v1/streaming/public") @Streaming @@ -131,6 +147,18 @@ interface MastodonService { limit: Long? = 30 ): Single>> + @GET("api/v1/favourites") + fun getFavouriteTimeline( + @Query("max_id") + maxId: Long? = null, + + @Query("since_id") + sinceId: Long? = null, + + @Query("limit") + limit: Long? = 40 + ): Single>> + @GET("api/v1/accounts/{id}/statuses") fun getAccountAllToots( @Path("id") diff --git a/app/src/main/java/com/geckour/egret/model/AccessToken.kt b/app/src/main/java/com/geckour/egret/model/AccessToken.kt index d3295e5..023ad8c 100644 --- a/app/src/main/java/com/geckour/egret/model/AccessToken.kt +++ b/app/src/main/java/com/geckour/egret/model/AccessToken.kt @@ -4,7 +4,7 @@ import com.github.gfx.android.orma.annotation.* import java.util.* @Table -class AccessToken( +data class AccessToken( @Setter("id") @PrimaryKey(autoincrement = true) val id: Long = -1L, @Setter("access_token") @Column val token: String = "", @Setter("instance_id") @Column val instanceId: Long = -1L, diff --git a/app/src/main/java/com/geckour/egret/model/Draft.kt b/app/src/main/java/com/geckour/egret/model/Draft.kt new file mode 100644 index 0000000..83ce697 --- /dev/null +++ b/app/src/main/java/com/geckour/egret/model/Draft.kt @@ -0,0 +1,27 @@ +package com.geckour.egret.model + +import com.geckour.egret.api.model.Attachment +import com.geckour.egret.api.service.MastodonService +import com.github.gfx.android.orma.annotation.Column +import com.github.gfx.android.orma.annotation.PrimaryKey +import com.github.gfx.android.orma.annotation.Setter +import com.github.gfx.android.orma.annotation.Table + +@Table +data class Draft( + @Setter("id") @PrimaryKey(autoincrement = true) val id: Long = -1L, + @Setter("tokenId") @Column(indexed = true) var tokenId: Long = -1L, + @Setter("body") @Column var body: String = "", + @Setter("alertBody") @Column var alertBody: String = "", + @Setter("inReplyToId") @Column var inReplyToId: Long? = null, + @Setter("inReplyToName") @Column var inReplyToName: String? = null, + @Setter("attachments") @Column var attachments: Attachments, + @Setter("warning") @Column var warning: Boolean = false, + @Setter("sensitive") @Column var sensitive: Boolean = false, + @Setter("visibility") @Column var visibility: Int = MastodonService.Visibility.public.ordinal, + @Setter("createdAt") @Column(indexed = true) var createdAt: Long = System.currentTimeMillis() +) { + data class Attachments( + val value: List = ArrayList() + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/model/InstanceAuthInfo.kt b/app/src/main/java/com/geckour/egret/model/InstanceAuthInfo.kt index f5658b5..2d24a1a 100644 --- a/app/src/main/java/com/geckour/egret/model/InstanceAuthInfo.kt +++ b/app/src/main/java/com/geckour/egret/model/InstanceAuthInfo.kt @@ -7,7 +7,7 @@ import com.github.gfx.android.orma.annotation.Table import java.util.* @Table -class InstanceAuthInfo( +data class InstanceAuthInfo( @Setter("id") @PrimaryKey(autoincrement = true) val id: Long = -1L, @Setter("instance") @Column val instance: String = "", @Setter("auth_id") @Column var authId: Long = -1L, diff --git a/app/src/main/java/com/geckour/egret/model/MuteHashTag.kt b/app/src/main/java/com/geckour/egret/model/MuteHashTag.kt index dfcce4c..ddcb29a 100644 --- a/app/src/main/java/com/geckour/egret/model/MuteHashTag.kt +++ b/app/src/main/java/com/geckour/egret/model/MuteHashTag.kt @@ -6,7 +6,7 @@ import com.github.gfx.android.orma.annotation.Setter import com.github.gfx.android.orma.annotation.Table @Table -class MuteHashTag( +data class MuteHashTag( @Setter("id") @PrimaryKey(autoincrement = true) val id: Long = -1L, @Setter("hashTag") @Column var hashTag: String = "" ) \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/model/MuteInstance.kt b/app/src/main/java/com/geckour/egret/model/MuteInstance.kt index e4095b8..e4f3316 100644 --- a/app/src/main/java/com/geckour/egret/model/MuteInstance.kt +++ b/app/src/main/java/com/geckour/egret/model/MuteInstance.kt @@ -6,7 +6,7 @@ import com.github.gfx.android.orma.annotation.Setter import com.github.gfx.android.orma.annotation.Table @Table -class MuteInstance( +data class MuteInstance( @Setter("id") @PrimaryKey(autoincrement = true) val id: Long = -1L, @Setter("instance") @Column var instance: String = "" ) \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/model/MuteKeyword.kt b/app/src/main/java/com/geckour/egret/model/MuteKeyword.kt index bb1d73c..18a305e 100644 --- a/app/src/main/java/com/geckour/egret/model/MuteKeyword.kt +++ b/app/src/main/java/com/geckour/egret/model/MuteKeyword.kt @@ -6,7 +6,7 @@ import com.github.gfx.android.orma.annotation.Setter import com.github.gfx.android.orma.annotation.Table @Table -class MuteKeyword( +data class MuteKeyword( @Setter("id") @PrimaryKey(autoincrement = true) val id: Long = -1L, @Setter("is_regex") @Column var isRegex: Boolean = false, @Setter("hashTag") @Column var keyword: String = "" diff --git a/app/src/main/java/com/geckour/egret/util/Common.kt b/app/src/main/java/com/geckour/egret/util/Common.kt index 01f4d1b..2d6098c 100644 --- a/app/src/main/java/com/geckour/egret/util/Common.kt +++ b/app/src/main/java/com/geckour/egret/util/Common.kt @@ -15,15 +15,12 @@ import android.text.Spanned import android.text.format.DateFormat import android.text.method.LinkMovementMethod import android.text.method.MovementMethod -import android.util.DisplayMetrics -import android.util.TypedValue import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.TextView import com.emojione.Emojione import com.geckour.egret.App -import com.geckour.egret.NotificationService import com.geckour.egret.R import com.geckour.egret.api.MastodonClient import com.geckour.egret.api.model.Account @@ -57,7 +54,7 @@ class Common { } private fun requestWeatherCertified(domain: String, callback: (hasCertified: Boolean, accountId: Long) -> Any) { - MastodonClient(domain).getSelfAccount() + MastodonClient(domain).getOwnAccount() .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ account -> @@ -104,6 +101,7 @@ class Common { status.url, status.account.id, status.account.avatarUrl, + status.account.isLocked, Emojione.shortnameToUnicode(status.account.displayName), "@${status.account.acct}", Date(status.createdAt.time), @@ -128,6 +126,7 @@ class Common { it.id, it.type, it.account.id, + it.account.isLocked, it.account.avatarUrl, it.account.displayName, "@${it.account.acct}", @@ -139,8 +138,12 @@ class Common { else TimelineContent() fun getProfileContent(account: Account): ProfileContent = ProfileContent( + account.id, account.avatarUrl, + null, account.headerUrl, + null, + account.isLocked, account.displayName, "@${account.acct}", getSpannedWithoutExtraMarginFromHtml("${account.url}"), diff --git a/app/src/main/java/com/geckour/egret/util/OrmaAttachmentsAdapter.java b/app/src/main/java/com/geckour/egret/util/OrmaAttachmentsAdapter.java new file mode 100644 index 0000000..55b8d1a --- /dev/null +++ b/app/src/main/java/com/geckour/egret/util/OrmaAttachmentsAdapter.java @@ -0,0 +1,19 @@ +package com.geckour.egret.util; + +import com.geckour.egret.App; +import com.geckour.egret.model.Draft; +import com.github.gfx.android.orma.annotation.StaticTypeAdapter; + +@StaticTypeAdapter( + targetType = Draft.Attachments.class, + serializedType = String.class +) +public class OrmaAttachmentsAdapter { + public static String serialize(Draft.Attachments attachments) { + return App.Companion.getGson().toJson(attachments); + } + + public static Draft.Attachments deserialize(String string) { + return App.Companion.getGson().fromJson(string, Draft.Attachments.class); + } +} diff --git a/app/src/main/java/com/geckour/egret/view/activity/MainActivity.kt b/app/src/main/java/com/geckour/egret/view/activity/MainActivity.kt index 7eadbf3..43e1e10 100644 --- a/app/src/main/java/com/geckour/egret/view/activity/MainActivity.kt +++ b/app/src/main/java/com/geckour/egret/view/activity/MainActivity.kt @@ -9,6 +9,7 @@ import android.os.Build import android.os.Bundle import android.preference.PreferenceManager import android.support.design.widget.Snackbar +import android.support.v4.app.ShareCompat import android.support.v4.content.ContextCompat import android.support.v7.widget.SearchView import android.text.Html @@ -39,19 +40,17 @@ import com.mikepenz.materialdrawer.DrawerBuilder import com.mikepenz.materialdrawer.model.DividerDrawerItem import com.mikepenz.materialdrawer.model.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.ProfileDrawerItem -import com.mikepenz.materialdrawer.model.interfaces.IProfile import io.reactivex.Observable import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import timber.log.Timber -import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper -class MainActivity : BaseActivity() { +class MainActivity : BaseActivity(), ListDialogFragment.OnItemClickListener { lateinit var binding: ActivityMainBinding - lateinit var drawer: Drawer + lateinit private var drawer: Drawer lateinit private var accountHeader: AccountHeader private val sharedPref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } lateinit private var currentCategory: TimelineFragment.Category @@ -59,12 +58,6 @@ class MainActivity : BaseActivity() { companion object { const val STATE_KEY_THEME_MODE = "stateKeyThemeMode" const val ARGS_KEY_CATEGORY = "argsKeyCategory" - const val NAV_ITEM_LOGIN: Long = 0 - const val NAV_ITEM_TL_PUBLIC: Long = 1 - const val NAV_ITEM_TL_LOCAL: Long = 2 - const val NAV_ITEM_TL_USER: Long = 3 - const val NAV_ITEM_TL_NOTIFICATION: Long = 4 - const val NAV_ITEM_SETTINGS: Long = 5 const val REQUEST_CODE_NOTIFICATION = 0 fun getIntent(context: Context, category: TimelineFragment.Category? = null): Intent { @@ -75,6 +68,20 @@ class MainActivity : BaseActivity() { } } + enum class NavItem { + NAV_ITEM_LOGIN, + NAV_ITEM_TL_PUBLIC, + NAV_ITEM_TL_LOCAL, + NAV_ITEM_TL_USER, + NAV_ITEM_TL_NOTIFICATION, + NAV_ITEM_SETTINGS, + NAV_ITEM_OTHERS + } + + interface OnBackPressedListener { + fun onBackPressedInMainActivity(callback: (doBack: Boolean) -> Any) + } + val timelineListener = object: TimelineAdapter.Callbacks { override val copyTootUrlToClipboard = { url: String -> val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -82,6 +89,15 @@ class MainActivity : BaseActivity() { clipboard.primaryClip = clip } + override val shareToot = { content: TimelineContent.TimelineStatus -> + ShareCompat.IntentBuilder.from(this@MainActivity).apply { + setChooserTitle(R.string.dialog_title_share_toot) + setSubject(getString(R.string.dialog_subject_share_toot)) + setText("${content.nameStrong}(${content.nameWeak}):\n${content.body}") + setType("text/plain") + }.startChooser() + } + override val showTootInBrowser = { content: TimelineContent.TimelineStatus -> val uri = Uri.parse(content.tootUrl) if (Common.isModeDefaultBrowser(this@MainActivity)) { @@ -112,7 +128,7 @@ class MainActivity : BaseActivity() { } ) ) - 2 -> Pair(R.string.array_item_mute_hash_tag, if (content.tags.isEmpty()) "" else s.format(content.tags.map { tag -> "#$tag" }.joinToString())) + 2 -> Pair(R.string.array_item_mute_hash_tag, if (content.tags.isEmpty()) "" else s.format(content.tags.joinToString { tag -> "#$tag" })) 3 -> { var instance = content.nameWeak.replace(Regex("^@.+@(.+)$"), "@$1") @@ -132,85 +148,25 @@ class MainActivity : BaseActivity() { ListDialogFragment.newInstance( getString(R.string.dialog_title_mute), items, - object: ListDialogFragment.OnItemClickListener { - override fun onClick(resId: Int) { - when (resId) { - R.string.array_item_mute_account -> { - Common.resetAuthInfo()?.let { domain -> - MastodonClient(domain).getSelfAccount() - .flatMap { if (it.id == content.accountId) Single.never() else MastodonClient(domain).muteAccount(content.accountId) } - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - Snackbar.make(binding.root, "Muted account: ${content.nameWeak}", Snackbar.LENGTH_SHORT).show() - }, Throwable::printStackTrace) - } - } - - R.string.array_item_mute_keyword -> { - val fragment = KeywordMuteFragment.newInstance(content.body.toString()) - supportFragmentManager.beginTransaction() - .replace(R.id.container, fragment, KeywordMuteFragment.TAG) - .addToBackStack(KeywordMuteFragment.TAG) - .commit() - } - - R.string.array_item_mute_hash_tag -> { - val fragment = HashTagMuteFragment.newInstance(content.tags) - supportFragmentManager.beginTransaction() - .replace(R.id.container, fragment, HashTagMuteFragment.TAG) - .addToBackStack(HashTagMuteFragment.TAG) - .commit() - } - - R.string.array_item_mute_instance -> { - var instance = content.nameWeak.replace(Regex("^@.+@(.+)$"), "@$1") - - if (content.nameWeak == instance) { - instance = "" - Common.getCurrentAccessToken()?.instanceId?.let { - instance = "@${OrmaProvider.db.selectFromInstanceAuthInfo().idEq(it).last().instance}" - } - } - - if (!TextUtils.isEmpty(instance)) { - OrmaProvider.db.prepareInsertIntoMuteInstanceAsSingle() - .map { inserter -> inserter.execute(MuteInstance(-1L, instance)) } - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ - Snackbar.make(binding.root, "Muted instance: $instance", Snackbar.LENGTH_SHORT).show() - }, Throwable::printStackTrace) - } - } - - R.string.array_item_mute_client -> { - content.app?.let { - OrmaProvider.db.prepareInsertIntoMuteClientAsSingle() - .map { inserter -> inserter.execute(MuteClient(-1L, it)) } - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ - Snackbar.make(binding.root, "Muted client: ${content.app}", Snackbar.LENGTH_SHORT).show() - }, Throwable::printStackTrace) - } - } - } - } - }).show(supportFragmentManager, ListDialogFragment.TAG) + content + ).show(supportFragmentManager, ListDialogFragment.TAG) } override val showProfile = { accountId: Long -> - AccountProfileFragment.newObservableInstance(accountId) - .subscribe( { - fragment -> - supportFragmentManager.beginTransaction() - .replace(R.id.container, fragment, AccountProfileFragment.TAG) - .addToBackStack(AccountProfileFragment.TAG) - .commit() - }, Throwable::printStackTrace) + Common.resetAuthInfo()?.let { + MastodonClient(it).getAccount(accountId) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ account -> + val fragment = AccountProfileFragment.newInstance(account) + + supportFragmentManager.beginTransaction() + .replace(R.id.container, fragment, AccountProfileFragment.TAG) + .addToBackStack(AccountProfileFragment.TAG) + .commit() + }) + } ?: let {} } override val onReply = { content: TimelineContent.TimelineStatus -> @@ -276,9 +232,11 @@ class MainActivity : BaseActivity() { } currentCategory = - if (intent.extras?.containsKey(ARGS_KEY_CATEGORY) ?: false) intent.extras[ARGS_KEY_CATEGORY] as TimelineFragment.Category - else if (sharedPref.contains(STATE_KEY_CATEGORY)) TimelineFragment.Category.values()[sharedPref.getInt(STATE_KEY_CATEGORY, TimelineFragment.Category.Public.ordinal)] - else TimelineFragment.Category.Public + when { + intent.extras?.containsKey(ARGS_KEY_CATEGORY) == true -> intent.extras[ARGS_KEY_CATEGORY] as TimelineFragment.Category + sharedPref.contains(STATE_KEY_CATEGORY) -> TimelineFragment.Category.values()[sharedPref.getInt(STATE_KEY_CATEGORY, TimelineFragment.Category.Public.ordinal)] + else -> TimelineFragment.Category.Public + } binding.appBarMain.contentMain.apply { simplicityTootBody.setOnKeyListener { v, keyCode, event -> @@ -334,7 +292,9 @@ class MainActivity : BaseActivity() { if (drawer.isDrawerOpen) { drawer.closeDrawer() } else { - super.onBackPressed() + (supportFragmentManager.fragments.lastOrNull { it?.isVisible ?: false } as? OnBackPressedListener)?.let { + it.onBackPressedInMainActivity { if (it) super.onBackPressed() } + } ?: super.onBackPressed() } } @@ -347,9 +307,7 @@ class MainActivity : BaseActivity() { override fun onPrepareOptionsMenu(menu: Menu?): Boolean { menu?.findItem(R.id.action_search)?.icon?.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) (menu?.findItem(R.id.action_search)?.actionView as SearchView?)?.setOnQueryTextListener(object: SearchView.OnQueryTextListener { - override fun onQueryTextChange(text: String?): Boolean { - return false - } + override fun onQueryTextChange(text: String?): Boolean = false override fun onQueryTextSubmit(text: String?): Boolean { if (text != null) { @@ -373,10 +331,6 @@ class MainActivity : BaseActivity() { return super.onOptionsItemSelected(item) } - override fun attachBaseContext(newBase: Context?) { - super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)) - } - fun showSearchResult(query: String) { Common.resetAuthInfo()?.let { MastodonClient(it).search(query) @@ -395,12 +349,79 @@ class MainActivity : BaseActivity() { } } + override fun onClickListDialogItem(resId: Int, content: TimelineContent.TimelineStatus) { + when (resId) { + R.string.array_item_mute_account -> { + Common.resetAuthInfo()?.let { domain -> + MastodonClient(domain).getOwnAccount() + .flatMap { if (it.id == content.accountId) Single.never() else MastodonClient(domain).muteAccount(content.accountId) } + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + Snackbar.make(binding.root, "Muted account: ${content.nameWeak}", Snackbar.LENGTH_SHORT).show() + }, Throwable::printStackTrace) + } + } + + R.string.array_item_mute_keyword -> { + val fragment = KeywordMuteFragment.newInstance(content.body.toString()) + supportFragmentManager.beginTransaction() + .replace(R.id.container, fragment, KeywordMuteFragment.TAG) + .addToBackStack(KeywordMuteFragment.TAG) + .commit() + } + + R.string.array_item_mute_hash_tag -> { + val fragment = HashTagMuteFragment.newInstance(content.tags) + supportFragmentManager.beginTransaction() + .replace(R.id.container, fragment, HashTagMuteFragment.TAG) + .addToBackStack(HashTagMuteFragment.TAG) + .commit() + } + + R.string.array_item_mute_instance -> { + var instance = content.nameWeak.replace(Regex("^@.+@(.+)$"), "@$1") + + if (content.nameWeak == instance) { + instance = "" + Common.getCurrentAccessToken()?.instanceId?.let { + instance = "@${OrmaProvider.db.selectFromInstanceAuthInfo().idEq(it).last().instance}" + } + } + + if (!TextUtils.isEmpty(instance)) { + OrmaProvider.db.prepareInsertIntoMuteInstanceAsSingle() + .map { inserter -> inserter.execute(MuteInstance(-1L, instance)) } + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ + Snackbar.make(binding.root, "Muted instance: $instance", Snackbar.LENGTH_SHORT).show() + }, Throwable::printStackTrace) + } + } + + R.string.array_item_mute_client -> { + content.app?.let { + OrmaProvider.db.prepareInsertIntoMuteClientAsSingle() + .map { inserter -> inserter.execute(MuteClient(-1L, it)) } + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ + Snackbar.make(binding.root, "Muted client: ${content.app}", Snackbar.LENGTH_SHORT).show() + }, Throwable::printStackTrace) + } + } + } + } + fun commitAccountsIntoAccountHeader() { accountHeader.clear() Observable.fromIterable(OrmaProvider.db.selectFromAccessToken()) .flatMap { - MastodonClient(Common.setAuthInfo(it) ?: throw IllegalArgumentException()).getSelfAccount() + MastodonClient(Common.setAuthInfo(it) ?: throw IllegalArgumentException()).getOwnAccount() .map { account -> Pair(it, account) } .toObservable() } @@ -454,7 +475,7 @@ class MainActivity : BaseActivity() { .withOnAccountHeaderListener { v, profile, current -> if (v.id == R.id.material_drawer_account_header_current) { Common.resetAuthInfo()?.let { - MastodonClient(it).getSelfAccount() + MastodonClient(it).getOwnAccount() .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -500,44 +521,51 @@ class MainActivity : BaseActivity() { .withActionBarDrawerToggleAnimated(true) .withToolbar(binding.appBarMain.toolbar) .addDrawerItems( - PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_public).withIdentifier(NAV_ITEM_TL_PUBLIC).withIcon(R.drawable.ic_public_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), - PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_local).withIdentifier(NAV_ITEM_TL_LOCAL).withIcon(R.drawable.ic_place_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), - PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_user).withIdentifier(NAV_ITEM_TL_USER).withIcon(R.drawable.ic_mood_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), - PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_notification).withIdentifier(NAV_ITEM_TL_NOTIFICATION).withIcon(R.drawable.ic_notifications_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), + PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_public).withIdentifier(NavItem.NAV_ITEM_TL_PUBLIC.ordinal.toLong()).withIcon(R.drawable.ic_public_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), + PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_local).withIdentifier(NavItem.NAV_ITEM_TL_LOCAL.ordinal.toLong()).withIcon(R.drawable.ic_place_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), + PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_user).withIdentifier(NavItem.NAV_ITEM_TL_USER.ordinal.toLong()).withIcon(R.drawable.ic_mood_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), + PrimaryDrawerItem().withName(R.string.navigation_drawer_item_tl_notification).withIdentifier(NavItem.NAV_ITEM_TL_NOTIFICATION.ordinal.toLong()).withIcon(R.drawable.ic_notifications_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), DividerDrawerItem(), - PrimaryDrawerItem().withName(R.string.navigation_drawer_item_login).withIdentifier(NAV_ITEM_LOGIN).withIcon(R.drawable.ic_person_add_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), + PrimaryDrawerItem().withName(R.string.navigation_drawer_item_login).withIdentifier(NavItem.NAV_ITEM_LOGIN.ordinal.toLong()).withIcon(R.drawable.ic_person_add_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), DividerDrawerItem(), - PrimaryDrawerItem().withName(R.string.navigation_drawer_item_settings).withIdentifier(NAV_ITEM_SETTINGS).withIcon(R.drawable.ic_settings_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark) + PrimaryDrawerItem().withName(R.string.navigation_drawer_item_settings).withIdentifier(NavItem.NAV_ITEM_SETTINGS.ordinal.toLong()).withIcon(R.drawable.ic_settings_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark), + PrimaryDrawerItem().withName(R.string.navigation_drawer_item_others).withIdentifier(NavItem.NAV_ITEM_OTHERS.ordinal.toLong()).withIcon(R.drawable.ic_extension_black_24px).withIconTintingEnabled(true).withIconColorRes(R.color.icon_tint_dark) ) .withOnDrawerItemClickListener { _, _, item -> return@withOnDrawerItemClickListener when (item.identifier) { - NAV_ITEM_LOGIN -> { + NavItem.NAV_ITEM_LOGIN.ordinal.toLong() -> { startActivity(LoginActivity.getIntent(this)) false } - NAV_ITEM_TL_PUBLIC -> { + NavItem.NAV_ITEM_TL_PUBLIC.ordinal.toLong() -> { showTimelineFragment(TimelineFragment.Category.Public) false } - NAV_ITEM_TL_LOCAL -> { + NavItem.NAV_ITEM_TL_LOCAL.ordinal.toLong() -> { showTimelineFragment(TimelineFragment.Category.Local) false } - NAV_ITEM_TL_USER -> { + NavItem.NAV_ITEM_TL_USER.ordinal.toLong() -> { showTimelineFragment(TimelineFragment.Category.User) false } - NAV_ITEM_TL_NOTIFICATION -> { + NavItem.NAV_ITEM_TL_NOTIFICATION.ordinal.toLong() -> { showTimelineFragment(TimelineFragment.Category.Notification) false } - NAV_ITEM_SETTINGS -> { - val intent = SettingActivity.getIntent(this) + NavItem.NAV_ITEM_SETTINGS.ordinal.toLong() -> { + val intent = SettingActivity.getIntent(this, SettingActivity.Type.Preference) + startActivity(intent) + false + } + + NavItem.NAV_ITEM_OTHERS.ordinal.toLong() -> { + val intent = SettingActivity.getIntent(this, SettingActivity.Type.Misc) startActivity(intent) false } diff --git a/app/src/main/java/com/geckour/egret/view/activity/SettingActivity.kt b/app/src/main/java/com/geckour/egret/view/activity/SettingActivity.kt index e8ad908..066527b 100644 --- a/app/src/main/java/com/geckour/egret/view/activity/SettingActivity.kt +++ b/app/src/main/java/com/geckour/egret/view/activity/SettingActivity.kt @@ -7,19 +7,27 @@ import android.os.Bundle import android.support.v7.widget.Toolbar import com.geckour.egret.R import com.geckour.egret.databinding.ActivityMainBinding +import com.geckour.egret.view.fragment.MiscFragment import com.geckour.egret.view.fragment.SettingMainFragment +import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper class SettingActivity: BaseActivity() { - lateinit var binding: ActivityMainBinding + enum class Type { + Preference, + Misc + } companion object { - fun getIntent(context: Context): Intent { - val intent = Intent(context, SettingActivity::class.java) - return intent - } + private val ARGS_KEY_TYPE = "argsKeyType" + fun getIntent(context: Context, type: Type) = Intent(context, SettingActivity::class.java) + .apply { + putExtra(ARGS_KEY_TYPE, type) + } } + lateinit var binding: ActivityMainBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setTheme(if (isModeDark()) R.style.AppTheme_Dark_NoActionBar else R.style.AppTheme_NoActionBar) @@ -28,8 +36,26 @@ class SettingActivity: BaseActivity() { setSupportActionBar(toolbar) binding.appBarMain.contentMain.fab.hide() - supportFragmentManager.beginTransaction() - .replace(R.id.container, SettingMainFragment.newInstance(), SettingMainFragment.TAG) - .commit() + if (savedInstanceState == null) { + if (intent.hasExtra(ARGS_KEY_TYPE)) { + when (intent.extras[ARGS_KEY_TYPE]) { + Type.Preference -> { + supportFragmentManager.beginTransaction() + .replace(R.id.container, SettingMainFragment.newInstance(), SettingMainFragment.TAG) + .commit() + } + + Type.Misc -> { + supportFragmentManager.beginTransaction() + .replace(R.id.container, MiscFragment.newInstance(), MiscFragment.TAG) + .commit() + } + } + } + } + } + + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)) } } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/activity/ShareActivity.kt b/app/src/main/java/com/geckour/egret/view/activity/ShareActivity.kt new file mode 100644 index 0000000..db5ac2b --- /dev/null +++ b/app/src/main/java/com/geckour/egret/view/activity/ShareActivity.kt @@ -0,0 +1,30 @@ +package com.geckour.egret.view.activity + +import android.content.Intent +import android.databinding.DataBindingUtil +import android.os.Bundle +import com.geckour.egret.R +import com.geckour.egret.databinding.ActivityShareBinding +import com.geckour.egret.util.Common +import com.geckour.egret.view.fragment.NewTootCreateFragment + +class ShareActivity: BaseActivity() { + + lateinit private var binding: ActivityShareBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_share) + + + val token = Common.getCurrentAccessToken() ?: return + + intent.apply { + if (type != "text/plain") return + + supportFragmentManager.beginTransaction() + .add(R.id.container, NewTootCreateFragment.newInstance(token.id, body = extras?.getString(Intent.EXTRA_TEXT, "")), NewTootCreateFragment.TAG) + .commit() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/activity/SplashActivity.kt b/app/src/main/java/com/geckour/egret/view/activity/SplashActivity.kt index 042dd6e..78b4e5b 100644 --- a/app/src/main/java/com/geckour/egret/view/activity/SplashActivity.kt +++ b/app/src/main/java/com/geckour/egret/view/activity/SplashActivity.kt @@ -3,10 +3,14 @@ package com.geckour.egret.view.activity import android.content.Context import android.databinding.DataBindingUtil import android.os.Bundle +import com.bumptech.glide.Glide import com.geckour.egret.R import com.geckour.egret.databinding.ActivitySplashBinding import com.geckour.egret.util.Common import com.trello.rxlifecycle2.components.support.RxAppCompatActivity +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper class SplashActivity : BaseActivity() { @@ -18,7 +22,7 @@ class SplashActivity : BaseActivity() { binding = DataBindingUtil.setContentView(this, R.layout.activity_splash) - Common.hasCertified { hasCertified, accountId -> + Common.hasCertified { hasCertified, _ -> val intent = if (hasCertified) MainActivity.getIntent(this@SplashActivity) else LoginActivity.getIntent(this@SplashActivity) startActivity(intent) } diff --git a/app/src/main/java/com/geckour/egret/view/adapter/SearchResultAdapter.kt b/app/src/main/java/com/geckour/egret/view/adapter/SearchResultAdapter.kt index 74b4a7b..9f022a5 100644 --- a/app/src/main/java/com/geckour/egret/view/adapter/SearchResultAdapter.kt +++ b/app/src/main/java/com/geckour/egret/view/adapter/SearchResultAdapter.kt @@ -54,19 +54,19 @@ class SearchResultAdapter(val listener: TimelineAdapter.Callbacks) : RecyclerVie content.mediaUrls.indices.forEach { when (it) { 0 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap1, true) + if (content.isSensitive == true) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap1, true) setupMedia(statusBinding.media1, content.mediaPreviewUrls, content.mediaUrls, it) } 1 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap2, true) + if (content.isSensitive == true) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap2, true) setupMedia(statusBinding.media2, content.mediaPreviewUrls, content.mediaUrls, it) } 2 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap3, true) + if (content.isSensitive == true) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap3, true) setupMedia(statusBinding.media3, content.mediaPreviewUrls, content.mediaUrls, it) } 3 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap4, true) + if (content.isSensitive == true) toggleMediaSpoiler(statusBinding.mediaSpoilerWrap4, true) setupMedia(statusBinding.media4, content.mediaPreviewUrls, content.mediaUrls, it) } } @@ -101,7 +101,7 @@ class SearchResultAdapter(val listener: TimelineAdapter.Callbacks) : RecyclerVie initVisibility(SearchResultFragment.Category.Account) (accountBinding.root as ViewGroup).apply { - for (i in 0..childCount - 1) getChildAt(i).setOnClickListener { listener.showProfile(accountBinding.account.id) } + for (i in 0 until childCount) getChildAt(i).setOnClickListener { listener.showProfile(accountBinding.account.id) } } accountBinding.account = content } diff --git a/app/src/main/java/com/geckour/egret/view/adapter/TimelineAdapter.kt b/app/src/main/java/com/geckour/egret/view/adapter/TimelineAdapter.kt index 24cfa58..d3fc2fd 100644 --- a/app/src/main/java/com/geckour/egret/view/adapter/TimelineAdapter.kt +++ b/app/src/main/java/com/geckour/egret/view/adapter/TimelineAdapter.kt @@ -23,10 +23,13 @@ import io.reactivex.schedulers.Schedulers import java.util.* import kotlin.collections.ArrayList -class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootCallback? = null, val doFilter: Boolean = true) : RecyclerView.Adapter() { +class TimelineAdapter( + val listener: Callbacks, + private val onAddTootListener: OnAddTootCallback? = null, + private val doFilter: Boolean = true): RecyclerView.Adapter() { companion object { - val DEFAULT_ITEMS_LIMIT = 100 + const val DEFAULT_ITEMS_LIMIT = 100 } enum class ContentType { @@ -39,6 +42,8 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC interface Callbacks { val copyTootUrlToClipboard: (url: String) -> Any + val shareToot: (content: TimelineContent.TimelineStatus) -> Any + val showTootInBrowser: (content: TimelineContent.TimelineStatus) -> Any val copyTootToClipboard: (content: TimelineContent.TimelineStatus) -> Any @@ -87,19 +92,19 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC content.mediaUrls.indices.forEach { when (it) { 0 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap1, true) + if (content.isSensitive == true) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap1, true) setupMedia(timelineBinding.media1, content.mediaPreviewUrls, content.mediaUrls, it) } 1 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap2, true) + if (content.isSensitive == true) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap2, true) setupMedia(timelineBinding.media2, content.mediaPreviewUrls, content.mediaUrls, it) } 2 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap3, true) + if (content.isSensitive == true) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap3, true) setupMedia(timelineBinding.media3, content.mediaPreviewUrls, content.mediaUrls, it) } 3 -> { - if (content.isSensitive ?: false) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap4, true) + if (content.isSensitive == true) toggleMediaSpoiler(timelineBinding.mediaSpoilerWrap4, true) setupMedia(timelineBinding.media4, content.mediaPreviewUrls, content.mediaUrls, it) } } @@ -155,7 +160,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC else toggleStatus(ContentType.Notification, true) } - fun initVisibility(type: ContentType) { + private fun initVisibility(type: ContentType) { toggleAction(type, false) toggleStatus(type, false) initSpoiler(type) @@ -164,6 +169,9 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC ContentType.Status -> { timelineBinding.body.text = null + timelineBinding.actionLock.visibility = View.GONE + timelineBinding.lock.visibility = View.GONE + listOf(timelineBinding.media1, timelineBinding.media2, timelineBinding.media3, timelineBinding.media4) .forEach { it.apply { @@ -182,6 +190,9 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC visibility = View.GONE } + notificationBinding.actionLock.visibility = View.GONE + notificationBinding.lock.visibility = View.GONE + listOf(notificationBinding.media1, notificationBinding.media2, notificationBinding.media3, notificationBinding.media4) .forEach { it.apply { @@ -196,7 +207,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - fun bindAction(contentType: ContentType, notificationType: Notification.NotificationType) { + private fun bindAction(contentType: ContentType, notificationType: Notification.NotificationType) { when (contentType) { ContentType.Status -> { if (notificationType == Notification.NotificationType.reblog) { @@ -230,7 +241,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - fun toggleAction(type: ContentType, show: Boolean) { + private fun toggleAction(type: ContentType, show: Boolean) { when(type) { ContentType.Status -> { listOf(timelineBinding.indicateAction, timelineBinding.actionIcon, timelineBinding.actionBy, timelineBinding.actionName) @@ -254,7 +265,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - fun toggleStatus(type: ContentType, show: Boolean) { + private fun toggleStatus(type: ContentType, show: Boolean) { when(type) { ContentType.Status -> { listOf( @@ -292,7 +303,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - fun initSpoiler(type: ContentType) { + private fun initSpoiler(type: ContentType) { when(type) { ContentType.Status -> { timelineBinding.bodyAdditional.visibility = View.GONE @@ -306,7 +317,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - fun toggleBodySpoiler(type: ContentType, show: Boolean) { + private fun toggleBodySpoiler(type: ContentType, show: Boolean) { when(type) { ContentType.Status -> { timelineBinding.clearSpoiler.apply { @@ -328,7 +339,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - fun showPopup(type: ContentType, view: View) { + private fun showPopup(type: ContentType, view: View) { val popup = PopupMenu(view.context, view) val currentAccountId = OrmaProvider.db.selectFromAccessToken().isCurrentEq(true).last().accountId val contentAccountId = when(type) { @@ -345,6 +356,11 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC true } + R.id.action_share -> { + listener.shareToot(timelineBinding.status) + true + } + R.id.action_open -> { listener.showTootInBrowser(timelineBinding.status) true @@ -402,14 +418,14 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC popup.show() } - fun toggleMediaSpoiler(view: View, show: Boolean) { + private fun toggleMediaSpoiler(view: View, show: Boolean) { view.apply { setOnClickListener { it.visibility = View.GONE } visibility = if (show) View.VISIBLE else View.GONE } } - fun setupMedia(view: ImageView, previewUrls: List, urls: List, position: Int) { + private fun setupMedia(view: ImageView, previewUrls: List, urls: List, position: Int) { view.apply { visibility = View.VISIBLE setOnClickListener { listener.onClickMedia(urls, position) } @@ -417,7 +433,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC Glide.with(view.context).load(previewUrls[position]).into(view) } - fun reflectTreeStatus() { + private fun reflectTreeStatus() { when (timelineBinding.status.treeStatus) { TimelineContent.TimelineStatus.TreeStatus.None -> { timelineBinding.treeLineUpper.visibility = View.GONE @@ -439,9 +455,8 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - override fun getItemViewType(position: Int): Int { - return getContent(position).let { if (it.status != null) ContentType.Status.ordinal else if (it.notification != null) ContentType.Notification.ordinal else -1 } - } + override fun getItemViewType(position: Int): Int = + getContent(position).let { if (it.status != null) ContentType.Status.ordinal else if (it.notification != null) ContentType.Notification.ordinal else -1 } override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { if (viewType == ContentType.Status.ordinal) { @@ -461,9 +476,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC item.notification?.let { holder?.bindData(it) } } - override fun getItemCount(): Int { - return contents.size - } + override fun getItemCount(): Int = contents.size fun getContent(index: Int): TimelineContent = this.contents[index] @@ -474,41 +487,31 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC fun getContents(): List = this.contents fun addContent(content: TimelineContent, limit: Int = DEFAULT_ITEMS_LIMIT) { - Single.just(content) - .map { Pair(it, shouldMute(it)) } - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ (c, b) -> - if (!b) { - this.contents.add(0, c) - notifyItemInserted(0) - onAddTootListener?.onAddOnTop() - removeItemsWhenOverLimit(limit) - } - }, Throwable::printStackTrace) + shouldMute(content).subscribe({ (c, b) -> + if (!b) { + this.contents.add(0, c) + notifyItemInserted(0) + onAddTootListener?.onAddOnTop() + removeItemsWhenOverLimit(limit) + } + }, Throwable::printStackTrace) } fun addContentAtLast(content: TimelineContent, limit: Int = DEFAULT_ITEMS_LIMIT) { - Single.just(content) - .map { Pair(it, shouldMute(it)) } - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ (c, b) -> - if (!b) { - this.contents.add(c) - notifyItemInserted(this.contents.lastIndex) - removeItemsWhenOverLimit(limit) - } - }, Throwable::printStackTrace) + shouldMute(content).subscribe({ (c, b) -> + if (!b) { + this.contents.add(c) + notifyItemInserted(this.contents.lastIndex) + removeItemsWhenOverLimit(limit) + } + }, Throwable::printStackTrace) } fun addAllContents(contents: List, limit: Int = DEFAULT_ITEMS_LIMIT) { val cs: ArrayList = ArrayList() Observable.fromIterable(contents) - .map { Pair(it, shouldMute(it)) } - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) + .flatMap { shouldMute(it).toObservable() } .subscribe({ (c, b) -> if (!b) { cs.add(c) @@ -525,7 +528,7 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC val cs: ArrayList = ArrayList() Observable.fromIterable(contents) - .map { Pair(it, shouldMute(it)) } + .flatMap { shouldMute(it).toObservable() } .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ (c, b) -> @@ -563,32 +566,41 @@ class TimelineAdapter(val listener: Callbacks, val onAddTootListener: OnAddTootC } } - fun shouldMute(content: TimelineContent): Boolean { - if (!doFilter) return false + private fun shouldMute(content: TimelineContent): Single> { + return Single.just(content) + .map { + val mute: Boolean = run { + if (!doFilter) return@run false - OrmaProvider.db.selectFromMuteClient().forEach { - if (content.status?.app == it.client) return true - } - OrmaProvider.db.selectFromMuteHashTag().forEach { tag -> - content.status?.tags?.forEach { if (tag.hashTag == it) return true } - } - OrmaProvider.db.selectFromMuteKeyword().forEach { - if (it.isRegex) { - if (content.status?.body?.toString()?.matches(Regex(it.keyword)) ?: false) return true - } else if (content.status?.body?.toString()?.contains(it.keyword) ?: false) return true - } - OrmaProvider.db.selectFromMuteInstance().forEach { - var instance = content.status?.nameWeak?.replace(Regex("^@.+@(.+)$"), "@$1") ?: "" - if (content.status?.nameWeak == instance) { - instance = Common.getCurrentAccessToken()?.instanceId?.let { - "@${OrmaProvider.db.selectFromInstanceAuthInfo().idEq(it).last().instance}" - } ?: "" - } + OrmaProvider.db.selectFromMuteClient().forEach { + if (content.status?.app == it.client) return@run true + } + OrmaProvider.db.selectFromMuteHashTag().forEach { tag -> + content.status?.tags?.forEach { if (tag.hashTag == it) return@run true } + } + OrmaProvider.db.selectFromMuteKeyword().forEach { + if (it.isRegex) { + if (content.status?.body?.toString()?.matches(Regex(it.keyword)) == true) return@run true + } else if (content.status?.body?.toString()?.contains(it.keyword) == true) return@run true + } + OrmaProvider.db.selectFromMuteInstance().forEach { + var instance = content.status?.nameWeak?.replace(Regex("^@.+@(.+)$"), "@$1") ?: "" + if (content.status?.nameWeak == instance) { + instance = Common.getCurrentAccessToken()?.instanceId?.let { + "@${OrmaProvider.db.selectFromInstanceAuthInfo().idEq(it).last().instance}" + } ?: "" + } - if (it.instance == instance) return true - } + if (it.instance == instance) return@run true + } - return false + return@run false + } + + Pair(it, mute) + } + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) } private fun removeItemsWhenOverLimit(limit: Int = DEFAULT_ITEMS_LIMIT) { diff --git a/app/src/main/java/com/geckour/egret/view/adapter/model/ProfileContent.kt b/app/src/main/java/com/geckour/egret/view/adapter/model/ProfileContent.kt index ba5123d..71025ca 100644 --- a/app/src/main/java/com/geckour/egret/view/adapter/model/ProfileContent.kt +++ b/app/src/main/java/com/geckour/egret/view/adapter/model/ProfileContent.kt @@ -3,8 +3,12 @@ package com.geckour.egret.view.adapter.model import android.text.Spanned data class ProfileContent( + val id: Long, var iconUrl: String, + var iconImg: String?, var headerUrl: String, + var headerImg: String?, + var locked: Boolean, var screenName: String, var username: String, var url: Spanned, @@ -12,5 +16,5 @@ data class ProfileContent( var followingCount: Long, var followerCount: Long, var tootCount: Long, - var createdAt: Long + val createdAt: Long ) \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/adapter/model/TimelineContent.kt b/app/src/main/java/com/geckour/egret/view/adapter/model/TimelineContent.kt index c69effb..67bb641 100644 --- a/app/src/main/java/com/geckour/egret/view/adapter/model/TimelineContent.kt +++ b/app/src/main/java/com/geckour/egret/view/adapter/model/TimelineContent.kt @@ -1,7 +1,7 @@ package com.geckour.egret.view.adapter.model import android.text.Spanned -import com.geckour.egret.api.model.Notification +import java.io.Serializable import java.util.* data class TimelineContent( @@ -13,6 +13,7 @@ data class TimelineContent( val tootUrl: String, val accountId: Long, var iconUrl: String, + var accountLocked: Boolean, var nameStrong: String, var nameWeak: String, val time: Date, @@ -29,7 +30,7 @@ data class TimelineContent( var rebloggedStatusContent: TimelineStatus?, var app: String?, var treeStatus: TreeStatus - ) { + ): Serializable { enum class TreeStatus { None, Top, @@ -42,6 +43,7 @@ data class TimelineContent( val id: Long, val type: String, val accountId: Long, + var accountLocked: Boolean, var iconUrl: String, var nameStrong: String, var nameWeak: String, diff --git a/app/src/main/java/com/geckour/egret/view/adapter/model/adapter/SpannedTypeAdapter.kt b/app/src/main/java/com/geckour/egret/view/adapter/model/adapter/SpannedTypeAdapter.kt index 447a445..4660369 100644 --- a/app/src/main/java/com/geckour/egret/view/adapter/model/adapter/SpannedTypeAdapter.kt +++ b/app/src/main/java/com/geckour/egret/view/adapter/model/adapter/SpannedTypeAdapter.kt @@ -1,46 +1,27 @@ package com.geckour.egret.view.adapter.model.adapter import android.os.Build -import android.os.Parcel import android.text.Html import android.text.Spanned -import android.util.Log import com.geckour.egret.util.Common import com.google.gson.* -import paperparcel.TypeAdapter import java.lang.reflect.Type -class SpannedTypeAdapter: TypeAdapter, JsonSerializer, JsonDeserializer, InstanceCreator { +class SpannedTypeAdapter: JsonSerializer, JsonDeserializer { companion object { - val KEY = "Spanned" + const val KEY = "Spanned" } - override fun writeToParcel(value: Spanned, outParcel: Parcel, flags: Int) { - outParcel.writeString(value.toString()) - } - - override fun readFromParcel(inParcel: Parcel): Spanned { - return Common.getSpannedWithoutExtraMarginFromHtml(inParcel.readString()) - } - - override fun createInstance(type: Type?): Spanned { - return Common.getSpannedWithoutExtraMarginFromHtml("") - } - - override fun serialize(src: Spanned?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - return JsonObject().apply { - if (src == null) { - addProperty(KEY, "") - } else { + override fun serialize(src: Spanned?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement = + JsonObject().apply { addProperty(KEY, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) Html.toHtml(src, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)?.toString() - else Html.toHtml(src).toString()) + src?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) Html.toHtml(src, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)?.toString() + else Html.toHtml(src).toString() + } ?: "") } - } - } - override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Spanned { - return Common.getSpannedWithoutExtraMarginFromHtml(if (json == null) "" else json.asJsonObject.get(KEY)?.asString ?: "") - } + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Spanned = + Common.getSpannedWithoutExtraMarginFromHtml(json?.asJsonObject?.get(KEY)?.asString ?: "") } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/AccountManageFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/AccountManageFragment.kt index ae0a3d8..0d0665f 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/AccountManageFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/AccountManageFragment.kt @@ -82,7 +82,7 @@ class AccountManageFragment: BaseFragment() { preItems.clear() Observable.fromIterable(OrmaProvider.db.selectFromAccessToken()) - .flatMap { token -> + .map { token -> MastodonClient(Common.setAuthInfo(token) ?: throw IllegalArgumentException()).getAccount(token.accountId) .map { val domain = OrmaProvider.db.selectFromInstanceAuthInfo().idEq(token.instanceId).last().instance @@ -90,11 +90,13 @@ class AccountManageFragment: BaseFragment() { } } .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) .subscribe({ - adapter.addItem(it) - preItems.add(it) + it.observeOn(AndroidSchedulers.mainThread()) + .subscribe({ content -> + adapter.addItem(content) + preItems.add(content) + }, Throwable::printStackTrace) }, Throwable::printStackTrace) } diff --git a/app/src/main/java/com/geckour/egret/view/fragment/AccountProfileFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/AccountProfileFragment.kt index b6141e4..c4aa9f8 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/AccountProfileFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/AccountProfileFragment.kt @@ -1,15 +1,25 @@ package com.geckour.egret.view.fragment -import android.content.SharedPreferences +import android.Manifest +import android.app.Activity +import android.content.ContentValues +import android.content.Intent +import android.content.pm.PackageManager import android.databinding.DataBindingUtil +import android.net.Uri import android.os.Bundle -import android.preference.PreferenceManager +import android.provider.MediaStore +import android.support.design.widget.Snackbar +import android.support.v4.app.ActivityCompat import android.support.v4.content.ContextCompat import android.support.v7.widget.RecyclerView +import android.text.method.TextKeyListener +import android.util.Base64 import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.Button import com.bumptech.glide.Glide import com.geckour.egret.R import com.geckour.egret.api.MastodonClient @@ -20,41 +30,50 @@ import com.geckour.egret.databinding.FragmentAccountProfileBinding import com.geckour.egret.util.Common import com.geckour.egret.view.activity.MainActivity import com.geckour.egret.view.adapter.TimelineAdapter -import io.reactivex.Observable +import com.geckour.egret.view.adapter.model.ProfileContent +import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import org.apache.commons.io.IOUtils import retrofit2.adapter.rxjava2.Result +import java.io.File +import java.io.InputStream class AccountProfileFragment: BaseFragment() { companion object { val TAG: String = this::class.java.simpleName - val ARGS_KEY_ACCOUNT = "account" + private val ARGS_KEY_ACCOUNT = "profile" + private val REQUEST_CODE_AVATAR_PICK_MEDIA = 101 + private val REQUEST_CODE_AVATAR_CAPTURE_IMAGE = 102 + private val REQUEST_CODE_HEADER_PICK_MEDIA = 201 + private val REQUEST_CODE_HEADER_CAPTURE_IMAGE = 202 + private val REQUEST_CODE_GRANT_READ_STORAGE_AVATAR = 1 + private val REQUEST_CODE_GRANT_READ_STORAGE_HEADER = 2 + private val REQUEST_CODE_GRANT_WRITE_STORAGE_AVATAR = 3 + private val REQUEST_CODE_GRANT_WRITE_STORAGE_HEADER = 4 fun newInstance(account: Account): AccountProfileFragment = AccountProfileFragment().apply { arguments = Bundle().apply { putSerializable(ARGS_KEY_ACCOUNT, account) } } + } - fun newObservableInstance(accountId: Long): Observable { - return MastodonClient(Common.resetAuthInfo() ?: throw IllegalArgumentException()).getAccount(accountId) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .flatMap { account -> - val fragment = newInstance(account) - Observable.just(fragment) - } - } + enum class UploadType { + Avatar, + Header } - private val account: Account by lazy { arguments[ARGS_KEY_ACCOUNT] as Account } + lateinit private var profile: ProfileContent + private val editedProfile: ProfileContent by lazy { profile.copy() } lateinit private var relationship: Relationship lateinit private var binding: FragmentAccountProfileBinding private var onTop = true private var inTouch = false private val adapter: TimelineAdapter by lazy { TimelineAdapter((activity as MainActivity).timelineListener) } - private val sharedPref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(activity) } + + private var capturedImageUri: Uri? = null private var maxId: Long = -1 private var sinceId: Long = -1 @@ -62,12 +81,13 @@ class AccountProfileFragment: BaseFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activity as MainActivity).supportActionBar?.hide() + profile = Common.getProfileContent(arguments[ARGS_KEY_ACCOUNT] as Account) } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_account_profile, container, false) - val content = Common.getProfileContent(account) + val content = profile binding.content = content binding.timeString = Common.getReadableDateString(content.createdAt, true) @@ -79,15 +99,41 @@ class AccountProfileFragment: BaseFragment() { override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.icon.setOnClickListener { showImageViewer(listOf(account.avatarUrl), 0) } - binding.header.setOnClickListener { showImageViewer(listOf(account.headerUrl), 0) } + initViewsVisibility() + + binding.icon.setOnClickListener { showImageViewer(listOf(profile.iconUrl), 0) } + binding.header.setOnClickListener { showImageViewer(listOf(profile.headerUrl), 0) } + + if (profile.id == Common.getCurrentAccessToken()?.accountId) { + binding.buttonFavList.apply { + setOnClickListener { showFavList() } + visibility = View.VISIBLE + } + + binding.buttonEdit.apply { + setOnClickListener { + val enter = text == getString(R.string.button_edit) + toggleEditMode(enter, this) + } + visibility = View.VISIBLE + } + + listOf( + binding.screenName, + binding.note + ) + .forEach { + it.tag = it.keyListener + it.setOnKeyListener(null) + } + } val movementMethod = Common.getMovementMethodFromPreference(binding.root.context) binding.url.movementMethod = movementMethod binding.note.movementMethod = movementMethod val domain = Common.resetAuthInfo() - if (domain != null) MastodonClient(domain).getAccountRelationships(account.id) + if (domain != null) MastodonClient(domain).getAccountRelationships(profile.id) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -95,31 +141,33 @@ class AccountProfileFragment: BaseFragment() { this.relationship = relationships.first() val currentAccountId = Common.getCurrentAccessToken()?.accountId - if (currentAccountId != null && currentAccountId != account.id) { + if (currentAccountId != null && currentAccountId != profile.id) { binding.follow.visibility = View.VISIBLE binding.block.visibility = View.VISIBLE binding.mute.visibility = View.VISIBLE } - setFollowButtonState(this.relationship.following) + setFollowButtonState(this.relationship) binding.follow.setOnClickListener { + if (this.relationship.hasSendFollowRequest) return@setOnClickListener + if (this.relationship.following) { - MastodonClient(domain).unFollowAccount(account.id) + MastodonClient(domain).unFollowAccount(profile.id) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) .subscribe( { relation -> this.relationship = relation - setFollowButtonState(relation.following) + setFollowButtonState(relation) }, Throwable::printStackTrace) } else { - MastodonClient(domain).followAccount(account.id) + MastodonClient(domain).followAccount(profile.id) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) .subscribe( { relation -> this.relationship = relation - setFollowButtonState(relation.following) + setFollowButtonState(relation) }, Throwable::printStackTrace) } } @@ -127,7 +175,7 @@ class AccountProfileFragment: BaseFragment() { setBlockButtonState(this.relationship.blocking) binding.block.setOnClickListener { if (this.relationship.blocking) { - MastodonClient(domain).unBlockAccount(account.id) + MastodonClient(domain).unBlockAccount(profile.id) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -136,7 +184,7 @@ class AccountProfileFragment: BaseFragment() { setBlockButtonState(relation.blocking) }, Throwable::printStackTrace) } else { - MastodonClient(domain).blockAccount(account.id) + MastodonClient(domain).blockAccount(profile.id) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -150,7 +198,7 @@ class AccountProfileFragment: BaseFragment() { setMuteButtonState(this.relationship.blocking) binding.mute.setOnClickListener { if (this.relationship.muting) { - MastodonClient(domain).unMuteAccount(account.id) + MastodonClient(domain).unMuteAccount(profile.id) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -159,7 +207,7 @@ class AccountProfileFragment: BaseFragment() { setMuteButtonState(relation.muting) }, Throwable::printStackTrace) } else { - MastodonClient(domain).muteAccount(account.id) + MastodonClient(domain).muteAccount(profile.id) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -222,7 +270,275 @@ class AccountProfileFragment: BaseFragment() { refreshBarTitle() } - fun showImageViewer(urls: List, position: Int) { + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + when (requestCode) { + REQUEST_CODE_AVATAR_PICK_MEDIA -> { + if (resultCode == Activity.RESULT_OK) { + data?.let { processPostMedia(it, UploadType.Avatar) } + } + } + + REQUEST_CODE_AVATAR_CAPTURE_IMAGE -> { + if (resultCode == Activity.RESULT_OK) { + capturedImageUri?.let { processPostImage(it, UploadType.Avatar) } + } + } + + REQUEST_CODE_HEADER_PICK_MEDIA -> { + if (resultCode == Activity.RESULT_OK) { + data?.let { processPostMedia(it, UploadType.Header) } + } + } + + REQUEST_CODE_HEADER_CAPTURE_IMAGE -> { + if (resultCode == Activity.RESULT_OK) { + capturedImageUri?.let { processPostImage(it, UploadType.Header) } + } + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + REQUEST_CODE_GRANT_READ_STORAGE_AVATAR -> { + if (grantResults.isNotEmpty() && + grantResults.none { it != PackageManager.PERMISSION_GRANTED }) { + pickMedia(UploadType.Avatar) + } else { + Snackbar.make(binding.root, R.string.message_necessity_wifi_grant, Snackbar.LENGTH_SHORT) + } + } + + REQUEST_CODE_GRANT_READ_STORAGE_HEADER -> { + if (grantResults.isNotEmpty() && + grantResults.none { it != PackageManager.PERMISSION_GRANTED }) { + pickMedia(UploadType.Header) + } else { + Snackbar.make(binding.root, R.string.message_necessity_wifi_grant, Snackbar.LENGTH_SHORT) + } + } + + REQUEST_CODE_GRANT_WRITE_STORAGE_AVATAR -> { + if (grantResults.isNotEmpty() && + grantResults.none { it != PackageManager.PERMISSION_GRANTED }) { + captureImage(UploadType.Avatar) + } else { + Snackbar.make(binding.root, R.string.message_necessity_wifi_grant, Snackbar.LENGTH_SHORT) + } + } + + REQUEST_CODE_GRANT_WRITE_STORAGE_HEADER -> { + if (grantResults.isNotEmpty() && + grantResults.none { it != PackageManager.PERMISSION_GRANTED }) { + captureImage(UploadType.Header) + } else { + Snackbar.make(binding.root, R.string.message_necessity_wifi_grant, Snackbar.LENGTH_SHORT) + } + } + } + } + + private fun initViewsVisibility() { + listOf( + binding.buttonFavList, + binding.buttonCancelEdit, + binding.buttonEdit + ) + .forEach { it.visibility = View.GONE } + } + + private fun showFavList() { + (activity as MainActivity).showTimelineFragment(TimelineFragment.Category.Fav) + } + + private fun toggleEditMode(enter: Boolean, button: Button, save: Boolean = true) { // FIXME: 常に編集できるようになってる + if (enter) { + button.text = getString(R.string.button_edit_save) + binding.buttonCancelEdit.apply { + setOnClickListener { toggleEditMode(false, button, false) } + visibility = View.VISIBLE + } + + binding.icon.setOnClickListener { showImageUploader(UploadType.Avatar) } + binding.header.setOnClickListener { showImageUploader(UploadType.Header) } + + listOf( + binding.screenName, + binding.note + ) + .forEach { editText -> + (editText.tag as? TextKeyListener)?.let { + editText.keyListener = it + } + } + } else { + button.text = getString(R.string.button_edit) + + binding.buttonCancelEdit.apply { + setOnClickListener {} + visibility = View.GONE + } + + binding.icon.setOnClickListener { showImageViewer(listOf(profile.iconUrl), 0) } + binding.header.setOnClickListener { showImageViewer(listOf(profile.headerUrl), 0) } + + listOf( + binding.screenName, + binding.note + ) + .forEach { + it.tag = it.keyListener + it.setOnKeyListener(null) + } + + if (save) submitEdit(button) + } + } + + private fun showImageUploader(type: UploadType) = ChooseImageSourceDialogFragment.newInstance(type) + .apply { + setTargetFragment(this@AccountProfileFragment, 0) + show(this@AccountProfileFragment.activity.supportFragmentManager, ChooseImageSourceDialogFragment.TAG) + } + + fun pickMedia(uploadType: UploadType) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), when (uploadType) { + UploadType.Avatar -> REQUEST_CODE_GRANT_READ_STORAGE_AVATAR + UploadType.Header -> REQUEST_CODE_GRANT_READ_STORAGE_HEADER + }) + } else { + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "image/* video/*" + } + startActivityForResult(intent, when (uploadType) { + UploadType.Avatar -> REQUEST_CODE_AVATAR_PICK_MEDIA + UploadType.Header -> REQUEST_CODE_HEADER_PICK_MEDIA + }) + } + } + + fun captureImage(uploadType: UploadType) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), when (uploadType) { + UploadType.Avatar -> REQUEST_CODE_GRANT_WRITE_STORAGE_AVATAR + UploadType.Header -> REQUEST_CODE_GRANT_WRITE_STORAGE_HEADER + }) + } else { + val values = ContentValues() + values.put(MediaStore.Images.Media.TITLE, "${System.currentTimeMillis()}.jpg") + capturedImageUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { + putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri) + } + + startActivityForResult(intent, when (uploadType) { + UploadType.Avatar -> REQUEST_CODE_AVATAR_CAPTURE_IMAGE + UploadType.Header -> REQUEST_CODE_HEADER_CAPTURE_IMAGE + }) + } + } + + private fun processPostMedia(data: Intent, type: UploadType) { + data.data?.let { uri -> + bindMedia(activity.contentResolver.openInputStream(uri), type) + } + } + + private fun processPostImage(uri: Uri, type: UploadType) { + getImagePathFromUriAsSingle(uri) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ path -> + bindImage(File(path), type) + }, Throwable::printStackTrace) + } + + private fun bindMedia(stream: InputStream, type: UploadType) { + val bytes = IOUtils.toByteArray(stream) + val base64String = "data:image/png;base64,${Base64.encodeToString(bytes, Base64.DEFAULT)}" + + when (type) { + UploadType.Avatar -> { + Glide.with(activity).load(bytes).into(binding.icon) + editedProfile.iconImg = base64String + } + + UploadType.Header -> { + Glide.with(activity).load(bytes).into(binding.header) + editedProfile.headerImg = base64String + } + } + } + + private fun bindImage(file: File, type: UploadType) { + val bytes = file.readBytes() + val base64String = "data:image/jpeg;base64,${Base64.encodeToString(bytes, Base64.DEFAULT)}" + + when (type) { + UploadType.Avatar -> { + Glide.with(activity).load(bytes).into(binding.icon) + editedProfile.iconImg = base64String + } + + UploadType.Header -> { + Glide.with(activity).load(bytes).into(binding.header) + editedProfile.headerImg = base64String + } + } + } + + private fun getImagePathFromUriAsSingle(uri: Uri): Single { + return Single.just(MediaStore.Images.Media.DATA) + .map { projection -> + val cursor = activity.contentResolver.query(uri, arrayOf(projection), null, null, null) + cursor.moveToFirst() + + cursor.getString(cursor.getColumnIndexOrThrow(projection)).apply { cursor.close() } + } + } + + private fun submitEdit(button: Button) { + editedProfile.apply { + screenName = binding.screenName.text.toString() + note = binding.note.text + } + val displayName = if (profile.screenName == editedProfile.screenName) null else editedProfile.screenName + val note = if (profile.note.toString() == editedProfile.note.toString()) null else editedProfile.note.toString() + val avatar = if (profile.iconImg == editedProfile.iconImg) null else editedProfile.iconImg + val header = if (profile.headerImg == editedProfile.headerImg) null else editedProfile.headerImg + + profile = editedProfile.copy() + + if (displayName == null && + note == null && + avatar == null && + header == null) { + Snackbar.make(binding.root, R.string.none_update_edit_profile, Snackbar.LENGTH_SHORT).show() + return + } + + Common.resetAuthInfo()?.let { + MastodonClient(it).updateOwnAccount(displayName, note, avatar, header) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ + if (displayName != null) refreshBarTitle(displayName) + Snackbar.make(binding.root, R.string.succeed_edit_profile, Snackbar.LENGTH_SHORT).show() + toggleEditMode(false, button, false) + }, Throwable::printStackTrace) + } + } + + private fun showImageViewer(urls: List, position: Int) { val fragment = ShowImagesDialogFragment.newInstance(urls, position) activity.supportFragmentManager.beginTransaction() .add(fragment, ShowImagesDialogFragment.TAG) @@ -230,11 +546,11 @@ class AccountProfileFragment: BaseFragment() { .commit() } - fun showToots(loadNext: Boolean = false) { + private fun showToots(loadNext: Boolean = false) { if (loadNext && maxId == -1L) return Common.resetAuthInfo()?.let { - MastodonClient(it).getAccountAllToots(account.id, if (loadNext) maxId else null, if (!loadNext && sinceId != -1L) sinceId else null) + MastodonClient(it).getAccountAllToots(profile.id, if (loadNext) maxId else null, if (!loadNext && sinceId != -1L) sinceId else null) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -244,10 +560,10 @@ class AccountProfileFragment: BaseFragment() { } } - fun getRegexExtractSinceId() = Regex(".*since_id=(\\d+?)>.*") - fun getRegexExtractMaxId() = Regex(".*max_id=(\\d+?)>.*") + private fun getRegexExtractSinceId() = Regex(".*since_id=(\\d+?)>.*") + private fun getRegexExtractMaxId() = Regex(".*max_id=(\\d+?)>.*") - fun reflectStatuses(result: Result>, next: Boolean) { + private fun reflectStatuses(result: Result>, next: Boolean) { result.response()?.let { if (next) adapter.addAllContentsAtLast(it.body().map { Common.getTimelineContent(it) }) else adapter.addAllContents(it.body().map { Common.getTimelineContent(it) }) @@ -277,39 +593,51 @@ class AccountProfileFragment: BaseFragment() { } } - fun setFollowButtonState(state: Boolean) { - if (state) { - binding.follow.setImageResource(R.drawable.ic_people_black_24px) - binding.follow.setColorFilter(ContextCompat.getColor(activity, R.color.accent)) - } else { - binding.follow.setImageResource(R.drawable.ic_person_add_black_24px) - binding.follow.setColorFilter(ContextCompat.getColor(activity, R.color.icon_tint_dark)) + private fun setFollowButtonState(relationship: Relationship) { + binding.follow.apply { + when { + relationship.hasSendFollowRequest -> { + isClickable = false + setImageResource(R.drawable.ic_hourglass_empty_black_24px) + setColorFilter(ContextCompat.getColor(activity, R.color.icon_tint_dark)) + } + relationship.following -> { + isClickable = true + setImageResource(R.drawable.ic_people_black_24px) + setColorFilter(ContextCompat.getColor(activity, R.color.accent)) + } + else -> { + isClickable = true + setImageResource(R.drawable.ic_person_add_black_24px) + setColorFilter(ContextCompat.getColor(activity, R.color.icon_tint_dark)) + } + } } } - fun setBlockButtonState(state: Boolean) { + private fun setBlockButtonState(state: Boolean) { binding.block.setColorFilter( ContextCompat.getColor(activity, if (state) R.color.accent else R.color.icon_tint_dark)) } - fun setMuteButtonState(state: Boolean) { + private fun setMuteButtonState(state: Boolean) { binding.mute.setColorFilter( ContextCompat.getColor(activity, if (state) R.color.accent else R.color.icon_tint_dark)) } - fun reflectSettings() { + private fun reflectSettings() { val movementMethod = Common.getMovementMethodFromPreference(binding.root.context) binding.url.movementMethod = movementMethod binding.note.movementMethod = movementMethod } - fun refreshBarTitle() { - (activity as MainActivity).supportActionBar?.title = "${account.displayName}'s profile" + private fun refreshBarTitle(name: String = profile.screenName) { + (activity as MainActivity).supportActionBar?.title = "$name's profile" } - fun toggleRefreshIndicatorState(show: Boolean) { + private fun toggleRefreshIndicatorState(show: Boolean) { binding.timeline.swipeRefreshLayout.isRefreshing = show } } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/BaseFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/BaseFragment.kt index 6fe9204..ada5581 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/BaseFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/BaseFragment.kt @@ -2,29 +2,41 @@ package com.geckour.egret.view.fragment import android.os.Bundle import android.view.View +import com.bumptech.glide.Glide import com.geckour.egret.util.Common import com.geckour.egret.view.activity.MainActivity import com.trello.rxlifecycle2.components.support.RxFragment +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers open class BaseFragment: RxFragment() { override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (activity is MainActivity) { - (activity as MainActivity).supportActionBar?.show() - (activity as MainActivity).binding.appBarMain.contentMain.fab.show() - Common.setSimplicityPostBarVisibility((activity as MainActivity).binding.appBarMain.contentMain, false) + (activity as? MainActivity)?.apply { + supportActionBar?.show() + binding.appBarMain.contentMain.fab.show() + Common.setSimplicityPostBarVisibility(binding.appBarMain.contentMain, false) } } override fun onResume() { super.onResume() - if (activity is MainActivity) { - (activity as MainActivity).supportActionBar?.show() - (activity as MainActivity).binding.appBarMain.contentMain.fab.show() - Common.setSimplicityPostBarVisibility((activity as MainActivity).binding.appBarMain.contentMain, false) + Single.just(activity) + .map { Glide.get(it).clearDiskCache() } + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe() + + (activity as? MainActivity)?.apply { + supportActionBar?.show() + binding.appBarMain.toolbar.setOnClickListener(null) + binding.appBarMain.contentMain.fab.show() + Common.setSimplicityPostBarVisibility(binding.appBarMain.contentMain, false) } } } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/ChooseImageSourceDialogFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/ChooseImageSourceDialogFragment.kt new file mode 100644 index 0000000..7b2afea --- /dev/null +++ b/app/src/main/java/com/geckour/egret/view/fragment/ChooseImageSourceDialogFragment.kt @@ -0,0 +1,37 @@ +package com.geckour.egret.view.fragment + +import android.app.Dialog +import android.os.Bundle +import android.support.v7.app.AlertDialog +import com.geckour.egret.R +import com.trello.rxlifecycle2.components.support.RxDialogFragment + +class ChooseImageSourceDialogFragment: RxDialogFragment() { + + companion object { + val TAG: String = this::class.java.simpleName + private val ARGS_KEY_UPLOAD_TYPE = "argsKeyUploadType" + + fun newInstance(type: AccountProfileFragment.UploadType): ChooseImageSourceDialogFragment = ChooseImageSourceDialogFragment().apply { + arguments = Bundle().apply { + putSerializable(ARGS_KEY_UPLOAD_TYPE, type) + } + } + } + + private val uploadType: AccountProfileFragment.UploadType by lazy { arguments[ARGS_KEY_UPLOAD_TYPE] as AccountProfileFragment.UploadType } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(activity) + .setTitle(R.string.dialog_title_choose_source) + .setMessage(R.string.dialog_message_choose_source) + .setPositiveButton(R.string.dialog_button_pick_media, { _, _ -> + parentFragment?.let { (it as? AccountProfileFragment)?.pickMedia(uploadType) } ?: targetFragment?.let { (it as? AccountProfileFragment)?.pickMedia(uploadType) } + }) + .setNegativeButton(R.string.dialog_button_capture_image, { _, _ -> + parentFragment?.let { (it as? AccountProfileFragment)?.captureImage(uploadType) } ?: targetFragment?.let { (it as? AccountProfileFragment)?.captureImage(uploadType) } + }) + .setNeutralButton(R.string.dialog_button_dismiss, { _, _ -> dismiss()}) + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/HashTagMuteFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/HashTagMuteFragment.kt index 5a2dd6f..8492a18 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/HashTagMuteFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/HashTagMuteFragment.kt @@ -15,7 +15,6 @@ import com.geckour.egret.util.Common import com.geckour.egret.util.OrmaProvider import com.geckour.egret.view.activity.MainActivity import com.geckour.egret.view.adapter.MuteHashTagAdapter -import com.google.gson.Gson import com.google.gson.reflect.TypeToken import io.reactivex.Observable import io.reactivex.Single diff --git a/app/src/main/java/com/geckour/egret/view/fragment/LicenseFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/LicenseFragment.kt new file mode 100644 index 0000000..a4664be --- /dev/null +++ b/app/src/main/java/com/geckour/egret/view/fragment/LicenseFragment.kt @@ -0,0 +1,85 @@ +package com.geckour.egret.view.fragment + +import android.databinding.DataBindingUtil +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.geckour.egret.R +import com.geckour.egret.databinding.FragmentLicenseBinding +import com.geckour.egret.view.activity.SettingActivity + +class LicenseFragment : BaseFragment() { + + enum class License { + Stetho, + RxJava, + RxAndroid, + RxKotlin, + RxLifecycle, + Orma, + Retrofit, + Glide, + Timber, + MaterialDrawer, + CircularImageView, + Calligraphy, + Emoji, + CommonsIO + } + + companion object { + val TAG: String = this::class.java.simpleName + + fun newInstance(): LicenseFragment = LicenseFragment() + } + + lateinit private var binding: FragmentLicenseBinding + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_license, container, false) + return binding.root + } + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + injectLicense() + } + + override fun onPause() { + super.onPause() + } + + override fun onResume() { + super.onResume() + + (activity as? SettingActivity)?.binding?.appBarMain?.toolbar?.title = getString(R.string.title_fragment_license) + } + + private fun injectLicense() { + listOf( + binding.licenseStetho, + binding.licenseReactiveXJava, + binding.licenseReactiveXAndroid, + binding.licenseReactiveXKotlin, + binding.licenseReactiveXLifecycle, + binding.licenseOrma, + binding.licenseRetrofit, + binding.licenseGlide, + binding.licenseTimber, + binding.licenseMaterialDrawer, + binding.licenseCircularImageView, + binding.licenseCalligraphy, + binding.licenseEmoji, + binding.licenseCommonsIo + ) + .forEach { + it.apply { + name.setOnClickListener { + body.visibility = if (body.visibility == View.VISIBLE) View.GONE else View.VISIBLE + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/ListDialogFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/ListDialogFragment.kt index 134124f..0c756a5 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/ListDialogFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/ListDialogFragment.kt @@ -7,28 +7,31 @@ import android.support.v7.app.AlertDialog import com.geckour.egret.R import com.geckour.egret.databinding.FragmentListDialogBinding import com.geckour.egret.view.adapter.ListDialogAdapter +import com.geckour.egret.view.adapter.model.TimelineContent import com.trello.rxlifecycle2.components.support.RxDialogFragment -class ListDialogFragment(val listener: OnItemClickListener? = null): RxDialogFragment() { +class ListDialogFragment: RxDialogFragment() { - lateinit var binding: FragmentListDialogBinding + lateinit private var binding: FragmentListDialogBinding + private val listener: OnItemClickListener? by lazy { (activity as? OnItemClickListener) } + private val content: TimelineContent.TimelineStatus by lazy { arguments[ARGS_KEY_CONTENT] as TimelineContent.TimelineStatus } companion object { val TAG: String = this::class.java.simpleName val ARGS_KEY_TITLE = "title" val ARGS_KEY_RES_IDS = "itemResIds" val ARGS_KEY_STRINGS = "itemStrings" - - fun newInstance(title: String, items: List>, listener: OnItemClickListener): ListDialogFragment { - val fragment = ListDialogFragment(listener) - val args = Bundle() - args.putString(ARGS_KEY_TITLE, title) - args.putIntArray(ARGS_KEY_RES_IDS, items.map { it.first }.toIntArray()) - args.putStringArray(ARGS_KEY_STRINGS, items.map { it.second }.toTypedArray()) - fragment.arguments = args - - return fragment - } + val ARGS_KEY_CONTENT = "argsKeyContent" + + fun newInstance(title: String, items: List>, content: TimelineContent.TimelineStatus): ListDialogFragment = ListDialogFragment() + .apply { + arguments = Bundle().apply { + putString(ARGS_KEY_TITLE, title) + putIntArray(ARGS_KEY_RES_IDS, items.map { it.first }.toIntArray()) + putStringArray(ARGS_KEY_STRINGS, items.map { it.second }.toTypedArray()) + putSerializable(ARGS_KEY_CONTENT, content) + } + } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -43,7 +46,7 @@ class ListDialogFragment(val listener: OnItemClickListener? = null): RxDialogFra }, object: ListDialogAdapter.OnItemClickListener { override fun onClick(resId: Int) { - listener?.onClick(resId) + listener?.onClickListDialogItem(resId, content) dismiss() } }) @@ -53,6 +56,6 @@ class ListDialogFragment(val listener: OnItemClickListener? = null): RxDialogFra } interface OnItemClickListener { - fun onClick(resId: Int) + fun onClickListDialogItem(resId: Int, content: TimelineContent.TimelineStatus) } } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/MiscFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/MiscFragment.kt new file mode 100644 index 0000000..9c2ba67 --- /dev/null +++ b/app/src/main/java/com/geckour/egret/view/fragment/MiscFragment.kt @@ -0,0 +1,49 @@ +package com.geckour.egret.view.fragment + +import android.os.Bundle +import android.support.v4.app.Fragment +import android.support.v7.preference.PreferenceFragmentCompat +import android.support.v7.preference.PreferenceScreen +import com.geckour.egret.R +import com.geckour.egret.view.activity.SettingActivity + +class MiscFragment: PreferenceFragmentCompat(), PreferenceFragmentCompat.OnPreferenceStartScreenCallback { + + companion object { + val TAG: String = this::class.java.simpleName + + fun newInstance(): MiscFragment = MiscFragment() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.preferences_misc, rootKey) + preferenceScreen.findPreference("show_licenses").setOnPreferenceClickListener { showLicenseFragment() } + } + + override fun getCallbackFragment(): Fragment = this + + override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat?, pref: PreferenceScreen?): Boolean { + caller?.preferenceScreen = pref + return true + } + + override fun onPause() { + super.onPause() + } + + override fun onResume() { + super.onResume() + + (activity as? SettingActivity)?.binding?.appBarMain?.toolbar?.title = getString(R.string.title_fragment_others) + } + + private fun showLicenseFragment(): Boolean { + val fragment = LicenseFragment.newInstance() + (activity as SettingActivity).supportFragmentManager.beginTransaction() + .replace(R.id.container, fragment, LicenseFragment.TAG) + .addToBackStack(LicenseFragment.TAG) + .commit() + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/NewTootCreateFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/NewTootCreateFragment.kt index 4b8c4d9..a6db2fd 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/NewTootCreateFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/NewTootCreateFragment.kt @@ -3,8 +3,8 @@ package com.geckour.egret.view.fragment import android.Manifest import android.app.Activity import android.content.ClipData -import android.content.ContentUris import android.content.ContentValues +import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.databinding.DataBindingUtil @@ -15,6 +15,7 @@ import android.provider.MediaStore import android.support.design.widget.Snackbar import android.support.v4.app.ActivityCompat import android.support.v4.content.ContextCompat +import android.support.v7.app.AlertDialog import android.text.Editable import android.view.KeyEvent import android.view.LayoutInflater @@ -22,15 +23,17 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.EditText -import android.widget.ImageView import com.bumptech.glide.Glide import com.geckour.egret.R import com.geckour.egret.api.MastodonClient +import com.geckour.egret.api.model.Attachment import com.geckour.egret.api.service.MastodonService import com.geckour.egret.databinding.FragmentCreateNewTootBinding +import com.geckour.egret.model.Draft import com.geckour.egret.util.Common import com.geckour.egret.util.OrmaProvider import com.geckour.egret.view.activity.MainActivity +import com.geckour.egret.view.activity.ShareActivity import io.reactivex.Observable import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers @@ -42,13 +45,17 @@ import okhttp3.RequestBody import java.io.File -class NewTootCreateFragment : BaseFragment() { +class NewTootCreateFragment : BaseFragment(), MainActivity.OnBackPressedListener, SelectDraftDialogFragment.OnSelectDraftItemListener { - lateinit var binding: FragmentCreateNewTootBinding + lateinit private var binding: FragmentCreateNewTootBinding private val postMediaReqs: ArrayList = ArrayList() - private var mediaCount: Int = 0 - private val mediaIds: ArrayList = ArrayList() + private val attachments: ArrayList = ArrayList() private var capturedImageUri: Uri? = null + lateinit private var initialBody: String + lateinit private var initialAlertBody: String + private var isSuccessPost = false + private val drafts: ArrayList = ArrayList() + private var draft: Draft? = null companion object { val TAG: String = this::class.java.simpleName @@ -56,6 +63,7 @@ class NewTootCreateFragment : BaseFragment() { private val ARGS_KEY_POST_TOKEN_ID = "postTokenId" private val ARGS_KEY_REPLY_TO_STATUS_ID = "replyToStatusId" private val ARGS_KEY_REPLY_TO_ACCOUNT_NAME = "replyToAccountName" + private val ARGS_KEY_BODY = "argsKeyBody" private val REQUEST_CODE_PICK_MEDIA = 1 private val REQUEST_CODE_CAPTURE_IMAGE = 2 private val REQUEST_CODE_GRANT_READ_STORAGE = 3 @@ -65,33 +73,30 @@ class NewTootCreateFragment : BaseFragment() { currentTokenId: Long, postTokenId: Long = currentTokenId, replyToStatusId: Long? = null, - replyToAccountName: String? = null): NewTootCreateFragment { - - val fragment = NewTootCreateFragment() - val args = Bundle() - args.putLong(ARGS_KEY_CURRENT_TOKEN_ID, currentTokenId) - args.putLong(ARGS_KEY_POST_TOKEN_ID, postTokenId) - if (replyToStatusId != null) args.putLong(ARGS_KEY_REPLY_TO_STATUS_ID, replyToStatusId) - if (replyToAccountName != null) args.putString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME, replyToAccountName) - fragment.arguments = args - - return fragment + replyToAccountName: String? = null, + body: String? = null) = NewTootCreateFragment().apply { + arguments = Bundle().apply { + putLong(ARGS_KEY_CURRENT_TOKEN_ID, currentTokenId) + putLong(ARGS_KEY_POST_TOKEN_ID, postTokenId) + if (replyToStatusId != null) putLong(ARGS_KEY_REPLY_TO_STATUS_ID, replyToStatusId) + if (replyToAccountName != null) putString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME, replyToAccountName) + if (body != null) putString(ARGS_KEY_BODY, body) + } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - (activity as MainActivity).supportActionBar?.hide() - (activity as MainActivity).binding.appBarMain.contentMain.fab.hide() + (activity as? MainActivity)?.supportActionBar?.hide() + (activity as? MainActivity)?.binding?.appBarMain?.contentMain?.fab?.hide() } override fun onResume() { super.onResume() - (activity as MainActivity).supportActionBar?.hide() - (activity as MainActivity).binding.appBarMain.contentMain.fab.hide() - (activity as MainActivity) + (activity as? MainActivity)?.supportActionBar?.hide() + (activity as? MainActivity)?.binding?.appBarMain?.contentMain?.fab?.hide() } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -106,7 +111,7 @@ class NewTootCreateFragment : BaseFragment() { val domain = OrmaProvider.db.selectFromInstanceAuthInfo().idEq(token.instanceId).last().instance OrmaProvider.db.updateAccessToken().isCurrentEq(true).isCurrent(false).executeAsSingle() .flatMap { OrmaProvider.db.updateAccessToken().idEq(token.id).isCurrent(true).executeAsSingle() } - .flatMap { MastodonClient(Common.resetAuthInfo() ?: throw IllegalArgumentException()).getSelfAccount() } + .flatMap { MastodonClient(Common.resetAuthInfo() ?: throw IllegalArgumentException()).getOwnAccount() } .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) @@ -121,6 +126,19 @@ class NewTootCreateFragment : BaseFragment() { binding.gallery.setOnClickListener { pickMedia() } binding.camera.setOnClickListener { captureImage() } + if (arguments.containsKey(ARGS_KEY_BODY)) + binding.tootBody.text = Editable.Factory.getInstance().newEditable(arguments.getString(ARGS_KEY_BODY)) + + if (arguments.containsKey(ARGS_KEY_REPLY_TO_STATUS_ID) + && arguments.containsKey(ARGS_KEY_REPLY_TO_ACCOUNT_NAME) + && arguments.getString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME) != null) { + binding.replyTo.text = "reply to: ${arguments.getString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME)}" + binding.replyTo.visibility = View.VISIBLE + val accountName = "${arguments.getString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME)} " + binding.tootBody.text = Editable.Factory.getInstance().newEditable(accountName) + binding.tootBody.setSelection(accountName.length) + } + binding.tootBody.setOnKeyListener { v, keyCode, event -> when (event.action) { KeyEvent.ACTION_DOWN -> { @@ -172,14 +190,24 @@ class NewTootCreateFragment : BaseFragment() { postToot(binding.tootBody.text.toString()) } - if (arguments.containsKey(ARGS_KEY_REPLY_TO_STATUS_ID) - && arguments.containsKey(ARGS_KEY_REPLY_TO_ACCOUNT_NAME) - && arguments.getString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME) != null) { - binding.replyTo.text = "reply to: ${arguments.getString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME)}" - binding.replyTo.visibility = View.VISIBLE - val accountName = "${arguments.getString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME)} " - binding.tootBody.text = Editable.Factory.getInstance().newEditable(accountName) - binding.tootBody.setSelection(accountName.length) + initialBody = binding.tootBody.text.toString() + initialAlertBody = binding.tootAlertBody.text.toString() + + Common.getCurrentAccessToken()?.id?.let { + drafts.addAll( + OrmaProvider.db.relationOfDraft() + .tokenIdEq(it) + .orderByCreatedAtAsc() + ) + } + binding.draft.apply { + if (drafts.isNotEmpty()) { + visibility = View.VISIBLE + setOnClickListener { onLoadDraft() } + } else { + visibility = View.INVISIBLE + setOnClickListener(null) + } } } @@ -194,28 +222,109 @@ class NewTootCreateFragment : BaseFragment() { when (requestCode) { REQUEST_CODE_PICK_MEDIA -> { if (resultCode == Activity.RESULT_OK) { - data?.let { bindMedia(it) } + data?.let { postMedia(it) } } } REQUEST_CODE_CAPTURE_IMAGE -> { if (resultCode == Activity.RESULT_OK) { - capturedImageUri?.let { bindImage(it) } + capturedImageUri?.let { postImage(it) } } } } } - fun postToot(body: String) { + override fun onBackPressedInMainActivity(callback: (Boolean) -> Any) { + if (isSuccessPost.not() && + (initialBody != binding.tootBody.text.toString() || initialAlertBody != binding.tootAlertBody.text.toString())) { + AlertDialog.Builder(activity) + .setTitle(R.string.dialog_title_confirm_save) + .setMessage(R.string.dialog_message_confirm_save) + .setPositiveButton(R.string.dialog_button_ok_confirm_save, { dialog, _ -> + saveAsDraft(dialog, callback) + }) + .setNegativeButton(R.string.dialog_button_dismiss_confirm_save, { dialog, _ -> + dialog.dismiss() + callback(true) + }) + .setNeutralButton(R.string.dialog_button_cancel_confirm_save, { dialog, _ -> + dialog.dismiss() + callback(false) + }) + .show() + } else callback(true) + } + + override fun onSelect(draft: Draft) { + this.draft = draft + OrmaProvider.db.relationOfDraft() + .deleter() + .idEq(draft.id) + .executeAsSingle() + .map { + Common.getCurrentAccessToken()?.id?.let { + OrmaProvider.db.relationOfDraft() + .tokenIdEq(it) + .orderByCreatedAtAsc() + .toList() + } ?: arrayListOf() + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ drafts -> + this.drafts.apply { + clear() + addAll(drafts) + } + if (this.drafts.isEmpty()) binding.draft.apply { + visibility = View.INVISIBLE + setOnClickListener(null) + } + var account = "" + if (draft.inReplyToId != null && draft.inReplyToName != null) { + binding.replyTo.text = "reply to: ${draft.inReplyToName}" + binding.replyTo.visibility = View.VISIBLE + account = "${draft.inReplyToName} " + } + val body = "$account${draft.body}" + binding.tootBody.setText(body) + binding.tootBody.setSelection(body.length) + binding.tootAlertBody.setText(draft.alertBody) + this.attachments.apply { + clear() + addAll(draft.attachments.value) + } + Observable.fromIterable(this.attachments.mapIndexed { i, attachment -> Pair(i, attachment)}) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ (i, attachment) -> + Glide.with(activity).load(attachment.previewImgUrl).into( + when (i) { + 0 -> binding.media1 + 1 -> binding.media2 + 2 -> binding.media3 + 3 -> binding.media4 + else -> throw IndexOutOfBoundsException("There are attachments over 4.") + } + ) + }, Throwable::printStackTrace) + binding.switchCw.isChecked = draft.warning + binding.switchNsfw.isChecked = draft.sensitive + binding.spinnerVisibility.setSelection(draft.visibility) + }, Throwable::printStackTrace) + } + + private fun postToot(body: String) { if (body.isBlank()) { Snackbar.make(binding.root, R.string.error_empty_toot, Snackbar.LENGTH_SHORT) return } + MastodonClient(Common.resetAuthInfo() ?: return) .postNewToot( body = body, - inReplyToId = if (binding.replyTo.visibility == View.VISIBLE) arguments.getLong(ARGS_KEY_REPLY_TO_STATUS_ID) else null, - mediaIds = if (mediaIds.size > 0) mediaIds else null, + inReplyToId = if (binding.replyTo.visibility == View.VISIBLE) draft?.inReplyToId ?: arguments.getLong(ARGS_KEY_REPLY_TO_STATUS_ID) else null, + mediaIds = if (attachments.size > 0) attachments.map { it.id } else null, isSensitive = binding.switchNsfw.isChecked, spoilerText = if (binding.switchCw.isChecked) binding.tootAlertBody.text.toString() else null, visibility = when (binding.spinnerVisibility.selectedItemPosition) { @@ -230,7 +339,80 @@ class NewTootCreateFragment : BaseFragment() { .subscribe( { onPostSuccess() }, Throwable::printStackTrace) } - fun pickMedia() { + private fun saveAsDraft(dialog: DialogInterface, callback: (Boolean) -> Any, draftId: Long? = null) { + Common.getCurrentAccessToken()?.let { (id) -> + if (draftId == null) { + OrmaProvider.db.relationOfDraft() + .insertAsSingle { + Draft( + tokenId = id, + body = binding.tootBody.text.toString(), + alertBody = binding.tootAlertBody.text.toString(), + inReplyToId = if (binding.replyTo.visibility == View.VISIBLE) draft?.inReplyToId ?: arguments.getLong(ARGS_KEY_REPLY_TO_STATUS_ID) else null, + inReplyToName = if (binding.replyTo.visibility == View.VISIBLE) draft?.inReplyToName ?: arguments.getString(ARGS_KEY_REPLY_TO_ACCOUNT_NAME) else null, + attachments = Draft.Attachments(attachments), + warning = binding.switchCw.isChecked, + sensitive = binding.switchNsfw.isChecked, + visibility = binding.spinnerVisibility.selectedItemPosition + ) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + Snackbar.make(binding.root, R.string.complete_save_draft, Snackbar.LENGTH_SHORT).show() + dialog.dismiss() + callback(true) + }, { throwable -> + throwable.printStackTrace() + Snackbar.make(binding.root, R.string.failure_save_draft, Snackbar.LENGTH_SHORT).show() + dialog.dismiss() + callback(false) + }) + } else { + OrmaProvider.db.relationOfDraft() + .upsertAsSingle( + Draft( + id = draftId, + tokenId = id, + body = binding.tootBody.text.toString(), + alertBody = binding.tootAlertBody.text.toString(), + inReplyToId = if (binding.replyTo.visibility == View.VISIBLE) arguments.getLong(ARGS_KEY_REPLY_TO_STATUS_ID) else null, + attachments = Draft.Attachments(attachments), + sensitive = binding.switchNsfw.isChecked, + visibility = when (binding.spinnerVisibility.selectedItemPosition) { + 0 -> MastodonService.Visibility.public.ordinal + 1 -> MastodonService.Visibility.unlisted.ordinal + 2 -> MastodonService.Visibility.private.ordinal + 3 -> MastodonService.Visibility.direct.ordinal + else -> -1 + } + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + Snackbar.make(binding.root, R.string.complete_save_draft, Snackbar.LENGTH_SHORT).show() + dialog.dismiss() + callback(true) + }, { throwable -> + throwable.printStackTrace() + Snackbar.make(binding.root, R.string.failure_save_draft, Snackbar.LENGTH_SHORT).show() + dialog.dismiss() + callback(false) + }) + } + } + } + + private fun onLoadDraft() { + SelectDraftDialogFragment.newInstance(drafts) + .apply { + setTargetFragment(this@NewTootCreateFragment, 0) + } + .show(activity.supportFragmentManager, SelectDraftDialogFragment.TAG) + } + + private fun pickMedia() { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE_GRANT_READ_STORAGE) @@ -243,7 +425,7 @@ class NewTootCreateFragment : BaseFragment() { } } - fun captureImage() { + private fun captureImage() { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_GRANT_WRITE_STORAGE) @@ -265,7 +447,7 @@ class NewTootCreateFragment : BaseFragment() { when (requestCode) { REQUEST_CODE_GRANT_READ_STORAGE -> { if (grantResults.isNotEmpty() && - grantResults.filter { it != PackageManager.PERMISSION_GRANTED }.isEmpty()) { + grantResults.none { it != PackageManager.PERMISSION_GRANTED }) { pickMedia() } else { Snackbar.make(binding.root, R.string.message_necessity_read_storage_grant, Snackbar.LENGTH_SHORT).show() @@ -274,7 +456,7 @@ class NewTootCreateFragment : BaseFragment() { REQUEST_CODE_GRANT_WRITE_STORAGE -> { if (grantResults.isNotEmpty() && - grantResults.filter { it != PackageManager.PERMISSION_GRANTED }.isEmpty()) { + grantResults.none { it != PackageManager.PERMISSION_GRANTED }) { captureImage() } else { Snackbar.make(binding.root, R.string.message_necessity_write_storage_grant_capture, Snackbar.LENGTH_SHORT).show() @@ -283,146 +465,90 @@ class NewTootCreateFragment : BaseFragment() { } } - fun bindMedia(data: Intent) { - Common.resetAuthInfo()?.let { domain -> - if (data.clipData != null) { - getMediaPathsFromClipDataAsObservable(data.clipData) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ (path, uri) -> - if (++mediaCount < 5) { - postMedia(domain, path, uri) - } else { - Snackbar.make(binding.root, R.string.error_too_many_media, Snackbar.LENGTH_SHORT).show() + private fun postMedia(data: Intent) { + if (attachments.size < 4) { + Common.resetAuthInfo()?.let { domain -> + if (data.clipData != null) { + getMediaPathsFromClipDataAsObservable(data.clipData) + .flatMap { (path, uri) -> + queryPostImageToAPI(domain, path, uri).toObservable() } - }, Throwable::printStackTrace) - } - if (data.data != null && ++mediaCount < 5) { - getMediaPathFromUriAsSingle(data.data) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ - if (++mediaCount < 5) { - postMedia(domain, it, data.data) - } else { - Snackbar.make(binding.root, R.string.error_too_many_media, Snackbar.LENGTH_SHORT).show() + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ + attachments.add(it) + indicateImage(it.previewImgUrl) + }, { throwable -> + throwable.printStackTrace() + Snackbar.make(binding.root, R.string.error_unable_upload_media, Snackbar.LENGTH_SHORT).show() + }) + } + + if (data.data != null) { + getMediaPathFromUriAsSingle(data.data) + .flatMap { (path, uri) -> + queryPostImageToAPI(domain, path, uri) } - }, Throwable::printStackTrace) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ + attachments.add(it) + indicateImage(it.previewImgUrl) + }, { throwable -> + throwable.printStackTrace() + Snackbar.make(binding.root, R.string.error_unable_upload_media, Snackbar.LENGTH_SHORT).show() + }) + } } - } + } else Snackbar.make(binding.root, R.string.error_too_many_media, Snackbar.LENGTH_SHORT).show() } - fun bindImage(uri: Uri) { - getImagePathFromUriAsSingle(uri) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ path -> - if (++mediaCount < 5) { - Common.resetAuthInfo()?.let { - postImage(it, path, uri) + private fun postImage(uri: Uri) { + if (attachments.size < 4) { + Common.resetAuthInfo()?.let { domain -> + getImagePathFromUriAsSingle(uri) + .flatMap { path -> + queryPostImageToAPI(domain, path, uri) } - } else { - Snackbar.make(binding.root, R.string.error_too_many_media, Snackbar.LENGTH_SHORT).show() - } - }, Throwable::printStackTrace) - } - - fun postMedia(domain: String, path: String, uri: Uri) { - val file = File(path) - val body = MultipartBody.Part.createFormData( - "file", - file.name, - RequestBody.create(MediaType.parse(activity.contentResolver.getType(uri)), file)) - - postMediaReqs.add( - MastodonClient(domain).postNewMedia(body) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .compose(bindToLifecycle()) .subscribe({ - mediaIds.add(it.id) - indicateMedia(uri) + attachments.add(it) + indicateImage(it.url) }, { throwable -> throwable.printStackTrace() - mediaCount-- Snackbar.make(binding.root, R.string.error_unable_upload_media, Snackbar.LENGTH_SHORT).show() }) - ) + } + } else { + Snackbar.make(binding.root, R.string.error_too_many_media, Snackbar.LENGTH_SHORT).show() + } } - fun postImage(domain: String, path: String, uri: Uri) { + private fun queryPostImageToAPI(domain: String, path: String, uri: Uri): Single { val file = File(path) val body = MultipartBody.Part.createFormData( "file", file.name, - RequestBody.create(MediaType.parse("jpeg"), file)) + RequestBody.create(MediaType.parse(activity.contentResolver.getType(uri)), file)) - postMediaReqs.add( - MastodonClient(domain).postNewMedia(body) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ - mediaIds.add(it.id) - indicateImage(uri) - }, { throwable -> - throwable.printStackTrace() - mediaCount-- - Snackbar.make(binding.root, R.string.error_unable_upload_media, Snackbar.LENGTH_SHORT).show() - }) - ) + return MastodonClient(domain).postNewMedia(body) } - fun indicateMedia(uri: Uri) { - Single.just( - MediaStore.Images.Thumbnails.getThumbnail( - activity.contentResolver, - DocumentsContract.getDocumentId(uri).split(":").last().toLong(), - MediaStore.Images.Thumbnails.MINI_KIND, - null)) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ - val mediaViews: List = listOf( - binding.media1, - binding.media2, - binding.media3, - binding.media4 - ) - - mediaViews.filter { it.drawable == null }.firstOrNull()?.setImageBitmap(it) - }, Throwable::printStackTrace) - } - - fun indicateImage(uri: Uri) { - Single.just( - MediaStore.Images.Thumbnails.getThumbnail( - activity.contentResolver, - ContentUris.parseId(uri), - MediaStore.Images.Thumbnails.MINI_KIND, - null)) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(bindToLifecycle()) - .subscribe({ - val mediaViews: List = listOf( - binding.media1, - binding.media2, - binding.media3, - binding.media4 - ) - - mediaViews.filter { it.drawable == null }.firstOrNull()?.setImageBitmap(it) - - deleteTempImage() - }, Throwable::printStackTrace) + private fun indicateImage(url: String, index: Int = attachments.size) { + when (index) { + 0 -> Glide.with(activity).load(url).into(binding.media1) + 1 -> Glide.with(activity).load(url).into(binding.media2) + 2 -> Glide.with(activity).load(url).into(binding.media3) + 3 -> Glide.with(activity).load(url).into(binding.media4) + else -> {} + } } - fun getMediaPathFromUriAsSingle(uri: Uri): Single { + private fun getMediaPathFromUriAsSingle(uri: Uri): Single> { val projection = MediaStore.Images.Media.DATA return Single.just(DocumentsContract.getDocumentId(uri).split(":").last()) @@ -434,15 +560,16 @@ class NewTootCreateFragment : BaseFragment() { arrayOf(it), null) cursor.moveToFirst() - cursor.getString(cursor.getColumnIndexOrThrow(projection)).apply { cursor.close() } + val path = cursor.getString(cursor.getColumnIndexOrThrow(projection)).apply { cursor.close() } + Pair(path, uri) } } - fun getMediaPathsFromClipDataAsObservable(clip: ClipData): Observable> { + private fun getMediaPathsFromClipDataAsObservable(clip: ClipData): Observable> { val docIds: ArrayList> = ArrayList() val projection = MediaStore.Images.Media.DATA - (0..clip.itemCount - 1).mapTo(docIds) { + (0 until clip.itemCount).mapTo(docIds) { val uri = clip.getItemAt(it).uri Pair(DocumentsContract.getDocumentId(uri).split(":").last(), uri) } @@ -462,7 +589,7 @@ class NewTootCreateFragment : BaseFragment() { } } - fun getImagePathFromUriAsSingle(uri: Uri): Single { + private fun getImagePathFromUriAsSingle(uri: Uri): Single { return Single.just(MediaStore.Images.Media.DATA) .map { projection -> val cursor = activity.contentResolver.query(uri, arrayOf(projection), null, null, null) @@ -472,7 +599,7 @@ class NewTootCreateFragment : BaseFragment() { } } - fun deleteTempImage() { + private fun deleteTempImage() { capturedImageUri?.let { getImagePathFromUriAsSingle(it) .subscribeOn(Schedulers.newThread()) @@ -487,7 +614,12 @@ class NewTootCreateFragment : BaseFragment() { } } - fun onPostSuccess() { - (activity as MainActivity).supportFragmentManager.popBackStack() + private fun onPostSuccess() { + isSuccessPost = true + (activity as? MainActivity)?.supportFragmentManager?.popBackStack() + (activity as? ShareActivity)?.apply { + supportFragmentManager?.popBackStack() + onBackPressed() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/SearchResultFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/SearchResultFragment.kt index 6f637d6..f8c4c8c 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/SearchResultFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/SearchResultFragment.kt @@ -22,7 +22,7 @@ class SearchResultFragment: BaseFragment() { } companion object { - val TAG = "searchResultFragment" + val TAG: String = this::class.java.simpleName private val ARGS_KEY_CATEGORY = "category" private val ARGS_KEY_QUERY = "query" private val ARGS_KEY_RESULT = "result" diff --git a/app/src/main/java/com/geckour/egret/view/fragment/SelectDraftDialogFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/SelectDraftDialogFragment.kt new file mode 100644 index 0000000..9306500 --- /dev/null +++ b/app/src/main/java/com/geckour/egret/view/fragment/SelectDraftDialogFragment.kt @@ -0,0 +1,54 @@ +package com.geckour.egret.view.fragment + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v7.app.AlertDialog +import com.geckour.egret.App +import com.geckour.egret.R +import com.geckour.egret.model.Draft +import com.geckour.egret.view.activity.MainActivity +import com.trello.rxlifecycle2.components.support.RxDialogFragment + +class SelectDraftDialogFragment: RxDialogFragment() { + + companion object { + val TAG: String = this::class.java.simpleName + private val ARGS_KEY_DRAFTS = "argsKeyDrafts" + + fun newInstance(drafts: List): SelectDraftDialogFragment = SelectDraftDialogFragment().apply { + arguments = Bundle().apply { + putStringArray(ARGS_KEY_DRAFTS, drafts.map { App.gson.toJson(it) }.toTypedArray()) + } + } + } + + interface OnSelectDraftItemListener { + fun onSelect(draft: Draft) + } + + private var listener: OnSelectDraftItemListener? = null + private val drafts: List by lazy { arguments.getStringArray(ARGS_KEY_DRAFTS).map { App.gson.fromJson(it, Draft::class.java) } } + + override fun onAttach(context: Context?) { + super.onAttach(context) + + targetFragment?.let { listener = it as OnSelectDraftItemListener} + parentFragment?.let { listener = it as OnSelectDraftItemListener} + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(activity) + .setTitle(R.string.dialog_message_select_draft) + .setItems(drafts.map { it.body }.toTypedArray(), { dialog, which -> + drafts.getOrNull(which)?.let { + listener?.onSelect(it) + } ?: Snackbar.make((activity as MainActivity).binding.root, R.string.error_unable_select_draft, Snackbar.LENGTH_SHORT).show() + + dialog.dismiss() + }) + .setNegativeButton(R.string.dialog_button_dismiss, null) + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/SettingAppDataManageFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/SettingAppDataManageFragment.kt new file mode 100644 index 0000000..a0c4f7e --- /dev/null +++ b/app/src/main/java/com/geckour/egret/view/fragment/SettingAppDataManageFragment.kt @@ -0,0 +1,109 @@ +package com.geckour.egret.view.fragment + +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v4.app.Fragment +import android.support.v7.preference.PreferenceFragmentCompat +import android.support.v7.preference.PreferenceManager +import android.support.v7.preference.PreferenceScreen +import com.geckour.egret.R +import com.geckour.egret.util.OrmaProvider +import com.geckour.egret.view.activity.SettingActivity +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class SettingAppDataManageFragment: PreferenceFragmentCompat(), PreferenceFragmentCompat.OnPreferenceStartScreenCallback { + + companion object { + val TAG: String = this::class.java.simpleName + + fun newInstance(): SettingAppDataManageFragment = SettingAppDataManageFragment() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.preferences_app_data, rootKey) + + preferenceScreen.findPreference("clear_all_preference").setOnPreferenceClickListener { clearAllPreference() } + preferenceScreen.findPreference("clear_all_draft").setOnPreferenceClickListener { clearAllDraft() } + preferenceScreen.findPreference("clear_all_restriction").setOnPreferenceClickListener { clearAllRestriction() } + preferenceScreen.findPreference("clear_all_db_except_login_info").setOnPreferenceClickListener { clearAllDBExceptLoginInfo() } + preferenceScreen.findPreference("clear_all_db").setOnPreferenceClickListener { clearAllDB() } + } + + override fun getCallbackFragment(): Fragment = this + + override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat?, pref: PreferenceScreen?): Boolean { + caller?.preferenceScreen = pref + return true + } + + private fun clearAllPreference(): Boolean { + PreferenceManager.getDefaultSharedPreferences(activity).edit().clear().apply() + Snackbar.make((activity as SettingActivity).binding.root, R.string.message_complete_clear_pref, Snackbar.LENGTH_SHORT).show() + return true + } + + private fun clearAllDraft(): Boolean { + OrmaProvider.db.deleteFromDraft().executeAsSingle() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + Snackbar.make((activity as SettingActivity).binding.root, R.string.message_complete_clear_db, Snackbar.LENGTH_SHORT).show() + }, Throwable::printStackTrace) + return true + } + + private fun clearAllRestriction(): Boolean { + Observable.merge( + listOf( + OrmaProvider.db.deleteFromMuteClient().executeAsSingle().toObservable(), + OrmaProvider.db.deleteFromMuteHashTag().executeAsSingle().toObservable(), + OrmaProvider.db.deleteFromMuteInstance().executeAsSingle().toObservable(), + OrmaProvider.db.deleteFromMuteKeyword().executeAsSingle().toObservable() + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({}, Throwable::printStackTrace, { + Snackbar.make((activity as SettingActivity).binding.root, R.string.message_complete_clear_db, Snackbar.LENGTH_SHORT).show() + }) + return true + } + + private fun clearAllDBExceptLoginInfo(): Boolean { + Observable.merge( + listOf( + OrmaProvider.db.deleteFromDraft().executeAsSingle().toObservable(), + OrmaProvider.db.deleteFromMuteClient().executeAsSingle().toObservable(), + OrmaProvider.db.deleteFromMuteHashTag().executeAsSingle().toObservable(), + OrmaProvider.db.deleteFromMuteInstance().executeAsSingle().toObservable(), + OrmaProvider.db.deleteFromMuteKeyword().executeAsSingle().toObservable() + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({}, Throwable::printStackTrace, { + Snackbar.make((activity as SettingActivity).binding.root, R.string.message_complete_clear_db, Snackbar.LENGTH_SHORT).show() + }) + return true + } + + private fun clearAllDB(): Boolean { + Single.just(OrmaProvider.db) + .map { it.deleteAll() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + Snackbar.make((activity as SettingActivity).binding.root, R.string.message_complete_clear_db, Snackbar.LENGTH_SHORT).show() + }, Throwable::printStackTrace) + + val intent = activity.baseContext.packageManager.getLaunchIntentForPackage(activity.baseContext.packageName) + .apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } + startActivity(intent) + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/SettingMainFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/SettingMainFragment.kt index 2307d77..b01471c 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/SettingMainFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/SettingMainFragment.kt @@ -7,29 +7,23 @@ import android.support.v7.preference.PreferenceFragmentCompat import android.support.v7.preference.PreferenceScreen import com.geckour.egret.App import com.geckour.egret.R -import com.geckour.egret.view.activity.SettingActivity class SettingMainFragment: PreferenceFragmentCompat(), PreferenceFragmentCompat.OnPreferenceStartScreenCallback { companion object { val TAG: String = this::class.java.simpleName - fun newInstance(): SettingMainFragment { - val fragment = SettingMainFragment() - - return fragment - } + fun newInstance(): SettingMainFragment = SettingMainFragment() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preferences_main, rootKey) preferenceScreen.findPreference("manage_accounts").setOnPreferenceClickListener { showAccountManageFragment() } preferenceScreen.findPreference("manage_restrictions").setOnPreferenceClickListener { showRestrictFragment() } + preferenceScreen.findPreference("manage_held_data").setOnPreferenceClickListener { showSettingAppDataManageFragment() } } - override fun getCallbackFragment(): Fragment { - return this - } + override fun getCallbackFragment(): Fragment = this override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat?, pref: PreferenceScreen?): Boolean { caller?.preferenceScreen = pref @@ -48,9 +42,9 @@ class SettingMainFragment: PreferenceFragmentCompat(), PreferenceFragmentCompat. } } - fun showAccountManageFragment(): Boolean { + private fun showAccountManageFragment(): Boolean { val fragment = AccountManageFragment.newInstance() - (activity as SettingActivity).supportFragmentManager.beginTransaction() + activity.supportFragmentManager.beginTransaction() .replace(R.id.container, fragment, AccountManageFragment.TAG) .addToBackStack(AccountManageFragment.TAG) .commit() @@ -58,13 +52,22 @@ class SettingMainFragment: PreferenceFragmentCompat(), PreferenceFragmentCompat. return true } - fun showRestrictFragment(): Boolean { + private fun showRestrictFragment(): Boolean { val fragment = SettingRestrictFragment.newInstance() - (activity as SettingActivity).supportFragmentManager.beginTransaction() + activity.supportFragmentManager.beginTransaction() .replace(R.id.container, fragment, SettingRestrictFragment.TAG) .addToBackStack(SettingRestrictFragment.TAG) .commit() return true } + + private fun showSettingAppDataManageFragment(): Boolean { + val fragment = SettingAppDataManageFragment.newInstance() + activity.supportFragmentManager.beginTransaction() + .replace(R.id.container, fragment, SettingAppDataManageFragment.TAG) + .addToBackStack(SettingAppDataManageFragment.TAG) + .commit() + return true + } } \ No newline at end of file diff --git a/app/src/main/java/com/geckour/egret/view/fragment/SettingRestrictFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/SettingRestrictFragment.kt index 76f5d0f..c0d2faf 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/SettingRestrictFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/SettingRestrictFragment.kt @@ -12,11 +12,7 @@ class SettingRestrictFragment: PreferenceFragmentCompat(), PreferenceFragmentCom companion object { val TAG: String = this::class.java.simpleName - fun newInstance(): SettingRestrictFragment { - val fragment = SettingRestrictFragment() - - return fragment - } + fun newInstance(): SettingRestrictFragment = SettingRestrictFragment() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { diff --git a/app/src/main/java/com/geckour/egret/view/fragment/ShowTootDetailFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/ShowTootDetailFragment.kt index dcb1522..406adc7 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/ShowTootDetailFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/ShowTootDetailFragment.kt @@ -20,7 +20,7 @@ import io.reactivex.schedulers.Schedulers class ShowTootDetailFragment : BaseFragment() { companion object { - val TAG = "showTootDetailFragment" + val TAG: String = this::class.java.simpleName val ARGS_KEY_STATUS_ID = "statusId" fun newInstance(statusId: Long): ShowTootDetailFragment = ShowTootDetailFragment().apply { diff --git a/app/src/main/java/com/geckour/egret/view/fragment/TimelineFragment.kt b/app/src/main/java/com/geckour/egret/view/fragment/TimelineFragment.kt index 0fdd11f..0ea2846 100644 --- a/app/src/main/java/com/geckour/egret/view/fragment/TimelineFragment.kt +++ b/app/src/main/java/com/geckour/egret/view/fragment/TimelineFragment.kt @@ -47,6 +47,7 @@ class TimelineFragment: BaseFragment() { User, HashTag, Notification, + Fav, Unknown } @@ -55,7 +56,6 @@ class TimelineFragment: BaseFragment() { val ARGS_KEY_CATEGORY = "category" val ARGS_KEY_HASH_TAG = "hashTag" val STATE_ARGS_KEY_CONTENTS = "contents" - val STATE_ARGS_KEY_RESUME = "resume" val REQUEST_CODE_GRANT_ACCESS_WIFI = 100 fun newInstance(category: Category, hashTag: String? = null): TimelineFragment = TimelineFragment().apply { @@ -64,8 +64,6 @@ class TimelineFragment: BaseFragment() { hashTag?.let { putString(ARGS_KEY_HASH_TAG, hashTag) } } } - - fun getCategoryById(rawValue: Int): Category = Category.values()[rawValue] } lateinit private var binding: FragmentTimelineBinding @@ -139,6 +137,8 @@ class TimelineFragment: BaseFragment() { Category.User -> showUserTimeline(loadPrev = true) Category.Notification -> showNotificationTimeline(loadPrev = true) Category.HashTag -> getHashTag()?.let { showHashTagTimeline(it, loadPrev = true) } + Category.Fav -> showFavouriteTimeline(true) + Category.Unknown -> {} } } } @@ -184,6 +184,8 @@ class TimelineFragment: BaseFragment() { override fun onResume() { super.onResume() + (activity as MainActivity).binding.appBarMain.toolbar.setOnClickListener { scrollToTop() } + (activity as MainActivity).supportActionBar?.show() refreshBarTitle() @@ -203,9 +205,9 @@ class TimelineFragment: BaseFragment() { else if (sharedPref.contains(ARGS_KEY_HASH_TAG)) sharedPref.getString(ARGS_KEY_HASH_TAG, "") else null - fun existsNoRunningStream() = listOf(publicStream, localStream, userStream).none { !(it?.isDisposed ?: true) } + private fun existsNoRunningStream() = listOf(publicStream, localStream, userStream).none { !(it?.isDisposed ?: true) } - fun refreshBarTitle() { + private fun refreshBarTitle() { val instanceId = Common.getCurrentAccessToken()?.instanceId val domain = if (instanceId == null) "not logged in" else OrmaProvider.db.selectFromInstanceAuthInfo().idEq(instanceId).last().instance val category = getCategory() @@ -213,11 +215,13 @@ class TimelineFragment: BaseFragment() { when (category) { Category.HashTag -> "$category TL${getHashTag()?.let { ": #$it" } ?: ""} - $domain" + Category.Fav -> "Your favourited toots list - $domain" + else -> "$category TL - $domain" } } - fun restoreTimeline() { + private fun restoreTimeline() { adapter.clearContents() val storeContentsKey = getStoreContentsKey(getCategory()).apply { Log.d("restoreTimeline", "storeContentsKey: $this") } @@ -243,28 +247,28 @@ class TimelineFragment: BaseFragment() { .apply() } - fun reflectCategorySelection() { + private fun reflectCategorySelection() { (activity as MainActivity).resetSelectionNavItem( when (getCategory()) { - Category.Public -> MainActivity.NAV_ITEM_TL_PUBLIC - Category.Local -> MainActivity.NAV_ITEM_TL_LOCAL - Category.User -> MainActivity.NAV_ITEM_TL_USER - Category.Notification -> MainActivity.NAV_ITEM_TL_NOTIFICATION + Category.Public -> MainActivity.NavItem.NAV_ITEM_TL_PUBLIC.ordinal.toLong() + Category.Local -> MainActivity.NavItem.NAV_ITEM_TL_LOCAL.ordinal.toLong() + Category.User -> MainActivity.NavItem.NAV_ITEM_TL_USER.ordinal.toLong() + Category.Notification -> MainActivity.NavItem.NAV_ITEM_TL_NOTIFICATION.ordinal.toLong() else -> -1 }) } - fun toggleRefreshIndicatorState(show: Boolean) = Common.toggleRefreshIndicatorState(binding.swipeRefreshLayout, show) + private fun toggleRefreshIndicatorState(show: Boolean) = Common.toggleRefreshIndicatorState(binding.swipeRefreshLayout, show) - fun toggleRefreshIndicatorActivity(show: Boolean) = Common.toggleRefreshIndicatorActivity(binding.swipeRefreshLayout, show) + private fun toggleRefreshIndicatorActivity(show: Boolean) = Common.toggleRefreshIndicatorActivity(binding.swipeRefreshLayout, show) - fun forceStopRefreshing() { + private fun forceStopRefreshing() { toggleRefreshIndicatorState(false) binding.swipeRefreshLayout.destroyDrawingCache() binding.swipeRefreshLayout.clearAnimation() } - fun showTimelineByCategory(category: Category) { + private fun showTimelineByCategory(category: Category) { val prefStream = sharedPref.getString("manage_stream", "1") if (prefStream == "1") { @@ -291,6 +295,9 @@ class TimelineFragment: BaseFragment() { Category.User -> showUserTimeline(true) Category.HashTag -> getHashTag()?.let { showHashTagTimeline(it, true) } Category.Notification -> showNotificationTimeline(true) + Category.Fav -> showFavouriteTimeline() + Category.Unknown -> {} + } } else { when (category) { @@ -299,6 +306,8 @@ class TimelineFragment: BaseFragment() { Category.User -> showUserTimeline() Category.HashTag -> getHashTag()?.let { showHashTagTimeline(it) } Category.Notification -> showNotificationTimeline() + Category.Fav -> showFavouriteTimeline() + Category.Unknown -> {} } } } @@ -308,7 +317,7 @@ class TimelineFragment: BaseFragment() { when (requestCode) { REQUEST_CODE_GRANT_ACCESS_WIFI -> { if (grantResults.isNotEmpty() && - grantResults.filter { it != PackageManager.PERMISSION_GRANTED }.isEmpty()) { + grantResults.none { it != PackageManager.PERMISSION_GRANTED }) { showTimelineByCategory(getCategory()) } else { Snackbar.make(binding.root, R.string.message_necessity_wifi_grant, Snackbar.LENGTH_SHORT) @@ -317,7 +326,7 @@ class TimelineFragment: BaseFragment() { } } - fun postToot(contentMainBinding: ContentMainBinding) { + private fun postToot(contentMainBinding: ContentMainBinding) { val button = contentMainBinding.buttonSimplicityToot.apply { isEnabled = false } val body = contentMainBinding.simplicityTootBody.text.toString() if (body.isBlank()) { @@ -345,7 +354,7 @@ class TimelineFragment: BaseFragment() { }) } - fun stopTimelineStreams() { + private fun stopTimelineStreams() { stopPublicTimelineStream() stopLocalTimelineStream() stopUserTimelineStream() @@ -353,7 +362,7 @@ class TimelineFragment: BaseFragment() { stopHashTagTimelineStream() } - fun startPublicTimelineStream() { + private fun startPublicTimelineStream() { publicStream?.dispose() publicStream = null Common.resetAuthInfo()?.let { @@ -374,11 +383,11 @@ class TimelineFragment: BaseFragment() { } } - fun stopPublicTimelineStream() { - if (!(publicStream?.isDisposed ?: true)) publicStream?.dispose() + private fun stopPublicTimelineStream() { + if (publicStream?.isDisposed == false) publicStream?.dispose() } - fun showPublicTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { + private fun showPublicTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { if (loadPrev && maxId == -1L) return MastodonClient(Common.resetAuthInfo() ?: return).getPublicTimeline(maxId = if (loadPrev) maxId else null, sinceId = if (!loadPrev && sinceId != -1L) sinceId else null) @@ -394,7 +403,7 @@ class TimelineFragment: BaseFragment() { }) } - fun startUserTimelineStream() { + private fun startUserTimelineStream() { userStream?.dispose() userStream = null @@ -416,11 +425,11 @@ class TimelineFragment: BaseFragment() { } } - fun stopUserTimelineStream() { - if (!(userStream?.isDisposed ?: true)) userStream?.dispose() + private fun stopUserTimelineStream() { + if (userStream?.isDisposed == false) userStream?.dispose() } - fun showUserTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { + private fun showUserTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { if (loadPrev && maxId == -1L) return MastodonClient(Common.resetAuthInfo() ?: return).getUserTimeline(maxId = if (loadPrev) maxId else null, sinceId = if (!loadPrev && sinceId != -1L) sinceId else null) @@ -436,7 +445,7 @@ class TimelineFragment: BaseFragment() { }) } - fun startLocalTimelineStream() { + private fun startLocalTimelineStream() { localStream?.dispose() localStream = null @@ -458,11 +467,11 @@ class TimelineFragment: BaseFragment() { } } - fun stopLocalTimelineStream() { - if (!(localStream?.isDisposed ?: true)) localStream?.dispose() + private fun stopLocalTimelineStream() { + if (localStream?.isDisposed == false) localStream?.dispose() } - fun showLocalTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { + private fun showLocalTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { if (loadPrev && maxId == -1L) return MastodonClient(Common.resetAuthInfo() ?: return).getPublicTimeline(true, maxId = if (loadPrev) maxId else null, sinceId = if (!loadPrev && sinceId != -1L) sinceId else null) @@ -478,7 +487,7 @@ class TimelineFragment: BaseFragment() { }) } - fun startNotificationTimelineStream() { + private fun startNotificationTimelineStream() { notificationStream?.dispose() notificationStream = null @@ -500,11 +509,11 @@ class TimelineFragment: BaseFragment() { } } - fun stopNotificationTimelineStream() { - if (getCategory() == Category.User && !(userStream?.isDisposed ?: true)) userStream?.dispose() + private fun stopNotificationTimelineStream() { + if (notificationStream?.isDisposed == false) notificationStream?.dispose() } - fun showNotificationTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { + private fun showNotificationTimeline(loadStream: Boolean = false, loadPrev: Boolean = false) { if (loadPrev && maxId == -1L) return MastodonClient(Common.resetAuthInfo() ?: return).getNotificationTimeline(maxId = if (loadPrev) maxId else null, sinceId = if (!loadPrev && sinceId != -1L) sinceId else null) @@ -520,7 +529,7 @@ class TimelineFragment: BaseFragment() { }) } - fun startHashTagTimelineStream() { + private fun startHashTagTimelineStream() { hashTagStream?.dispose() hashTagStream = null @@ -543,11 +552,11 @@ class TimelineFragment: BaseFragment() { } } - fun stopHashTagTimelineStream() { - if (getCategory() == Category.HashTag && !(hashTagStream?.isDisposed ?: true)) hashTagStream?.dispose() + private fun stopHashTagTimelineStream() { + if (hashTagStream?.isDisposed == false) hashTagStream?.dispose() } - fun showHashTagTimeline(hashTag: String, loadStream: Boolean = false, loadPrev: Boolean = false) { + private fun showHashTagTimeline(hashTag: String, loadStream: Boolean = false, loadPrev: Boolean = false) { if (loadPrev && maxId == -1L) return MastodonClient(Common.resetAuthInfo() ?: return).getHashTagTimeline(hashTag, maxId = if (loadPrev) maxId else null, sinceId = if (!loadPrev && sinceId != -1L) sinceId else null) @@ -563,7 +572,22 @@ class TimelineFragment: BaseFragment() { }) } - fun reflectContents(result: Result>, next: Boolean) { + private fun showFavouriteTimeline(loadPrev: Boolean = false) { + if (loadPrev && maxId == -1L) return + + MastodonClient(Common.resetAuthInfo() ?: return).getFavouriteTimeline(maxId = if (loadPrev) maxId else null, sinceId = if (!loadPrev && sinceId != -1L) sinceId else null) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(bindToLifecycle()) + .subscribe({ + reflectContents(it, loadPrev) + }, { throwable -> + throwable.printStackTrace() + toggleRefreshIndicatorState(false) + }) + } + + private fun reflectContents(result: Result>, next: Boolean) { result.response()?.let { it.body()?.let { if (it.isNotEmpty()) { @@ -618,4 +642,8 @@ class TimelineFragment: BaseFragment() { waitingDeletedId = source == "event: delete" } } + + private fun scrollToTop() { + binding.recyclerView.smoothScrollToPosition(0) + } } \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_extension_black_24px.xml b/app/src/main/res/drawable-v21/ic_extension_black_24px.xml new file mode 100644 index 0000000..9452176 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_extension_black_24px.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_history_black_24px.xml b/app/src/main/res/drawable-v21/ic_history_black_24px.xml new file mode 100644 index 0000000..181ee36 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_history_black_24px.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_hourglass_empty_black_24px.xml b/app/src/main/res/drawable-v21/ic_hourglass_empty_black_24px.xml new file mode 100644 index 0000000..2fbb769 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_hourglass_empty_black_24px.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_lock_black_24px.xml b/app/src/main/res/drawable-v21/ic_lock_black_24px.xml new file mode 100644 index 0000000..d5f931c --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_lock_black_24px.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_star_black_12px.xml b/app/src/main/res/drawable-v21/ic_star_black_12px.xml new file mode 100755 index 0000000..2a4318c --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_star_black_12px.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_share.xml b/app/src/main/res/layout/activity_share.xml new file mode 100644 index 0000000..8cae74e --- /dev/null +++ b/app/src/main/res/layout/activity_share.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index bad904d..620ee18 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -32,7 +32,7 @@ + app:layout_constraintTop_toTopOf="parent" + android:layout_marginEnd="8dp" /> - + app:layout_constraintTop_toTopOf="@+id/button_simplicity_toot" /> diff --git a/app/src/main/res/layout/fragment_account_profile.xml b/app/src/main/res/layout/fragment_account_profile.xml index 0a7a927..8f424ff 100644 --- a/app/src/main/res/layout/fragment_account_profile.xml +++ b/app/src/main/res/layout/fragment_account_profile.xml @@ -4,6 +4,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"> + @@ -36,7 +37,8 @@ app:layout_constraintBottom_toBottomOf="@+id/icon" app:layout_constraintLeft_toLeftOf="@+id/header" app:layout_constraintRight_toRightOf="@+id/header" - app:layout_constraintTop_toTopOf="@+id/icon"> + app:layout_constraintTop_toTopOf="@+id/icon" + android:id="@+id/frameLayout"> @@ -50,12 +52,13 @@ app:layout_constraintLeft_toLeftOf="@+id/header" app:layout_constraintTop_toTopOf="@+id/header" /> - + tools:text="おなまえ" /> + tools:text="あいでぃー" /> + + - + +